Skip to content

Your First Schema Change with the Ptah Kubernetes Operator

Publish a shipment schema with Ptah, inspect the operator's plan, approve that exact change, and verify the database has converged.

A fulfillment service needs a shipments table before it can accept orders. The schema is in Git, but someone still needs to compare it with the database, review the SQL, and apply the change.

The Ptah Kubernetes operator makes that process visible through Kubernetes resources. We’ll publish the desired schema with the Ptah CLI, inspect the operator’s plan, and approve it. Then we’ll check the database itself.

The operator observes the database and runs Ptah in short-lived Jobs. Its resources separate the desired state from the decision to apply a change:

Resource What it records
PtahSchema The target database, desired schema artifact, and apply policy
PtahSchemaPlan The proposed change and the identity of its stored SQL
PtahSchemaApproval Approval of one specific plan

This example uses PtahSchema, which converges a database toward a declared schema. If you need to execute an ordered migration directory, the operator’s separate PtahMigration resource handles that workflow.

The commands below start with that operator installed. NAMESPACE names the application namespace. It contains a database URL Secret, registry credentials, and the immutable verification-policy ConfigMap included with the example. The installation guide explains the required image pins. The kubectl ptah plugin in this run was built from the same operator commit.

The desired schema is ordinary SQL:

examples/schema.sql
CREATE TABLE public.shipments (
id bigint PRIMARY KEY,
order_id bigint NOT NULL,
status text NOT NULL DEFAULT 'queued'
);

Publish it to the test registry, with credentials supplied through Ptah’s registry authentication settings:

Terminal window
ptah schema push "oci://$REGISTRY/schemas/blog-shipments:20260923-v1" \
--schema-file schema.sql --dialect postgres --plain-http

The registry in this disposable lab uses HTTP on a private test network, so this command opts into it explicitly. Use TLS for your normal registry. The returned digest identifies the published content. Copy that digest into desired.ociRef in shipments.yaml.

The resource also names the database Secret and sets apply: OnApproval. Its coordinationKey identifies the physical database: every resource that addresses that same database must use the same key, including through aliases. Keep one resource responsible for this schema.

The verification policy in this example accepts Ptah schema artifacts. It does not require a publisher signature; registry access controls still matter.

Apply the resource and wait for the approval condition:

Terminal window
kubectl -n "$NAMESPACE" apply -f shipments.yaml
kubectl -n "$NAMESPACE" wait ptahschema/shipments \
--for=condition=ApprovalRequired=True --timeout=180s

The operator resolves and verifies the artifact, observes PostgreSQL, and publishes a plan. At this point the phase is AwaitingApproval. Our database check confirmed that public.shipments was still absent.

Read the SQL through the plugin:

Terminal window
kubectl ptah plan shipments --current -n "$NAMESPACE" -o sql

The plan contains CREATE SCHEMA IF NOT EXISTS "public" followed by the shipment table definition. The plugin verifies the stored plan’s chunks before printing them. Reading the desired SQL file alone would miss how Ptah plans to change the actual database.

An approval binds the schema name and UID, the plan name and UID, and the plan fingerprint. Read those values from Kubernetes:

Terminal window
kubectl -n "$NAMESPACE" get ptahschema shipments -o yaml

Use metadata.uid and the current status.plan fields to fill approval.json. Its recorded identifiers belong to this test run; a new installation needs the identifiers from its own plan. The admission webhook supplies the remaining execution bindings and the authenticated approver identity.

We first submitted an approval with the wrong fingerprint. Kubernetes rejected it with:

examples/expected/rejected.txt
approval plan fingerprint does not match the immutable plan

The shipment table remained absent. The valid approval then allowed the change:

Terminal window
kubectl -n "$NAMESPACE" create -f approval.json

This requires permission to create approvals and to read the plan’s stored SQL. In this disposable cluster we used its administrator identity. An application deployment identity does not need approval permission simply because it can update the desired schema.

Wait for convergence and read the latest observed status:

Terminal window
kubectl -n "$NAMESPACE" wait ptahschema/shipments \
--for=condition=InSync=True --timeout=180s
kubectl ptah schema shipments -n "$NAMESPACE"

The relevant output was:

examples/expected/converged.txt
Phase: InSync
Drift: none
InSync: True (ScopedConverged)

JobCompleted means the apply Job finished. ScopedConverged means a database observation found the managed scope matched the desired state. In this run, the recorded observation was later than the apply completion, and both referred to the current resource generation. The approval was marked Consumed=True, with reason DispatchCommitted.

A converged schema has no pending plan. To inspect what was applied, select the applied plan explicitly:

Terminal window
kubectl ptah plan shipments --applied -n "$NAMESPACE" -o sql

Finally, we inserted shipment 1 for order 42. PostgreSQL returned the row with its default status, queued. The full insert and output are in the example files.

The useful result is a reviewable change with a recorded decision and a fresh database check. For the next schema change, publish a new artifact, update the resource’s digest, and review the new plan. The previous approval does not authorize it.

Example files