# The schema, brought up to this image's expectations. Once, before any pod of it starts serving. # # A JOB, and deliberately neither of the two alternatives: # # NOT an initContainer. It would run once per pod: N replicas would race the same DDL while the # previous generation is still selecting the columns being altered. # # NOT a leader election inside the API. A Job is the primitive built for run-once work: it has an # observable status, a bounded number of retries, and a failure that stops the rollout instead of # letting it proceed against a schema that was never applied. # # UNDER ARGO CD IT IS A SYNC HOOK, and that is not decoration — it is what makes this directory # reconcilable at all. A Job's `spec.template` is immutable, so an ordinary Job whose image digest # changed is a patch the API server REFUSES: the sync fails on "field is immutable" and the release # stops, every time, from the second release onwards. `BeforeHookCreation` deletes the previous Job # before creating this one, which is the supported way of saying "run it again, with new contents". # # It costs one thing worth knowing: the previous run's Job object, and its logs, are gone by the # time the next one starts. Ship the logs elsewhere if you need them after the fact. # # With Helm this is a `pre-install,pre-upgrade` hook instead. Applied by hand, the ordering is yours # to keep — see the README. # # It is safe to run twice: every pass is idempotent and the whole phase is wrapped in a Postgres # advisory lock, so a second copy waits rather than racing. apiVersion: batch/v1 kind: Job metadata: name: jarvis-migrate namespace: jarvis annotations: # The SYNC phase, not PreSync. A PreSync hook runs in a phase of its own, before any ordinary # resource is applied — which on a first sync means before the namespace, the ConfigMap and the # Secret this Job reads exist. In the Sync phase it is ordered by the same waves as everything # else, so "after the config, before the api" is one number rather than a second mechanism. argocd.argoproj.io/hook: Sync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation # After the config and the minting (waves -2 and -1), before any api pod (wave 1). Argo does not # begin the next wave until this Job reports complete, which is the ordering the README's # `kubectl wait` keeps by hand. argocd.argoproj.io/sync-wave: "0" spec: # One writer. The lock makes a second one wait rather than corrupt, but there is no reason to # have one waiting. parallelism: 1 completions: 1 # Two retries, because the failures worth retrying are transient (a database not finished failing # over). A schema `jarvis migrate` REFUSES is not transient and must not be retried into submission — # it is refused for a reason, and the reason is printed. backoffLimit: 2 # A migration that has not finished in fifteen minutes is stuck, not slow, and the rollout is # blocked behind it. activeDeadlineSeconds: 900 template: metadata: labels: app.kubernetes.io/name: jarvis app.kubernetes.io/component: migrate spec: restartPolicy: Never # Required by the `restricted` Pod Security Standard that 00-namespace.yaml enforces. At pod # level so it covers anything added beside this container later. Without it the Job is # accepted and its pod is refused, so the rollout waits on a Job that will never complete. securityContext: seccompProfile: type: RuntimeDefault containers: - name: migrate # THE SAME IMAGE, by the same digest, as the API that is about to roll. A migration from a # different build is a schema that matches nothing. image: git.luxit.be/luxit/jarvis-api@sha256:REPLACE_ME command: ["jarvis", "migrate"] envFrom: - configMapRef: name: jarvis-api-config # Generated first, hand-written last, for the reason spelled out in 04-api.yaml: the # last occurrence of a name wins, and a generated value must never override a typed one. - secretRef: name: jarvis-generated optional: true - secretRef: name: jarvis-secrets env: # Its own, smaller pool: this is one process doing one thing, and the connections it # takes are connections the running API cannot have while it works. - name: DATABASE_URL valueFrom: secretKeyRef: name: jarvis-secrets key: DATABASE_URL # Off, and it should stay off between deliberate acts. Left on, every upgrade could # destroy a column with nobody having decided to — survivable on one container, not # during a rolling update where the previous generation is still selecting it. # # Without it a pending migration that drops a table or a column is REFUSED, naming them, # and fails the Job — so the rollout stops and nothing is serving a half-migrated schema. # Set it to "true" for the ONE deploy where that drop is intended, then remove it. - name: SCHEMA_ACCEPT_DATA_LOSS value: "false" resources: requests: cpu: 200m memory: 512Mi limits: memory: 1Gi securityContext: runAsNonRoot: true runAsUser: 1000 allowPrivilegeEscalation: false capabilities: drop: ["ALL"]