43962 vpp managed config migration (#44435)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #43962 Adds two tables: `vpp_app_configurations` and `in_house_app_configurations` `vpp_app_configurations` has `team_id` unsigned **not nullable**, rather than `team_id` nullable + `global_or_team_id`. This is following the pattern in `software_title_display_names` and `software_title_icons`, since software installers are team only and cannot be global. `android_app_configurations` uses team_id + global_or_team_id but that seems to be unnecessary. `in_house_app_configurations` keys on `in_house_app_id` only — the parent `in_house_apps` row already pins the team and platform. Both use MEDIUMTEXT to store the XML configuration. # Checklist for submitter If some of the following don't apply, delete the relevant line. ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Added database tables for storing VPP and in-house app configurations, organized by team/platform with automatic cleanup when parent apps are deleted. * **Tests** * Added migration tests to validate config storage fidelity, uniqueness and platform-specific constraints, foreign-key enforcement, and cascade-delete behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
+58
@@ -0,0 +1,58 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20260429180725, Down_20260429180725)
|
||||
}
|
||||
|
||||
func Up_20260429180725(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
CREATE TABLE vpp_app_configurations (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
application_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
team_id INT UNSIGNED NOT NULL,
|
||||
platform VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
configuration MEDIUMTEXT NOT NULL,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_vpp_app_config_team_app_platform (team_id, application_id, platform),
|
||||
CONSTRAINT fk_vpp_app_configurations_app
|
||||
FOREIGN KEY (application_id, platform)
|
||||
REFERENCES vpp_apps (adam_id, platform)
|
||||
ON DELETE CASCADE
|
||||
) DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create table vpp_app_configurations: %w", err)
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE in_house_app_configurations (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
in_house_app_id INT UNSIGNED NOT NULL,
|
||||
configuration MEDIUMTEXT NOT NULL,
|
||||
created_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
updated_at timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_in_house_app_config_app (in_house_app_id),
|
||||
CONSTRAINT fk_in_house_app_configurations_app
|
||||
FOREIGN KEY (in_house_app_id)
|
||||
REFERENCES in_house_apps (id)
|
||||
ON DELETE CASCADE
|
||||
) DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create table in_house_app_configurations: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Down_20260429180725(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20260429180725(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// Seed parent rows: same VPP adam_id on both platforms, and a separate
|
||||
// in_house_apps row per platform (in-house apps are platform-specific).
|
||||
const adamID = "1234567890"
|
||||
execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, ?)`, adamID, "ios")
|
||||
execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, ?)`, adamID, "ipados")
|
||||
iosAppID := execNoErrLastID(t, db, `INSERT INTO in_house_apps (filename, storage_id, platform) VALUES (?, ?, ?)`, "test.ipa", "abc123", "ios")
|
||||
ipadosAppID := execNoErrLastID(t, db, `INSERT INTO in_house_apps (filename, storage_id, platform) VALUES (?, ?, ?)`, "test.ipa", "def456", "ipados")
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
// Realistic plist managed-config payload (multi-line raw string).
|
||||
const iosPlist = `<dict>
|
||||
<key>ServerURL</key>
|
||||
<string>https://fleetdm.com</string>
|
||||
<key>EnableTelemetry</key>
|
||||
<true/>
|
||||
<key>MaxRetries</key>
|
||||
<integer>5</integer>
|
||||
</dict>`
|
||||
|
||||
// VPP: same adam_id can be configured for ios and ipados independently on the same team.
|
||||
execNoErr(t, db, `INSERT INTO vpp_app_configurations (application_id, team_id, platform, configuration) VALUES (?, ?, ?, ?)`,
|
||||
adamID, 1, "ios", iosPlist)
|
||||
execNoErr(t, db, `INSERT INTO vpp_app_configurations (application_id, team_id, platform, configuration) VALUES (?, ?, ?, ?)`,
|
||||
adamID, 1, "ipados", "<dict><key>platform</key><string>ipados</string></dict>")
|
||||
|
||||
// Round-trip the plist back out to make sure MEDIUMTEXT preserves it byte-for-byte
|
||||
// (whitespace, newlines, and angle brackets all intact).
|
||||
var got string
|
||||
require.NoError(t, db.Get(&got, `SELECT configuration FROM vpp_app_configurations WHERE application_id = ? AND team_id = ? AND platform = ?`,
|
||||
adamID, 1, "ios"))
|
||||
assert.Equal(t, iosPlist, got)
|
||||
|
||||
// VPP: same adam_id+platform on a different team is allowed.
|
||||
execNoErr(t, db, `INSERT INTO vpp_app_configurations (application_id, team_id, platform, configuration) VALUES (?, ?, ?, ?)`,
|
||||
adamID, 2, "ios", "<dict/>")
|
||||
|
||||
// In-house: each in_house_apps row (one per platform, already team-scoped) gets its own config.
|
||||
execNoErr(t, db, `INSERT INTO in_house_app_configurations (in_house_app_id, configuration) VALUES (?, ?)`,
|
||||
iosAppID, "<dict><key>platform</key><string>ios</string></dict>")
|
||||
execNoErr(t, db, `INSERT INTO in_house_app_configurations (in_house_app_id, configuration) VALUES (?, ?)`,
|
||||
ipadosAppID, "<dict><key>platform</key><string>ipados</string></dict>")
|
||||
|
||||
// VPP duplicate (team_id, application_id, platform) — must fail.
|
||||
_, err := db.Exec(`INSERT INTO vpp_app_configurations (application_id, team_id, platform, configuration) VALUES (?, ?, ?, ?)`,
|
||||
adamID, 1, "ios", "<dict/>")
|
||||
require.Error(t, err)
|
||||
|
||||
// In-house duplicate in_house_app_id — must fail.
|
||||
_, err = db.Exec(`INSERT INTO in_house_app_configurations (in_house_app_id, configuration) VALUES (?, ?)`,
|
||||
iosAppID, "<dict/>")
|
||||
require.Error(t, err)
|
||||
|
||||
// VPP composite FK rejects an adam_id that exists only for the other platform.
|
||||
execNoErr(t, db, `INSERT INTO vpp_apps (adam_id, platform) VALUES (?, ?)`, "iosonly", "ios")
|
||||
_, err = db.Exec(`INSERT INTO vpp_app_configurations (application_id, team_id, platform, configuration) VALUES (?, ?, ?, ?)`,
|
||||
"iosonly", 1, "ipados", "<dict/>")
|
||||
require.Error(t, err)
|
||||
|
||||
// VPP composite FK rejects an unknown adam_id.
|
||||
_, err = db.Exec(`INSERT INTO vpp_app_configurations (application_id, team_id, platform, configuration) VALUES (?, ?, ?, ?)`,
|
||||
"9999999999", 1, "ios", "<dict/>")
|
||||
require.Error(t, err)
|
||||
|
||||
// In-house FK rejects an unknown id.
|
||||
_, err = db.Exec(`INSERT INTO in_house_app_configurations (in_house_app_id, configuration) VALUES (?, ?)`,
|
||||
999999, "<dict/>")
|
||||
require.Error(t, err)
|
||||
|
||||
// Cascade: deleting only the iOS row from vpp_apps drops its config but leaves the iPadOS config intact.
|
||||
execNoErr(t, db, `DELETE FROM vpp_apps WHERE adam_id = ? AND platform = ?`, adamID, "ios")
|
||||
var vppCount int
|
||||
require.NoError(t, db.Get(&vppCount, `SELECT COUNT(*) FROM vpp_app_configurations WHERE application_id = ? AND platform = ?`, adamID, "ios"))
|
||||
assert.Equal(t, 0, vppCount)
|
||||
require.NoError(t, db.Get(&vppCount, `SELECT COUNT(*) FROM vpp_app_configurations WHERE application_id = ? AND platform = ?`, adamID, "ipados"))
|
||||
assert.Equal(t, 1, vppCount)
|
||||
|
||||
// Cascade: deleting only the iOS in_house_apps row drops its config but leaves iPadOS intact.
|
||||
execNoErr(t, db, `DELETE FROM in_house_apps WHERE id = ?`, iosAppID)
|
||||
var inHouseCount int
|
||||
require.NoError(t, db.Get(&inHouseCount, `SELECT COUNT(*) FROM in_house_app_configurations WHERE in_house_app_id = ?`, iosAppID))
|
||||
assert.Equal(t, 0, inHouseCount)
|
||||
require.NoError(t, db.Get(&inHouseCount, `SELECT COUNT(*) FROM in_house_app_configurations WHERE in_house_app_id = ?`, ipadosAppID))
|
||||
assert.Equal(t, 1, inHouseCount)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user