Files
fleet/server/platform/mysql/errors.go
T
Magnus Jensen cfcca6a6ac Handle not found bootstrap package in GitOps flows (#46802)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #45441 

The issue is when hitting the `svc.DeleteMDMAppleBootstrapPackage` via
the API/UI, it only clears the row in `mdm_apple_bootstrap_packages`.
However when GitOps runs the next time, it compares the old team config,
which has a stale `macos_setup.bootstrap_package` config value. Which
forces it to call the same Delete method again. This PR adds the
defensive approach to gracefully handle a not found bootstrap package
when GitOps wants to delete it.
The reason the second run works, is that we only attempt to delete the
bootstrap package after we called SaveTeam with the new empty
`bootstrap_package` value. So next run sees it as empty and avoid
calling the Delete method.

_One question is if we want to add a more active approach on the delete
service method, which also handles updating the team config clearing out
this value? That would have prevented the cause, I think either keeping
only this layer, or doing both solutions is a good approach._

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* GitOps automation no longer fails on its first run after a bootstrap
package is deleted via the UI.
* Clearing a macOS bootstrap package (team or app config) now succeeds
even if the underlying package record is already missing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-05 11:48:13 +02:00

107 lines
2.8 KiB
Go

package mysql
import (
"database/sql"
"errors"
"fmt"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
platform_errors "github.com/fleetdm/fleet/v4/server/platform/errors"
"github.com/go-sql-driver/mysql"
)
type NotFoundError struct {
ID uint
FleetID uint
Name string
Message string
ResourceType string
}
// Compile-time interface check.
var _ platform_errors.NotFoundError = &NotFoundError{}
func NotFound(kind string) *NotFoundError {
return &NotFoundError{
ResourceType: kind,
}
}
func (e *NotFoundError) Error() string {
if e.ID != 0 {
return fmt.Sprintf("%s %d was not found in the datastore", e.ResourceType, e.ID)
}
if e.FleetID != 0 {
return fmt.Sprintf("%s for fleet %d was not found in the datastore", e.ResourceType, e.FleetID)
}
if e.Name != "" {
return fmt.Sprintf("%s %s was not found in the datastore", e.ResourceType, e.Name)
}
if e.Message != "" {
return fmt.Sprintf("%s %s was not found in the datastore", e.ResourceType, e.Message)
}
return fmt.Sprintf("%s was not found in the datastore", e.ResourceType)
}
func (e *NotFoundError) WithID(id uint) error {
e.ID = id
return e
}
func (e *NotFoundError) WithFleetID(fleetID uint) error {
e.FleetID = fleetID
return e
}
func (e *NotFoundError) WithName(name string) *NotFoundError {
e.Name = name
return e
}
func (e *NotFoundError) WithMessage(msg string) error {
e.Message = msg
return e
}
func (e *NotFoundError) IsNotFound() bool {
return true
}
// IsClientError implements ErrWithIsClientError.
func (e *NotFoundError) IsClientError() bool {
return true
}
// Is helps so that errors.Is(err, sql.ErrNoRows) returns true for an
// error of type *NotFoundError, without having to wrap sql.ErrNoRows
// explicitly.
func (e *NotFoundError) Is(other error) bool {
return other == sql.ErrNoRows
}
// MySQL error numbers for read-only conditions. These are not included in the
// VividCortex/mysqlerr package, so we define them here.
const (
// erReadOnlyTransaction is MySQL error 1792: Cannot execute statement in a READ ONLY transaction.
erReadOnlyTransaction = 1792
// erOptionPreventsStatement is MySQL error 1290: The MySQL server is running with the --read-only option.
erOptionPreventsStatement = 1290
// erReadOnlyMode is MySQL error 1836: Running in read-only mode.
erReadOnlyMode = 1836
)
// IsReadOnlyError returns true if the error is a MySQL error indicating that
// the server is in read-only mode. This typically happens after an Aurora
// failover when the primary has been demoted to a reader.
func IsReadOnlyError(err error) bool {
err = ctxerr.Cause(err)
var mySQLErr *mysql.MySQLError
if errors.As(err, &mySQLErr) {
switch mySQLErr.Number {
case erReadOnlyTransaction, erOptionPreventsStatement, erReadOnlyMode:
return true
}
}
return false
}