From 0bc95b5045e70a7188452b6b648eedd78b3b0a69 Mon Sep 17 00:00:00 2001 From: "piotr.laczykowski" Date: Wed, 12 Aug 2026 21:44:15 +0200 Subject: [PATCH] feat: allow sidecars to set their own env vars and secret env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidecar containers could only inherit the release-wide `envVars` / `envVarsFromSecret`, with no way to add or override anything per sidecar. Each sidecar entry now accepts its own `envVars` (list of {name, value}) and `envVarsFromSecret` (map, rendered into a per-sidecar Secret named `--secret-env`). Both merge on top of the release-wide values: on a name collision the sidecar's value wins. Two Kubernetes precedence rules make that work: - the sidecar's secretRef is listed last in `envFrom`, and the last source wins for duplicate keys; - container `env` always beats `envFrom`, so a release-wide `envVars` entry whose name the sidecar sources from its own Secret is dropped from the rendered `env` list — otherwise it would shadow the Secret value. Co-Authored-By: claude-flow --- helm/helm-framework-test-template/README.md | 17 +++++++++ helm/helm-framework-test-template/values.yaml | 10 +++++ helm/helm-framework/README.md | 4 +- .../templates/_deployment-global.tpl | 1 + .../helm-framework/templates/_deployment.yaml | 17 +++++++-- helm/helm-framework/templates/_helpers.tpl | 38 +++++++++++++++++++ helm/helm-framework/templates/_secret-env.tpl | 23 +++++++++++ helm/helm-framework/values.yaml | 8 +++- .../helm-framework/resources/example-chart.md | 10 +++++ .../resources/values-reference.md | 4 +- 10 files changed, 123 insertions(+), 9 deletions(-) diff --git a/helm/helm-framework-test-template/README.md b/helm/helm-framework-test-template/README.md index a3eae45..9c567bb 100644 --- a/helm/helm-framework-test-template/README.md +++ b/helm/helm-framework-test-template/README.md @@ -17,6 +17,8 @@ Helm Framework Test Template chart | affinity | object | `{}` | | | appSettings.test1.test2 | bool | `true` | | | appSettings.testSetting | string | `"testValue"` | | +| args[0] | string | `"-c"` | | +| args[1] | string | `"echo hello && sleep 3600"` | | | authorizationPolicy[0].action | string | `"ALLOW"` | | | authorizationPolicy[0].enabled | bool | `true` | | | authorizationPolicy[0].name | string | `"allow-frontend"` | | @@ -30,6 +32,7 @@ Helm Framework Test Template chart | authorizationPolicy[1].enabled | bool | `true` | | | authorizationPolicy[1].name | string | `"deny-legacy"` | | | authorizationPolicy[1].rules[0].to[0].operation.paths[0] | string | `"/legacy/*"` | | +| command[0] | string | `"/bin/sh"` | | | envVarsFromSecret.secretKey | string | `"secretValue"` | | | envVars[0].name | string | `"foo"` | | | envVars[0].value | string | `"bar"` | | @@ -86,6 +89,14 @@ Helm Framework Test Template chart | horizontalPodAutoscaler.targetCPUUtilizationPercentage | int | `75` | | | horizontalPodAutoscaler.targetMemoryUtilizationPercentage | int | `80` | | | hostAliases | list | `[]` | | +| httpRoute.enabled | bool | `true` | | +| httpRoute.hostnames[0] | string | `"app.local"` | | +| httpRoute.parentRefs[0] | string | `"my-gateway"` | | +| httpRoute.paths[0].path | string | `"/"` | | +| httpRoute.paths[1].destination.port | int | `8080` | | +| httpRoute.paths[1].destination.weight | int | `1` | | +| httpRoute.paths[1].path | string | `"/api"` | | +| httpRoute.paths[1].pathType | string | `"PathPrefix"` | | | image.pullPolicy | string | `"IfNotPresent"` | | | image.repository | string | `"alpine"` | | | image.tag | string | `"1.0.0"` | | @@ -180,6 +191,12 @@ Helm Framework Test Template chart | serviceAccount.name | string | `""` | | | sidecars[0].appSettings.proxySetting | string | `"proxyValue"` | | | sidecars[0].enabled | bool | `true` | | +| sidecars[0].envVarsFromSecret.proxySecret | string | `"proxySecretValue"` | | +| sidecars[0].envVarsFromSecret.secretKey | string | `"sidecarSecretValue"` | | +| sidecars[0].envVars[0].name | string | `"foo"` | | +| sidecars[0].envVars[0].value | string | `"sidecar-bar"` | | +| sidecars[0].envVars[1].name | string | `"PROXY_MODE"` | | +| sidecars[0].envVars[1].value | string | `"transparent"` | | | sidecars[0].image.pullPolicy | string | `"IfNotPresent"` | | | sidecars[0].image.repository | string | `"alpine"` | | | sidecars[0].image.tag | string | `"1.0.0"` | | diff --git a/helm/helm-framework-test-template/values.yaml b/helm/helm-framework-test-template/values.yaml index fc1ba9f..cf62050 100644 --- a/helm/helm-framework-test-template/values.yaml +++ b/helm/helm-framework-test-template/values.yaml @@ -428,6 +428,16 @@ sidecars: targetScheme: HTTP appSettings: proxySetting: "proxyValue" + # Sidecar-owned env: `foo` overrides the release-wide envVars entry, `PROXY_MODE` + # is sidecar-only, and `secretKey` overrides the release-wide envVarsFromSecret key. + envVars: + - name: foo + value: sidecar-bar + - name: PROXY_MODE + value: transparent + envVarsFromSecret: + secretKey: sidecarSecretValue + proxySecret: proxySecretValue # ============================================================================= # APPLICATION CONFIGURATION diff --git a/helm/helm-framework/README.md b/helm/helm-framework/README.md index f5ad868..4bb4e5c 100644 --- a/helm/helm-framework/README.md +++ b/helm/helm-framework/README.md @@ -13,8 +13,8 @@ Base Helm framework library | args | list | `[]` | Override the main container's default command arguments. Empty uses the image's own CMD (or nothing, if `command` above is also set). | | authorizationPolicy | list | `[]` | Istio AuthorizationPolicy entries (list; one AuthorizationPolicy resource per enabled entry, named -). | | command | list | `[]` | Override the main container's entrypoint. Empty uses the image's own ENTRYPOINT. Useful when several releases of this chart share one image but each needs to run a different process (e.g. an API server vs. a background worker) — set `command`/`args` per release instead of building distinct images. | -| envVars | list | `[]` | Plain environment variables (list of {name, value}). Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. | -| envVarsFromSecret | object | `{}` | Environment variables sourced from a Secret (map of key: value). | +| envVars | list | `[]` | Plain environment variables (list of {name, value}), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per name. Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. | +| envVarsFromSecret | object | `{}` | Environment variables sourced from a Secret (map of key: value), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per key. | | externalSecrets | object | `{}` | External Secrets Operator ExternalSecret entries (map; one ExternalSecret resource per key, named after the map key). | | forceReload | bool | `false` | When true, adds a randomized pod annotation on each render so the Deployment restarts its pods, even when nothing else changed. | | fullnameOverride | string | `""` | Override the full release name used for resource names. | diff --git a/helm/helm-framework/templates/_deployment-global.tpl b/helm/helm-framework/templates/_deployment-global.tpl index 11c22e9..d9368f3 100644 --- a/helm/helm-framework/templates/_deployment-global.tpl +++ b/helm/helm-framework/templates/_deployment-global.tpl @@ -10,6 +10,7 @@ (include "helm-framework.deployment.secret-scripts" .) (include "helm-framework.deployment.secret-authorities" .) (include "helm-framework.deployment.secret-env" .) + (include "helm-framework.deployment.secret-sidecar-env" .) (include "helm-framework.deployment.secret-app-settings" .) (include "helm-framework.deployment.secret-sidecar-app-settings" .) (include "helm-framework.deployment.secretstore" .) diff --git a/helm/helm-framework/templates/_deployment.yaml b/helm/helm-framework/templates/_deployment.yaml index 53c6dd5..9e46815 100644 --- a/helm/helm-framework/templates/_deployment.yaml +++ b/helm/helm-framework/templates/_deployment.yaml @@ -210,15 +210,26 @@ spec: {{- if ($sc.healthChecks).enabled }} {{- include "helm-framework.probes" (dict "healthChecks" $sc.healthChecks "portName" (include "helm-framework.sidecar.portName" $sc) "scheme" (include "helm-framework.values.service.targetScheme" $sc)) | nindent 10 }} {{- end }} - {{- with $.Values.envVars }} + {{- /* Sidecars inherit the release-wide envVars/envVarsFromSecret; anything the + sidecar sets under its own envVars/envVarsFromSecret wins on a name collision. + The sidecar's secretRef is listed last on purpose — with duplicate keys across + envFrom sources, the last source wins. */}} + {{- with include "helm-framework.sidecar.env" (dict "root" $ "sidecar" $sc) }} env: - {{- toYaml . | nindent 12 }} + {{- . | nindent 12 }} {{- end }} - {{- if $.Values.envVarsFromSecret }} + {{- if or $.Values.envVarsFromSecret $sc.envVarsFromSecret }} envFrom: + {{- if $.Values.envVarsFromSecret }} - secretRef: name: {{ include "helm-framework.secret-env-name" $ }} optional: false + {{- end }} + {{- if $sc.envVarsFromSecret }} + - secretRef: + name: {{ include "helm-framework.secret-sidecar-env-name" (dict "root" $ "name" $sc.name) }} + optional: false + {{- end }} {{- end }} {{- if $vpaManaging }} {{- $cv := include "helm-framework.vpa.controlledValues" (dict "root" $ "containerName" $scContainerName) }} diff --git a/helm/helm-framework/templates/_helpers.tpl b/helm/helm-framework/templates/_helpers.tpl index 06f1af7..ff4ef81 100644 --- a/helm/helm-framework/templates/_helpers.tpl +++ b/helm/helm-framework/templates/_helpers.tpl @@ -98,6 +98,44 @@ Secret name for a sidecar's appSettings. Expects a dict: { root, name }. {{- printf "%s-%s-app-settings" (include "helm-framework.fullname" .root) .name | trunc 63 | trimSuffix "-" -}} {{- end }} +{{/* +Secret name for a sidecar's envVarsFromSecret. Expects a dict: { root, name }. +*/}} +{{- define "helm-framework.secret-sidecar-env-name" -}} +{{- printf "%s-%s-secret-env" (include "helm-framework.fullname" .root) .name | trunc 63 | trimSuffix "-" -}} +{{- end }} + +{{/* +Plain `env` list for a sidecar container: the release-wide envVars, minus every name +the sidecar redefines, followed by the sidecar's own envVars. Names the sidecar +sources from its own Secret are dropped from the list too — a container's `env` +always beats `envFrom`, so a release-wide entry left in place would shadow the +sidecar's Secret value. Expects a dict: { root, sidecar }. +Renders nothing when the sidecar ends up with no plain env vars at all. +*/}} +{{- define "helm-framework.sidecar.env" -}} +{{- $sc := .sidecar -}} +{{- $overridden := dict -}} +{{- range $sc.envVars -}} +{{- $_ := set $overridden .name true -}} +{{- end -}} +{{- range $key, $value := ($sc.envVarsFromSecret | default dict) -}} +{{- $_ := set $overridden $key true -}} +{{- end -}} +{{- $env := list -}} +{{- range .root.Values.envVars -}} +{{- if not (hasKey $overridden .name) -}} +{{- $env = append $env . -}} +{{- end -}} +{{- end -}} +{{- range $sc.envVars -}} +{{- $env = append $env . -}} +{{- end -}} +{{- if $env -}} +{{- toYaml $env -}} +{{- end -}} +{{- end }} + {{/* Container/Service port name for a sidecar. Expects the sidecar entry (uses .name). Port names are limited to 15 chars, so the sidecar name must stay short and unique. diff --git a/helm/helm-framework/templates/_secret-env.tpl b/helm/helm-framework/templates/_secret-env.tpl index a032727..b92e6c1 100644 --- a/helm/helm-framework/templates/_secret-env.tpl +++ b/helm/helm-framework/templates/_secret-env.tpl @@ -13,3 +13,26 @@ data: {{- end}} {{- end }} {{- end }} + +{{- define "helm-framework.deployment.secret-sidecar-env" -}} +{{- $state := dict "rendered" false -}} +{{- range $sc := .Values.sidecars }} +{{- if and $sc.enabled $sc.envVarsFromSecret }} +{{- if $state.rendered }} +--- +{{- end }} +{{- $_ := set $state "rendered" true }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "helm-framework.secret-sidecar-env-name" (dict "root" $ "name" $sc.name) }} + annotations: + helm.sh/hook: pre-upgrade, pre-install + helm.sh/hook-weight: "-20" +data: + {{- range $key, $value := $sc.envVarsFromSecret }} + {{$key}}: {{$value | toString | b64enc}} + {{- end}} +{{- end }} +{{- end }} +{{- end }} diff --git a/helm/helm-framework/values.yaml b/helm/helm-framework/values.yaml index 0e855b3..8bc903a 100644 --- a/helm/helm-framework/values.yaml +++ b/helm/helm-framework/values.yaml @@ -767,6 +767,10 @@ sidecars: [] # targetScheme: HTTP # resources: {} # appSettings: {} +# # Sidecars inherit the release-wide `envVars` / `envVarsFromSecret`. The two keys +# # below add to that set, and win over the release-wide value on a name collision. +# envVars: [] # plain env vars (list of {name, value}); same shape as top-level envVars +# envVarsFromSecret: {} # map of key: value, rendered into a per-sidecar Secret # ============================================================================= # APPLICATION CONFIGURATION @@ -780,7 +784,7 @@ helmFrameworkSettings: # -- App configuration rendered into a Secret and mounted as the config file. appSettings: {} -# -- Plain environment variables (list of {name, value}). Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. +# -- Plain environment variables (list of {name, value}), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per name. Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. envVars: [] -# -- Environment variables sourced from a Secret (map of key: value). +# -- Environment variables sourced from a Secret (map of key: value), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per key. envVarsFromSecret: {} diff --git a/plugins/helm-framework/skills/helm-framework/resources/example-chart.md b/plugins/helm-framework/skills/helm-framework/resources/example-chart.md index 6ae94ef..724ae17 100644 --- a/plugins/helm-framework/skills/helm-framework/resources/example-chart.md +++ b/plugins/helm-framework/skills/helm-framework/resources/example-chart.md @@ -459,6 +459,16 @@ sidecars: targetScheme: HTTP appSettings: proxySetting: "proxyValue" + # Sidecar-owned env: `foo` overrides the release-wide envVars entry, `PROXY_MODE` + # is sidecar-only, and `secretKey` overrides the release-wide envVarsFromSecret key. + envVars: + - name: foo + value: sidecar-bar + - name: PROXY_MODE + value: transparent + envVarsFromSecret: + secretKey: sidecarSecretValue + proxySecret: proxySecretValue # ============================================================================= # APPLICATION CONFIGURATION diff --git a/plugins/helm-framework/skills/helm-framework/resources/values-reference.md b/plugins/helm-framework/skills/helm-framework/resources/values-reference.md index dac7990..8c04884 100644 --- a/plugins/helm-framework/skills/helm-framework/resources/values-reference.md +++ b/plugins/helm-framework/skills/helm-framework/resources/values-reference.md @@ -11,8 +11,8 @@ Generated from `helm/helm-framework/README.md` (itself auto-generated by helm-do | args | list | `[]` | Override the main container's default command arguments. Empty uses the image's own CMD (or nothing, if `command` above is also set). | | authorizationPolicy | list | `[]` | Istio AuthorizationPolicy entries (list; one AuthorizationPolicy resource per enabled entry, named -). | | command | list | `[]` | Override the main container's entrypoint. Empty uses the image's own ENTRYPOINT. Useful when several releases of this chart share one image but each needs to run a different process (e.g. an API server vs. a background worker) — set `command`/`args` per release instead of building distinct images. | -| envVars | list | `[]` | Plain environment variables (list of {name, value}). Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. | -| envVarsFromSecret | object | `{}` | Environment variables sourced from a Secret (map of key: value). | +| envVars | list | `[]` | Plain environment variables (list of {name, value}), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per name. Set proxy vars (HTTPS_PROXY, NO_PROXY, ...) here too. | +| envVarsFromSecret | object | `{}` | Environment variables sourced from a Secret (map of key: value), applied to the main and sidecar containers. A sidecar's own `envVars`/`envVarsFromSecret` override these per key. | | externalSecrets | object | `{}` | External Secrets Operator ExternalSecret entries (map; one ExternalSecret resource per key, named after the map key). | | forceReload | bool | `false` | When true, adds a randomized pod annotation on each render so the Deployment restarts its pods, even when nothing else changed. | | fullnameOverride | string | `""` | Override the full release name used for resource names. |