Skip to content

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.

sh · adopt-existing-database
# 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.

StepClaimHeld
1exits 0, prints "EU-1001", prints "US-2002", prints "EU-1003", prints "(3 rows)"yes
2exits 0, prints "shipments", never prints "schema_migrations"yes
3exits 0, prints "0000000001_create_shipments.up.sql", prints "0000000002_add_carrier.up.sql"yes
4exits 0, prints "created"yes
5exits 0, prints "baseline shadow check failed", prints "missing column", prints "carrier"yes
6exits 0, never prints "schema_migrations"yes
7exits 0, prints "Baselined 1 migration(s) through version 1"yes
8exits 0, prints "1|applied", prints "1|EU-1001", prints "2|US-2002", prints "3|EU-1003"yes
9exits 0, prints "created"yes
10ptahmigration/shipments: ApprovalRequired=True (AwaitingApproval)yes
10exits 0, prints "planned: 2 Add Carrier", prints "created", never prints "planned: 1"yes
11ptahmigration/shipments: Ready=True (HistoryMatched)yes
11exits 0, prints "2|applied", prints "1 EU-1001 dhl", prints "2 US-2002 ups", prints "3 EU-1003 dhl"yes
11ptahmigration/shipments: status.lastRun.outcome=Appliedyes

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.