Adopt a database that already has a schema
Record the history a database already earned, and let the operator carry on from there.
What it shows. How baseline proves a database matches its migrations before recording anything, and what it refuses.
A replay of a verified run, not a live cluster. 14 checks held while it was recorded. Play types it out in the frame below; the transcript is the same session, already there.
# This database already has a shipments table and three rows. An application put them there.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "SELECT * FROM shipments ORDER BY id" id | reference----+----------- 1 | EU-1001 2 | US-2002 3 | EU-1003(3 rows) # It has no Ptah history. There is no revision table at all.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\dt" List of relations Schema | Name | Type | Owner--------+-----------+-------+--------------- public | shipments | table | ptah_external(1 row) # The artifact the operator will run holds two migrations, published from these files.$ ls demo/migrations0000000001_create_shipments.down.sql0000000001_create_shipments.up.sql0000000002_add_carrier.down.sql0000000002_add_carrier.up.sql # Baseline reads the same files. Hand them to the cluster, where it runs with the executor.$ kubectl -n "$NAMESPACE" create configmap adopt-migrations --from-file=demo/migrationsconfigmap/adopt-migrations created # Baseline through version 2 is refused. The shadow has a carrier column this database lacks.$ BASELINE_VERSION=2 demo/bin/lab manifest adopt-baseline "$(cat demo/.lab/adopt-digest)" | kubectl apply -f -kubectl -n "$NAMESPACE" wait --for=condition=Failed job/adopt-baseline-v2 --timeout=180skubectl -n "$NAMESPACE" logs job/adopt-baseline-v2 | tail -n 1job.batch/adopt-baseline-v2 createdjob.batch/adopt-baseline-v2 condition meterror: baseline shadow check failed: missing column shipments.carrier # A refused baseline records nothing. There is still no revision table.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\dt" List of relations Schema | Name | Type | Owner--------+-----------+-------+--------------- public | shipments | table | ptah_external(1 row) # This database is at release 1, so baseline through version 1.$ BASELINE_VERSION=1 demo/bin/lab manifest adopt-baseline "$(cat demo/.lab/adopt-digest)" | kubectl apply -f -kubectl -n "$NAMESPACE" wait --for=condition=Complete job/adopt-baseline-v1 --timeout=180skubectl -n "$NAMESPACE" logs job/adopt-baseline-v1 | tail -n 1job.batch/adopt-baseline-v1 createdjob.batch/adopt-baseline-v1 condition metBaselined 1 migration(s) through version 1 in "schema_migrations" # The history records version 1, and the rows are the rows it had.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -tAc "SELECT version, state FROM schema_migrations"kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -tAc "SELECT id, reference FROM shipments ORDER BY id"1|applied1|EU-10012|US-20023|EU-1003 # Now the resource. It reads that history, and plans version 2 alone.$ APPLY=OnApproval demo/bin/lab manifest shipments "$(cat demo/.lab/adopt-digest)" | kubectl apply -f -ptahmigration.operator.ptah.run/shipments created # Version 1 is history, so it is not offered. Approve the plan for version 2.$ kubectl -n "$NAMESPACE" get ptahmigration shipments -o json > demo/.lab/migration.jsonkubectl -n "$NAMESPACE" get ptahmigrationplan \ "$(jq -r .status.plan.name demo/.lab/migration.json)" -o json > demo/.lab/migration-plan.jsonjq -r '.spec.migrations[] | "planned: \(.version) \(.description)"' demo/.lab/migration-plan.jsonjq -n --slurpfile migration demo/.lab/migration.json --slurpfile plan demo/.lab/migration-plan.json ' { apiVersion: "operator.ptah.run/v1alpha1", kind: "PtahMigrationApproval", metadata: {name: "shipments-adopted"}, spec: { migrationRef: {name: $migration[0].metadata.name, uid: $migration[0].metadata.uid}, planRef: {name: $plan[0].metadata.name, uid: $plan[0].metadata.uid}, planFingerprint: $plan[0].spec.fingerprint } }' | kubectl -n "$NAMESPACE" create -f -planned: 2 Add Carrierptahmigrationapproval.operator.ptah.run/shipments-adopted created # Version 2 ran on the rows the application wrote, and the history carries on from there.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -tAc "SELECT version, state FROM schema_migrations ORDER BY version"kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -tAc \ "SELECT id || ' ' || reference || ' ' || carrier FROM shipments ORDER BY id"1|applied2|applied1 EU-1001 dhl2 US-2002 ups3 EU-1003 dhl
What was checked
Every step states what has to hold before its output may be published. A condition is read as its type, status and reason on the live object, never as a phrase in a message.
| Step | Claim | Held |
|---|---|---|
| 1 | exits 0, prints "EU-1001", prints "US-2002", prints "EU-1003", prints "(3 rows)" | yes |
| 2 | exits 0, prints "shipments", never prints "schema_migrations" | yes |
| 3 | exits 0, prints "0000000001_create_shipments.up.sql", prints "0000000002_add_carrier.up.sql" | yes |
| 4 | exits 0, prints "created" | yes |
| 5 | exits 0, prints "baseline shadow check failed", prints "missing column", prints "carrier" | yes |
| 6 | exits 0, never prints "schema_migrations" | yes |
| 7 | exits 0, prints "Baselined 1 migration(s) through version 1" | yes |
| 8 | exits 0, prints "1|applied", prints "1|EU-1001", prints "2|US-2002", prints "3|EU-1003" | yes |
| 9 | exits 0, prints "created" | yes |
| 10 | ptahmigration/shipments: ApprovalRequired=True (AwaitingApproval) | yes |
| 10 | exits 0, prints "planned: 2 Add Carrier", prints "created", never prints "planned: 1" | yes |
| 11 | ptahmigration/shipments: Ready=True (HistoryMatched) | yes |
| 11 | exits 0, prints "2|applied", prints "1 EU-1001 dhl", prints "2 US-2002 ups", prints "3 EU-1003 dhl" | yes |
| 11 | ptahmigration/shipments: status.lastRun.outcome=Applied | yes |
What it ran against
- Kubernetes1.37.0
- Operatorc08fc73ebbae
- Ptahf6e562c
- Executorsha256:137662e6db45
- Recorded2026-09-28
- Scenarios atc08fc73ebbae
What the commands read
A session is repeated, not admired, so every name in it is one your own environment can carry. There is nothing else: no helper of ours stands between a command and the cluster, and the recorder refuses a step that reads a name this list does not hold. The commands arekubectl, ptah and kubectl ptah, each of which you install once; the last one is the operator’s own read-only client.
- KUBECONFIGthe cluster the commands run against
- NAMESPACEthe namespace the schema and its database credentials live in
- OPERATOR_NAMESPACEwhere the chart installed the operator
- CONTROLLERthe controller Deployment’s name, from that release
- REGISTRY_IN_CLUSTERthe registry address a Pod in the cluster resolves
- PTAH_OCI_REGISTRYthe registry address the push goes to
- PTAH_OCI_USERNAMEthe account that push uses
- PTAH_OCI_PASSWORDits password, which no command prints
A session also uses demo/bin/lab in two places, and both are the lab handing over what it generated rather than doing the demonstration’s work:lab credentials prints the three registry values above, and lab manifestfills the published template with values the step states. Rundemo/acceptance/reproduce.sh to watch the same scenario repeated from a directory where neither exists, by an account that may not create a Job, with the plan approved by a second account that may create neither a schema nor a Job.
The scenario is demo/scenarios/adopt-existing-database.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.