migrate status ignores baseline/allow-dirty when revisions table exists but is empty — wedges Atlas Operator on non-clean databases (e.g. GCP Cloud SQL)
Environment
- atlas
v1.3.1-629a976-canary(bundled inarigaio/atlas-operator:0.7.39); also reproduced onv1.2.1and confirmed by inspection onmaster - Target: PostgreSQL 18 on GCP Cloud SQL (any DB with a provider-managed schema, e.g.
google_vacuum_mgmt) - Driver path: Atlas Kubernetes Operator
AtlasMigrationwithspec.baselineset
Summary
atlas migrate status fails with sql/migrate: connected database is not clean: found schema "google_vacuum_mgmt". baseline version or allow-dirty is required even when baseline is configured, whenever the revisions table exists but contains zero rows. atlas migrate apply succeeds on the identical state, because only apply threads baseline into the executor. Because the Atlas Kubernetes Operator runs migrate status before every migrate apply, this state permanently wedges the operator: status fails → apply never runs → backoff → Stalled.
Root cause
cmd/atlas/internal/cmdlog/cmdlog.go—StatusReporter.Report: when the revisions table exists, it builds the executor with no options:ex, err := migrate.NewExecutor(r.Client.Driver, r.Dir, rrw) // no WithBaselineVersion / WithAllowDirtythen calls
ex.Pending(ctx).sql/migrate/migrate.go—Executor.Pending: withlen(revs) == 0, it runsCheckClean; aNotCleanErroris only tolerated whenbaselineVer != "" || allowDirty— neither is ever set on the status executor.cmd/atlas/internal/cmdapi/migrate.go—migrateStatusCmddoesn't even register--baseline/--allow-dirtyflags (contrastmigrateApplyCmd), somaySetFlag(cmd, flagBaseline, env.Migration.Baseline)silently no-ops (f == nil) and an env-levelmigration { baseline = ... }is dropped for status.
So on any database that isn't clean — notably every Cloud SQL database, which always carries google_vacuum_mgmt — status is strictly weaker than apply.
How the empty-but-present revisions table arises
migrateApplyRun calls mrrw.Migrate(ctx) (creates the revisions table) before ex.Pending(ctx) writes the baseline revision. Any interruption between those steps (operator pod restart/OOM, crash, connectivity loss) leaves exactly this state. External state loss (manual cleanup, row deletion) produces it too. The next migrate status then fails forever, and since apply is never reached, the system cannot self-heal.
Minimal repro
docker run -d --name pg -e POSTGRES_PASSWORD=pass -p 5432:5432 postgres:18-alpine
# provider-managed schema Atlas doesn't control:
psql "postgres://postgres:pass@localhost:5432/postgres" -c 'CREATE SCHEMA google_vacuum_mgmt'
mkdir migrations
printf 'CREATE TABLE t1 (id int);\n' > migrations/20260101000000_baseline.sql
atlas migrate hash --dir file://migrations
cat > atlas.hcl <<'EOF'
env "k8s" {
url = "postgres://postgres:pass@localhost:5432/postgres?sslmode=disable"
migration {
dir = "file://migrations"
baseline = "20260101000000"
}
}
EOF
atlas migrate apply --env k8s # OK — baseline honored
# Simulate crash-between-table-create-and-first-row, or external state loss:
psql "postgres://postgres:pass@localhost:5432/postgres" \
-c 'DELETE FROM atlas_schema_revisions.atlas_schema_revisions'
atlas migrate status --env k8s
# Error: sql/migrate: connected database is not clean: found schema
# "google_vacuum_mgmt". baseline version or allow-dirty is required ← BUG
atlas migrate apply --env k8s # OK again — apply tolerates the same stateExpected behavior
migrate status honors baseline/allow-dirty exactly like migrate apply — or at minimum never fails on a state where apply succeeds.
Suggested fix
Thread the options through: register --baseline/--allow-dirty on migrateStatusCmd, add BaselineVersion/AllowDirty fields to cmdlog.StatusReporter, and pass migrate.WithBaselineVersion(...)/migrate.WithAllowDirty(...) into the NewExecutor call in Report.
Impact / workaround
Any Atlas Operator deployment against non-clean databases (all GCP Cloud SQL instances; likely Azure/AWS managed Postgres equivalents) can be permanently wedged by a single interrupted first apply. Manual repair is required: atlas migrate apply --baseline <version> once per database (which also demonstrates the asymmetry — apply works, status doesn't). Affected operator-side flow: migrationRun.migrateStatus → applyChanges in internal/controller/atlasmigration_controller.go.
Related: #3609 (--exclude not working for schema apply, same google_vacuum_mgmt provider-schema class of problem).
Source: ariga/atlas