#34950 Cleanup nano refetch commands in the background (#42472)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34950

I changed from the original spec of 100 old commands to 3 due to load
test results. Admittedly my load test meant a very large number of hosts
all checked in and triggered deletion at once but at 100 per host and
per command the load was too high. 3 still results in cleanup over time
and doesn't seem to cause load issues.

# 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] 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] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Jordan Montgomery 2026-04-02 06:16:55 -04:00 committed by GitHub
parent b9a53136bf
commit ee3bfb759d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 377 additions and 0 deletions

View file

@ -0,0 +1 @@
* Updated iOS/iPadOS refetch logic to slowly clear out old/stale results

View file

@ -1205,6 +1205,9 @@ func newCleanupsAndAggregationSchedule(
} }
return nil return nil
}), }),
schedule.WithJob("cleanup_orphaned_nano_refetch_commands", func(ctx context.Context) error {
return ds.CleanupOrphanedNanoRefetchCommands(ctx)
}),
) )
return s, nil return s, nil

View file

@ -6843,6 +6843,126 @@ WHERE (
return nil return nil
} }
func (ds *Datastore) CleanupStaleNanoRefetchCommands(ctx context.Context, enrollmentID string, commandUUIDPrefix string, currentCommandUUID string) error {
// Step 1: Get up to 3 old command UUIDs from nano_enrollment_queue for this
// enrollment. The PK is (id, command_uuid) so filtering by id first is efficient,
// and the LIKE prefix on command_uuid narrows within that enrollment's entries.
// 3 may seem like too few but in load testing because of how often this runs it
// was found that larger numbers can cause too much contention
const selectOldCmds = `
SELECT command_uuid FROM nano_enrollment_queue
WHERE id = ?
AND command_uuid LIKE ?
AND command_uuid != ?
AND created_at < NOW() - INTERVAL 30 DAY
LIMIT 3`
var oldCmdUUIDs []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &oldCmdUUIDs, selectOldCmds, enrollmentID, commandUUIDPrefix+"%", currentCommandUUID); err != nil {
return ctxerr.Wrap(ctx, err, "select old nano refetch commands")
}
if len(oldCmdUUIDs) == 0 {
return nil
}
// Step 2: From ncr, find which of those command UUIDs have been acknowledged or
// errored for this enrollment. The PK (id, command_uuid) makes this efficient
// since we provide both id and the command_uuid IN list.
selectAckQuery, args, err := sqlx.In(`
SELECT command_uuid FROM nano_command_results
WHERE id = ? AND command_uuid IN (?) AND status IN ('Acknowledged', 'Error')`,
enrollmentID, oldCmdUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build IN query for nano command results")
}
var ackCmdUUIDs []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &ackCmdUUIDs, selectAckQuery, args...); err != nil {
return ctxerr.Wrap(ctx, err, "select acknowledged nano command results")
}
if len(ackCmdUUIDs) == 0 {
return nil
}
// Step 3: Delete from neq and ncr in the same transaction, scoped to this enrollment. We may not
// need to delete in a transaction here but it's the easiest way to ensure we cleanup ncr and further
// we absolutely must ensure we don't delete from ncr before deleting from neq to avoid command resends
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
deleteNEQ, delArgs, err := sqlx.In(
`DELETE FROM nano_enrollment_queue WHERE id = ? AND command_uuid IN (?)`,
enrollmentID, ackCmdUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build delete neq query")
}
if _, err := tx.ExecContext(ctx, deleteNEQ, delArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "delete stale nano enrollment queue entries")
}
deleteNCR, delArgs, err := sqlx.In(
`DELETE FROM nano_command_results WHERE id = ? AND command_uuid IN (?)`,
enrollmentID, ackCmdUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build delete ncr query")
}
if _, err := tx.ExecContext(ctx, deleteNCR, delArgs...); err != nil {
return ctxerr.Wrap(ctx, err, "delete stale nano command results entries")
}
return nil
})
}
func (ds *Datastore) CleanupOrphanedNanoRefetchCommands(ctx context.Context) error {
// Find up to 100 old REFETCH- commands. Note I am doing this as two queries since nano_commands
// can be large and I want to make sure in the case of a large number of active commands there's
// not going to be a full table scan or something happening. This is a best effort deletion so it
// is OK if our sample deletes nothing
const selectStmt = `
SELECT command_uuid FROM nano_commands nc
WHERE nc.command_uuid LIKE 'REFETCH-%'
AND nc.created_at < NOW() - INTERVAL 30 DAY
LIMIT 100`
var cmdUUIDs []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &cmdUUIDs, selectStmt); err != nil {
return ctxerr.Wrap(ctx, err, "get mdm apple refetch commands")
}
if len(cmdUUIDs) == 0 {
return nil
}
// Delete those that don't have a corresponding entry in nano_enrollment_queue
selectOrphanedCommandsStmt := `
SELECT command_uuid FROM nano_commands nc
WHERE nc.command_uuid IN (?) AND NOT EXISTS (
SELECT 1 FROM nano_enrollment_queue neq
WHERE neq.command_uuid = nc.command_uuid AND neq.active = 1
LIMIT 1
)`
selectOrphanedCommandsStmt, args, err := sqlx.In(selectOrphanedCommandsStmt, cmdUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build IN query for orphaned refetch commands")
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &cmdUUIDs, selectOrphanedCommandsStmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "get orphaned refetch commands")
}
if len(cmdUUIDs) == 0 {
return nil
}
deleteOrphanedCommandsStmt := `
DELETE FROM nano_commands
WHERE command_uuid IN (?)`
deleteOrphanedCommandsStmt, args, err = sqlx.In(deleteOrphanedCommandsStmt, cmdUUIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build IN query for deleting orphaned refetch commands")
}
if _, err := ds.writer(ctx).ExecContext(ctx, deleteOrphanedCommandsStmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete orphaned refetch commands")
}
return nil
}
func (ds *Datastore) GetMDMAppleOSUpdatesSettingsByHostSerial(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) { func (ds *Datastore) GetMDMAppleOSUpdatesSettingsByHostSerial(ctx context.Context, serial string) (string, *fleet.AppleOSUpdateSettings, error) {
stmt := ` stmt := `
SELECT SELECT

View file

@ -123,6 +123,8 @@ func TestMDMApple(t *testing.T) {
{"GetHostRecoveryLockPasswordStatus", testGetHostRecoveryLockPasswordStatus}, {"GetHostRecoveryLockPasswordStatus", testGetHostRecoveryLockPasswordStatus},
{"ClaimHostsForRecoveryLockClear", testClaimHostsForRecoveryLockClear}, {"ClaimHostsForRecoveryLockClear", testClaimHostsForRecoveryLockClear},
{"RecoveryLockRotation", testRecoveryLockRotation}, {"RecoveryLockRotation", testRecoveryLockRotation},
{"CleanupStaleNanoRefetchCommands", testCleanupStaleNanoRefetchCommands},
{"CleanupOrphanedNanoRefetchCommands", testCleanupOrphanedNanoRefetchCommands},
{"RecoveryLockAutoRotation", testRecoveryLockAutoRotation}, {"RecoveryLockAutoRotation", testRecoveryLockAutoRotation},
} }
@ -11392,6 +11394,202 @@ func testRecoveryLockRotation(t *testing.T, ds *Datastore) {
}) })
} }
func testCleanupStaleNanoRefetchCommands(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Create a host and enroll it in nano MDM.
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-cleanup-host",
OsqueryHostID: ptr.String("cleanup-osquery-id"),
NodeKey: ptr.String("cleanup-node-key"),
UUID: "cleanup-test-uuid",
Platform: "ios",
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
enrollmentID := host.UUID
// Helper to insert a nano command with a specific created_at.
insertNanoCmd := func(cmdUUID, reqType string, createdAt time.Time) {
_, err := ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command, created_at) VALUES (?, ?, '<?xml', ?)`,
cmdUUID, reqType, createdAt)
require.NoError(t, err)
}
// Helper to insert a nano_enrollment_queue entry.
insertNEQ := func(id, cmdUUID string, createdAt time.Time) {
_, err := ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_enrollment_queue (id, command_uuid, active, priority, created_at) VALUES (?, ?, 0, 0, ?)`,
id, cmdUUID, createdAt)
require.NoError(t, err)
}
// Helper to insert a nano_command_results entry.
insertNCR := func(id, cmdUUID, status string) {
_, err := ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_command_results (id, command_uuid, status, result) VALUES (?, ?, ?, '<?xml')`,
id, cmdUUID, status)
require.NoError(t, err)
}
now := time.Now()
oldTime := now.Add(-31 * 24 * time.Hour) // 31 days ago
recentTime := now.Add(-1 * 24 * time.Hour) // 1 day ago
// Create old REFETCH-APPS- command (should be cleaned up).
cmdUUID := "REFETCH-APPS-old-acknowledged"
insertNanoCmd(cmdUUID, "InstalledApplicationList", oldTime)
insertNEQ(enrollmentID, cmdUUID, oldTime)
insertNCR(enrollmentID, cmdUUID, "Acknowledged")
// Create an old REFETCH-APPS- command that has Error status (should also be cleaned up).
insertNanoCmd("REFETCH-APPS-old-error", "InstalledApplicationList", oldTime)
insertNEQ(enrollmentID, "REFETCH-APPS-old-error", oldTime)
insertNCR(enrollmentID, "REFETCH-APPS-old-error", "Error")
// Create an old REFETCH-APPS- command with no result (should NOT be cleaned up).
insertNanoCmd("REFETCH-APPS-old-noresult", "InstalledApplicationList", oldTime)
insertNEQ(enrollmentID, "REFETCH-APPS-old-noresult", oldTime)
// Create a recent REFETCH-APPS- command (should NOT be cleaned up).
insertNanoCmd("REFETCH-APPS-recent", "InstalledApplicationList", recentTime)
insertNEQ(enrollmentID, "REFETCH-APPS-recent", recentTime)
insertNCR(enrollmentID, "REFETCH-APPS-recent", "Acknowledged")
// Create old REFETCH-DEVICE- commands (different prefix, should NOT be affected by APPS cleanup).
insertNanoCmd("REFETCH-DEVICE-old-0", "DeviceInformation", oldTime)
insertNEQ(enrollmentID, "REFETCH-DEVICE-old-0", oldTime)
insertNCR(enrollmentID, "REFETCH-DEVICE-old-0", "Acknowledged")
// The "current" command that triggered the cleanup.
currentCmdUUID := "REFETCH-APPS-current"
insertNanoCmd(currentCmdUUID, "InstalledApplicationList", now)
insertNEQ(enrollmentID, currentCmdUUID, now)
insertNCR(enrollmentID, currentCmdUUID, "Acknowledged")
// Run cleanup for REFETCH-APPS- prefix, scoped to this enrollment.
err = ds.CleanupStaleNanoRefetchCommands(ctx, enrollmentID, fleet.RefetchAppsCommandUUIDPrefix, currentCmdUUID)
require.NoError(t, err)
// Verify: old acknowledged/errored REFETCH-APPS- entries should be deleted from neq and ncr.
var neqCount int
err = sqlx.GetContext(ctx, ds.reader(ctx), &neqCount,
`SELECT COUNT(*) FROM nano_enrollment_queue WHERE command_uuid LIKE 'REFETCH-APPS-old-%'`)
require.NoError(t, err)
// Only the one with no result should remain.
assert.Equal(t, 1, neqCount, "only the no-result entry should remain in neq")
var ncrCount int
err = sqlx.GetContext(ctx, ds.reader(ctx), &ncrCount,
`SELECT COUNT(*) FROM nano_command_results WHERE command_uuid LIKE 'REFETCH-APPS-old-%'`)
require.NoError(t, err)
assert.Equal(t, 0, ncrCount, "all old acknowledged/errored ncr entries should be deleted")
// Verify: recent command should still exist.
err = sqlx.GetContext(ctx, ds.reader(ctx), &neqCount,
`SELECT COUNT(*) FROM nano_enrollment_queue WHERE command_uuid = 'REFETCH-APPS-recent'`)
require.NoError(t, err)
assert.Equal(t, 1, neqCount, "recent command should not be deleted")
// Verify: current command should still exist.
err = sqlx.GetContext(ctx, ds.reader(ctx), &neqCount,
`SELECT COUNT(*) FROM nano_enrollment_queue WHERE command_uuid = ?`, currentCmdUUID)
require.NoError(t, err)
assert.Equal(t, 1, neqCount, "current command should not be deleted")
// Verify: REFETCH-DEVICE- command should not be affected.
err = sqlx.GetContext(ctx, ds.reader(ctx), &neqCount,
`SELECT COUNT(*) FROM nano_enrollment_queue WHERE command_uuid = 'REFETCH-DEVICE-old-0'`)
require.NoError(t, err)
assert.Equal(t, 1, neqCount, "different prefix should not be affected")
}
func testCleanupOrphanedNanoRefetchCommands(t *testing.T, ds *Datastore) {
ctx := t.Context()
// Create a host and enroll it for FK constraints.
host, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "test-orphan-host",
OsqueryHostID: ptr.String("orphan-osquery-id"),
NodeKey: ptr.String("orphan-node-key"),
UUID: "orphan-test-uuid",
Platform: "ios",
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
})
require.NoError(t, err)
nanoEnroll(t, ds, host, false)
now := time.Now()
oldTime := now.Add(-31 * 24 * time.Hour)
recentTime := now.Add(-1 * 24 * time.Hour)
// Insert an old REFETCH command WITH a neq reference (should NOT be deleted).
_, err = ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command, created_at) VALUES (?, ?, '<?xml', ?)`,
"REFETCH-APPS-with-ref", "InstalledApplicationList", oldTime)
require.NoError(t, err)
_, err = ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_enrollment_queue (id, command_uuid, active, priority, created_at) VALUES (?, ?, 1, 0, ?)`,
host.UUID, "REFETCH-APPS-with-ref", oldTime)
require.NoError(t, err)
// Insert an old REFETCH command WITHOUT neq reference (should be deleted).
_, err = ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command, created_at) VALUES (?, ?, '<?xml', ?)`,
"REFETCH-APPS-orphan", "InstalledApplicationList", oldTime)
require.NoError(t, err)
// Insert a recent REFETCH command WITHOUT neq reference (should NOT be deleted - too new).
_, err = ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command, created_at) VALUES (?, ?, '<?xml', ?)`,
"REFETCH-APPS-recent-orphan", "InstalledApplicationList", recentTime)
require.NoError(t, err)
// Insert an old non-REFETCH command WITHOUT neq reference (should NOT be deleted - wrong prefix).
_, err = ds.writer(ctx).ExecContext(ctx,
`INSERT INTO nano_commands (command_uuid, request_type, command, created_at) VALUES (?, ?, '<?xml', ?)`,
"OTHER-CMD-orphan", "ProfileList", oldTime)
require.NoError(t, err)
// Run cleanup.
err = ds.CleanupOrphanedNanoRefetchCommands(ctx)
require.NoError(t, err)
// Verify: old orphaned REFETCH command should be gone.
var count int
err = sqlx.GetContext(ctx, ds.reader(ctx), &count,
`SELECT COUNT(*) FROM nano_commands WHERE command_uuid = 'REFETCH-APPS-orphan'`)
require.NoError(t, err)
assert.Equal(t, 0, count, "old orphaned REFETCH command should be deleted")
// Verify: old REFETCH command with reference should still exist.
err = sqlx.GetContext(ctx, ds.reader(ctx), &count,
`SELECT COUNT(*) FROM nano_commands WHERE command_uuid = 'REFETCH-APPS-with-ref'`)
require.NoError(t, err)
assert.Equal(t, 1, count, "REFETCH command with neq reference should not be deleted")
// Verify: recent orphaned REFETCH command should still exist.
err = sqlx.GetContext(ctx, ds.reader(ctx), &count,
`SELECT COUNT(*) FROM nano_commands WHERE command_uuid = 'REFETCH-APPS-recent-orphan'`)
require.NoError(t, err)
assert.Equal(t, 1, count, "recent orphaned REFETCH command should not be deleted")
// Verify: non-REFETCH command should still exist.
err = sqlx.GetContext(ctx, ds.reader(ctx), &count,
`SELECT COUNT(*) FROM nano_commands WHERE command_uuid = 'OTHER-CMD-orphan'`)
require.NoError(t, err)
assert.Equal(t, 1, count, "non-REFETCH command should not be deleted")
}
func testRecoveryLockAutoRotation(t *testing.T, ds *Datastore) { func testRecoveryLockAutoRotation(t *testing.T, ds *Datastore) {
ctx := t.Context() ctx := t.Context()

View file

@ -421,6 +421,16 @@ type Datastore interface {
// CleanupHostMDMAppleProfiles removes abandoned host MDM Apple profiles entries. // CleanupHostMDMAppleProfiles removes abandoned host MDM Apple profiles entries.
CleanupHostMDMAppleProfiles(ctx context.Context) error CleanupHostMDMAppleProfiles(ctx context.Context) error
// CleanupStaleNanoRefetchCommands deletes up to 3 nano_enrollment_queue and
// their corresponding nano_command_results entries for the given enrollment ID
// and REFETCH command prefix type that were sent and acknowledged/errored at
// least 30 days ago. The current command UUID is excluded from deletion.
CleanupStaleNanoRefetchCommands(ctx context.Context, enrollmentID string, commandUUIDPrefix string, currentCommandUUID string) error
// CleanupOrphanedNanoRefetchCommands deletes up to 100 REFETCH-prefixed nano_commands
// older than 30 days that have no remaining references in nano_enrollment_queue.
CleanupOrphanedNanoRefetchCommands(ctx context.Context) error
// IsHostConnectedToFleetMDM verifies if the host has an active Fleet MDM enrollment with this server // IsHostConnectedToFleetMDM verifies if the host has an active Fleet MDM enrollment with this server
IsHostConnectedToFleetMDM(ctx context.Context, host *Host) (bool, error) IsHostConnectedToFleetMDM(ctx context.Context, host *Host) (bool, error)

View file

@ -325,6 +325,10 @@ type CleanupHostMDMCommandsFunc func(ctx context.Context) error
type CleanupHostMDMAppleProfilesFunc func(ctx context.Context) error type CleanupHostMDMAppleProfilesFunc func(ctx context.Context) error
type CleanupStaleNanoRefetchCommandsFunc func(ctx context.Context, enrollmentID string, commandUUIDPrefix string, currentCommandUUID string) error
type CleanupOrphanedNanoRefetchCommandsFunc func(ctx context.Context) error
type IsHostConnectedToFleetMDMFunc func(ctx context.Context, host *fleet.Host) (bool, error) type IsHostConnectedToFleetMDMFunc func(ctx context.Context, host *fleet.Host) (bool, error)
type ListHostCertificatesFunc func(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificateRecord, *fleet.PaginationMetadata, error) type ListHostCertificatesFunc func(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificateRecord, *fleet.PaginationMetadata, error)
@ -2301,6 +2305,12 @@ type DataStore struct {
CleanupHostMDMAppleProfilesFunc CleanupHostMDMAppleProfilesFunc CleanupHostMDMAppleProfilesFunc CleanupHostMDMAppleProfilesFunc
CleanupHostMDMAppleProfilesFuncInvoked bool CleanupHostMDMAppleProfilesFuncInvoked bool
CleanupStaleNanoRefetchCommandsFunc CleanupStaleNanoRefetchCommandsFunc
CleanupStaleNanoRefetchCommandsFuncInvoked bool
CleanupOrphanedNanoRefetchCommandsFunc CleanupOrphanedNanoRefetchCommandsFunc
CleanupOrphanedNanoRefetchCommandsFuncInvoked bool
IsHostConnectedToFleetMDMFunc IsHostConnectedToFleetMDMFunc IsHostConnectedToFleetMDMFunc IsHostConnectedToFleetMDMFunc
IsHostConnectedToFleetMDMFuncInvoked bool IsHostConnectedToFleetMDMFuncInvoked bool
@ -5644,6 +5654,20 @@ func (s *DataStore) CleanupHostMDMAppleProfiles(ctx context.Context) error {
return s.CleanupHostMDMAppleProfilesFunc(ctx) return s.CleanupHostMDMAppleProfilesFunc(ctx)
} }
func (s *DataStore) CleanupStaleNanoRefetchCommands(ctx context.Context, enrollmentID string, commandUUIDPrefix string, currentCommandUUID string) error {
s.mu.Lock()
s.CleanupStaleNanoRefetchCommandsFuncInvoked = true
s.mu.Unlock()
return s.CleanupStaleNanoRefetchCommandsFunc(ctx, enrollmentID, commandUUIDPrefix, currentCommandUUID)
}
func (s *DataStore) CleanupOrphanedNanoRefetchCommands(ctx context.Context) error {
s.mu.Lock()
s.CleanupOrphanedNanoRefetchCommandsFuncInvoked = true
s.mu.Unlock()
return s.CleanupOrphanedNanoRefetchCommandsFunc(ctx)
}
func (s *DataStore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) { func (s *DataStore) IsHostConnectedToFleetMDM(ctx context.Context, host *fleet.Host) (bool, error) {
s.mu.Lock() s.mu.Lock()
s.IsHostConnectedToFleetMDMFuncInvoked = true s.IsHostConnectedToFleetMDMFuncInvoked = true

View file

@ -4024,6 +4024,11 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchAppsResults(ctx contex
} }
} }
// Best-effort cleanup of stale refetch commands of the same type.
if err := svc.ds.CleanupStaleNanoRefetchCommands(ctx, host.UUID, fleet.RefetchAppsCommandUUIDPrefix, cmdResult.CommandUUID); err != nil {
svc.logger.ErrorContext(ctx, "cleanup stale nano refetch apps commands", "err", err, "host_uuid", host.UUID, "command_prefix", fleet.RefetchAppsCommandUUIDPrefix)
}
return nil, nil return nil, nil
} }
@ -4552,6 +4557,11 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchCertsResults(ctx conte
return nil, ctxerr.Wrap(ctx, err, "refetch certs: update host certificates") return nil, ctxerr.Wrap(ctx, err, "refetch certs: update host certificates")
} }
// Best-effort cleanup of stale refetch commands of the same type.
if err := svc.ds.CleanupStaleNanoRefetchCommands(ctx, host.UUID, fleet.RefetchCertsCommandUUIDPrefix, cmdResult.CommandUUID); err != nil {
svc.logger.ErrorContext(ctx, "cleanup stale nano refetch certs commands", "err", err, "host_uuid", host.UUID, "command_prefix", fleet.RefetchCertsCommandUUIDPrefix)
}
return nil, nil return nil, nil
} }
@ -4660,6 +4670,11 @@ func (svc *MDMAppleCheckinAndCommandService) handleRefetchDeviceResults(ctx cont
} }
} }
// Best-effort cleanup of stale refetch commands of the same type.
if err := svc.ds.CleanupStaleNanoRefetchCommands(ctx, host.UUID, fleet.RefetchDeviceCommandUUIDPrefix, cmdResult.CommandUUID); err != nil {
svc.logger.ErrorContext(ctx, "cleanup stale nano refetch device commands", "err", err, "host_uuid", host.UUID, "command_prefix", fleet.RefetchDeviceCommandUUIDPrefix)
}
return nil, nil return nil, nil
} }

View file

@ -4924,6 +4924,12 @@ func TestMDMCommandAndReportResultsIOSIPadOSRefetch(t *testing.T) {
require.Equal(t, lostModeCommandUUID, commandUUID) require.Equal(t, lostModeCommandUUID, commandUUID)
return nil return nil
} }
ds.CleanupStaleNanoRefetchCommandsFunc = func(ctx context.Context, enrollmentID string, commandUUIDPrefix string, currentCommandUUID string) error {
require.Equal(t, hostUUID, enrollmentID)
require.Equal(t, fleet.RefetchDeviceCommandUUIDPrefix, commandUUIDPrefix)
require.Equal(t, commandUUID, currentCommandUUID)
return nil
}
_, err := svc.CommandAndReportResults( _, err := svc.CommandAndReportResults(
&mdm.Request{Context: ctx}, &mdm.Request{Context: ctx},