-
Notifications
You must be signed in to change notification settings - Fork 453
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixing issue where deleted profiles were being sent to devices. #25095
Changes from all commits
ef36e13
dbb362a
a222c38
60ba883
e7915f4
edec508
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Fixed issue where deleted Apple config profiles were installing on devices because devices were offline when the profile was added. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package common_mysql | ||
|
||
// BatchProcessSimple is a simple utility function to batch process a slice of payloads. | ||
// Provide a slice of payloads, a batch size, and a function to execute on each batch. | ||
func BatchProcessSimple[T any]( | ||
payloads []T, | ||
batchSize int, | ||
executeBatch func(payloadsInThisBatch []T) error, | ||
) error { | ||
if len(payloads) == 0 || batchSize <= 0 || executeBatch == nil { | ||
return nil | ||
} | ||
|
||
for i := 0; i < len(payloads); i += batchSize { | ||
start := i | ||
end := i + batchSize | ||
if end > len(payloads) { | ||
end = len(payloads) | ||
} | ||
if err := executeBatch(payloads[start:end]); err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package common_mysql | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestBatchProcessSimple(t *testing.T) { | ||
payloads := []int{1, 2, 3, 4, 5} | ||
executeBatch := func(payloadsInThisBatch []int) error { | ||
t.Fatal("executeBatch should not be called") | ||
return nil | ||
} | ||
|
||
// No payloads | ||
err := BatchProcessSimple(nil, 10, executeBatch) | ||
assert.NoError(t, err) | ||
|
||
// No batch size | ||
err = BatchProcessSimple(payloads, 0, executeBatch) | ||
assert.NoError(t, err) | ||
|
||
// No executeBatch | ||
err = BatchProcessSimple(payloads, 10, nil) | ||
assert.NoError(t, err) | ||
|
||
// Large batch size -- all payloads executed in one batch | ||
executeBatch = func(payloadsInThisBatch []int) error { | ||
assert.Equal(t, payloads, payloadsInThisBatch) | ||
return nil | ||
} | ||
err = BatchProcessSimple(payloads, 10, executeBatch) | ||
assert.NoError(t, err) | ||
|
||
// Small batch size | ||
numCalls := 0 | ||
executeBatch = func(payloadsInThisBatch []int) error { | ||
numCalls++ | ||
switch numCalls { | ||
case 1: | ||
assert.Equal(t, []int{1, 2, 3}, payloadsInThisBatch) | ||
case 2: | ||
assert.Equal(t, []int{4, 5}, payloadsInThisBatch) | ||
default: | ||
t.Errorf("Unexpected number of calls to executeBatch: %d", numCalls) | ||
} | ||
return nil | ||
} | ||
err = BatchProcessSimple(payloads, 3, executeBatch) | ||
assert.NoError(t, err) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package tables | ||
|
||
import ( | ||
"database/sql" | ||
"fmt" | ||
) | ||
|
||
func init() { | ||
MigrationClient.AddMigration(Up_20250102121439, Down_20250102121439) | ||
} | ||
|
||
func Up_20250102121439(tx *sql.Tx) error { | ||
_, err := tx.Exec(`ALTER TABLE host_mdm_apple_profiles | ||
ADD COLUMN ignore_error TINYINT(1) NOT NULL DEFAULT 0`) | ||
if err != nil { | ||
return fmt.Errorf("failed to add ignore_error to host_mdm_apple_profiles table: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
func Down_20250102121439(_ *sql.Tx) error { | ||
return nil | ||
} |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import ( | |
"fmt" | ||
"strings" | ||
|
||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr" | ||
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql" | ||
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm" | ||
"github.com/google/uuid" | ||
|
@@ -260,3 +261,54 @@ WHERE | |
) | ||
return err | ||
} | ||
|
||
// BulkDeleteHostUserCommandsWithoutResults deletes all commands without results for the given host/user IDs. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is for the given "host/ command IDs", right? I see you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just future-proofing. The nano ID is either a device ID or user ID. I assume we will support user enrollments in the future. |
||
// This is used to clean up the queue when a profile is deleted from Fleet. | ||
func (m *MySQLStorage) BulkDeleteHostUserCommandsWithoutResults(ctx context.Context, commandToIDs map[string][]string) error { | ||
if len(commandToIDs) == 0 { | ||
return nil | ||
} | ||
return common_mysql.WithRetryTxx(ctx, sqlx.NewDb(m.db, ""), func(tx sqlx.ExtContext) error { | ||
return m.bulkDeleteHostUserCommandsWithoutResults(ctx, tx, commandToIDs) | ||
}, loggerWrapper{m.logger}) | ||
} | ||
|
||
func (m *MySQLStorage) bulkDeleteHostUserCommandsWithoutResults(ctx context.Context, tx sqlx.ExtContext, | ||
commandToIDs map[string][]string) error { | ||
stmt := ` | ||
DELETE | ||
eq | ||
FROM | ||
nano_enrollment_queue AS eq | ||
LEFT JOIN nano_command_results AS cr | ||
ON cr.command_uuid = eq.command_uuid AND cr.id = eq.id | ||
WHERE | ||
cr.command_uuid IS NULL AND eq.command_uuid = ? AND eq.id IN (?);` | ||
|
||
// We process each commandUUID one at a time, in batches of hostUserIDs. | ||
// This is because the number of hostUserIDs can be large, and number of unique commands is normally small. | ||
// If we have a use case where each host has a unique command, we can create a separate method for that use case. | ||
for commandUUID, hostUserIDs := range commandToIDs { | ||
if len(hostUserIDs) == 0 { | ||
continue | ||
} | ||
|
||
batchSize := 10000 | ||
err := common_mysql.BatchProcessSimple(hostUserIDs, batchSize, func(hostUserIDsToProcess []string) error { | ||
expanded, args, err := sqlx.In(stmt, commandUUID, hostUserIDsToProcess) | ||
if err != nil { | ||
return ctxerr.Wrap(ctx, err, "expanding bulk delete nano commands") | ||
} | ||
_, err = tx.ExecContext(ctx, expanded, args...) | ||
if err != nil { | ||
return ctxerr.Wrap(ctx, err, "bulk delete nano commands") | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
|
||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