From 16f3277e2395f5eb3c3d16c077392e99089d86bf Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Fri, 28 Aug 2026 13:21:27 +1000 Subject: [PATCH 1/3] feat: add a chart-wide podLabels value Adds a top-level `podLabels` that lands on every pod the chart renders, including the migration Job, with each component's own `podLabels` applied on top. The labels stay off the Deployment selectors, which are immutable, so an existing release can take one without a reinstall. Without this a cluster convention such as an environment label had to go on after rendering, through a Kustomize post-renderer per workload kind. --- helm/README.md | 8 +++++ helm/templates/_helpers.tpl | 13 +++++++ helm/templates/api/deployment.yaml | 3 ++ helm/templates/flower/deployment.yaml | 3 ++ helm/templates/migrate-job.yaml | 3 ++ helm/templates/providers/deployment.yaml | 3 ++ helm/values.yaml | 3 ++ tests/test_helm_render.py | 45 ++++++++++++++++++++++++ 8 files changed, 81 insertions(+) diff --git a/helm/README.md b/helm/README.md index 9534151..dbadcf7 100644 --- a/helm/README.md +++ b/helm/README.md @@ -208,6 +208,14 @@ For ephemeral test deployments (no persistence across upgrades), `/ref` can also | `imagePullSecrets` | Docker registry secrets | `[]` | | `nameOverride` | Override chart name | `""` | | `fullnameOverride` | Override full release name | `""` | +| `podLabels` | Labels added to every pod | `{}` | + +`podLabels` covers every pod the chart renders, including the migration Job, +and a component's own `podLabels` is applied on top of it. +The labels stay off the Deployment selectors, which are immutable, +so setting one on an existing release does not need a reinstall. +This is the hook for a cluster-wide convention, such as an environment or +ownership label that a log or metric collector reads off the pod. ### Diagnostic providers diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index f4b5bba..a40c4c3 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -42,6 +42,19 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} +{{/* +Labels added to every pod the chart renders, from the chart-wide `podLabels`. +Rendered before a component's own podLabels, so a component can still override one. +Kept out of ref.labels because a Deployment's selector is immutable, +so a label that reached the selector would break the next upgrade. +Returns an empty string when unset, so callers must use it through `with`. +*/}} +{{- define "ref.podLabels" -}} +{{- with .Values.podLabels -}} +{{- toYaml . -}} +{{- end -}} +{{- end -}} + {{/* Selector labels */}} diff --git a/helm/templates/api/deployment.yaml b/helm/templates/api/deployment.yaml index a824398..44f5660 100644 --- a/helm/templates/api/deployment.yaml +++ b/helm/templates/api/deployment.yaml @@ -29,6 +29,9 @@ spec: labels: app.kubernetes.io/component: api {{- include "ref.labels" . | nindent 8 }} + {{- with (include "ref.podLabels" .) }} + {{- . | nindent 8 }} + {{- end }} {{- with .Values.api.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/helm/templates/flower/deployment.yaml b/helm/templates/flower/deployment.yaml index 0aa04fb..6153c8f 100644 --- a/helm/templates/flower/deployment.yaml +++ b/helm/templates/flower/deployment.yaml @@ -24,6 +24,9 @@ spec: labels: app.kubernetes.io/component: flower {{- include "ref.labels" . | nindent 8 }} + {{- with (include "ref.podLabels" .) }} + {{- . | nindent 8 }} + {{- end }} {{- with .Values.flower.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/helm/templates/migrate-job.yaml b/helm/templates/migrate-job.yaml index 4e46be8..4bc2997 100644 --- a/helm/templates/migrate-job.yaml +++ b/helm/templates/migrate-job.yaml @@ -31,6 +31,9 @@ spec: metadata: labels: {{- include "ref.selectorLabels" . | nindent 8 }} + {{- with (include "ref.podLabels" .) }} + {{- . | nindent 8 }} + {{- end }} spec: {{- with $migrate.priorityClassName }} priorityClassName: {{ . | quote }} diff --git a/helm/templates/providers/deployment.yaml b/helm/templates/providers/deployment.yaml index 7abd100..8315e80 100644 --- a/helm/templates/providers/deployment.yaml +++ b/helm/templates/providers/deployment.yaml @@ -78,6 +78,9 @@ spec: labels: app.kubernetes.io/component: {{ $instance }} {{- include "ref.labels" $ | nindent 8 }} + {{- with (include "ref.podLabels" $) }} + {{- . | nindent 8 }} + {{- end }} {{- with $spec.podLabels }} {{- toYaml . | nindent 8 }} {{- end }} diff --git a/helm/values.yaml b/helm/values.yaml index 5436ce4..2cf8337 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -2,6 +2,9 @@ imagePullSecrets: [] nameOverride: "" fullnameOverride: "" +# Labels added to every pod the chart renders, on top of each component's own podLabels. +podLabels: {} + api: enabled: true replicaCount: 1 diff --git a/tests/test_helm_render.py b/tests/test_helm_render.py index 9fcfe0b..8ab2e41 100644 --- a/tests/test_helm_render.py +++ b/tests/test_helm_render.py @@ -1064,3 +1064,48 @@ def test_the_migrate_job_follows_an_orchestrator_priority_class_override(): "orchestrator.priorityClassName=ref-orchestrator", ) assert _pod_spec(docs, "migrate", kind="Job")["priorityClassName"] == "ref-orchestrator" + + +def _pod_labels(docs: list[dict], component: str, kind: str = "Deployment") -> dict: + return find(docs, kind, f"-{component}")["spec"]["template"]["metadata"]["labels"] + + +POD_TEMPLATES = [ + ("api", "Deployment"), + ("flower", "Deployment"), + ("orchestrator", "Deployment"), + ("pmp", "Deployment"), + ("migrate", "Job"), +] + + +@pytest.mark.parametrize("component,kind", POD_TEMPLATES) +def test_chart_wide_pod_labels_reach_every_pod(component, kind): + docs = render("flower.enabled=true", "podLabels.environment=production") + assert _pod_labels(docs, component, kind=kind)["environment"] == "production" + + +@pytest.mark.parametrize("component,kind", POD_TEMPLATES) +def test_no_chart_wide_pod_labels_by_default(component, kind): + docs = render("flower.enabled=true") + assert "environment" not in _pod_labels(docs, component, kind=kind) + + +def test_chart_wide_pod_labels_stay_off_the_selectors(): + # A Deployment's selector is immutable, so a label that reached it + # would make the next upgrade of an existing release fail. + docs = render("podLabels.environment=production") + for component in ("api", "orchestrator", "pmp"): + selector = find(docs, "Deployment", f"-{component}")["spec"]["selector"]["matchLabels"] + assert "environment" not in selector + + +def test_a_component_overrides_a_chart_wide_pod_label(): + docs = render( + "podLabels.environment=production", + "api.podLabels.environment=staging", + "providers.pmp.podLabels.environment=staging", + ) + assert _pod_labels(docs, "api")["environment"] == "staging" + assert _pod_labels(docs, "pmp")["environment"] == "staging" + assert _pod_labels(docs, "orchestrator")["environment"] == "production" From e7ace2c578d3a5ffad264d27b59065b73565a1e8 Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Fri, 28 Aug 2026 13:22:20 +1000 Subject: [PATCH 2/3] docs: add the changelog fragment --- changelog/55.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/55.feature.md diff --git a/changelog/55.feature.md b/changelog/55.feature.md new file mode 100644 index 0000000..ce292bc --- /dev/null +++ b/changelog/55.feature.md @@ -0,0 +1 @@ +Adds a chart-wide `podLabels` value, applied to every pod the chart renders. From 58b51df7b23967ebca0e8a176f94190e2930b90f Mon Sep 17 00:00:00 2001 From: Jared Lewis Date: Fri, 28 Aug 2026 13:27:15 +1000 Subject: [PATCH 3/3] chore: clean up comments --- helm/README.md | 8 ++------ helm/templates/_helpers.tpl | 3 --- tests/test_helm_render.py | 4 ++-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/helm/README.md b/helm/README.md index dbadcf7..03f1ea3 100644 --- a/helm/README.md +++ b/helm/README.md @@ -210,12 +210,8 @@ For ephemeral test deployments (no persistence across upgrades), `/ref` can also | `fullnameOverride` | Override full release name | `""` | | `podLabels` | Labels added to every pod | `{}` | -`podLabels` covers every pod the chart renders, including the migration Job, -and a component's own `podLabels` is applied on top of it. -The labels stay off the Deployment selectors, which are immutable, -so setting one on an existing release does not need a reinstall. -This is the hook for a cluster-wide convention, such as an environment or -ownership label that a log or metric collector reads off the pod. +`podLabels` covers every pod the chart renders. +A component's own `podLabels` is applied on top of it so they take precendence. ### Diagnostic providers diff --git a/helm/templates/_helpers.tpl b/helm/templates/_helpers.tpl index a40c4c3..d6c04f4 100644 --- a/helm/templates/_helpers.tpl +++ b/helm/templates/_helpers.tpl @@ -45,9 +45,6 @@ app.kubernetes.io/managed-by: {{ .Release.Service }} {{/* Labels added to every pod the chart renders, from the chart-wide `podLabels`. Rendered before a component's own podLabels, so a component can still override one. -Kept out of ref.labels because a Deployment's selector is immutable, -so a label that reached the selector would break the next upgrade. -Returns an empty string when unset, so callers must use it through `with`. */}} {{- define "ref.podLabels" -}} {{- with .Values.podLabels -}} diff --git a/tests/test_helm_render.py b/tests/test_helm_render.py index 8ab2e41..f3573fc 100644 --- a/tests/test_helm_render.py +++ b/tests/test_helm_render.py @@ -1092,8 +1092,8 @@ def test_no_chart_wide_pod_labels_by_default(component, kind): def test_chart_wide_pod_labels_stay_off_the_selectors(): - # A Deployment's selector is immutable, so a label that reached it - # would make the next upgrade of an existing release fail. + # A Deployment's selector is immutable + # A label that reached it would make the next upgrade of an existing release fail. docs = render("podLabels.environment=production") for component in ("api", "orchestrator", "pmp"): selector = find(docs, "Deployment", f"-{component}")["spec"]["selector"]["matchLabels"]