diff --git a/chart/templates/_helpers.tpl b/chart/templates/_helpers.tpl index 5f009a7..bc36721 100644 --- a/chart/templates/_helpers.tpl +++ b/chart/templates/_helpers.tpl @@ -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 -}} diff --git a/chart/templates/migrations/pre-upgrade-3-20-0.yaml b/chart/templates/migrations/pre-upgrade-3-20-0.yaml deleted file mode 100644 index 25d6ba6..0000000 --- a/chart/templates/migrations/pre-upgrade-3-20-0.yaml +++ /dev/null @@ -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 }} diff --git a/chart/templates/migrations/pre-upgrade-4-0-0.yaml b/chart/templates/migrations/pre-upgrade-4-0-0.yaml deleted file mode 100644 index 90e9f11..0000000 --- a/chart/templates/migrations/pre-upgrade-4-0-0.yaml +++ /dev/null @@ -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 }} diff --git a/chart/templates/migrations/pre-upgrade.yaml b/chart/templates/migrations/pre-upgrade.yaml new file mode 100644 index 0000000..134b5b3 --- /dev/null +++ b/chart/templates/migrations/pre-upgrade.yaml @@ -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 }} diff --git a/docs/migration-v3-to-v4.md b/docs/migration-v3-to-v4.md index 490aa5e..e2a27cd 100644 --- a/docs/migration-v3-to-v4.md +++ b/docs/migration-v3-to-v4.md @@ -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`