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..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 @@ -249,6 +249,27 @@ 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. + +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 +``` + +`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/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 69a5025d300..ada724db4e0 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -358,11 +358,42 @@ 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`. + +**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. + +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). +> +> 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 ./helm/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..8a50ff957b6 --- /dev/null +++ b/helm/sim/tests/env-null-deletion_test.yaml @@ -0,0 +1,141 @@ +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 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 + 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"