Skip to content

Commit 58158c7

Browse files
committed
fix(pg): let the below-marker backstop admit declared porting wrappers
The first Phase-4 prod migration was refused by checkPGBelowMarkerDrift: prod's history has no rows for the two back-dated upstream migrations, and the check runs before the goose Up that would execute their porting wrapper. Local runs never hit this — fresh DBs seed history through the marker. PortedBelowMarker (declared next to the wrapper) maps each ported version to its wrapper; the check now exempts a version whose wrapper is either already applied or registered above the DB max (so the imminent goose Up runs it). Anything else below the max still fails. The wrapper also records the ported versions in goose history so steady state needs no exemption. New TestPostgresPortBackdatedWrapper executes the wrapper's PG path against a prod-shaped DB (DDL absent, history rows missing) — without it that path would first run in production — plus exemption cases in the drift test. Verified end-to-end: a DB reset to prod's exact pre-port state now runs prepare db to completion (tables, vars, and history all land). Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
1 parent 694aa2c commit 58158c7

3 files changed

Lines changed: 107 additions & 2 deletions

File tree

server/datastore/mysql/migrations/tables/20260729190000_PortBackdatedUpstreamMigrations.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ func init() {
99
MigrationClient.AddMigration(Up_20260729190000, Down_20260729190000)
1010
}
1111

12+
// PortedBelowMarker maps each back-dated upstream migration version to the
13+
// post-marker wrapper migration that ports its DDL on PG. The below-marker
14+
// drift backstop consults this so a deploy carrying the wrapper is allowed
15+
// through (the wrapper runs in the same goose Up); any other unapplied
16+
// below-marker version still fails the check.
17+
var PortedBelowMarker = map[int64]int64{
18+
20260727083533: 20260729190000,
19+
20260727084359: 20260729190000,
20+
}
21+
1222
// Up_20260729190000 ports upstream migrations 20260727083533 (apple software
1323
// update assets + host OS update tracking) and 20260727084359 (host target OS
1424
// version fleet vars) to existing PostgreSQL databases. Both are numbered
@@ -63,6 +73,18 @@ func Up_20260729190000(tx *sql.Tx) error {
6373
return err
6474
}
6575
}
76+
// Record the ported versions in goose's history so the below-marker
77+
// drift check is satisfied by real state on every later boot, not by
78+
// the PortedBelowMarker exemption (which only needs to cover the boot
79+
// that runs this wrapper).
80+
for _, v := range []int64{20260727083533, 20260727084359} {
81+
if _, err := tx.Exec(`
82+
INSERT INTO migration_status_tables (version_id, is_applied)
83+
SELECT ?, true
84+
WHERE NOT EXISTS (SELECT 1 FROM migration_status_tables WHERE version_id = ?)`, v, v); err != nil {
85+
return err
86+
}
87+
}
6688
return nil
6789
}
6890

