Skip to content

From OCI Artifacts to Database Plans in Ptah Operator

Follow a schema or migration update from an OCI registry to a verified database change, with real Ptah Console plans and approval decisions.

Ptah helps you plan, review, and apply database migrations. Try in your browser

An engineer adds shipment tracking to orders. CI publishes the change, but the database still has the old columns. Publishing an artifact and changing a database are separate events. Ptah Operator connects them through a Kubernetes resource, verified inputs, and a plan.

We followed the same change through a desired schema and prepared migrations with Ptah 0.13.0, Ptah Operator 0.2.4, and PostgreSQL 17.11. The example files use two disposable copies of the same orders database so each workflow manages its own target. Real Ptah Console views show the plans and the result of verification.

Publish the desired schema or migration files

Section titled “Publish the desired schema or migration files”

The delivery path starts in Git. CI publishes an OCI artifact. A Kubernetes resource selects that artifact and a database target. The operator then reconciles the resource.

CI publishes reviewed source files to an OCI registry. A Kubernetes resource selects a reference and policy; operation Jobs fetch the artifact bytes and read the target database before planning and applying.

The reference selects the input. The artifact bytes travel from the registry to operation Jobs. Open any diagram or screenshot to read it at full size.

The resource kind determines what those bytes mean:

Resource Source field Artifact contents What the plan compares
PtahSchema spec.desired.ociRef Desired schema and declared reference data The live database against desired state
PtahMigration spec.artifact.ociRef Prepared, ordered migration files Recorded revision history against the selected files

Our desired schema adds tracking_code, shipped_at, and a tracking index:

examples/schema.sql
CREATE TABLE orders (
id bigint PRIMARY KEY,
status text NOT NULL,
tracking_code text,
shipped_at timestamp with time zone
);
CREATE INDEX orders_tracking_idx ON orders (tracking_code);

The migration directory expresses those additions as versions 011, 012, and 013, after version 010 created orders. The filenames pad the version numbers with leading zeros. Both copies start with the old table and a row whose status is queued; the migration copy has version 010 recorded.

These are the publication commands for the updated inputs:

Terminal window
ptah schema push "oci://$REGISTRY/orders-schema:v13" --schema-file schema.sql --dialect postgres --version v13 --tag stable --plain-http
Terminal window
ptah migrations hash --dir migrations
ptah migrations push "oci://$REGISTRY/orders-migrations:v13" --migrations-dir migrations --dir-format ptah --verify-sum --version v13 --plain-http

The fixture uses an isolated HTTP registry, hence --plain-http. Its publishing address is $REGISTRY; Jobs reach the same registry through registry.delivery.svc.cluster.local:5000.

The commands use --version for write-once release tags. --tag stable deliberately moves the schema alias.

Schema publication resolves the supplied source formats into the schema artifact’s canonical HCL. SQL, YAML, HCL, DBML, and annotated Go input are publication choices. The operator consumes the published artifact. Migration publication retains the prepared files and their checksum manifest.

OCI is the source transport exposed by both operator resources. A GitOps tool can deliver or update the Kubernetes resource. The operator does not clone Git or accept a ConfigMap, PVC, or arbitrary HTTP URL as its SQL source.

Select the input and its verification policy

Section titled “Select the input and its verification policy”

Our schema resource selects the movable stable tag. This is the source portion of the complete manifest:

examples/schema-source.yaml
spec:
desired:
ociRef: oci://registry.delivery.svc.cluster.local:5000/orders-schema:stable
registryAuthFrom:
name: registry-access
mode: Environment
verificationPolicyFrom:
name: schema-verification
key: policy.yaml
transport:
plainHTTP: true

The migration resource selects the digest returned by publication. Its source portion uses artifact, with the same separation of credentials and policy:

examples/migration-source.yaml
spec:
artifact:
ociRef: oci://registry.delivery.svc.cluster.local:5000/orders-migrations@sha256:572692f033d25eb6d8cc68a90ce375655796652ed9097f7c02abad96009cdb56
registryAuthFrom:
name: registry-access
mode: Environment
verificationPolicyFrom:
name: migrations-verification
key: policy.yaml
transport:
plainHTTP: true

The complete migration manifest also names its target. Both resources get the database URL from spec.target.urlFrom, a Secret reference separate from registry access.

registryAuthFrom supplies access to the selected registry. The immutable ConfigMap named by verificationPolicyFrom says which artifacts are accepted. Our schema policy is:

examples/schema-policy.yaml
version: 1
artifact_types:
- application/vnd.stokaro.ptah.schema.v1

The migration policy accepts application/vnd.stokaro.ptah.migrations.v1 instead. These ConfigMaps contain verification rules; the SQL input remains in the OCI artifact. The fixture’s policies check artifact type. Publisher authenticity can be enforced with a signature policy and registry access controls.

Publishing a release does not make it the selected release. The reference strategy determines what needs to change:

Selected reference CI publishes an update How the operator discovers it
Mutable tag, such as :stable Move that tag to the new artifact Periodic Resolve finds its new digest
Release tag, such as :v12 Publish :v13 Update the resource’s ociRef; publishing v13 alone leaves v12 selected
Digest, @sha256:… Publish another artifact Update ociRef to select the new digest

Moving stable leaves the resource spec unchanged and is discovered by periodic Resolve. Selecting a new release or digest changes ociRef and the resource generation. Both paths fix one resolved digest for subsequent operations.

The operator resolves the reference it was given. It neither scans for the highest version tag nor receives registry-push notifications.

A spec update changes the Kubernetes generation and triggers reconciliation. A tag move leaves the spec unchanged. spec.interval controls periodic refresh: its default is 10m, and its permitted range is 10s–24h. Our fixture uses 1m. This is a cadence; scheduling, active work, and retries also affect when an update is discovered.

