Skip to content

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.

  1. Publish v2Harbor stores a new schema artifact. The database still uses v1.
  2. Move the tagThe production alias selects v2. The Kubernetes resource still pins v1.
  3. Select the digestUpdate the resource to v2, inspect its plan, and approve the change.
Changing a registry alias does not edit a digest-pinned Kubernetes resource.

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.

Start with the shipment schema from the previous article:

Terminal window
ptah schema push "oci://$REGISTRY/fulfillment/shipments:candidate" \
--schema-file schema-v1.sql --dialect postgres --version v1

The relevant output was:

examples/expected/published-v1.txt
Digest: sha256:1f729335ce618fe83366cb5133cf35209da7adaa05e6d13071a0a5122e044ff2
Version: v1
Tags: [v1 candidate]

Inspect what Harbor now holds:

Terminal window
ptah oci inspect "oci://$REGISTRY/fulfillment/shipments:candidate" --format json
ptah 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:

Terminal window
ptah oci tag "$SCHEMA_V1" production

The command returned the same manifest digest. Promotion does not require another schema build or upload.

Update the existing shipments resource to use Harbor. This is the relevant part of its desired-state configuration:

examples/desired.yaml
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.crt

The 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.

The new desired schema adds one nullable column:

examples/schema-v2.sql
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:

Terminal window
ptah schema push "oci://$REGISTRY/fulfillment/shipments:candidate" \
--schema-file schema-v2.sql --dialect postgres --version v2
ptah oci resolve "oci://$REGISTRY/fulfillment/shipments:candidate"

Keep that full reference as SCHEMA_V2. Move the production alias onto it:

Terminal window
ptah oci tag "$SCHEMA_V2" production
ptah oci resolve "oci://$REGISTRY/fulfillment/shipments:production"

The tag operation returned:

examples/expected/tag-v2.txt
Digest: sha256:4d5e619a35731d7c09422fda8e52c217330d609d12964e87362243a0331cd62c
Tags: [production]

Now compare what the original digest and current alias contain:

Terminal window
ptah schema render --schema-file "$SCHEMA_V1" --dialect postgres
ptah schema render \
--schema-file "oci://$REGISTRY/fulfillment/shipments:production" --dialect postgres

Only 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.

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:

Terminal window
kubectl -n "$NAMESPACE" apply -f shipments-v2.json
kubectl -n "$NAMESPACE" wait ptahschema/shipments \
--for=condition=ApprovalRequired=True --timeout=180s
kubectl ptah plan shipments --current -n "$NAMESPACE" -o sql

The plan was:

examples/expected/plan.sql
-- 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.

Terminal window
kubectl -n "$NAMESPACE" create -f approval.json
kubectl -n "$NAMESPACE" wait ptahschema/shipments \
--for=condition=InSync=True --timeout=180s
kubectl 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