server/datastore/mysql/mysql.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -795,11 +795,27 @@ func (ds *Datastore) checkPGBelowMarkerDrift(ctx context.Context, marker int64)
795795
dbMax = v
796796
}
797797
}
798+
registered := make(map[int64]struct{}, len(tables.MigrationClient.Migrations))
799+
for _, m := range tables.MigrationClient.Migrations {
800+
registered[m.Version] = struct{}{}
801+
}
798802
var missing []int64
799803
for _, m := range tables.MigrationClient.Migrations {
800-
if _, ok := appliedSet[m.Version]; !ok && m.Version < dbMax {
801-
missing = append(missing, m.Version)
804+
if _, ok := appliedSet[m.Version]; ok || m.Version >= dbMax {
805+
continue
806+
}
807+
// A back-dated migration with a declared porting wrapper is not
808+
// drift when that wrapper already ran, or is registered above the
809+
// DB's max version so the goose Up following this check runs it.
810+
if wrapper, ok := tables.PortedBelowMarker[m.Version]; ok {
811+
if _, done := appliedSet[wrapper]; done {
812+
continue
813+
}
814+
if _, reg := registered[wrapper]; reg && wrapper > dbMax {
815+
continue
816+
}
802817
}
818+
missing = append(missing, m.Version)
803819
}
804820
if len(missing) == 0 {
805821
return nil

server/datastore/mysql/postgres_smoke_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88
"time"
99

10+
"github.com/fleetdm/fleet/v4/server/datastore/mysql/migrations/tables"
1011
"github.com/fleetdm/fleet/v4/server/fleet"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
@@ -667,6 +668,72 @@ func TestPostgresBelowMarkerDriftCheck(t *testing.T) {
667668
err = ds.checkPGBelowMarkerDrift(ctx, marker)
668669
require.Error(t, err, "missing below-marker record must error")
669670
require.Contains(t, err.Error(), "PG migration drift")
671+
_, err = ds.primary.Exec(`INSERT INTO migration_status_tables (version_id, is_applied) VALUES ($1, true)`, victim)
672+
require.NoError(t, err)
673+
674+
// Back-dated versions with a declared porting wrapper are exempt — both
675+
// when the wrapper is already applied (steady state after the port ran)…
676+
require.NotEmpty(t, tables.PortedBelowMarker)
677+
var wrapper int64
678+
for ported, w := range tables.PortedBelowMarker {
679+
wrapper = w
680+
_, err = ds.primary.Exec(`DELETE FROM migration_status_tables WHERE version_id = $1`, ported)
681+
require.NoError(t, err)
682+
}
683+
require.NoError(t, ds.checkPGBelowMarkerDrift(ctx, marker),
684+
"ported back-dated versions with an applied wrapper are not drift")
685+
686+
// …and when the wrapper is itself pending above the DB max (the boot
687+
// that is about to run it — the exact state of the first Phase-4 prod
688+
// deploy, which the check aborted before this exemption existed).
689+
_, err = ds.primary.Exec(`DELETE FROM migration_status_tables WHERE version_id = $1`, wrapper)
690+
require.NoError(t, err)
691+
require.NoError(t, ds.checkPGBelowMarkerDrift(ctx, marker),
692+
"ported back-dated versions with a pending goose-reachable wrapper are not drift")
693+
}
694+
695+
// TestPostgresPortBackdatedWrapper executes Up_20260729190000's PG path
696+
// directly against a database shaped like prod before the port ran: the
697+
// back-dated upstream DDL absent and no history rows for the ported versions.
698+
// Fresh test DBs seed the wrapper as applied (its effects are in the
699+
// baseline), so without this test the wrapper's real code path would only
700+
// ever run for the first time in production.
701+
func TestPostgresPortBackdatedWrapper(t *testing.T) {
702+
ds := CreatePostgresDS(t)
703+
704+
mustExec := func(q string, args ...interface{}) {
705+
_, err := ds.primary.Exec(q, args...)
706+
require.NoError(t, err, q)
707+
}
708+
mustExec(`DROP TABLE IF EXISTS apple_software_update_assets CASCADE`)
709+
mustExec(`DROP TABLE IF EXISTS host_mdm_apple_os_updates CASCADE`)
710+
mustExec(`DELETE FROM fleet_variables WHERE name LIKE 'FLEET_VAR_HOST_TARGET%'`)
711+
for ported := range tables.PortedBelowMarker {
712+
mustExec(`DELETE FROM migration_status_tables WHERE version_id = $1`, ported)
713+
}
714+
715+
runWrapper := func() {
716+
tx, err := ds.primary.DB.Begin()
717+
require.NoError(t, err)
718+
require.NoError(t, tables.Up_20260729190000(tx))
719+
require.NoError(t, tx.Commit())
720+
}
721+
runWrapper()
722+
723+
var n int
724+
require.NoError(t, ds.primary.Get(&n, `SELECT COUNT(*) FROM apple_software_update_assets`))
725+
require.NoError(t, ds.primary.Get(&n, `SELECT COUNT(*) FROM host_mdm_apple_os_updates`))
726+
require.NoError(t, ds.primary.Get(&n, `SELECT COUNT(*) FROM fleet_variables WHERE name LIKE 'FLEET_VAR_HOST_TARGET%'`))
727+
require.Equal(t, 2, n, "both fleet vars inserted")
728+
require.NoError(t, ds.primary.Get(&n, `SELECT COUNT(*) FROM migration_status_tables WHERE version_id IN (20260727083533, 20260727084359) AND is_applied`))
729+
require.Equal(t, 2, n, "ported versions recorded in goose history")
730+
731+
// Idempotent: a re-run must not duplicate vars or history rows.
732+
runWrapper()
733+
require.NoError(t, ds.primary.Get(&n, `SELECT COUNT(*) FROM fleet_variables WHERE name LIKE 'FLEET_VAR_HOST_TARGET%'`))
734+
require.Equal(t, 2, n)
735+
require.NoError(t, ds.primary.Get(&n, `SELECT COUNT(*) FROM migration_status_tables WHERE version_id IN (20260727083533, 20260727084359)`))
736+
require.Equal(t, 2, n)
670737
}
671738

672739
// TestPostgresDBDiagnostics regression-covers DBLocks and InnoDBStatus on PG:

0 commit comments

Comments
 (0)