diff --git a/charts/substrate-podcert/README.md b/charts/substrate-podcert/README.md index 368142e2f5..0089f39dfd 100644 --- a/charts/substrate-podcert/README.md +++ b/charts/substrate-podcert/README.md @@ -37,6 +37,9 @@ Configure the controller image through `image.registry`, `image.repository`, and `image.tag`. `global.imageRegistry` overrides the registry, and `imagePullSecrets` and `global.imagePullSecrets` are merged for the controller pod. +`podLabels` adds labels to the controller pod, next to its `app` label, which +they cannot replace. + For an existing installation, the controller resources must be transferred out of the application release before the new release can own them. Installing this chart over resources still owned by the application release fails Helm ownership diff --git a/charts/substrate-podcert/templates/_helpers.tpl b/charts/substrate-podcert/templates/_helpers.tpl index 5ba9d45f72..eb1ad1d9a4 100644 --- a/charts/substrate-podcert/templates/_helpers.tpl +++ b/charts/substrate-podcert/templates/_helpers.tpl @@ -46,3 +46,17 @@ imagePullSecrets: {{- toYaml $merged | nindent 0 }} {{- end -}} {{- end -}} + +{{/* +The podLabels entries for the controller's pod template, as YAML map entries. +Renders nothing when podLabels is empty. `app` is the Deployment's selector +label, so a podLabels entry cannot replace it. +*/}} +{{- define "podcert.podLabels" -}} +{{- with .Values.podLabels -}} +{{- if hasKey . "app" -}} +{{- fail "podLabels must not set app: it is the selector label of the controller Deployment" -}} +{{- end -}} +{{- toYaml . -}} +{{- end -}} +{{- end -}} diff --git a/charts/substrate-podcert/templates/pod-certificate-controller.yaml b/charts/substrate-podcert/templates/pod-certificate-controller.yaml index 80284bb991..1944ef62dc 100644 --- a/charts/substrate-podcert/templates/pod-certificate-controller.yaml +++ b/charts/substrate-podcert/templates/pod-certificate-controller.yaml @@ -149,6 +149,7 @@ spec: metadata: labels: app: podcertificate-controller + {{- with include "podcert.podLabels" . }}{{- . | nindent 8 }}{{- end }} spec: {{- with include "podcert.imagePullSecrets" . }}{{- . | nindent 6 }}{{- end }} containers: diff --git a/charts/substrate-podcert/tests/pod_labels_test.yaml b/charts/substrate-podcert/tests/pod_labels_test.yaml new file mode 100644 index 0000000000..3f85cab2ab --- /dev/null +++ b/charts/substrate-podcert/tests/pod_labels_test.yaml @@ -0,0 +1,53 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +suite: Pod labels +templates: +- pod-certificate-controller.yaml +tests: +- it: keeps only the app label on the controller pod by default + documentSelector: + path: kind + value: Deployment + asserts: + - equal: + path: spec.template.metadata.labels + value: + app: podcertificate-controller + +- it: adds podLabels to the controller pod next to the app label + set: + podLabels: + giantswarm.io/tenant: team-a + documentSelector: + path: kind + value: Deployment + asserts: + - equal: + path: spec.template.metadata.labels + value: + app: podcertificate-controller + giantswarm.io/tenant: team-a + - equal: + path: spec.selector.matchLabels + value: + app: podcertificate-controller + +- it: refuses a podLabels entry that replaces the app selector label + set: + podLabels: + app: other + asserts: + - failedTemplate: + errorMessage: "podLabels must not set app: it is the selector label of the controller Deployment" diff --git a/charts/substrate-podcert/values.yaml b/charts/substrate-podcert/values.yaml index 30674c848f..e6658b9a56 100644 --- a/charts/substrate-podcert/values.yaml +++ b/charts/substrate-podcert/values.yaml @@ -38,6 +38,10 @@ global: imageRegistry: "" imagePullSecrets: [] +# Labels added to the controller pod, next to its own `app` label, which they +# cannot replace. +podLabels: {} + imagePullSecrets: [] image: registry: ghcr.io diff --git a/charts/substrate/README.md b/charts/substrate/README.md index f80beb3809..1f71363a15 100644 --- a/charts/substrate/README.md +++ b/charts/substrate/README.md @@ -73,6 +73,7 @@ See `values.yaml` for the full set; the important keys: | `ateApi.logLevel` | `info` | API server log level in its mounted config; one of `debug`, `info`, `warn`, or `error` | | `ateApi.authorization.bootstrapOwners` | `[]` | Global owners; empty defaults to the release's shared `ate-client` ServiceAccount | | `ateApi.extraArgs` | `[]` | Additional command-line arguments appended to the ateapi defaults | +| `podLabels` | `{}` | Labels added to every long-running pod, next to the chart's own `app` label | | `otel.endpoint` | `""` | Set to an OTLP endpoint to export traces, metrics, the actor lifecycle events and the router access log | | `otel.traces.enabled` | `true` | Set to `false` to export no traces from the router; the Go components do not honor this yet | | `otel.traces.endpoint` | `""` | OTLP endpoint for traces, overriding `otel.endpoint` | diff --git a/charts/substrate/templates/_helpers.tpl b/charts/substrate/templates/_helpers.tpl index d8a497a4ef..7becd6d158 100644 --- a/charts/substrate/templates/_helpers.tpl +++ b/charts/substrate/templates/_helpers.tpl @@ -231,6 +231,20 @@ imagePullSecrets: {{- end -}} {{- end -}} +{{/* +The podLabels entries for a pod template, as YAML map entries. Renders nothing +when podLabels is empty. The chart's own `app` label is the selector of every +workload, so a podLabels entry cannot replace it. +*/}} +{{- define "substrate.podLabels" -}} +{{- with .Values.podLabels -}} +{{- if hasKey . "app" -}} +{{- fail "podLabels must not set app: it is the selector label of every workload in the chart" -}} +{{- end -}} +{{- toYaml . -}} +{{- end -}} +{{- end -}} + {{/* imagePullPolicy: global.imagePullPolicy when set, IfNotPresent otherwise. One definition so the fallback cannot drift between pods. diff --git a/charts/substrate/templates/ate-api-server.yaml b/charts/substrate/templates/ate-api-server.yaml index 8089720d3e..d1f256982a 100644 --- a/charts/substrate/templates/ate-api-server.yaml +++ b/charts/substrate/templates/ate-api-server.yaml @@ -77,6 +77,7 @@ spec: metadata: labels: app: ate-api-server + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} annotations: checksum/api-config: {{ .Values.ateApi.logLevel | sha256sum }} prometheus.io/scrape: "true" diff --git a/charts/substrate/templates/ate-controller.yaml b/charts/substrate/templates/ate-controller.yaml index 7ac38e2409..d2309cc654 100644 --- a/charts/substrate/templates/ate-controller.yaml +++ b/charts/substrate/templates/ate-controller.yaml @@ -79,6 +79,7 @@ spec: metadata: labels: app: ate-controller + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} spec: serviceAccountName: {{ include "substrate.fullname" (list "ate-controller" .) }} {{- with include "substrate.imagePullSecrets" . }}{{- . | nindent 6 }}{{- end }} diff --git a/charts/substrate/templates/atelet.yaml b/charts/substrate/templates/atelet.yaml index 5abad9d1c5..8a96d9526a 100644 --- a/charts/substrate/templates/atelet.yaml +++ b/charts/substrate/templates/atelet.yaml @@ -126,6 +126,7 @@ spec: metadata: labels: app: atelet + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} annotations: prometheus.io/scrape: "true" prometheus.io/port: "9090" diff --git a/charts/substrate/templates/atenet-egress.yaml b/charts/substrate/templates/atenet-egress.yaml index 76cb93ad50..4557255264 100644 --- a/charts/substrate/templates/atenet-egress.yaml +++ b/charts/substrate/templates/atenet-egress.yaml @@ -132,6 +132,7 @@ spec: metadata: labels: app: atenet-egress + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} spec: serviceAccountName: {{ include "substrate.fullname" (list "atenet-egress" .) }} securityContext: diff --git a/charts/substrate/templates/atenet-router.yaml b/charts/substrate/templates/atenet-router.yaml index 761386f4b6..50ca5295e4 100644 --- a/charts/substrate/templates/atenet-router.yaml +++ b/charts/substrate/templates/atenet-router.yaml @@ -177,6 +177,7 @@ spec: metadata: labels: app: atenet-router + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} annotations: prometheus.io/scrape: "true" prometheus.io/port: "15020" diff --git a/charts/substrate/templates/k8s-credential-provider.yaml b/charts/substrate/templates/k8s-credential-provider.yaml index 718e60879c..8b5e93ec71 100644 --- a/charts/substrate/templates/k8s-credential-provider.yaml +++ b/charts/substrate/templates/k8s-credential-provider.yaml @@ -65,6 +65,7 @@ spec: checksum/namespace-policy: {{ toJson .Values.credentialProvider.namespacePolicies | sha256sum }} labels: app: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }} + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} spec: serviceAccountName: {{ include "substrate.fullname" (list "k8s-credential-provider" .) }} {{- with include "substrate.imagePullSecrets" . }}{{- . | nindent 6 }}{{- end }} diff --git a/charts/substrate/templates/rustfs.yaml b/charts/substrate/templates/rustfs.yaml index ab04cb1a20..aed53e3e56 100644 --- a/charts/substrate/templates/rustfs.yaml +++ b/charts/substrate/templates/rustfs.yaml @@ -64,6 +64,7 @@ spec: metadata: labels: app: rustfs + {{- with include "substrate.podLabels" $ }}{{- . | nindent 8 }}{{- end }} spec: serviceAccountName: {{ include "substrate.fullname" (list "rustfs" .) }} securityContext: diff --git a/charts/substrate/tests/pod_labels_guard_test.yaml b/charts/substrate/tests/pod_labels_guard_test.yaml new file mode 100644 index 0000000000..f93bee7caa --- /dev/null +++ b/charts/substrate/tests/pod_labels_guard_test.yaml @@ -0,0 +1,25 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +suite: Pod labels selector guard +templates: +- ate-controller.yaml +tests: +- it: refuses a podLabels entry that replaces the app selector label + set: + podLabels: + app: other + asserts: + - failedTemplate: + errorMessage: "podLabels must not set app: it is the selector label of every workload in the chart" diff --git a/charts/substrate/tests/pod_labels_test.yaml b/charts/substrate/tests/pod_labels_test.yaml new file mode 100644 index 0000000000..f48d22edb0 --- /dev/null +++ b/charts/substrate/tests/pod_labels_test.yaml @@ -0,0 +1,75 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +suite: Pod labels +templates: +- ate-api-server.yaml +- ate-controller.yaml +- atelet.yaml +- atenet-egress.yaml +- atenet-router.yaml +- k8s-credential-provider.yaml +- rustfs.yaml +tests: +- it: adds podLabels to every Deployment pod next to the app label + set: + podLabels: + giantswarm.io/tenant: team-a + example.com/tier: control + documentSelector: + path: kind + value: Deployment + skipEmptyTemplates: true + matchMany: true + asserts: + - isSubset: + path: spec.template.metadata.labels + content: + giantswarm.io/tenant: team-a + example.com/tier: control + - exists: + path: spec.template.metadata.labels.app + - notExists: + path: spec.selector.matchLabels["giantswarm.io/tenant"] + +- it: adds podLabels to the atelet DaemonSet pod + template: atelet.yaml + set: + podLabels: + giantswarm.io/tenant: team-a + documentSelector: + path: kind + value: DaemonSet + asserts: + - equal: + path: spec.template.metadata.labels + value: + app: atelet + giantswarm.io/tenant: team-a + - equal: + path: spec.selector.matchLabels + value: + app: atelet + +- it: leaves the rustfs-bucket-init Job pod without podLabels + template: rustfs.yaml + set: + podLabels: + giantswarm.io/tenant: team-a + documentSelector: + path: kind + value: Job + asserts: + - notExists: + path: spec.template.metadata.labels["giantswarm.io/tenant"] diff --git a/charts/substrate/values.yaml b/charts/substrate/values.yaml index 6cd8426931..c32bd2e362 100644 --- a/charts/substrate/values.yaml +++ b/charts/substrate/values.yaml @@ -219,6 +219,13 @@ global: # -- Fallback imagePullPolicy where a container does not set one. imagePullPolicy: "" +# Labels added to every long-running pod the chart renders, next to the chart's +# own `app` label, which they cannot replace. For example the label a +# multi-tenant telemetry collector routes a pod's OTLP export by. The +# rustfs-bucket-init Job does not get them: a Job's pod template is immutable, +# so a change would fail the upgrade. +podLabels: {} + # Pull secrets for every pod this chart renders. Merged with global.imagePullSecrets. imagePullSecrets: []