Deliver from a ConfigMap or Git
A PtahSchema reads its desired schema, and a PtahMigration its migration
directory, from one of three sources: an OCI artifact, a ConfigMap in the
resource’s namespace, or a Git repository. This page covers the last two.
Neither needs a registry or a verification policy, and both work for both
resources.
Choose by what the files are trusted by. A ConfigMap is trusted by Kubernetes RBAC: whoever may write it may change the desired state, exactly as whoever may write the resource can. It fits a team whose manifests already carry the files, through Kustomize, Helm or a GitOps tool. A Git source is trusted by the repository: whoever may push to the branch or move the tag decides what the operator plans next. It fits a team that wants the operator to follow a branch or a tag itself.
Whatever the source, the operator plans first and applies only what was
approved or what spec.policy lets it apply, and every operation reads the
exact files the plan was computed from.
Reconciliation says how
each source is resolved and what each operation checks.
From a ConfigMap
Section titled “From a ConfigMap”configMap names a ConfigMap and the keys that become files:
spec: desired: configMap: name: inventory-schema items: - key: schema.sql path: schema.sqlA schema selects exactly one file, and its extension tells Ptah the format. A
migration directory selects up to 1,000 files, named as Ptah names migrations,
such as 0000000001_create_orders.up.sql. A key cannot hold /, so a nested
path is spelled out in path. A key the list does not name is not part of the
source.
The manager copies the selected files into an immutable snapshot ConfigMap that the resource owns, and every operation reads the snapshot. The selected files and their manifest have to fit in one ConfigMap: 1 MiB together. An operation that is already running reads its snapshot, so replacing or deleting the source ConfigMap does not disturb it.
An edit to a selected key is read at once, and the plan it retires is replaced by a new one. An edit to any other key changes nothing. A ConfigMap deleted and created again under the same name is a new source too: a plan binds the ConfigMap’s UID with the content.
With Kustomize
Section titled “With Kustomize”examples/configmap-kustomize delivers a schema and a migration sequence from
ConfigMaps that Kustomize generates from files beside it:
apiVersion: kustomize.config.k8s.io/v1beta1kind: Kustomizationnamespace: applicationresources: - ptahschema.yaml - ptahmigration.yamlconfigurations: - ptah-references.yamlgeneratorOptions: immutable: trueconfigMapGenerator: - name: inventory-schema files: - schema.sql=db/schema.sql - name: orders-migrations files: - db/migrations/0000000001_create_orders.up.sql - db/migrations/0000000001_create_orders.down.sql - db/migrations/0000000002_add_order_total.up.sql - db/migrations/0000000002_add_order_total.down.sqlA generator appends a hash of the content to the ConfigMap’s name, so every
change of content is a new ConfigMap, and immutable: true costs nothing.
Kustomize rewrites a reference to the new name only in the fields it knows
name a ConfigMap, and it does not know the Ptah resources. The configurations
entry tells it:
nameReference: - kind: ConfigMap version: v1 fieldSpecs: - group: operator.ptah.run kind: PtahSchema path: spec/desired/configMap/name - group: operator.ptah.run kind: PtahMigration path: spec/artifact/configMap/nameWithout it the build still succeeds, and the resources name
inventory-schema while the cluster holds inventory-schema-<hash>: the
source never resolves. Add a field spec of the same shape for any other
ConfigMap you generate and a resource names, such as sourcePolicyFrom.
Render the example to see the names it produces:
kubectl kustomize examples/configmap-kustomizeThe generator keys a file by its name, so in the PtahMigration each item’s
key and path are the file name. A new migration is a line in the generator and
an item in the resource. ConfigMaps from earlier builds stay in the cluster
until something prunes them; the operator does not need them once the new
name has resolved.
With Helm
Section titled “With Helm”examples/configmap-helm is a chart that renders the same two resources.
Files under files/ become ConfigMap keys through .Files.Glob, which keeps
their bytes exactly, and the ConfigMap is named after a hash of its data:
{{- $files := .Files.Glob "files/migrations/*.sql" -}}{{- $data := $files.AsConfig -}}{{- $name := printf "%s-migrations-%s" .Release.Name ($data | sha256sum | trunc 10) -}}apiVersion: v1kind: ConfigMapmetadata: name: {{ $name }}immutable: truedata:{{ $data | indent 2 }}---apiVersion: operator.ptah.run/v1alpha1kind: PtahMigrationmetadata: name: {{ .Release.Name }}-ordersspec: target: engine: PostgreSQL coordinationKey: {{ .Values.migrations.coordinationKey }} urlFrom: name: {{ .Values.migrations.databaseSecret }} key: url artifact: configMap: name: {{ $name }} items: {{- range $path, $_ := $files }} - key: {{ base $path }} path: {{ base $path }} {{- end }}Here a new migration is only a new file. helm upgrade creates the new
ConfigMap, points the resource at it and deletes the previous one. A chart can
also keep one mutable ConfigMap under a fixed name: an upgrade then edits it,
and the operator reads the edit at once.
helm template inventory examples/configmap-helm --namespace applicationA source policy and generated names
Section titled “A source policy and generated names”A source policy can restrict which ConfigMaps
a source may name with allowedConfigMaps. It compares exact names, so a
generated name with a content hash never matches one. With generated names,
bound the source with maxContentBytes and leave allowedConfigMaps out, or
give the ConfigMap a fixed name: generatorOptions.disableNameSuffixHash in
Kustomize, a name without the hash in Helm.
From Git
Section titled “From Git”git names a repository, a ref and a path:
spec: artifact: git: url: https://git.example.com/acme/orders.git ref: branch: main path: db/migrationsA Resolve Job fetches the commit the ref names and records it with the content digest of the selected files, and every later operation fetches that commit again and refuses other content. The manager itself never reads the repository or its credential.
ref names exactly one of:
branch, read at its tip, again at every interval. A commit that leaves the selected files as they were keeps the plan.tag, read at the commit it names, again at every interval.commit, by its full 40-character id. A server that does not say it gives any commit by id gives one only while a branch or a tag points at it, so a pinned commit can stop resolving once its branch moves on; to hold a resource at one commit, prefer a tag. Reconciliation says which servers allow what.
path is the schema file, or the migration directory, which may be ., the
root of the repository. A submodule or a Git LFS pointer at the selected path is refused
with a reason, rather than read as SQL.
A public repository needs nothing else. A private one needs a credential, and the credential’s owner decides where it may be sent.
A private repository over HTTPS
Section titled “A private repository over HTTPS”authFrom names a Secret in the resource’s namespace. Its keys are fixed:
password holds the token, username is optional, and host is the only
host[:port] the fetch sends the credential to. The fetch refuses a URL whose
host is not that one, so a resource’s author cannot point the credential at
another server.
kubectl -n application create secret generic orders-git \ --from-literal=host=git.example.com \ --from-literal=password="$GIT_TOKEN"A server whose certificate chains to a private CA names the CA bundle in
transport.caFrom, a ConfigMap key. With a credential, the Secret must also
hold caSHA256, the SHA-256 of those exact bytes, so the credential is sent
only under the trust its owner granted:
kubectl -n application create configmap git-example-ca --from-file=ca.pemkubectl -n application create secret generic orders-git \ --from-literal=host=git.example.com \ --from-literal=password="$GIT_TOKEN" \ --from-literal=caSHA256="sha256:$(shasum -a 256 ca.pem | cut -d' ' -f1)"spec: artifact: git: url: https://git.example.com/acme/orders.git ref: branch: main path: db/migrations authFrom: name: orders-git transport: caFrom: name: git-example-ca key: ca.pemA private repository over SSH
Section titled “A private repository over SSH”An ssh:// URL needs transport.knownHostsFrom, the host keys the server may
present; a fetch never trusts a key on first use. Take the keys from the
fingerprints your Git provider publishes rather than from whatever the
network answers. The Secret holds sshPrivateKey, host, and
knownHostsSHA256, the SHA-256 of the known-hosts bytes:
kubectl -n application create configmap git-example-known-hosts --from-file=known_hostskubectl -n application create secret generic orders-deploy-key \ --from-file=sshPrivateKey=./deploy_key \ --from-literal=host=git.example.com \ --from-literal=knownHostsSHA256="sha256:$(shasum -a 256 known_hosts | cut -d' ' -f1)"spec: artifact: git: url: ssh://git@git.example.com/acme/orders.git ref: tag: v1.4.0 path: db/migrations authFrom: name: orders-deploy-key transport: knownHostsFrom: name: git-example-known-hosts key: known_hostsA deploy key with read access to the one repository is the narrowest credential that works.
What resolved
Section titled “What resolved”ArtifactResolved says SnapshotTaken for a ConfigMap source and
CommitResolved for a Git one. kubectl ptah schema and
kubectl ptah migration print the source, the commit or snapshot, and the
content digest; the status records the same:
kubectl -n application get ptahschema inventory \ -o jsonpath='{.status.source.snapshot.name}{"\n"}'kubectl -n application get ptahmigration orders \ -o jsonpath='{.status.artifact.git.commit} {.status.artifact.git.contentDigest}{"\n"}'A plan computed from either source is bound to that content and to the source’s identity, the ConfigMap’s UID or the repository URL and path, in place of an artifact digest. Plans and approvals says what an approval binds.