From 38c5a99c4f0b929505c98da3ecd1317f777783d1 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 17 Aug 2026 17:41:33 -0700 Subject: [PATCH 1/3] docs(helm): document null as the way to remove an inherited env key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting `app.env.KEY: ""` cannot clear a key that `app.envDefaults` sets: the Secret template drops empty values, and the deployment template treats an empty override as "not overridden" and still inlines the default. Helm's own `KEY: null` deletion is the supported mechanism and already works. The empty-string behavior is load-bearing, not a bug — every key under `app.env` ships as a "" placeholder, and ten collide with a real `envDefaults` value (NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, ...), so "" has to read as "unspecified" or a default install would blank them out. - README: document `null`, with the --reuse-values and Argo CD valuesObject caveats; correct the claim that `app.env` always wins over `app.envDefaults` - values.yaml + self-hosting docs: same guidance where operators look - sim-helm skill: record why an unset list is the wrong shape here - tests: lock in that null removes a key and "" does not --- .../self-hosting/environment-variables.mdx | 19 +++ .../sim-helm/references/values-model.md | 18 ++- helm/sim/README.md | 31 ++++- helm/sim/tests/env-null-deletion_test.yaml | 112 ++++++++++++++++++ helm/sim/tests/values/preset-free-limits.yaml | 8 ++ helm/sim/tests/values/unset-free-limits.yaml | 7 ++ helm/sim/values.yaml | 5 + 7 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 helm/sim/tests/env-null-deletion_test.yaml create mode 100644 helm/sim/tests/values/preset-free-limits.yaml create mode 100644 helm/sim/tests/values/unset-free-limits.yaml diff --git a/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx b/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx index 555f2c7eec8..cf4e2618556 100644 --- a/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx +++ b/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx @@ -249,6 +249,25 @@ Self-hosted deployments (billing disabled) run without plan limits: no rate limi Neither deployment presets these. The Helm chart previously did, which enforced hosted-plan caps on self-hosted installs; chart 1.5.0 removed the presets so Compose and Kubernetes behave identically. +### Removing a limit you already inherited + +If a limit is still enforced after upgrading — most often a `FREE_TABLES_LIMIT` or `FREE_TABLE_ROWS_LIMIT` carried forward from a chart older than 1.5.0, or copied into your own values file — the variable is still reaching the pod. On Helm, remove it by overriding it with `null`: + +```yaml +app: + envDefaults: + FREE_TABLES_LIMIT: null + FREE_TABLE_ROWS_LIMIT: null +``` + +Setting the variable to an empty string does **not** remove it: the chart reads an empty value as "not specified", so the inherited value still applies. Confirm what the pod will actually receive before rolling out: + +```bash +helm template sim/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output +``` + +`null` deletion has no effect under `helm upgrade --reuse-values` — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm 3.14+). If you deploy with Argo CD, put the `null` in `valueFiles` or the `values` string rather than `valuesObject`, which strips nulls. On Docker Compose, delete the line from your `.env` file. + ## Example .env ```bash diff --git a/helm/sim/.claude/skills/sim-helm/references/values-model.md b/helm/sim/.claude/skills/sim-helm/references/values-model.md index 58cb68c96f4..1dcf2984c79 100644 --- a/helm/sim/.claude/skills/sim-helm/references/values-model.md +++ b/helm/sim/.claude/skills/sim-helm/references/values-model.md @@ -46,7 +46,7 @@ The Sim chart splits configuration across **four** layers. Understanding which l **ESO compatibility.** When `externalSecrets.enabled=true`, the chart-managed Secret is **not rendered** — ESO renders one instead. Anything in Layer 1 must be mapped via `remoteRefs.app.` or it's silently missing. Layers 2–4 are unaffected by ESO. -**Override precedence.** Values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work. +**Override precedence.** *Non-empty* values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work. An *empty* `app.env` value does not override: it reads as "not specified" and the Layer 2 default still applies. To remove a key, override it with `null` (see "I want to REMOVE a key the chart sets" below). ## Where keys live — the canonical list @@ -103,6 +103,22 @@ app: Prefer Layer 2 for non-sensitive tunables — keeps the Secret lean and ESO mapping minimal. +### "I want to REMOVE a key the chart sets" + +Override it with `null` — Helm's documented deletion mechanism. It drops the key from the merged values, so no template emits it. + +```yaml +app: + envDefaults: + FREE_TABLES_LIMIT: null +``` + +**Do not use `""` — it is a silent no-op — and do not add a chart-level "unset list" to work around that.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates must read `""` as "not specified"; ten of those collide with a real `envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) and would blank themselves out on every default install if `""` meant "delete". A list-shaped unset key is also the wrong interface — Helm merges dicts but not lists, so it cannot be modified or unset downstream. + +The `(ne (toString $value) "")` guards throughout the templates are what make `null` deletion work — preserve them in any new render path. Regression net: `tests/env-null-deletion_test.yaml`. + +Caveat worth passing to operators: `null` has no effect under `helm upgrade --reuse-values`. + ### "I want to set my production app URL" ```yaml diff --git a/helm/sim/README.md b/helm/sim/README.md index 69a5025d300..7a33e448f0c 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -358,11 +358,40 @@ User-supplied `securityContext` values are merged with the defaults — your val Other security features: * `automountServiceAccountToken: false` on the ServiceAccount **and** every pod. -* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A value placed in `app.env` always wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists). +* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A **non-empty** value placed in `app.env` wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists). An **empty** value does not — to remove a key rather than change it, see [Removing an inherited env key](#removing-an-inherited-env-key). * Optional `networkPolicy.enabled=true` enforces east-west isolation and blocks cloud metadata endpoints in egress. --- +## Removing an inherited env key + +To remove a key the chart (or an older values file) sets, override it with `null` — [Helm's documented way](https://helm.sh/docs/chart_template_guide/values_files/) to delete a default key: + +```yaml +app: + envDefaults: + FREE_TABLES_LIMIT: null + FREE_TABLE_ROWS_LIMIT: null +``` + +Or on the CLI: `--set app.envDefaults.FREE_TABLES_LIMIT=null`. This works for `app.env` and `realtime.env` too, and in all three secret modes. + +**Setting the key to `""` instead does not remove it.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates have to treat an empty string as "the operator said nothing" — if they did not, the ten placeholders that collide with a real `app.envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) would blank themselves out on every default install. An empty entry is a silent no-op; `null` is the deletion. + +Nulling a key the application cannot start without (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, or `CRON_SECRET` with `cronjobs.enabled=true`) fails at template time with the existing required-secret error, not at runtime. + +> **Caveats.** `null` deletion does not take effect under `helm upgrade --reuse-values` ([helm#30765](https://github.com/helm/helm/issues/30765)) — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm ≥ 3.14). +> +> On **Argo CD**, put the `null` in `spec.source.helm.valueFiles` or the `values` string. Argo CD strips nulls from the structured `valuesObject` field ([argo-cd#16312](https://github.com/argoproj/argo-cd/issues/16312), [#19781](https://github.com/argoproj/argo-cd/issues/19781)), so a null written there silently does nothing. + +The common case is a free-tier cap inherited from a chart release older than the one that stopped presetting them, which shipped `FREE_TABLES_LIMIT: "3"` and `FREE_TABLE_ROWS_LIMIT: "1000"` under `app.envDefaults`. With billing disabled, Sim reads an unset limit as unlimited, so nulling these lifts the cap. Verify before rolling out: + +```bash +helm template sim/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output +``` + +--- + ## Autoscaling ```yaml diff --git a/helm/sim/tests/env-null-deletion_test.yaml b/helm/sim/tests/env-null-deletion_test.yaml new file mode 100644 index 00000000000..63b877bf386 --- /dev/null +++ b/helm/sim/tests/env-null-deletion_test.yaml @@ -0,0 +1,112 @@ +suite: removing an inherited env key — null deletion, and why "" does not work +release: + name: t + namespace: sim +defaults: &defaults + app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + app.env.INTERNAL_API_SECRET: x + app.env.CRON_SECRET: x + postgresql.auth.password: xxxxxxxx +tests: + - it: baseline — a preset envDefaults cap is inlined on the app pod + template: deployment-app.yaml + values: + - values/preset-free-limits.yaml + set: + <<: *defaults + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLES_LIMIT + value: "3" + - contains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLE_ROWS_LIMIT + value: "1000" + + - it: an empty string does NOT clear an envDefaults key — it reads as "unspecified" + template: deployment-app.yaml + values: + - values/preset-free-limits.yaml + set: + <<: *defaults + app.env.FREE_TABLES_LIMIT: "" + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLES_LIMIT + value: "3" + + - it: null removes the key from the inlined envDefaults + template: deployment-app.yaml + values: + - values/preset-free-limits.yaml + - values/unset-free-limits.yaml + set: + <<: *defaults + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLES_LIMIT + value: "3" + - notContains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLE_ROWS_LIMIT + value: "1000" + + - it: null removes an app.env key from the chart-managed Secret + template: secrets-app.yaml + values: + - values/preset-free-limits.yaml + - values/unset-free-limits.yaml + set: + <<: *defaults + asserts: + - isNull: + path: stringData.FREE_STORAGE_LIMIT_GB + + - it: the app.env key is present in the Secret before it is nulled + template: secrets-app.yaml + values: + - values/preset-free-limits.yaml + set: + <<: *defaults + asserts: + - equal: + path: stringData.FREE_STORAGE_LIMIT_GB + value: "5" + + - it: nulling unrelated keys leaves the rest of envDefaults intact + template: deployment-app.yaml + values: + - values/preset-free-limits.yaml + - values/unset-free-limits.yaml + set: + <<: *defaults + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: BETTER_AUTH_URL + value: http://localhost:3000 + - contains: + path: spec.template.spec.containers[0].env + content: + name: BILLING_CONCURRENCY_LIMIT_FREE + value: "10" + + - it: nulling a boot-critical key is still refused by the existing validator + values: + - values/preset-free-limits.yaml + set: + <<: *defaults + app.env.ENCRYPTION_KEY: null + asserts: + - failedTemplate: + errorMessage: app.env.ENCRYPTION_KEY is required for production deployment diff --git a/helm/sim/tests/values/preset-free-limits.yaml b/helm/sim/tests/values/preset-free-limits.yaml new file mode 100644 index 00000000000..6e40cc71ea1 --- /dev/null +++ b/helm/sim/tests/values/preset-free-limits.yaml @@ -0,0 +1,8 @@ +# Simulates a chart release older than the one that stopped presetting free-tier +# caps, plus a values file that carried those keys forward. +app: + envDefaults: + FREE_TABLES_LIMIT: "3" + FREE_TABLE_ROWS_LIMIT: "1000" + env: + FREE_STORAGE_LIMIT_GB: "5" diff --git a/helm/sim/tests/values/unset-free-limits.yaml b/helm/sim/tests/values/unset-free-limits.yaml new file mode 100644 index 00000000000..aa38c0557a8 --- /dev/null +++ b/helm/sim/tests/values/unset-free-limits.yaml @@ -0,0 +1,7 @@ +# Helm removes a key from the merged values when it is overridden with null. +app: + envDefaults: + FREE_TABLES_LIMIT: null + FREE_TABLE_ROWS_LIMIT: null + env: + FREE_STORAGE_LIMIT_GB: null diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 2c91b63ef64..88a5a1f77c2 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -322,6 +322,11 @@ app: # app container — NOT written into the chart-managed Secret and NOT required to be mapped # when externalSecrets.enabled=true. Override any key by setting `app.envDefaults.KEY` in # your values file. Move a key into `app.env` above only if it must be treated as secret. + # + # To REMOVE a key rather than change it, override it with `null` — Helm deletes a default + # key when it is overridden with null (e.g. `FREE_TABLES_LIMIT: null`). Setting it to "" + # does NOT remove it: every key under `app.env` ships as a "" placeholder, so the templates + # must read "" as "unspecified". See "Removing an inherited env key" in README.md. envDefaults: # Application URLs (override in app.env or app.envDefaults for production) NEXT_PUBLIC_APP_URL: "http://localhost:3000" From e49c3564823b62f4a19f804bbe5099ec402c11e8 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 17 Aug 2026 17:47:54 -0700 Subject: [PATCH 2/3] docs(helm): correct the verify command's chart path and scope the required-secret claim - The verify snippet used a `sim/sim` repo alias that this chart never publishes; every other instruction installs from the local `./helm/sim` path, so the command could not run as written - Nulling a boot-critical key only fails at template time with the chart-managed Secret. `existingSecret` mode skips that validation entirely (the chart cannot read a pre-created Secret), and under ESO the key must instead be mapped in externalSecrets.remoteRefs.app --- .../docs/en/platform/self-hosting/environment-variables.mdx | 2 +- helm/sim/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx b/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx index cf4e2618556..8ccf16f519d 100644 --- a/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx +++ b/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx @@ -263,7 +263,7 @@ app: Setting the variable to an empty string does **not** remove it: the chart reads an empty value as "not specified", so the inherited value still applies. Confirm what the pod will actually receive before rolling out: ```bash -helm template sim/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output +helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output ``` `null` deletion has no effect under `helm upgrade --reuse-values` — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm 3.14+). If you deploy with Argo CD, put the `null` in `valueFiles` or the `values` string rather than `valuesObject`, which strips nulls. On Docker Compose, delete the line from your `.env` file. diff --git a/helm/sim/README.md b/helm/sim/README.md index 7a33e448f0c..7d51c81a39c 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -378,7 +378,7 @@ Or on the CLI: `--set app.envDefaults.FREE_TABLES_LIMIT=null`. This works for `a **Setting the key to `""` instead does not remove it.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates have to treat an empty string as "the operator said nothing" — if they did not, the ten placeholders that collide with a real `app.envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) would blank themselves out on every default install. An empty entry is a silent no-op; `null` is the deletion. -Nulling a key the application cannot start without (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, or `CRON_SECRET` with `cronjobs.enabled=true`) fails at template time with the existing required-secret error, not at runtime. +With the chart-managed Secret (the default), nulling a key the application cannot start without (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, or `CRON_SECRET` with `cronjobs.enabled=true`) fails at template time with the existing required-secret error rather than at runtime. In `existingSecret` mode the chart skips that validation entirely — those values come from your pre-created Secret, which the chart cannot read — so a null there renders successfully and the key is simply absent from `app.env`. Under ESO the key must still be mapped in `externalSecrets.remoteRefs.app`, which is validated at template time. > **Caveats.** `null` deletion does not take effect under `helm upgrade --reuse-values` ([helm#30765](https://github.com/helm/helm/issues/30765)) — pass your full values with `-f`, or use `--reset-then-reuse-values` (Helm ≥ 3.14). > @@ -387,7 +387,7 @@ Nulling a key the application cannot start without (`BETTER_AUTH_SECRET`, `ENCRY The common case is a free-tier cap inherited from a chart release older than the one that stopped presetting them, which shipped `FREE_TABLES_LIMIT: "3"` and `FREE_TABLE_ROWS_LIMIT: "1000"` under `app.envDefaults`. With billing disabled, Sim reads an unset limit as unlimited, so nulling these lifts the cap. Verify before rolling out: ```bash -helm template sim/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output +helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output ``` --- From f5316c58670fffd1ea2e2a2b7c2239e9c737c7c2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 17 Aug 2026 18:11:12 -0700 Subject: [PATCH 3/3] docs(helm): say null must be applied in every layer that sets a key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `null` deletes a key from the map it is applied to, not from the pod. A key set in both `app.env` and `app.envDefaults` survives a null on the app.env entry alone — the deployment then inlines the envDefaults value again. Under ESO a retained `externalSecrets.remoteRefs.app` mapping keeps syncing the key regardless of app.env. - README and self-hosting docs: drop the "works in all three secret modes" shorthand and spell out that every layer setting the key must be nulled, including the ESO remote mapping - tests: cover both halves — nulling only app.env restores the envDefault, nulling both actually removes the key - chart 1.5.4; staging took 1.5.3 in the meantime --- .../self-hosting/environment-variables.mdx | 4 ++- helm/sim/Chart.yaml | 2 +- helm/sim/README.md | 4 ++- helm/sim/tests/env-null-deletion_test.yaml | 29 +++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx b/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx index 8ccf16f519d..9ebf2a55ecf 100644 --- a/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx +++ b/apps/docs/content/docs/en/platform/self-hosting/environment-variables.mdx @@ -260,7 +260,9 @@ app: FREE_TABLE_ROWS_LIMIT: null ``` -Setting the variable to an empty string does **not** remove it: the chart reads an empty value as "not specified", so the inherited value still applies. Confirm what the pod will actually receive before rolling out: +Setting the variable to an empty string does **not** remove it: the chart reads an empty value as "not specified", so the inherited value still applies. + +Null the variable in every layer that sets it. If it appears in both `app.env` and `app.envDefaults`, nulling only the `app.env` entry lets the `envDefaults` value apply again and the limit stays in force. With External Secrets, also drop the key from `externalSecrets.remoteRefs.app`, which keeps syncing it independently. Confirm what the pod will actually receive before rolling out: ```bash helm template sim ./helm/sim -f values.yaml | grep -A1 FREE_TABLE # expect no output diff --git a/helm/sim/Chart.yaml b/helm/sim/Chart.yaml index 464def36213..c60e2c89ce9 100644 --- a/helm/sim/Chart.yaml +++ b/helm/sim/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: sim description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents type: application -version: 1.5.3 +version: 1.5.4 appVersion: "v0.7.44" kubeVersion: ">=1.25.0-0" home: https://sim.ai diff --git a/helm/sim/README.md b/helm/sim/README.md index 7d51c81a39c..ada724db4e0 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -374,7 +374,9 @@ app: FREE_TABLE_ROWS_LIMIT: null ``` -Or on the CLI: `--set app.envDefaults.FREE_TABLES_LIMIT=null`. This works for `app.env` and `realtime.env` too, and in all three secret modes. +Or on the CLI: `--set app.envDefaults.FREE_TABLES_LIMIT=null`. + +**Null the key in every layer that sets it.** `null` deletes the key from the map you null, not from the pod — so if a key is set in both `app.env` and `app.envDefaults`, nulling only the `app.env` entry makes the inline `envDefaults` value apply again and the variable stays on the pod. The same holds for `realtime.env` / `realtime.envDefaults`. Under ESO there is a third source: a key mapped in `externalSecrets.remoteRefs.app` keeps being synced into the Secret regardless of `app.env`, so remove that mapping too. Rendering the manifest (below) is the reliable way to confirm the key is actually gone. **Setting the key to `""` instead does not remove it.** Every key under `app.env` in `values.yaml` ships as a `""` placeholder, so the templates have to treat an empty string as "the operator said nothing" — if they did not, the ten placeholders that collide with a real `app.envDefaults` value (`NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_BRAND_NAME`, `VERTEX_LOCATION`, `EMAIL_VERIFICATION_ENABLED`, …) would blank themselves out on every default install. An empty entry is a silent no-op; `null` is the deletion. diff --git a/helm/sim/tests/env-null-deletion_test.yaml b/helm/sim/tests/env-null-deletion_test.yaml index 63b877bf386..8a50ff957b6 100644 --- a/helm/sim/tests/env-null-deletion_test.yaml +++ b/helm/sim/tests/env-null-deletion_test.yaml @@ -101,6 +101,35 @@ tests: name: BILLING_CONCURRENCY_LIMIT_FREE value: "10" + - it: nulling only app.env lets a matching envDefaults value apply again + template: deployment-app.yaml + values: + - values/preset-free-limits.yaml + set: + <<: *defaults + app.env.FREE_TABLES_LIMIT: null + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLES_LIMIT + value: "3" + + - it: nulling both layers is what actually removes the key + template: deployment-app.yaml + values: + - values/preset-free-limits.yaml + - values/unset-free-limits.yaml + set: + <<: *defaults + app.env.FREE_TABLES_LIMIT: null + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: FREE_TABLES_LIMIT + value: "3" + - it: nulling a boot-critical key is still refused by the existing validator values: - values/preset-free-limits.yaml