Shipping a Schema Through Harbor by Digest
Publish a schema to Harbor with Ptah, promote the same artifact, and prove that a digest-pinned Kubernetes deployment changes only when you select new content.
The fulfillment service has its shipment table. The next release adds a tracking code, and the team wants to review that schema once, publish it to Harbor, and deploy the artifact they reviewed.
A tag such as production is convenient, but its name does not identify fixed
content. We’ll use Ptah to publish and inspect the schema, move that tag, and
show what happens to a Kubernetes resource pinned to the earlier digest.
Then we’ll explicitly deploy the new digest.
This continues Your First Schema Change with the Ptah Kubernetes Operator. That article covers the database resource, SQL plan, and approval. Here the focus is the artifact that connects publication to deployment.
- Publish v2Harbor stores a new schema artifact. The database still uses v1.
- Move the tagThe production alias selects v2. The Kubernetes resource still pins v1.
- Select the digestUpdate the resource to v2, inspect its plan, and approve the change.
Give the publisher and operator different credentials
Section titled “Give the publisher and operator different credentials”We ran Harbor 2.15.2, installed with its 1.19.2 Helm chart, behind HTTPS. The publishing CLI was Ptah 0.8.0. The operator, Kubernetes, and PostgreSQL versions match the previous article. The verification files record their exact revisions and running image digests.
Create a private Harbor project named fulfillment. Give its publisher robot
repository pull and push permissions, and give a separate operator robot
repository pull permission. Harbor’s administrator password is not a publishing
credential in this example.
REGISTRY below is the registry authority reachable by the client and operator.
Our lab used harbor.blog-harbor.svc.cluster.local, with the CLI running inside
the same cluster. The publisher’s credentials were supplied through
PTAH_OCI_USERNAME and PTAH_OCI_PASSWORD.
The lab used its own CA. The client trusted that certificate through
SSL_CERT_FILE; the operator used the CA binding shown below. TLS verification
stayed enabled. A request without the CA failed certificate verification, and
an anonymous pull from the private project returned 401.
Publish and inspect the first schema
Section titled “Publish and inspect the first schema”Start with the shipment schema from the previous article:
ptah schema push "oci://$REGISTRY/fulfillment/shipments:candidate" \ --schema-file schema-v1.sql --dialect postgres --version v1The relevant output was:
Digest: sha256:1f729335ce618fe83366cb5133cf35209da7adaa05e6d13071a0a5122e044ff2Version: v1Tags: [v1 candidate]Inspect what Harbor now holds:
ptah oci inspect "oci://$REGISTRY/fulfillment/shipments:candidate" --format jsonptah oci resolve "oci://$REGISTRY/fulfillment/shipments:candidate"The manifest declares a Ptah schema artifact with a canonical schema.hcl
layer. resolve returns the full reference ending in @sha256:1f729335….
Keep that complete reference as SCHEMA_V1; the shortened digest here is only
for readability. A new publication can have a different digest, so use the
value your push returned.
Promote those existing bytes:
ptah oci tag "$SCHEMA_V1" productionThe command returned the same manifest digest. Promotion does not require another schema build or upload.
Pin the operator to the reviewed artifact
Section titled “Pin the operator to the reviewed artifact”Update the existing shipments resource to use Harbor. This is the relevant
part of its desired-state configuration:
desired: ociRef: oci://harbor.blog-harbor.svc.cluster.local/fulfillment/shipments@sha256:1f729335ce618fe83366cb5133cf35209da7adaa05e6d13071a0a5122e044ff2 registryAuthFrom: name: harbor-operator mode: Environment verificationPolicyFrom: name: blog-schema-policy key: policy.yaml transport: caFrom: name: harbor-ca key: ca.crtThe Secret contains the pull-only robot’s username and password, plus
registry set to the exact Harbor authority. For this custom CA, it also
contains caSHA256: sha256: followed by the lowercase SHA-256 of the exact
CA bytes in the ConfigMap. The CA and the credential owner’s grant must agree.
The complete resource preserves the existing
database and OnApproval policy.
The operator fetched v1 over HTTPS and reported InSync=True. Its schema
matched the shipment table already deployed in the previous article.
Move the alias and check what changed
Section titled “Move the alias and check what changed”The new desired schema adds one nullable column:
CREATE TABLE public.shipments ( id bigint PRIMARY KEY, order_id bigint NOT NULL, status text NOT NULL DEFAULT 'queued', tracking_code text);Publish it and resolve its digest:
ptah schema push "oci://$REGISTRY/fulfillment/shipments:candidate" \ --schema-file schema-v2.sql --dialect postgres --version v2ptah oci resolve "oci://$REGISTRY/fulfillment/shipments:candidate"Keep that full reference as SCHEMA_V2. Move the production alias onto it:
ptah oci tag "$SCHEMA_V2" productionptah oci resolve "oci://$REGISTRY/fulfillment/shipments:production"The tag operation returned:
Digest: sha256:4d5e619a35731d7c09422fda8e52c217330d609d12964e87362243a0331cd62cTags: [production]Now compare what the original digest and current alias contain:
ptah schema render --schema-file "$SCHEMA_V1" --dialect postgresptah schema render \ --schema-file "oci://$REGISTRY/fulfillment/shipments:production" --dialect postgresOnly the second output contained tracking_code. We also pulled v1 with
ptah schema pull and checked its payload against the layer digest recorded
before promotion. The bytes still matched.
After another operator observation, the database still had only id,
order_id, and status. The resource remained pinned to v1 and converged
against v1. Moving the alias had changed Harbor’s selection, not the resource’s
desired schema.
Deploy the new digest
Section titled “Deploy the new digest”Set desired.ociRef to SCHEMA_V2 in the resource, then apply it. The
v2 resource contains the reference from this run.
Read the resulting plan:
kubectl -n "$NAMESPACE" apply -f shipments-v2.jsonkubectl -n "$NAMESPACE" wait ptahschema/shipments \ --for=condition=ApprovalRequired=True --timeout=180skubectl ptah plan shipments --current -n "$NAMESPACE" -o sqlThe plan was:
-- Add/modify columns for table: public.shipments ---- ALTER statements: --ALTER TABLE "public"."shipments" ADD COLUMN "tracking_code" text;Use the current schema and plan identifiers to prepare the approval, as in the operator walkthrough. The earlier approval does not authorize this new plan.
kubectl -n "$NAMESPACE" create -f approval.jsonkubectl -n "$NAMESPACE" wait ptahschema/shipments \ --for=condition=InSync=True --timeout=180skubectl ptah schema shipments -n "$NAMESPACE"The operator applied v2 and reached ScopedConverged at the current resource
generation. We recorded another observation after apply completion, then
updated shipment 1 with tracking code TRACK-0042. PostgreSQL returned the
existing order and status alongside the new value.
Use tags to make releases easy to find. Use the reviewed digest in the deployment resource so the plan and approval refer to the content you selected.
Example files
- application-query.sql
- approval.json
- columns.sql
- desired.yaml
- expected/plan.sql
- expected/published-v1.txt
- expected/tag-v2.txt
- measured/anonymous-pull.txt
- measured/application-query.txt
- measured/approval-consumed.json
- measured/approve-v2.txt
- measured/capabilities.json
- measured/columns-after-tag-move.txt
- measured/commands.json
- measured/harbor-runtime.json
- measured/inspect-v1.json
- measured/inspect-v2.json
- measured/lab-ca.crt.txt
- measured/login.txt
- measured/operator-after-tag-move.json
- measured/operator-awaiting-v2.json
- measured/operator-commands.json
- measured/operator-converged-v2.json
- measured/operator-invalid-ca-grant.json
- measured/pin-v2.txt
- measured/ptah-version.txt
- measured/pull-v1.txt
- measured/pulled-v1-sha256.txt
- measured/push-v1.txt
- measured/push-v2.txt
- measured/read-status.txt
- measured/rendered-v1.txt
- measured/rendered-v2.txt
- measured/resolve-production.txt
- measured/resolve-v1.txt
- measured/review-v2.txt
- measured/tag-production-v1.txt
- measured/tag-production-v2.txt
- measured/untrusted-ca.txt
- operator-robot-request.json
- pin-v1.json
- pin-v2.json
- publisher-robot-request.json
- README.md
- schema-v1.sql
- schema-v2.sql
- shipments-v1.json
- shipments-v2.json
- values.yaml
- verified.json