# Exactly-once schema migration Job (task 46). # # Run BEFORE the Deployment rolls (CI applies the Job, waits for completion, # then rolls the Deployment). `migrate` applies forward-only sqlx migrations # under Postgres advisory lock, so concurrent starts serialize and the schema # is applied exactly once — replicas MUST NOT migrate (they share one # database; per-replica migration races and double-applies). The Deployment's # readiness gate (`_sqlx_migrations` version match) keeps new replicas out of # traffic until this Job succeeds. # # Hardened like the Deployment: non-root 65532, # read-only rootfs, ALL capabilities dropped, `automountServiceAccountToken: # false`, and every secret (token pepper, TOTP key, mandatory PII key, DB/Redis # passwords) mounted as 0400 tmpfs files under `/run/secrets` (read through the # `*_FILE` variables) instead of env vars. It runs the same mutable `:latest` # image reference as the Deployment. apiVersion: batch/v1 kind: Job metadata: name: d-oauth-migrate namespace: d-oauth labels: app.kubernetes.io/name: d-oauth-server app.kubernetes.io/part-of: d-oauth spec: backoffLimit: 3 activeDeadlineSeconds: 300 ttlSecondsAfterFinished: 3600 template: metadata: labels: app.kubernetes.io/name: d-oauth-server app.kubernetes.io/part-of: d-oauth app.kubernetes.io/component: migrate spec: restartPolicy: Never automountServiceAccountToken: false securityContext: runAsNonRoot: true runAsUser: 65532 runAsGroup: 65532 fsGroup: 65532 fsGroupChangePolicy: OnRootMismatch seccompProfile: type: RuntimeDefault containers: - name: migrate # Same image reference as the Deployment: mutable `:latest`. image: ghcr.io/syavne/d-oauth-server:latest imagePullPolicy: IfNotPresent # Overrides the image ENTRYPOINT (["/d-oauth-server","serve"]). command: ["/d-oauth-server", "migrate"] envFrom: - configMapRef: name: d-oauth-config env: # Same file-only secret rule as the Deployment; the DSNs come from # the ConfigMap. - name: D_OAUTH_DATABASE__PASSWORD_FILE value: /run/secrets/database-password - name: D_OAUTH_REDIS__PASSWORD_FILE value: /run/secrets/redis-password - name: D_OAUTH_TOKEN_PEPPER_FILE value: /run/secrets/token-pepper - name: D_OAUTH_TOTP_ENCRYPTION_KEY_FILE value: /run/secrets/totp-encryption-key - name: D_OAUTH_PII_ENCRYPTION_KEY_FILE value: /run/secrets/pii-encryption-key resources: requests: cpu: 50m memory: 64Mi limits: cpu: 500m memory: 256Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL volumeMounts: - name: certs mountPath: /certs readOnly: true - name: secrets mountPath: /run/secrets readOnly: true volumes: - name: certs secret: secretName: d-oauth-tls defaultMode: 0o400 - name: secrets secret: secretName: d-oauth-secrets defaultMode: 0o400 items: - key: database-password path: database-password - key: redis-password path: redis-password - key: token-pepper path: token-pepper - key: totp-encryption-key path: totp-encryption-key - key: pii-encryption-key path: pii-encryption-key