<!-- 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 -->
107 lines
2.8 KiB
Go
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
|
|
}
|