refactor(chart): consolidate pre-upgrade hooks into one task-oriented job

This commit is contained in:
Thibault VINCENT
2026-05-07 12:21:34 +02:00
committed by Thibault VINCENT
parent 36b8902ffb
commit 26c1a48c87
5 changed files with 242 additions and 301 deletions
+105
View File
@@ -204,3 +204,108 @@ When digest is set the tag is omitted (digest is immutable).
{{- printf "%s:%s" $repo (.Capabilities.KubeVersion.Version | regexFind "v[0-9]+\\.[0-9]+\\.[0-9]+") -}}
{{- end -}}
{{- end -}}
{{/*
Detect the previous chart version from any chart-managed resource that
is still in the cluster. Probes Service Deployment DaemonSets and
returns the version stripped from the `helm.sh/chart` label of the
first hit. Returns empty when not an upgrade or when no chart-labeled
resource is found.
*/}}
{{- define "migration.prevVersion" -}}
{{- $result := "" -}}
{{- if .Release.IsUpgrade -}}
{{- $ns := include "x509-certificate-exporter.namespace" . -}}
{{- $existingResource := dict -}}
{{- if .Values.service.create -}}
{{- $svc := lookup "v1" "Service" $ns (include "x509-certificate-exporter.fullname" .) -}}
{{- if $svc }}{{ $existingResource = $svc }}{{ end -}}
{{- end -}}
{{- if not $existingResource -}}
{{- $dep := lookup "apps/v1" "Deployment" $ns (include "x509-certificate-exporter.secretsExporterName" .) -}}
{{- if $dep }}{{ $existingResource = $dep }}{{ end -}}
{{- end -}}
{{- if not $existingResource -}}
{{- range $name, $_ := .Values.hostPathsExporter.daemonSets -}}
{{- if not $existingResource -}}
{{- $dsName := printf "%s-%s" (include "x509-certificate-exporter.fullname" $) $name -}}
{{- $ds := lookup "apps/v1" "DaemonSet" $ns $dsName -}}
{{- if $ds }}{{ $existingResource = $ds }}{{ end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $existingResource -}}
{{- $chartLabel := index $existingResource.metadata.labels "helm.sh/chart" -}}
{{- if $chartLabel -}}
{{- $result = trimPrefix "x509-certificate-exporter-" $chartLabel -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $result -}}
{{- end -}}
{{/*
Compute the set of pre-upgrade cleanup tasks to run, based on the
detected previous chart version. Returns a YAML-encoded dict with
boolean keys. Callers parse it via `fromYaml`.
Rule table (all guards OR-merged into the same dict):
prev<3.20.0 AND target<4.0.0 deleteDeployment, deleteDaemonsets
prev<4.0.0 deleteService (if service.create),
deleteDeployment, deleteDaemonsets
To extend, add a rule branch below. Each task is further gated on
its own preconditions (deleteService requires service.create;
deleteDaemonsets requires hostPathsExporter.daemonSets non-empty),
so the Role's verbs and the Job's args stay in sync automatically.
*/}}
{{- define "migration.tasks" -}}
{{- $tasks := dict "deleteService" false "deleteDeployment" false "deleteDaemonsets" false -}}
{{- $prev := include "migration.prevVersion" . -}}
{{- if $prev -}}
{{- if and (semverCompare "<3.20.0" $prev) (semverCompare "<4.0.0" .Chart.Version) -}}
{{- $_ := set $tasks "deleteDeployment" true -}}
{{- $_ := set $tasks "deleteDaemonsets" true -}}
{{- end -}}
{{- if semverCompare "<4.0.0" $prev -}}
{{- if .Values.service.create -}}{{- $_ := set $tasks "deleteService" true -}}{{- end -}}
{{- $_ := set $tasks "deleteDeployment" true -}}
{{- $_ := set $tasks "deleteDaemonsets" true -}}
{{- end -}}
{{- if not .Values.hostPathsExporter.daemonSets -}}
{{- $_ := set $tasks "deleteDaemonsets" false -}}
{{- end -}}
{{- end -}}
{{- $tasks | toYaml -}}
{{- end -}}
{{/*
Truthy ("true") iff at least one migration task is enabled. Used to
gate the entire pre-upgrade hook bundle (SA + Role + RoleBinding +
Job).
*/}}
{{- define "migration.needsHook" -}}
{{- $tasks := fromYaml (include "migration.tasks" .) -}}
{{- if or $tasks.deleteService $tasks.deleteDeployment $tasks.deleteDaemonsets -}}true{{- end -}}
{{- end -}}
{{/*
Build the Role's `rules:` list as the union of the verbs needed by the
enabled tasks. Returns a YAML list (no leading newline) suitable for
`{{ include "migration.roleRules" . | nindent 2 }}` under `rules:`.
*/}}
{{- define "migration.roleRules" -}}
{{- $tasks := fromYaml (include "migration.tasks" .) -}}
{{- $rules := list -}}
{{- if $tasks.deleteService -}}
{{- $rules = append $rules (dict "apiGroups" (list "") "resources" (list "services") "verbs" (list "get" "list" "delete")) -}}
{{- end -}}
{{- $appsRes := list -}}
{{- if $tasks.deleteDeployment -}}{{- $appsRes = append $appsRes "deployments" -}}{{- end -}}
{{- if $tasks.deleteDaemonsets -}}{{- $appsRes = append $appsRes "daemonsets" -}}{{- end -}}
{{- if $appsRes -}}
{{- $rules = append $rules (dict "apiGroups" (list "apps") "resources" $appsRes "verbs" (list "get" "list" "delete")) -}}
{{- end -}}
{{- $rules | toYaml -}}
{{- end -}}
@@ -1,130 +0,0 @@
{{- $needsMigration := false }}
{{- if .Release.IsUpgrade }}
{{- $ns := include "x509-certificate-exporter.namespace" . }}
{{- $existingResource := lookup "apps/v1" "Deployment" $ns (include "x509-certificate-exporter.secretsExporterName" .) }}
{{- if not $existingResource }}
{{- range $name, $_ := .Values.hostPathsExporter.daemonSets }}
{{- if not $existingResource }}
{{- $dsName := printf "%s-%s" (include "x509-certificate-exporter.fullname" $) $name }}
{{- $existingResource = lookup "apps/v1" "DaemonSet" $ns $dsName }}
{{- end }}
{{- end }}
{{- end }}
{{- if $existingResource }}
{{- $chartLabel := index $existingResource.metadata.labels "helm.sh/chart" }}
{{- if $chartLabel }}
{{- $prevVersion := trimPrefix "x509-certificate-exporter-" $chartLabel }}
{{/* Only fire when both the source release is pre-3.20.0 AND the
target release is still in the v3 line. Upgrades that cross
into v4+ are handled by pre-upgrade-4-0-0, which subsumes
the same Deployment/DaemonSet recreation alongside its own
Service migration. Without this gate the two hooks would run
in parallel and double-delete the same resources. */}}
{{- if and (semverCompare "<3.20.0" $prevVersion) (semverCompare "<4.0.0" .Chart.Version) }}
{{- $needsMigration = true }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- if $needsMigration }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
rules:
- apiGroups: ["apps"]
resources: ["daemonsets", "deployments"]
verbs: ["get", "list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
subjects:
- kind: ServiceAccount
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
template:
metadata:
{{- with .Values.migration.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.migration.extraLabels }}
labels:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-3-20-0
restartPolicy: OnFailure
{{- with .Values.migration.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: kubectl
image: {{ include "migration.kubectlImage" . | quote }}
imagePullPolicy: {{ .Values.migration.image.pullPolicy }}
{{- with .Values.migration.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.migration.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{- end }}
command:
- kubectl
- delete
- --ignore-not-found=true
- -n
- {{ include "x509-certificate-exporter.namespace" . }}
- deployment/{{ include "x509-certificate-exporter.secretsExporterName" . }}
{{- range $name, $_ := .Values.hostPathsExporter.daemonSets }}
- daemonset/{{ printf "%s-%s" (include "x509-certificate-exporter.fullname" $) $name }}
{{- end }}
{{- end }}
@@ -1,168 +0,0 @@
{{/*
Pre-upgrade hook for v3 → v4 chart upgrades.
Two unrelated immutable-field changes force a delete-and-recreate cycle:
1. Service.spec.clusterIP — v4 flipped `service.headless` to `true` by
default, which sets `clusterIP: None`. Kubernetes refuses to mutate
a Service's clusterIP in place (`spec.clusterIPs[0]: may not
change once set`).
2. Deployment.spec.selector and DaemonSet.spec.selector — v4 reworked
the per-component label set, which propagates to the immutable
selector. `helm upgrade` reports `field is immutable` on these.
Strategy: if any v4-managed resource (Service, secrets-exporter
Deployment, or any host-paths-exporter DaemonSet) still carries a
`helm.sh/chart` label from a chart version <4.0.0, delete all of them
pre-upgrade so helm can recreate them with the new schema. The pods
they back are stateless: a few seconds of unavailability during the
upgrade is the only side-effect.
This hook also subsumes the responsibility of pre-upgrade-3-20-0 when
the upgrade target is v4+ — see the version-gate inside that hook.
*/}}
{{- $needsMigration := false }}
{{- if .Release.IsUpgrade }}
{{- $ns := include "x509-certificate-exporter.namespace" . }}
{{/* Find any chart-managed resource still on chart <4.0.0. Probe the
Service first (always present unless the user disabled it), then
fall back to the Deployment, then any DaemonSet — covers users
who run with service.create=false. */}}
{{- $existingResource := dict }}
{{- if .Values.service.create }}
{{- $svc := lookup "v1" "Service" $ns (include "x509-certificate-exporter.fullname" .) }}
{{- if $svc }}{{ $existingResource = $svc }}{{ end }}
{{- end }}
{{- if not $existingResource }}
{{- $dep := lookup "apps/v1" "Deployment" $ns (include "x509-certificate-exporter.secretsExporterName" .) }}
{{- if $dep }}{{ $existingResource = $dep }}{{ end }}
{{- end }}
{{- if not $existingResource }}
{{- range $name, $_ := .Values.hostPathsExporter.daemonSets }}
{{- if not $existingResource }}
{{- $dsName := printf "%s-%s" (include "x509-certificate-exporter.fullname" $) $name }}
{{- $ds := lookup "apps/v1" "DaemonSet" $ns $dsName }}
{{- if $ds }}{{ $existingResource = $ds }}{{ end }}
{{- end }}
{{- end }}
{{- end }}
{{- if $existingResource }}
{{- $chartLabel := index $existingResource.metadata.labels "helm.sh/chart" }}
{{- if $chartLabel }}
{{- $prevVersion := trimPrefix "x509-certificate-exporter-" $chartLabel }}
{{- if semverCompare "<4.0.0" $prevVersion }}
{{- $needsMigration = true }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- if $needsMigration }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "delete"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets"]
verbs: ["get", "list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
subjects:
- kind: ServiceAccount
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
namespace: {{ include "x509-certificate-exporter.namespace" . }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
template:
metadata:
{{- with .Values.migration.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.migration.extraLabels }}
labels:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "x509-certificate-exporter.fullname" . }}-pre-upgrade-4-0-0
restartPolicy: OnFailure
{{- with .Values.migration.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: kubectl
image: {{ include "migration.kubectlImage" . | quote }}
imagePullPolicy: {{ .Values.migration.image.pullPolicy }}
{{- with .Values.migration.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.migration.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{- end }}
command:
- kubectl
- delete
- --ignore-not-found=true
- -n
- {{ include "x509-certificate-exporter.namespace" . }}
{{- if .Values.service.create }}
- service/{{ include "x509-certificate-exporter.fullname" . }}
{{- end }}
- deployment/{{ include "x509-certificate-exporter.secretsExporterName" . }}
{{- range $name, $_ := .Values.hostPathsExporter.daemonSets }}
- daemonset/{{ printf "%s-%s" (include "x509-certificate-exporter.fullname" $) $name }}
{{- end }}
{{- end }}
+132
View File
@@ -0,0 +1,132 @@
{{/*
Pre-upgrade hook: task-oriented cleanup driven by a version-jump rule
table.
Some chart releases force a delete-and-recreate cycle on a subset of
in-cluster resources because Kubernetes refuses certain in-place
mutations:
- Service.spec.clusterIP — flipped to None by the v4 default
`service.headless: true`.
- Deployment.spec.selector / DaemonSet.spec.selector — reworked
label sets in v3.20 and again in v4 propagate to the immutable
selector.
The previous chart version is detected from the `helm.sh/chart` label
of any in-cluster resource the chart owns. The `migration.tasks`
helper walks a list of version-jump rules and OR-merges the enabled
cleanup tasks. The Job's kubectl invocation receives the union of
enabled tasks; the Role grants only the verbs needed for those tasks.
To extend, add a rule branch in the `migration.tasks` helper.
*/}}
{{- $tasks := fromYaml (include "migration.tasks" .) -}}
{{- if include "migration.needsHook" . -}}
{{- $name := printf "%s-pre-upgrade" (include "x509-certificate-exporter.fullname" .) -}}
{{- $ns := include "x509-certificate-exporter.namespace" . -}}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
rules:
{{- include "migration.roleRules" . | nindent 2 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "-5"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $name }}
subjects:
- kind: ServiceAccount
name: {{ $name }}
namespace: {{ $ns }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ $name }}
namespace: {{ $ns }}
labels:
{{- include "x509-certificate-exporter.labels" . | nindent 4 }}
annotations:
helm.sh/hook: pre-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
template:
metadata:
{{- with .Values.migration.annotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.migration.extraLabels }}
labels:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ $name }}
restartPolicy: OnFailure
{{- with .Values.migration.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: kubectl
image: {{ include "migration.kubectlImage" . | quote }}
imagePullPolicy: {{ .Values.migration.image.pullPolicy }}
{{- with .Values.migration.securityContext }}
securityContext:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .Values.migration.resources }}
resources:
{{- toYaml . | nindent 10 }}
{{- end }}
command:
- kubectl
- delete
- --ignore-not-found=true
- -n
- {{ $ns }}
{{- if $tasks.deleteService }}
- service/{{ include "x509-certificate-exporter.fullname" . }}
{{- end }}
{{- if $tasks.deleteDeployment }}
- deployment/{{ include "x509-certificate-exporter.secretsExporterName" . }}
{{- end }}
{{- if $tasks.deleteDaemonsets }}
{{- range $n, $_ := .Values.hostPathsExporter.daemonSets }}
- daemonset/{{ printf "%s-%s" (include "x509-certificate-exporter.fullname" $) $n }}
{{- end }}
{{- end }}
{{- end }}
+5 -3
View File
@@ -235,9 +235,11 @@ service:
> `clusterIP: None`) would fail with `spec.clusterIPs[0]: may not change
> once set`. The chart ships a pre-upgrade hook that detects a v3 release
> via the existing Service's `helm.sh/chart` label and deletes the old
> Service before the upgrade reconciles, so helm can recreate it cleanly
> from the new template. A few seconds of scrape disruption during the
> upgrade window; Pods themselves stay up.
> Service, Deployment, and DaemonSets before the upgrade reconciles, so
> helm can recreate them cleanly from the new templates (the v4 label
> rework also rotates the immutable Deployment/DaemonSet selectors). A
> few seconds of scrape disruption during the upgrade window; Pods
> themselves stay up.
### Image schema — `digest` and split `migration.image`