Repository navigation
47713 auld software update assets migration #50036
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
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b0a8666
Add AULD migrations: apple_software_update_assets host_mdm_apple_os_u…
andymFleet 9db4f2f
Expand AULD migration test coverage for constraints and defaults
andymFleet 375314e
Assert first_seen_at is preserved and updated_at advances on asset up…
andymFleet 4cc6155
Merge branch 'main' into 47713-auld-software-update-assets-migration
andymFleet fb04cb3
Merge branch 'main' into 47713-auld-software-update-assets-migration
andymFleet d7fe099
Merge branch 'main' into 47713-auld-software-update-assets-migration
andymFleet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
62 changes: 62 additions & 0 deletions
62
...mysql/migrations/tables/20260727083533_CreateAppleSoftwareUpdateAssetsAndHostOSUpdates.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package tables | ||
|
|
||
| import ( | ||
| "database/sql" | ||
| "fmt" | ||
| ) | ||
|
|
||
| func init() { | ||
| MigrationClient.AddMigration(Up_20260727083533, Down_20260727083533) | ||
| } | ||
|
|
||
| func Up_20260727083533(tx *sql.Tx) error { | ||
| // apple_software_update_assets caches the set of available OS update | ||
| // versions Apple's GDMF service reports for macOS/iOS, refreshed by a | ||
| // periodic cron. first_seen_at is only set on insert; updated_at advances | ||
| // on every successful fetch even when the version set is unchanged. | ||
| _, err := tx.Exec(` | ||
| CREATE TABLE apple_software_update_assets ( | ||
| id INT UNSIGNED NOT NULL AUTO_INCREMENT, | ||
| class enum('macos','ios') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, | ||
| product_version varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, | ||
| build varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', | ||
| posting_date date DEFAULT NULL, | ||
| expiration_date date DEFAULT NULL, | ||
| supported_devices json NOT NULL, | ||
| first_seen_at datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), | ||
| created_at datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), | ||
| updated_at datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), | ||
| PRIMARY KEY (id), | ||
| UNIQUE KEY idx_asset_class_version_build (class, product_version, build) | ||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci | ||
| `) | ||
| if err != nil { | ||
| return fmt.Errorf("creating apple_software_update_assets table: %w", err) | ||
| } | ||
|
|
||
| // host_mdm_apple_os_updates tracks, per host, the resolved target OS | ||
| // version/deadline when automatic enforcement is set to "latest". | ||
| // resolved_at is set once the target has been recomputed for the host's | ||
| // current team/setting; target_deadline is nullable until resolved. | ||
| _, err = tx.Exec(` | ||
| CREATE TABLE host_mdm_apple_os_updates ( | ||
| host_uuid varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, | ||
| software_update_device_id varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', | ||
| target_os_version varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', | ||
| target_deadline datetime(6) DEFAULT NULL, | ||
| resolved_at datetime(6) DEFAULT NULL, | ||
| created_at datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), | ||
| updated_at datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), | ||
| PRIMARY KEY (host_uuid) | ||
| ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci | ||
| `) | ||
| if err != nil { | ||
| return fmt.Errorf("creating host_mdm_apple_os_updates table: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func Down_20260727083533(tx *sql.Tx) error { | ||
| return nil | ||
| } | ||
143 changes: 143 additions & 0 deletions
143
.../migrations/tables/20260727083533_CreateAppleSoftwareUpdateAssetsAndHostOSUpdates_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| package tables | ||
|
|
||
| import ( | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestUp_20260727083533(t *testing.T) { | ||
| db := applyUpToPrev(t) | ||
|
|
||
| // Apply current migration. | ||
| applyNext(t, db) | ||
|
|
||
| // apple_software_update_assets: insert a row, uniqueness on | ||
| // (class, product_version, build). | ||
| assetID := execNoErrLastID(t, db, ` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, build, posting_date, expiration_date, supported_devices) | ||
| VALUES | ||
| ('macos', '15.1', '24B83', '2026-01-01', NULL, '["J123AP"]')`) | ||
| require.NotZero(t, assetID) | ||
|
|
||
| // expiration_date is nullable; first_seen_at and updated_at are populated by | ||
| // their defaults. | ||
| var ( | ||
| gotExpirationDate *time.Time | ||
| gotFirstSeenAt time.Time | ||
| gotUpdatedAt time.Time | ||
| ) | ||
| err := db.QueryRow(` | ||
| SELECT expiration_date, first_seen_at, updated_at | ||
| FROM apple_software_update_assets WHERE id = ?`, assetID, | ||
| ).Scan(&gotExpirationDate, &gotFirstSeenAt, &gotUpdatedAt) | ||
| require.NoError(t, err) | ||
| require.Nil(t, gotExpirationDate) | ||
| require.False(t, gotFirstSeenAt.IsZero()) | ||
| require.False(t, gotUpdatedAt.IsZero()) | ||
|
|
||
| _, err = db.Exec(` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, build, supported_devices) | ||
| VALUES | ||
| ('macos', '15.1', '24B83', '["J123AP"]')`) | ||
| require.Error(t, err, "duplicate (class, product_version, build) should be rejected") | ||
|
|
||
| // An upsert on the same (class, product_version, build) — the shape the GDMF | ||
| // refresh uses — keeps first_seen_at from the original insert while | ||
| // updated_at advances. posting_date is changed so the row is a real update: | ||
| // MySQL leaves updated_at alone when no column value actually changes. | ||
| execNoErr(t, db, ` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, build, posting_date, supported_devices) | ||
| VALUES | ||
| ('macos', '15.1', '24B83', '2026-01-02', '["J123AP","J456AP"]') | ||
| ON DUPLICATE KEY UPDATE | ||
| posting_date = VALUES(posting_date), | ||
| supported_devices = VALUES(supported_devices)`) | ||
|
|
||
| var ( | ||
| gotFirstSeenAtAfterUpsert time.Time | ||
| gotUpdatedAtAfterUpsert time.Time | ||
| ) | ||
| err = db.QueryRow(` | ||
| SELECT first_seen_at, updated_at | ||
| FROM apple_software_update_assets WHERE id = ?`, assetID, | ||
| ).Scan(&gotFirstSeenAtAfterUpsert, &gotUpdatedAtAfterUpsert) | ||
| require.NoError(t, err) | ||
| require.True(t, gotFirstSeenAt.Equal(gotFirstSeenAtAfterUpsert), | ||
| "first_seen_at must not change on upsert") | ||
| require.True(t, gotUpdatedAtAfterUpsert.After(gotUpdatedAt), | ||
| "updated_at must advance on upsert") | ||
|
|
||
| // A different build for the same class/version is allowed. | ||
| execNoErr(t, db, ` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, build, supported_devices) | ||
| VALUES | ||
| ('macos', '15.1', '24B84', '["J123AP"]')`) | ||
|
|
||
| // supported_devices is NOT NULL. | ||
| _, err = db.Exec(` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, build, supported_devices) | ||
| VALUES | ||
| ('ios', '18.1', '', NULL)`) | ||
| require.Error(t, err, "NULL supported_devices should be rejected") | ||
|
|
||
| // Invalid class tvos is rejected by the ENUM. | ||
| _, err = db.Exec(` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, build, supported_devices) | ||
| VALUES | ||
| ('tvos', '18.1', '22J1', '["J123AP"]')`) | ||
| require.Error(t, err, "class outside the enum should be rejected") | ||
|
|
||
| // build defaults to '' when omitted. | ||
| iosAssetID := execNoErrLastID(t, db, ` | ||
| INSERT INTO apple_software_update_assets | ||
| (class, product_version, supported_devices) | ||
| VALUES | ||
| ('ios', '18.1', '["iPhone16,1"]')`) | ||
|
|
||
| var gotBuild string | ||
| err = db.QueryRow(` | ||
| SELECT build FROM apple_software_update_assets WHERE id = ?`, iosAssetID, | ||
| ).Scan(&gotBuild) | ||
| require.NoError(t, err) | ||
| require.Empty(t, gotBuild) | ||
|
|
||
| // host_mdm_apple_os_updates: keyed by host_uuid, defaults apply. | ||
| execNoErr(t, db, ` | ||
| INSERT INTO host_mdm_apple_os_updates (host_uuid) | ||
| VALUES ('host-1-uuid')`) | ||
|
|
||
| // Scanning the two string columns into non-pointers also asserts they | ||
| // default to '' rather than NULL; target_deadline and resolved_at are | ||
| // nullable until the target is resolved for the host. | ||
| var ( | ||
| gotTargetOSVersion string | ||
| gotSoftwareUpdateDeviceID string | ||
| gotTargetDeadline *time.Time | ||
| gotResolvedAt *time.Time | ||
| gotCreatedAt time.Time | ||
| ) | ||
| err = db.QueryRow(` | ||
| SELECT target_os_version, software_update_device_id, target_deadline, | ||
| resolved_at, created_at | ||
| FROM host_mdm_apple_os_updates WHERE host_uuid = 'host-1-uuid'`, | ||
| ).Scan(&gotTargetOSVersion, &gotSoftwareUpdateDeviceID, &gotTargetDeadline, &gotResolvedAt, &gotCreatedAt) | ||
| require.NoError(t, err) | ||
| require.Empty(t, gotTargetOSVersion) | ||
| require.Empty(t, gotSoftwareUpdateDeviceID) | ||
| require.Nil(t, gotTargetDeadline) | ||
| require.Nil(t, gotResolvedAt) | ||
| require.False(t, gotCreatedAt.IsZero()) | ||
|
|
||
| _, err = db.Exec(` | ||
| INSERT INTO host_mdm_apple_os_updates (host_uuid) | ||
| VALUES ('host-1-uuid')`) | ||
| require.Error(t, err, "duplicate host_uuid should be rejected") | ||
| } |
38 changes: 38 additions & 0 deletions
38
...tore/mysql/migrations/tables/20260727084359_AddHostTargetOSVersionAndDeadlineFleetVars.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package tables | ||
|
|
||
| import ( | ||
| "database/sql" | ||
| "fmt" | ||
| "time" | ||
|
|
||
| "github.com/jmoiron/sqlx" | ||
| ) | ||
|
|
||
| func init() { | ||
| MigrationClient.AddMigration(Up_20260727084359, Down_20260727084359) | ||
| } | ||
|
|
||
| func Up_20260727084359(tx *sql.Tx) error { | ||
| insStmt := ` | ||
| INSERT INTO fleet_variables ( | ||
| name, is_prefix, created_at | ||
| ) VALUES | ||
| ('FLEET_VAR_HOST_TARGET_OS_VERSION', 0, :created_at), | ||
| ('FLEET_VAR_HOST_TARGET_OS_DEADLINE', 0, :created_at) | ||
| ` | ||
| // use a constant time so that the generated schema is deterministic | ||
| createdAt := time.Date(2026, 7, 27, 0, 0, 0, 0, time.UTC) | ||
| stmt, args, err := sqlx.Named(insStmt, map[string]any{"created_at": createdAt}) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to prepare insert for FLEET_VAR_HOST_TARGET_OS_VERSION/DEADLINE: %w", err) | ||
| } | ||
| _, err = tx.Exec(stmt, args...) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to insert FLEET_VAR_HOST_TARGET_OS_VERSION/DEADLINE into fleet_variables: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func Down_20260727084359(tx *sql.Tx) error { | ||
| return nil | ||
| } |
31 changes: 31 additions & 0 deletions
31
...mysql/migrations/tables/20260727084359_AddHostTargetOSVersionAndDeadlineFleetVars_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package tables | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestUp_20260727084359(t *testing.T) { | ||
| db := applyUpToPrev(t) | ||
|
|
||
| var count int | ||
| err := db.Get(&count, `SELECT COUNT(*) FROM fleet_variables WHERE name IN ('FLEET_VAR_HOST_TARGET_OS_VERSION', 'FLEET_VAR_HOST_TARGET_OS_DEADLINE')`) | ||
| require.NoError(t, err) | ||
| require.Equal(t, 0, count) | ||
|
|
||
| applyNext(t, db) | ||
|
|
||
| err = db.Get(&count, `SELECT COUNT(*) FROM fleet_variables WHERE name IN ('FLEET_VAR_HOST_TARGET_OS_VERSION', 'FLEET_VAR_HOST_TARGET_OS_DEADLINE')`) | ||
| require.NoError(t, err) | ||
| require.Equal(t, 2, count) | ||
|
|
||
| var isPrefix bool | ||
| err = db.Get(&isPrefix, `SELECT is_prefix FROM fleet_variables WHERE name = 'FLEET_VAR_HOST_TARGET_OS_VERSION'`) | ||
| require.NoError(t, err) | ||
| require.False(t, isPrefix) | ||
|
|
||
| err = db.Get(&isPrefix, `SELECT is_prefix FROM fleet_variables WHERE name = 'FLEET_VAR_HOST_TARGET_OS_DEADLINE'`) | ||
| require.NoError(t, err) | ||
| require.False(t, isPrefix) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.