In our run, the schema detected the moved tag without a generation change. Publishing migration v13 alone left the pinned resource on v10, with only version 010 recorded. Updating its ociRef increased the generation and selected the new artifact.

Resolve pins one digest. Verification and subsequent operations use those bytes. A tag move cannot replace the SQL inside an already dispatched Apply. When a verification policy sets require_digest_pin, the original reference must itself name a digest; resolving a tag does not satisfy that requirement.

The target is another input. An unchanged artifact can still produce a new finding when somebody changes the database schema or revision history.

For PtahSchema, the path is Resolve → Verify → Observe → Plan. Each step runs as a Job. Observe uses native ptah schema drift; Plan uses native ptah schema plan to calculate the transition from the live database now to the selected desired state.

The runner requires independent native plans to return identical outcomes and bytes. When there are changes, it validates the saved plan through schema apply --plan ... --dry-run before publication. That validates the plan through the native execution path; it does not execute the SQL against a shadow database.

The managed scope, including exclusions, governs planning and Apply. InSync describes convergence within that configured scope.

Console shows the orders-schema plan with two ALTER TABLE statements and one CREATE INDEX statement. It verifies the stored SQL chunks and identifies the bound artifact and OnApproval policy.

The schema plan contains SQL computed for the current database. Console reads and checks the stored bytes before displaying them.

PtahSchemaPlan identifies the immutable plan. Immutable PtahSchemaPlanChunk objects hold its SQL bytes. The operator can plan directly from the live database to a later desired-state snapshot; it does not require the database to visit each earlier published snapshot.

For PtahMigration, the path is Resolve → Verify → History → Plan. The History Job runs native ptah migrations status --migrations-dir ... --json. Ptah reports recorded history and the pending selection. The manager builds an ordered PtahMigrationPlan from that report. This path has no separate Plan Job and no operator-managed shadow replay.

The common Resolve and Verify steps lead either to Observe and Plan Jobs for a desired schema, or to a History Job and manager-side sequence selection for migrations. Both paths apply authorized work and read the database again.

Schema planning stores SQL. Migration planning stores the selected sequence and leaves its SQL in the verified artifact.

Our database was at 010. The selected cumulative artifact carried files through 013. The plan selected 011 → 012 → 013:

Console shows a migration plan bound to history at version 10, with versions 11, 12, and 13 in order and their checksums. It states that migration SQL is unavailable through this operator API.

The migration plan records versions and checksums. It does not provide a migration SQL preview.

The plan also records checkpoint and transaction metadata where applicable, the selected artifact, and the history against which the sequence was chosen. The SQL stays in that artifact.

If CI publishes several intermediate releases before reconciliation, the operator can observe the latest cumulative artifact and plan all required pending files. Skipping an unobserved artifact release does not skip a required migration. Those files must be present in the selected artifact; the operator does not retrieve missing files from older release tags.

This differs from planning directly to a later desired-schema snapshot. Prepared migrations preserve their authored execution order. Incompatible history can refuse that path; the versioned migration guide covers dirty revisions, modified files, ordering, adoption, and recovery.

Both workflows publish a plan before considering Apply:

Apply policy What happens
OnApproval, the default Wait for an approval naming this exact plan
Always Proceed when the plan and other policy gates permit it
Never Publish the plan without executing it

A plan binds its content or sequence to the artifact, target, observed state or history, policies, and execution semantics. An approval authorizes that plan. Manager image and revision are recorded rather than bound, so changing only the manager image does not itself invalidate an approval.

Schema plans have additional gates. Destructive changes are disabled by default and still require an exact-plan approval when enabled. Privilege changes require an exact-plan approval even under Always. Those schema-plan classification rules do not classify arbitrary SQL in prepared migrations.

Periodic refresh continues while approval is pending. A due refresh updates the evidence before Apply is considered. Changed bindings can retire the old plan and approval; unchanged bindings can retain the same immutable plan. During our Console review, a due refresh temporarily prevented approval until the resource was awaiting a decision again. The unchanged plan was retained.

We reviewed and approved each plan in Console. Schema Apply consumed the stored plan bytes. A single migration Apply Job passed the approved sequence to Ptah with --expect-sequence, which checks the selection under Ptah’s migration lock before execution.

A successful Apply process is followed by an independent database read. For the schema, fresh observation and planning establish convergence. For migrations, a fresh history read establishes what is applied and still pending.

The schema resource card in Console shows Resolve, Plan, Approve, Apply, and Verify complete, with the message that a fresh read shows no differences.

The schema’s result is a fresh read showing no differences.

The migration resource card in Console shows Apply and Verify history complete, with the message that the database has every migration the artifact carries.

The migration’s result comes from reading its revision history again.

A separate native CLI read confirmed that history:

Terminal window
ptah migrations status --db-url "$MIGRATION_DB_URL" --migrations-dir migrations

Excerpt:

examples/measured/status-excerpt.txt
Current Version: 13
Total Migrations: 4
Applied Migrations: 4
Pending Migrations: 0
Out-of-order Migrations: 0
Status: ✅ Database is up to date

The direct database query recorded 010, 011, 012, and 013 as applied. Independent schema comparisons found no differences in either copy, and the original queued row remained with NULL values in the new columns.

Choose the resource from the input your team owns: desired state or prepared migration files. Then choose how the resource selects new artifacts. The published plan makes that selected input reviewable; the fresh database read establishes the result. The reconciliation reference and plans and approvals guide cover the lifecycle and binding rules.

For accumulated releases, stale approvals, and interrupted runs, continue with When Migration Releases Arrive Faster Than Reconciliation.

Put Ptah to work

Example files