Files
jarvis-selfhost/kubernetes/03-migrate-job.yaml
T
2026-10-21 12:12:12 +02:00

114 lines
5.7 KiB
YAML

# 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 — which is exactly the bug this replaces, where
# every API container ran `prisma db push` at boot and N replicas raced the same DDL while the
# previous generation was 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. `prisma db push` used to run with
# `--accept-data-loss` at every boot, which meant an 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"]