From 31fa9165ff62cf610c3c11a9a36ae1f6a135b39f Mon Sep 17 00:00:00 2001 From: Nguyen Minh Phuc Date: Mon, 20 Jul 2026 06:33:31 +0000 Subject: [PATCH] chart: give the migrate hook its own ServiceAccount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Job ran as the api ServiceAccount, which is an ordinary chart resource. Hooks are created before the release's ordinary manifests, so on a first install that account does not exist and the Job never starts: Error creating: pods "svcforge-migrate-" is forbidden: error looking up service account svcforge/svcforge-api: serviceaccount "svcforge-api" not found Not an ArgoCD quirk — helm orders hooks the same way, so both deploy paths failed identically. It survived review because the chart was only ever checked with `helm template` and `helm install --dry-run=server`, and neither creates a Job. The pod is what fails, so only a real install can catch it. The new account is a hook at weight -10, ahead of the Job at -5, and is bound to no Role: the migration talks to Postgres and wants nothing from the Kubernetes API. automountServiceAccountToken is off for the same reason. Verified with a real `helm install` into a scratch namespace: STATUS deployed, job Complete 1/1, four migrations applied, and the hook ServiceAccount was 27s old against 10s for the ordinary ones — the ordering the bug turned on. --- deploy/chart/templates/migrate-job.yaml | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/deploy/chart/templates/migrate-job.yaml b/deploy/chart/templates/migrate-job.yaml index 20f937e..9ee439e 100644 --- a/deploy/chart/templates/migrate-job.yaml +++ b/deploy/chart/templates/migrate-job.yaml @@ -13,6 +13,37 @@ after a failure — the one time you actually want it — and clears it on the n Deliberately no terminationGracePeriodSeconds: 60 here. Three Deployments carry it; a migration is not one of them. */}} +{{/* +The hook needs its own ServiceAccount, and it has to be a hook itself. + +It used to run as the api ServiceAccount, which is an ordinary chart resource. Hooks are +created before the release's ordinary manifests, so on a first install that account does +not exist yet and the Job never starts: + + Error creating: pods "svcforge-migrate-" is forbidden: error looking up service + account svcforge/svcforge-api: serviceaccount "svcforge-api" not found + +This is not an ArgoCD quirk. `helm install` orders hooks the same way, so it failed +identically on both paths. It survived review because the chart was only ever checked with +`helm template` and `helm install --dry-run=server`, and neither creates a Job — the pod is +what fails, so nothing short of a real install can catch it. + +Weight -10 so it is created before the Job at -5. Deliberately bound to no Role: the +migration talks to Postgres and needs nothing from the Kubernetes API. +*/}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "svcforge.fullname" . }}-migrate + labels: + {{- include "svcforge.labels" . | nindent 4 }} + app.kubernetes.io/component: migrate + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": before-hook-creation +automountServiceAccountToken: false +--- apiVersion: batch/v1 kind: Job metadata: @@ -36,7 +67,7 @@ spec: app.kubernetes.io/component: migrate spec: restartPolicy: Never - serviceAccountName: {{ include "svcforge.serviceAccountName" (dict "ctx" $ "component" "api") }} + serviceAccountName: {{ include "svcforge.fullname" . }}-migrate {{- with .Values.image.pullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }}