improvement(helm): helm chart updates with security, ESO, and docs overhaul (#4565)

* improvement(helm): production-ready chart with security, ESO, and docs overhaul

Comprehensive Helm chart improvements bringing the chart up to industry
standards for security, secret management, and documentation.

Security
- Pod Security Standards "restricted" defaults on every pod and container
  (runAsNonRoot, allowPrivilegeEscalation=false, capabilities.drop=[ALL],
  seccompProfile=RuntimeDefault)
- automountServiceAccountToken=false on ServiceAccount and every pod
- NetworkPolicy egress blocks cloud metadata endpoints by default
- Sensitive app/realtime env keys auto-partitioned into chart-managed Secret
  via envFrom; no more plaintext secrets on container specs

Secret management
- Three modes: inline, existingSecret, ExternalSecrets Operator (ESO)
- ESO sync supports arbitrary sensitive keys
- Fail-fast template rendering when ESO enabled but sensitive key unmapped
- AWS/Azure/GCP example files document all three modes

Reliability
- Headless Services for both Postgres StatefulSets
- HPA-aware replicas (omits spec.replicas when autoscaling.enabled)
- PodDisruptionBudget auto-activates when replicaCount > 1
- Startup / liveness / readiness probes with distinct timings
- CronJob ttlSecondsAfterFinished for automatic cleanup

Chart hygiene
- Image tags default to Chart.AppVersion; pullPolicy IfNotPresent
- Optional image.digest pin for content-addressed deploys
- kubeVersion >=1.25.0-0 enforced
- Ollama pinned to 0.23.2; mount moved to /data

Documentation
- README rewritten in cert-manager / Bitnami style
- NOTES.txt with post-install guidance
- Example values files annotated with usage and secret-strategy guidance

* fix(helm): correct resource names in README (sim-sim-* → sim-*)

The sim.fullname helper collapses to the release name when the release
name contains the chart name. With the documented release name 'sim',
actual resources are 'sim-app', 'sim-postgresql', etc. — not the
'sim-sim-*' form previously documented. Fixes copy-paste commands in the
pre-1.0.0 upgrade walkthrough and several troubleshooting snippets.

Also expands the cronjobs component description to reflect the full set
of 13 scheduled jobs (was understated as just Gmail/Outlook polling).

* improvement(helm): split app/realtime env into Secret-bound + inline defaults

- Add app.envDefaults / realtime.envDefaults for chart-shipped operational
  tunables (rate limits, timeouts, IVM, feature-flag defaults, localhost URL
  fallbacks). Rendered inline on the container, not into the Secret
- Remove operational defaults from app.env / realtime.env so the chart-managed
  Secret stays minimal and External Secrets Operator users only map keys they
  actually set, not every chart default
- Skip an envDefaults key when the user explicitly sets it in env (K8s `env`
  overrides `envFrom`, so an inline default would otherwise mask a Secret
  value at runtime)
- Relax values.schema.json to allow empty strings on NEXT_PUBLIC_APP_URL,
  BETTER_AUTH_URL, NEXT_PUBLIC_SUPPORT_EMAIL (defaults supplied via envDefaults)

* fix(helm): address PR review — cronjob validation, ESO apiVersion, secret merge order, image guard

- CronJobs reference CRON_SECRET via secretKeyRef; fail-fast at template
  time when cronjobs.enabled=true and app.env.CRON_SECRET is empty so users
  get a clear error instead of a CreateContainerConfigError loop
- Default externalSecrets.apiVersion to "v1beta1" (supported by every ESO
  release since v0.7). The previous "v1" default targets only ESO v0.17+
- Swap merge order in secrets-app.yaml so app.env wins over realtime.env
  for shared keys (BETTER_AUTH_SECRET, BETTER_AUTH_URL, …) — both pods
  consume the same Secret via envFrom, so the app value must be canonical
- Add `required` guard on sim.image so an empty tag + empty digest +
  empty Chart.AppVersion surfaces as a clear template-time error instead
  of rendering an invalid `repo:` reference

* fix(helm): require critical secrets to be mapped when ESO is enabled

Previously, enabling externalSecrets without mapping BETTER_AUTH_SECRET /
ENCRYPTION_KEY / INTERNAL_API_SECRET (and CRON_SECRET when cronjobs are
on) rendered cleanly but produced CrashLoopBackOff at runtime with
cryptic missing-env errors. Fail at template time instead.

* fix(helm): auto-enable PDB when HPA minReplicas > 1

Previously the auto-enable predicate only checked the static
app.replicaCount, which defaults to 1 even when autoscaling is on
(HPA owns spec.replicas). PDB now also activates when
autoscaling.enabled=true and minReplicas > 1.

* fix(helm): prevent realtime envDefaults from masking app.env Secret values; add StatefulSet upgrade NOTES

- Realtime override-skip now considers keys set in either app.env or
  realtime.env. The shared app Secret is mounted via envFrom on the
  realtime pod, so a key set in app.env (e.g. NEXT_PUBLIC_APP_URL) would
  previously be masked by the realtime envDefault (inline env overrides
  envFrom in K8s).
- NOTES.txt now prints a StatefulSet orphan-delete reminder on upgrade,
  surfacing the immutable serviceName issue documented in the README.

* feat(helm): add Claude Skill for chart deployment

Adds a skill at helm/sim/.claude/skills/sim-helm/ that teaches agents how
to deploy and troubleshoot the Sim Helm chart: install path selection
(inline / existingSecret / ESO), secret generation, the values.yaml
four-layer mental model, common-failure troubleshooting, and the
pre-1.0.0 StatefulSet orphan-delete upgrade procedure.

Skill is loadable by Claude Code, Codex, and OpenCode via the standard
skills convention (directory name matches frontmatter name).

* docs(helm): add CRON_SECRET to TL;DR, dry-run, and example install headers

The validateSecrets guard requires CRON_SECRET when cronjobs.enabled=true
(the default), but the quickstart and example file install commands
omitted it — users following the docs hit a hard template-render failure.
Adds CRON_SECRET to README TL;DR, validate-the-install dry-run snippet,
and the install command headers in all example values files.

* fix(helm): require INTERNAL_API_SECRET in inline secret mode

The ESO coverage validator already required INTERNAL_API_SECRET, but the
inline validateSecrets path only checked BETTER_AUTH_SECRET, ENCRYPTION_KEY,
and CRON_SECRET — letting inline installs render successfully and then
crash at runtime when the realtime↔app shared auth secret was missing.
Adds the same fail-fast check to the inline path.

* docs(helm): surface INTERNAL_API_SECRET upgrade requirement in NOTES.txt

The new validateSecrets check makes app.env.INTERNAL_API_SECRET mandatory
on upgrade. Existing installs that never set it would hit a template
render failure with no in-context guidance. Adds an upgrade-only note
with the generation snippet and storage guidance alongside the existing
StatefulSet orphan-delete instructions.

* fix(helm): NetworkPolicy egress to OTEL collector + external-db example format

- Add app/realtime NetworkPolicy egress rules for the OpenTelemetry
  collector pod on ports 4317 (OTLP gRPC) and 4318 (OTLP HTTP) when
  telemetry.enabled=true. Without these, traces and metrics were silently
  dropped with connection-refused errors when both telemetry and
  networkPolicy were enabled.
- Migrate values-external-db.yaml from the legacy list-shaped egress
  format to the new {exceptCidrs, extraRules} object. The list form would
  replace the default object on merge and crash template rendering when
  the chart tried to access .exceptCidrs on a list.

* fix(helm): NOTES.txt no longer prints false secret warning for ESO users

The secrets-empty warning only checked app.secrets.existingSecret.enabled
before scanning app.env. ESO users intentionally leave app.env empty —
secrets come from the ESO-synced Secret — so every ESO install/upgrade
printed a misleading 'pods will fail to start' warning.

Reorders the branches so externalSecrets.enabled takes precedence: ESO
users now see a confirmation message with kubectl commands to verify the
ExternalSecret has synced. The empty-app.env warning only fires when
both ESO and existingSecret are disabled.

* fix(helm): existingSecret mode no longer drops app.env / realtime.env values

In existingSecret mode the chart-managed Secret is not rendered, so non-empty
values in app.env / realtime.env had nowhere to land — yet the envDefaults
skip logic still suppressed the matching defaults. Result: keys like
NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, and NODE_ENV silently went missing
on both pods (the example values-existing-secret.yaml hit this directly).

Both app and realtime deployments now inline non-empty values from app.env
(plus realtime.env on the realtime container) when existingSecret is enabled
and ESO is not. Inline / ESO modes are unchanged: inline still flows through
the chart-managed Secret, ESO still owns the synced Secret.

* fix(helm): correct realtime env overlay + filter chart-computed keys in existingSecret mode

Realtime: Sprig merge gives the first source precedence and treats "" as a
real value, so realtime.env empty defaults for shared keys shadowed
non-empty app.env values. Replace with deepCopy($appEnv) base + manual
non-empty overlay of $rtEnv.

Both deployments: exclude DATABASE_URL/SOCKET_SERVER_URL/OLLAMA_URL from
the existingSecret inline path so user-supplied values can't override
chart-computed ones via last-wins env semantics.

* fix(helm): skip envDefaults in existingSecret mode + document egress rename

In existingSecret mode the user's pre-existing Secret is the source of
truth (loaded via envFrom). Inlining localhost envDefaults for URL keys
(BETTER_AUTH_URL, NEXT_PUBLIC_APP_URL, ALLOWED_ORIGINS) silently shadowed
the Secret-bound values because K8s env always wins over envFrom. Skip
envDefaults entirely on both deployments when existingSecret is enabled.

Also call out the networkPolicy.egress shape change (list -> map with
exceptCidrs + extraRules) in the NOTES.txt upgrade block so operators
migrate their custom rules rather than silently losing them.

* fix(helm): copy-pasteable install commands in copilot + ESO examples

values-copilot.yaml: the install header was missing every required
copilot.server.env.* secret (AGENT_API_DB_ENCRYPTION_KEY, INTERNAL_API_SECRET,
LICENSE_KEY, SIM_BASE_URL, SIM_AGENT_API_KEY, REDIS_URL, one model key) plus
copilot.postgresql.auth.password. Pasting it as-is failed at template render.

values-external-secrets.yaml: NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, etc. were
declared under app.env / realtime.env. In ESO mode the chart-managed Secret
isn't rendered, so the validator (rightly) rejects keys in app.env that
aren't mapped under externalSecrets.remoteRefs. Moved non-secret URL/config
to envDefaults, which is inlined and not subject to the ESO mapping rule.

* polish(helm): configurable NetworkPolicy ingress peers + clearer API_ENCRYPTION_KEY comment

- networkPolicy.ingressFrom lets operators scope the ingress-controller
  rule to a specific namespace/podSelector. Defaults to a single empty
  peer (`- {}`), which is the explicit form of "any source" — same
  effective behavior as the old `from: []` but unambiguous across CNIs.
  To restrict, override with e.g.:
    networkPolicy:
      ingressFrom:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx

- API_ENCRYPTION_KEY comment: drop the "must be exactly 64 hex
  characters" phrasing that sat awkwardly next to `openssl rand -hex 32`.
  The generation command already produces the required length.

* test(helm): add helm-unittest suites + CI workflow + ci values matrix

- 7 helm-unittest suites covering smoke, validators, secret modes,
  envDefaults secret-mode-aware inlining (round-9 regression net),
  chart-computed env keys (round-8 regression net), NetworkPolicy
  shape, and PDB/HPA conditional rendering (38 tests, ~265ms).
- ci/*.yaml render fixtures for default, production, existingSecret,
  ESO, and external-db install modes.
- GitHub Actions workflow runs helm lint --strict, helm unittest,
  helm template across the ci matrix, and kubeconform validation
  against Kubernetes 1.30 schemas.
- CONTRIBUTING.md documents how to run the same gates locally.

* test(helm): add helm test hook + kind apiserver dry-run in CI

- New templates/tests/test-connection.yaml renders a Pod with
  helm.sh/hook=test that wgets the app Service (and realtime when
  enabled). Lets users run `helm test <release>` after install for
  a real in-cluster connectivity check. Restricted PSS context.
- tests.* values block (image, timeoutSeconds, resources) is the
  knob to disable or tune the probe; documented in values.schema.json.
- 3 helm-unittest tests cover the hook annotations, PSS context,
  and tests.enabled=false skip path (41 tests total).
- New CI job spins up a kind v1.30 cluster and runs
  `kubectl apply --dry-run=server` against the rendered manifests
  for the CRD-free ci fixtures (default / existing-secret /
  external-db). Catches admission and validation issues the static
  kubeconform schema check can't see.

* chore(helm): remove pre-1.0.0 upgrade fluff + tighten .helmignore

This is the 1.0.0 release of the chart — there is no pre-1.0.0
predecessor for users to upgrade from, so all of the dedicated upgrade
narration was hypothetical.

- Drop the 'Upgrading from a pre-1.0.0 build' README section and the
  matching troubleshooting entry.
- Drop the .Release.IsUpgrade block from NOTES.txt: items 5 (StatefulSet
  orphan-delete), 6 (INTERNAL_API_SECRET 'new in 1.0.0'), 7
  (networkPolicy.egress shape change). Each described a migration off a
  chart version that never shipped.
- Delete references/upgrade-pre-1.0.0.md and remove the corresponding
  pointers from SKILL.md.
- Anchor .helmignore patterns to chart root so /tests/ (unit suites)
  and /examples/ are dropped from the packaged tarball without also
  dropping templates/tests/ (the helm test hook).

* chore(helm): drop CI workflow + ci/ fixtures + CONTRIBUTING.md

The helm-unittest suites in helm/sim/tests/ and the helm test hook
in helm/sim/templates/tests/ stay — those are chart-internal quality
scaffolding, not CI. Removed:

- .github/workflows/helm-chart.yml
- helm/sim/ci/*.yaml (5 render fixtures used only by the workflow)
- helm/sim/CONTRIBUTING.md (mostly documented those gates)
- dead /ci/ and /CONTRIBUTING.md entries in .helmignore

* feat(helm): pod rollout on Secret change + topologySpreadConstraints

- Add checksum/secret pod annotations on app, realtime, and copilot
  Deployments (plus checksum/config on app when branding ConfigMap is
  enabled). Closes the long-standing footgun where 'helm upgrade' with
  a changed Secret would silently leave pods running the old values
  until a manual rollout restart.
- New top-level topologySpreadConstraints value (and sim.topologySpreadConstraints
  helper) applied to app and realtime Deployments. Mirrors how affinity
  and tolerations are plumbed; users supply their own labelSelector
  to mirror Bitnami convention.
- 5 helm-unittest cases cover the checksum annotations and topology
  spread rendering (46 tests total).

* fix(helm): drop empty-string shadowing in app/realtime env merge

Sprig 'merge' treats "" as a real value, so a default-empty
app.env.BETTER_AUTH_URL would shadow a non-empty realtime.env override
and the URL would never reach the rendered Secret. Replace 'merge'
with an explicit two-pass overlay that filters empties before writing,
mirroring the same pattern already used in deployment-realtime.yaml's
existingSecret block.

Adds two regression tests: realtime.env-only value reaches the Secret
when app.env is empty, and app.env still wins on collision when both
are non-empty (48 tests total).

* fix(helm): make topologySpreadConstraints per-component to match docstring

Greptile flagged that sim.topologySpreadConstraints helper docstring promised
per-component config (.Values.app, .Values.realtime, ...) but call sites
passed .Values, so any app.topologySpreadConstraints / realtime.topologySpreadConstraints
set by the user was silently dropped. The single global key also prevented
distinct app-vs-realtime spread rules.

Pass .Values.app / .Values.realtime to the helper at each call site; move
the top-level topologySpreadConstraints key into both component sections in
values.yaml. Adds a regression test that app constraints don't leak onto
the realtime pod.

* fix(helm): allow cron pods through app NetworkPolicy

Cursor flagged that when networkPolicy.enabled=true and cronjobs.enabled=true
(the recommended production config), the app NetworkPolicy only allowed
ingress from realtime and the ingress controller — silently blocking every
cron pod's HTTP call to /api/schedules/execute, webhook polls, etc. All 13
default cronjobs would fail.

Tag cron pods with a stable simstudio.ai/component-group: cronjob label so
the app NetworkPolicy can allow them with a single rule (no per-job
enumeration). Rule is conditional on cronjobs.enabled. Adds positive and
negative regression tests.
This commit is contained in:
Waleed
2026-05-12 10:44:33 -07:00
committed by GitHub
parent 1b94424b4b
commit 9d2dd8f550
49 changed files with 3314 additions and 1072 deletions
+154
View File
@@ -0,0 +1,154 @@
---
name: sim-helm
description: Install, upgrade, and operate the Sim Helm chart on Kubernetes. Covers install path selection (inline / existingSecret / External Secrets Operator), required secret generation, the values.yaml mental model (env vs envDefaults vs Secret), and common failure triage. Invoke when a user asks about deploying Sim to a cluster, authoring a Sim values.yaml, debugging a Sim pod that won't start, upgrading a Sim release, or wiring Sim into a secret manager.
license: Apache-2.0
---
# Sim Helm Chart — Operations Skill
This skill helps an agent deploy and operate the **Sim** Helm chart at `helm/sim/` in the [simstudioai/sim](https://github.com/simstudioai/sim) repository. Use it when the user is installing, upgrading, troubleshooting, or authoring values for the Sim chart.
The skill is **diagnostic-first**: capture context, classify the situation, load only the references that apply, then act. Do not dump the README at the user. Do not invent values that are not in their current state.
---
## Workflow — follow in order
### 1. Capture context
Before recommending anything, ask (or infer from the conversation) all of these. **Never skip this step.** A wrong assumption here corrupts every downstream step.
| Question | Why it matters |
|---|---|
| Cluster: EKS / GKE / AKS / OpenShift / kind / other? | Storage class, ingress class, identity provider differ |
| Secret strategy: inline `--set`, pre-existing K8s Secret, or External Secrets Operator (ESO)? | The chart has three distinct code paths |
| Postgres: chart-bundled, or external (RDS / Cloud SQL / Azure DB)? | Different value blocks (`postgresql.*` vs `externalDatabase.*`) |
| Public-facing? Ingress class? TLS? | `ingress.enabled`, `ingress.className`, cert-manager wiring |
| HA? (target replicas) | Drives `autoscaling.enabled`, `app.replicaCount`, PDB activation |
| Existing values.yaml the user is editing? | Always read it before proposing a diff — never write blind |
If the user has a `values.yaml`, read it. If they don't, ask before writing one.
### 2. Diagnose
Map the user's request to one of these categories and load the matching reference(s):
| Situation | Reference |
|---|---|
| User wants to install for the first time | `references/install-paths.md` then `references/secrets.md` |
| User needs to generate the required secrets | `references/secrets.md` |
| User asks "what does this value do" / wants to author values.yaml | `references/values-model.md` |
| Pod won't start, error message, `CrashLoopBackOff`, image pull error, ingress not routing | `references/troubleshooting.md` |
| User asks about ESO / Vault / AWS Secrets Manager / Azure Key Vault / GCP Secret Manager | `references/install-paths.md` (ESO section) |
| User asks "is X production-ready" / autoscaling / network policy / security context | Read the README's "Production checklist" section directly — no separate reference |
Load **only** what the situation requires. Loading every reference burns tokens and produces vague answers.
### 3. Propose
When proposing values changes:
- Show the **minimal diff** against the user's current values.yaml. Don't rewrite the file.
- Name the **risk** (e.g., "this puts the secret in `helm get values` output — fine for dev, not for prod").
- Name the **rollback** (e.g., "if this breaks, `helm rollback sim 1` reverts").
- Cite the canonical source (`helm/sim/values.yaml` line numbers, README section, or this skill's reference file).
### 4. Validate before applying
Always run these before telling the user to `helm install` / `helm upgrade`:
```bash
# Schema + value validation
helm lint helm/sim --values <user-values>.yaml
# Render full manifest set to catch template errors
helm template sim helm/sim --values <user-values>.yaml > /tmp/render.yaml
# For upgrades, render against the live release first
helm upgrade --dry-run sim helm/sim --values <user-values>.yaml
```
If lint or template fails, fix the values — do not work around chart validation. The chart's `fail` statements exist to catch misconfigurations that would otherwise surface as `CrashLoopBackOff` at runtime.
### 5. Deliver
Every recommendation should include:
- The exact command(s) to run
- A one-line summary of what will change
- The success signal (e.g., "`kubectl rollout status deploy/sim-app` returns Ready")
- The rollback command if something breaks
---
## Quick reference — the three secret modes
| Mode | When | Code path |
|---|---|---|
| **Inline (`--set`)** | Dev / kind / dry-run only. Values leak into `helm get values`. | `app.env.<KEY>: "..."` |
| **Pre-existing Secret** | GitOps with Sealed Secrets / SOPS, or hand-managed Secrets. Chart references a Secret you create. | `app.secrets.existingSecret.enabled: true` + `.name` |
| **External Secrets Operator (recommended for prod)** | Vault, AWS SM, Azure KV, GCP SM. Chart renders an `ExternalSecret` that ESO syncs. | `externalSecrets.enabled: true` + `secretStoreRef` + `remoteRefs.app.<KEY>` |
These modes are **mutually exclusive** for the app Secret. ESO takes precedence over inline. `existingSecret` takes precedence over inline. The chart **fails template rendering** when ESO is enabled and a required key (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, plus `CRON_SECRET` when cronjobs are enabled) is neither in `app.env` nor mapped in `remoteRefs.app` — see `references/install-paths.md`.
---
## Quick reference — the four required secrets
| Key | Generate with | Notes |
|---|---|---|
| `BETTER_AUTH_SECRET` | `openssl rand -hex 32` | Session signing |
| `ENCRYPTION_KEY` | `openssl rand -hex 32` | App-level encryption |
| `INTERNAL_API_SECRET` | `openssl rand -hex 32` | Service-to-service auth (app ↔ realtime) |
| `CRON_SECRET` | `openssl rand -hex 32` | Required iff `cronjobs.enabled=true` (default true) |
Optional but commonly needed:
| Key | Generate with | Notes |
|---|---|---|
| `API_ENCRYPTION_KEY` | `openssl rand -hex 32` | Must be **exactly 64 hex chars**. Required to encrypt user API keys at rest. |
| `postgresql.auth.password` | `openssl rand -base64 24 \| tr -d '/+='` | Only if using chart-bundled Postgres. Must match `^[a-zA-Z0-9._-]+$` for DATABASE_URL compatibility. |
See `references/secrets.md` for storage patterns and rotation guidance.
---
## Rules of engagement
These are non-negotiable. Violating any of these has burned users in the past.
1. **Never recommend `--set` for production secrets.** They land in `helm get values` and Helm release history. Direct users to `existingSecret` or ESO.
2. **Never set `image.tag: latest`.** The chart defaults to `Chart.AppVersion` for a reason — reproducible rollouts. If the user pinned `latest`, push back.
3. **Never edit chart templates to work around a `fail` statement.** The validation exists because a misconfiguration would otherwise surface as a runtime CrashLoopBackOff with cryptic env errors.
4. **Never drop `automountServiceAccountToken: false`** unless the workload genuinely needs in-cluster API access (Sim's app/realtime/postgres pods do not).
5. **Never `kubectl delete sts` without `--cascade=orphan`** on a live Postgres. It deletes the pods and PVCs.
6. **Never tell a user "the chart works on your cluster" without `helm lint` + `helm template` against their values.** Static reading is not validation.
7. **Always confirm before `helm uninstall` in a shared namespace.** PVCs survive but other namespace resources may not.
---
## When the user is stuck and you can't diagnose
Get logs from every component in parallel. This single block answers ~80% of "it's broken" questions:
```bash
kubectl --namespace <ns> get pods,events --sort-by='.lastTimestamp'
kubectl --namespace <ns> logs deploy/sim-app --tail=200
kubectl --namespace <ns> logs deploy/sim-realtime --tail=200
kubectl --namespace <ns> logs sts/sim-postgresql --tail=200
kubectl --namespace <ns> logs job/sim-migrations --tail=200 2>/dev/null
kubectl --namespace <ns> describe pod -l app.kubernetes.io/name=sim
```
Then map the symptom to `references/troubleshooting.md`.
---
## What this skill does **not** cover
- Sim application configuration beyond env vars (provider keys, knowledge base setup, etc.) — that's the Sim app docs at https://docs.sim.ai
- Kubernetes cluster setup (creating an EKS cluster, installing ingress-nginx, etc.) — that's cloud-provider docs
- Authoring new chart templates — that's `helm/sim/templates/_helpers.tpl` and the chart's own contributor docs
- Running Sim outside Kubernetes (Docker Compose, bare-metal) — see the root `README.md`
If the user's question falls outside this scope, say so and point them at the right doc.
@@ -0,0 +1,192 @@
# Install Path Selection
Three mutually-exclusive paths for the app Secret. Pick exactly one. The chart enforces this at template time.
## Decision tree
```
Is this a production install?
├── No (dev / kind / minikube / dry-run)
│ → Inline `--set` is fine. Skip to "Path A".
│
└── Yes
│
Do you already manage secrets with Vault / AWS Secrets Manager /
Azure Key Vault / GCP Secret Manager / 1Password Connect?
│
├── Yes → External Secrets Operator. Path C.
│
└── No
│
Do you use GitOps with Sealed Secrets, SOPS, or
hand-managed Kubernetes Secrets?
│
├── Yes → Pre-existing Secret. Path B.
│
└── No → Install ESO and go to Path C.
(Don't skip to inline `--set` for prod —
secrets land in `helm get values` and release history.)
```
---
## Path A — Inline `--set` (dev only)
```bash
helm install sim ./helm/sim \
--namespace sim --create-namespace \
--set app.env.BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
--set app.env.ENCRYPTION_KEY=$(openssl rand -hex 32) \
--set app.env.INTERNAL_API_SECRET=$(openssl rand -hex 32) \
--set app.env.CRON_SECRET=$(openssl rand -hex 32) \
--set postgresql.auth.password=$(openssl rand -base64 24 | tr -d '/+=')
```
The chart generates a `Secret` named `<release>-app-secrets` containing every non-empty key from `app.env` + `realtime.env`. Both `app` and `realtime` Deployments mount it via `envFrom`.
**Risks:**
- Secrets are visible in `helm get values <release>` and `helm history <release>`.
- Anyone with read access to the release's ConfigMap (`sh.helm.release.v1.<release>.v<N>`) can recover the secrets — they're stored base64-encoded inside.
---
## Path B — Pre-existing Kubernetes Secret
Create the Secret first, then point the chart at it.
```bash
kubectl create namespace sim
kubectl create secret generic sim-app-secrets --namespace sim \
--from-literal=BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
--from-literal=ENCRYPTION_KEY=$(openssl rand -hex 32) \
--from-literal=INTERNAL_API_SECRET=$(openssl rand -hex 32) \
--from-literal=CRON_SECRET=$(openssl rand -hex 32)
kubectl create secret generic sim-postgres-secret --namespace sim \
--from-literal=POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
```
```yaml
# values.yaml
app:
secrets:
existingSecret:
enabled: true
name: sim-app-secrets
postgresql:
auth:
existingSecret:
enabled: true
name: sim-postgres-secret
passwordKey: POSTGRES_PASSWORD
```
**The chart cannot introspect your Secret.** If you forget a required key, the pod will fail at runtime with `CreateContainerConfigError: secret key "X" not found`. The required keys are: `BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, plus `CRON_SECRET` when cronjobs are enabled.
For GitOps (Sealed Secrets / SOPS), seal/encrypt the Secret YAML before committing — never commit a plain `kubectl create secret` output.
---
## Path C — External Secrets Operator (production recommended)
ESO syncs from your existing secret store (Vault, AWS SM, Azure KV, GCP SM, etc.) into a Kubernetes Secret on a refresh interval. The chart renders the `ExternalSecret` resource; ESO does the syncing.
### Prerequisites
1. Install ESO once per cluster:
```bash
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace
```
2. Create a `ClusterSecretStore` (or namespace-scoped `SecretStore`) that points at your secret manager. ESO's docs cover the auth wiring for each provider.
### Values
```yaml
externalSecrets:
enabled: true
apiVersion: v1beta1 # v1beta1 works on ESO >= 0.7. Bump to v1 only on ESO >= 0.17.
refreshInterval: 1h
secretStoreRef:
name: my-cluster-secret-store
kind: ClusterSecretStore # or SecretStore for namespace-scoped
remoteRefs:
app:
BETTER_AUTH_SECRET: sim/app/better-auth-secret
ENCRYPTION_KEY: sim/app/encryption-key
INTERNAL_API_SECRET: sim/app/internal-api-secret
CRON_SECRET: sim/app/cron-secret # required iff cronjobs.enabled
# Optional but commonly mapped:
API_ENCRYPTION_KEY: sim/app/api-encryption-key
OPENAI_API_KEY: sim/providers/openai
postgresql:
password: sim/postgresql/password # required if postgresql.enabled
externalDatabase:
password: sim/postgresql/password # required if externalDatabase.enabled
# Leave app.env empty (or only set non-secret values like NEXT_PUBLIC_APP_URL).
app:
env: {}
```
### Fail-fast behavior
The chart will refuse to render if:
- `externalSecrets.enabled=true` and any of `BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET` (or `CRON_SECRET` when cronjobs are enabled) is **neither** set in `app.env` **nor** mapped in `remoteRefs.app`. Error message names the missing key.
- A key is set in `app.env` with a non-empty value but not mapped in `remoteRefs.app` (would be silently dropped from the rendered Secret).
These checks catch the "renders cleanly, CrashLoopBackOffs at runtime" failure mode that plagued earlier chart versions.
### Remote ref shapes
Each `remoteRefs.app.<KEY>` value can be either:
```yaml
# Shorthand — just the path/key in the store
BETTER_AUTH_SECRET: sim/app/better-auth-secret
```
```yaml
# Full form — pass any field ESO supports
BETTER_AUTH_SECRET:
key: sim/app/better-auth-secret
property: value # for stores that return JSON
version: "v3" # pin a specific version
decodingStrategy: Base64 # for base64-stored values
```
---
## Cross-cutting: things that are NOT secrets
Operational tunables (rate limits, timeouts, IVM pool size, branding) live in `app.envDefaults` and `realtime.envDefaults`. They're rendered as **inline `env:`** on the Deployment, not written to the Secret. See `values-model.md` for the full mental model.
Don't try to push these into ESO — they're not sensitive, they'd just bloat the secret store.
---
## Verifying your choice
After `helm install`:
```bash
# What Secret will the pods mount?
helm template sim helm/sim -f my-values.yaml | grep -A2 "envFrom:"
# For ESO: did the ExternalSecret render?
helm template sim helm/sim -f my-values.yaml | grep -B1 -A10 "kind: ExternalSecret"
# For existingSecret: is your pre-created Secret referenced?
helm template sim helm/sim -f my-values.yaml | grep -E "name: .*-app-secrets"
```
For ESO, after `helm install`, verify the sync:
```bash
kubectl get externalsecret -n sim
kubectl describe externalsecret <release>-app-secrets -n sim
# Status should show 'SecretSynced=True'
```
@@ -0,0 +1,88 @@
# Secret Generation
The Sim chart requires four cryptographic secrets at install time. Generate them once and store them in your chosen path (see `install-paths.md`). Never reuse these across environments.
## Generate all four at once
```bash
export BETTER_AUTH_SECRET=$(openssl rand -hex 32)
export ENCRYPTION_KEY=$(openssl rand -hex 32)
export INTERNAL_API_SECRET=$(openssl rand -hex 32)
export CRON_SECRET=$(openssl rand -hex 32)
# Optional but commonly needed:
export API_ENCRYPTION_KEY=$(openssl rand -hex 32) # MUST be exactly 64 hex chars
export POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=') # if using chart-bundled Postgres
```
## What each secret does
| Key | Purpose | Length | Rotation impact |
|---|---|---|---|
| `BETTER_AUTH_SECRET` | Signs user session JWTs (Better Auth) | 32 bytes = 64 hex chars | Rotating invalidates all active sessions — users must re-login |
| `ENCRYPTION_KEY` | App-level encryption for sensitive fields | 32 bytes = 64 hex chars | Rotating breaks decryption of existing data — requires migration |
| `INTERNAL_API_SECRET` | Shared auth between `sim-app` ↔ `sim-realtime` pods | 32 bytes = 64 hex chars | Both deployments must roll together — temporary realtime errors during the rollout |
| `CRON_SECRET` | Authenticates scheduled CronJob pods to the app | 32 bytes = 64 hex chars | Rotating just needs `helm upgrade`; next cron run uses the new value |
| `API_ENCRYPTION_KEY` (optional) | Encrypts user-stored API keys (OpenAI tokens, etc.) at rest in Postgres | **Exactly 64 hex chars** (the app rejects other lengths) | Without it, keys are stored plain. Once set, never rotate without a migration |
| `POSTGRES_PASSWORD` (chart-bundled Postgres only) | Postgres superuser password | Any length ≥ 12 chars matching `^[a-zA-Z0-9._-]+$` | Requires Postgres pod restart + app rollout |
The `^[a-zA-Z0-9._-]+$` constraint on the Postgres password exists because the chart embeds the password into `DATABASE_URL` without URL-encoding. The `tr -d '/+='` strips the three problematic characters from `openssl rand -base64` output. The chart enforces this regex at template time.
## Storage by path
### Path A (inline `--set`)
Pass each on the command line — see `install-paths.md` Path A.
### Path B (pre-existing Kubernetes Secret)
```bash
kubectl create namespace sim
kubectl create secret generic sim-app-secrets --namespace sim \
--from-literal=BETTER_AUTH_SECRET=$BETTER_AUTH_SECRET \
--from-literal=ENCRYPTION_KEY=$ENCRYPTION_KEY \
--from-literal=INTERNAL_API_SECRET=$INTERNAL_API_SECRET \
--from-literal=CRON_SECRET=$CRON_SECRET \
--from-literal=API_ENCRYPTION_KEY=$API_ENCRYPTION_KEY
kubectl create secret generic sim-postgres-secret --namespace sim \
--from-literal=POSTGRES_PASSWORD=$POSTGRES_PASSWORD
```
For GitOps, run the `kubectl create secret ... --dry-run=client -o yaml` and pipe through `kubeseal` (Sealed Secrets) or `sops` before committing.
### Path C (External Secrets Operator)
Push the generated values into your secret manager first. Example for AWS Secrets Manager:
```bash
aws secretsmanager create-secret --name sim/app/better-auth-secret --secret-string "$BETTER_AUTH_SECRET"
aws secretsmanager create-secret --name sim/app/encryption-key --secret-string "$ENCRYPTION_KEY"
aws secretsmanager create-secret --name sim/app/internal-api-secret --secret-string "$INTERNAL_API_SECRET"
aws secretsmanager create-secret --name sim/app/cron-secret --secret-string "$CRON_SECRET"
aws secretsmanager create-secret --name sim/app/api-encryption-key --secret-string "$API_ENCRYPTION_KEY"
aws secretsmanager create-secret --name sim/postgresql/password --secret-string "$POSTGRES_PASSWORD"
```
Then map the paths in `externalSecrets.remoteRefs.app` (see `install-paths.md` Path C).
## Rotation
Sim doesn't have built-in rotation hooks. The procedure is:
1. Generate a new value, store it.
2. `helm upgrade` (or let ESO pick up the change on its next refresh).
3. Restart the affected workloads to force re-read of `envFrom`:
```bash
kubectl rollout restart deploy/sim-app deploy/sim-realtime -n sim
```
4. For `BETTER_AUTH_SECRET`: expect a wave of `401`s as old sessions invalidate.
5. For `ENCRYPTION_KEY` / `API_ENCRYPTION_KEY`: **do not rotate** without an explicit data migration. Existing ciphertext becomes undecryptable.
## What NOT to do
- **Don't reuse the same secret across dev/staging/prod.** A leak in one tier compromises all.
- **Don't commit secrets to git, even in private repos.** Use sealed-secrets / SOPS / ESO.
- **Don't paste secrets into Slack, Discord, GitHub issues, or screenshots.** Treat them like database passwords.
- **Don't store secrets in `values.yaml` files committed to git.** That's worse than `--set` — values files persist forever in history.
- **Don't generate secrets with weak entropy.** No `date | md5`, no `password123`, no developer's birthday. `openssl rand` or `/dev/urandom` only.
@@ -0,0 +1,183 @@
# Troubleshooting
Map symptom → root cause → fix. Always run the diagnostic block first, then match.
## Diagnostic block (run this first)
```bash
NS=sim # adjust to your namespace
kubectl --namespace $NS get pods,events --sort-by='.lastTimestamp'
kubectl --namespace $NS describe pod -l app.kubernetes.io/instance=sim
kubectl --namespace $NS logs deploy/sim-app --tail=200
kubectl --namespace $NS logs deploy/sim-realtime --tail=200
kubectl --namespace $NS logs sts/sim-postgresql --tail=200
kubectl --namespace $NS logs job/sim-migrations --tail=200 2>/dev/null || true
```
---
## `Error: execution error at (sim/templates/...): app.env.BETTER_AUTH_SECRET is required for production deployment`
**Cause:** `helm install` / `helm upgrade` ran without the required secrets set.
**Fix:** Generate and pass the four required secrets (see `references/secrets.md`). For production, switch to `existingSecret` or ESO instead of `--set` (see `references/install-paths.md`).
---
## `Error: execution error ...: Required key 'X' is missing: externalSecrets.enabled=true but the key is neither set in app.env nor mapped in externalSecrets.remoteRefs.app`
**Cause:** ESO is enabled but one of the required keys (`BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, or `CRON_SECRET` when cronjobs are enabled) isn't mapped via `remoteRefs.app`. The chart fails fast at template time to avoid CrashLoopBackOff later.
**Fix:** Add the mapping:
```yaml
externalSecrets:
remoteRefs:
app:
<KEY>: path/in/your/secret/store
```
Or, if you really don't need cronjobs, set `cronjobs.enabled=false` to drop the `CRON_SECRET` requirement.
---
## `Error: execution error ...: Key 'X' is set in app.env but externalSecrets.enabled=true and externalSecrets.remoteRefs.app.X is not configured`
**Cause:** ESO is enabled. The chart-managed Secret is not rendered. A key set in `app.env` would be silently dropped — pods would start with the wrong (missing) value.
**Fix:** Either map the key via `remoteRefs.app.X` so ESO syncs it, OR remove the key from `app.env` if you don't need it.
---
## App pods stuck in `CrashLoopBackOff`
Get the logs first:
```bash
kubectl logs --namespace sim deploy/sim-app --tail 200
```
Match the error:
| Log line | Cause | Fix |
|---|---|---|
| `Invalid env: ... NEXT_PUBLIC_APP_URL: Invalid url` | URL field set to empty string or invalid format | Set `app.env.NEXT_PUBLIC_APP_URL` to a valid URL — `https://sim.example.com` in prod, `http://localhost:3000` in dev |
| `getaddrinfo ENOTFOUND ... -postgresql` / `connect ECONNREFUSED` | App can't reach Postgres | Check `kubectl get pod -l app.kubernetes.io/name=postgresql` is `Running`; check `postgresql.auth.password` matches the password in the Secret |
| `password authentication failed for user "sim"` | Postgres password rotated but app pod wasn't restarted, OR password contains URL-unsafe chars | `kubectl rollout restart deploy/sim-app -n sim`; regenerate password with `openssl rand -base64 24 \| tr -d '/+='` |
| `BETTER_AUTH_SECRET is missing` / `INTERNAL_API_SECRET is required` | Required env var not present in the Secret | Verify with `kubectl get secret sim-app-secrets -o jsonpath='{.data}' \| jq 'keys'`; if missing, fix your secret strategy |
| `Migration failed` or app starts before migration | Migration Job hasn't completed | `kubectl logs job/sim-migrations -n sim`; rerun with `kubectl delete job/sim-migrations && helm upgrade ...` |
---
## Image pull errors (`ErrImagePull` / `ImagePullBackOff`)
```bash
kubectl describe pod -l app.kubernetes.io/name=sim -n sim | grep -A5 "Failed\|Warning"
```
| Cause | Fix |
|---|---|
| Private registry, no pull secret | Set `global.imagePullSecrets: [{name: my-regcred}]` and create the regcred Secret: `kubectl create secret docker-registry my-regcred --docker-server=... --docker-username=... --docker-password=...` |
| Image tag doesn't exist in the registry | `helm get values sim`, check the rendered `image.tag`; correct it or fall back to `Chart.AppVersion` |
| Air-gapped cluster | Mirror the image to your internal registry, set `global.imageRegistry=my-registry.example.com` |
---
## Postgres pod `Pending`
```bash
kubectl describe pvc --namespace sim
```
Always one of:
| `Events` says | Cause | Fix |
|---|---|---|
| `no persistent volumes available for this claim and no storage class is set` | No default StorageClass | Set `global.storageClass: <your-class>` or annotate one as default |
| `Failed to provision volume with StorageClass "X"` | No PV provisioner installed | Install one (`local-path-provisioner` for kind, EBS CSI for EKS, PD CSI for GKE, Azure Disk CSI for AKS) |
| `only ReadWriteOnce access modes are supported` | StorageClass doesn't support RWO | Pick a different `global.storageClass` |
| `pod has unbound immediate PersistentVolumeClaims` and no events | StorageClass uses `WaitForFirstConsumer` and pod isn't schedulable | Check pod's `nodeSelector` / `tolerations` against your nodes |
---
## Ingress not routing
```bash
kubectl get ingress -n sim
kubectl describe ingress -n sim
```
| Cause | Fix |
|---|---|
| No `ADDRESS` in `kubectl get ingress` | Ingress controller not installed — install `ingress-nginx`, AWS LBC, GCP LB controller, etc. |
| `ingressClassName` doesn't match installed controller | `kubectl get ingressclass` to list installed classes, set `ingress.className` to match |
| Address is set but DNS resolves to wrong IP | `dig <your-host>` — point DNS at the ingress controller's external IP / LoadBalancer / CNAME |
| TLS cert errors | If using cert-manager, check `kubectl describe certificate -n sim`; verify `ingress.tls.issuerRef` |
| `503 Service Unavailable` | Ingress routing is fine but app pod isn't `Ready` — go back to the diagnostic block |
---
## CronJob pods fail with `CreateContainerConfigError: couldn't find key CRON_SECRET in Secret`
**Cause:** `cronjobs.enabled=true` (the default) but `CRON_SECRET` isn't in the app Secret. Two paths:
1. Inline mode: `app.env.CRON_SECRET=""` — the chart will fail at template time. If you somehow got past that, regenerate and set it.
2. Existing-Secret mode: your pre-created Secret doesn't include `CRON_SECRET`. Add it:
```bash
kubectl patch secret sim-app-secrets -n sim --type='json' \
-p='[{"op":"add","path":"/data/CRON_SECRET","value":"'$(openssl rand -hex 32 | base64)'"}]'
```
3. ESO mode: missing `remoteRefs.app.CRON_SECRET` mapping. Add it.
Or set `cronjobs.enabled=false` if you don't need scheduled jobs.
---
## `app.kubernetes.io/managed-by: Helm` collisions when upgrading
You installed once with `--name foo`, then tried to install again with `--name bar` into the same namespace. Resources collide on labels.
**Fix:** Use distinct namespaces per release, or `helm uninstall foo -n <ns>` first.
---
## Pods get OOMKilled
```bash
kubectl get events -n sim --field-selector reason=OOMKilling
```
Bump the relevant resource limit. Defaults:
| Workload | Default request | Default limit |
|---|---|---|
| `app` | `1000m` CPU / `4Gi` memory | `2000m` CPU / `8Gi` memory |
| `realtime` | `250m` CPU / `512Mi` memory | `500m` CPU / `1Gi` memory |
| `postgresql` | `250m` CPU / `512Mi` memory | `1000m` CPU / `2Gi` memory |
Override in values:
```yaml
app:
resources:
requests:
memory: 8Gi
limits:
memory: 16Gi
```
---
## Logs to grab when filing a support issue
```bash
helm version
kubectl version --short
helm get values sim -n sim --revision $(helm history sim -n sim | tail -1 | awk '{print $1}')
kubectl get all,pvc,ingress,externalsecret -n sim -o wide
kubectl describe pods -n sim -l app.kubernetes.io/instance=sim | head -200
kubectl logs --tail=500 -n sim deploy/sim-app
kubectl logs --tail=500 -n sim deploy/sim-realtime
```
Redact any secrets before sharing.
@@ -0,0 +1,151 @@
# The values.yaml Mental Model
The Sim chart splits configuration across **four** layers. Understanding which layer owns which key is the difference between a working install and a five-hour debugging session.
## The four layers
```
┌───────────────────────────────────────────────────────────────────────────┐
│ Layer 1: app.env / realtime.env │
│ → Written to a chart-managed Kubernetes Secret │
│ → Mounted on pods via envFrom: secretRef │
│ → Use for: anything sensitive OR anything that varies per-environment │
│ → Examples: BETTER_AUTH_SECRET, NEXT_PUBLIC_APP_URL, OPENAI_API_KEY │
└───────────────────────────────────────────────────────────────────────────┘
│
│ env: (inline) overrides envFrom (Secret)
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Layer 2: app.envDefaults / realtime.envDefaults │
│ → Rendered as inline env: on the Deployment │
│ → SKIPPED for any key already set in app.env (or realtime.env) │
│ → Use for: operational tunables and safe fallback defaults │
│ → Examples: NODE_ENV=production, RATE_LIMIT_*, IVM_*, brand defaults │
└───────────────────────────────────────────────────────────────────────────┘
│
│ chart-computed values are always inline
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Layer 3: chart-computed (inline env: on the Deployment) │
│ → DATABASE_URL, SOCKET_SERVER_URL, OLLAMA_URL │
│ → Derived from postgresql.* / externalDatabase.* / service.* values │
│ → CANNOT be overridden via app.env — chart filters them out │
└───────────────────────────────────────────────────────────────────────────┘
│
│ extraEnvVars appends at the end
▼
┌───────────────────────────────────────────────────────────────────────────┐
│ Layer 4: extraEnvVars (escape hatch) │
│ → Raw env: list appended after everything else │
│ → Use for: things the chart doesn't model (valueFrom: configMapKeyRef, │
│ custom fieldRef, downward API) │
└───────────────────────────────────────────────────────────────────────────┘
```
## Why this layering exists
**Single source of truth per concern.** Secrets live in a Secret. Operational defaults live where users can override them. Chart-computed values live where the chart can authoritatively compute them.
**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.<KEY>` or it's silently missing. Layers 2–4 are unaffected by ESO. Putting operational tunables in `envDefaults` instead of `env` means ESO users don't have to map dozens of tunables — just the real secrets.
**Backwards compatibility.** Layer 2 was added in chart 1.0.0 (formerly all defaults lived in `app.env`). The override-skip logic in the Deployment template means existing users who set values in `app.env` continue to work — those values win over `envDefaults`.
## Where keys live — the canonical list
The exhaustive list of keys per layer lives in `helm/sim/values.yaml`. Read the file directly when you need to know "is X a Secret key or a tunable?" — it's grouped by layer with comments.
| Concern | Layer | Example keys |
|---|---|---|
| Auth secrets | 1 (app.env) | `BETTER_AUTH_SECRET`, `ENCRYPTION_KEY`, `INTERNAL_API_SECRET`, `CRON_SECRET`, `API_ENCRYPTION_KEY` |
| Provider API keys | 1 (app.env) | `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_CLIENT_SECRET`, etc. |
| Per-environment URLs | 1 (app.env) | `NEXT_PUBLIC_APP_URL`, `BETTER_AUTH_URL`, `NEXT_PUBLIC_SOCKET_URL` |
| Feature flags | 1 (app.env) | `ACCESS_CONTROL_ENABLED`, `ORGANIZATIONS_ENABLED`, `SSO_ENABLED`, all `NEXT_PUBLIC_*_ENABLED` |
| Brand / whitelabel | 1 (app.env) | `NEXT_PUBLIC_BRAND_NAME`, `NEXT_PUBLIC_BRAND_LOGO_URL`, etc. |
| Operational defaults | 2 (envDefaults) | `NODE_ENV=production`, `EMAIL_VERIFICATION_ENABLED=false`, `VERTEX_LOCATION=us-central1`, `NEXT_PUBLIC_SUPPORT_EMAIL=help@sim.ai` |
| Rate limits | 2 (envDefaults) | `RATE_LIMIT_WINDOW_MS`, `RATE_LIMIT_FREE_SYNC`, etc. |
| Execution timeouts | 2 (envDefaults) | `EXECUTION_TIMEOUT_FREE`, `EXECUTION_TIMEOUT_PRO`, etc. |
| IVM pool / quotas | 2 (envDefaults) | `IVM_POOL_SIZE`, `IVM_MAX_CONCURRENT`, `IVM_MAX_PER_WORKER`, etc. |
| Connection strings | 3 (chart-computed) | `DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL` |
| Custom downward API / configMapKeyRef | 4 (extraEnvVars) | anything that needs `valueFrom:` |
## Common authoring patterns
### "I want to set OPENAI_API_KEY for the app"
```yaml
app:
env:
OPENAI_API_KEY: "sk-..." # ends up in the app Secret, mounted via envFrom
```
For ESO:
```yaml
externalSecrets:
remoteRefs:
app:
OPENAI_API_KEY: sim/providers/openai-api-key
```
### "I want to bump the rate limit"
```yaml
app:
envDefaults:
RATE_LIMIT_FREE_SYNC: "100" # overrides the chart's default of 50
```
Or override it as a regular env var (also valid — Layer 1 wins over Layer 2):
```yaml
app:
env:
RATE_LIMIT_FREE_SYNC: "100"
```
Prefer Layer 2 for non-sensitive tunables — keeps the Secret lean and ESO mapping minimal.
### "I want to set my production app URL"
```yaml
app:
env:
NEXT_PUBLIC_APP_URL: "https://sim.example.com"
BETTER_AUTH_URL: "https://sim.example.com"
```
This is the right answer for any clustered deploy. The chart's default is `http://localhost:3000` (Layer 2) — fine for kind/minikube, broken for production. The realtime Deployment also reads these via the shared Secret.
### "I want to inject a value from another ConfigMap"
Use Layer 4:
```yaml
extraEnvVars:
- name: SOME_VALUE
valueFrom:
configMapKeyRef:
name: my-config
key: some-key
```
### "I want to change DATABASE_URL"
You can't override it directly — it's Layer 3, chart-computed. Set the inputs instead:
- For chart-bundled Postgres: edit `postgresql.auth.username`, `.database`, `.port`
- For external Postgres: enable `externalDatabase.enabled=true` and set `host`, `port`, `username`, `database`, `sslMode`
The chart will compose `DATABASE_URL` from those values.
## Override precedence — the actual K8s rule
When a key exists in both inline `env:` and `envFrom:`:
```
container.env (Layer 2, 3, 4) WINS over container.envFrom (Layer 1)
```
This is the Kubernetes spec, not a chart quirk. It's the reason for the override-skip logic in Layer 2: if you set `NEXT_PUBLIC_APP_URL` in Layer 1 (the Secret), the chart **must not** inline the same key in Layer 2 — otherwise the localhost default would mask your prod URL on the realtime pod (which mounts the same shared Secret as the app pod).
The chart handles this correctly for both `app` and `realtime` Deployments. If you ever see a stale value on a pod, check whether the same key is set in **both** `app.env` and `realtime.env` — the merge order in `secrets-app.yaml` makes `app.env` authoritative for shared keys.
+6 -5
View File
@@ -21,8 +21,9 @@
.idea/
*.tmproj
.vscode/
# Examples directory (included in chart but ignored during packaging)
examples/
# Test files
*_test.yaml
test/
# Repo tooling — not part of the chart artifact (anchored to chart root)
/.claude/
# Examples — published in-repo, not in the packaged chart
/examples/
# Unit-test suites (helm-unittest) — distinct from templates/tests/ which IS shipped
/tests/
+15 -5
View File
@@ -1,9 +1,10 @@
apiVersion: v2
name: sim
description: A Helm chart for Sim - AI agent workflow platform
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
type: application
version: 0.1.0
appVersion: "1.0.0"
version: 1.0.0
appVersion: "0.6.73"
kubeVersion: ">=1.25.0-0"
home: https://sim.ai
icon: https://raw.githubusercontent.com/simstudioai/sim/main/apps/sim/public/logo/primary/primary.svg
sources:
@@ -19,5 +20,14 @@ keywords:
- agents
- nextjs
annotations:
category: AI/ML Platform
licenses: Apache-2.0
category: developer-tools
artifacthub.io/license: Apache-2.0
artifacthub.io/links: |
- name: Homepage
url: https://sim.ai
- name: Documentation
url: https://docs.sim.ai
- name: Source
url: https://github.com/simstudioai/sim
- name: Discord
url: https://discord.gg/Hr4UWYEcTT
+343 -728
View File
File diff suppressed because it is too large Load Diff
+77 -4
View File
@@ -1,10 +1,51 @@
# AWS-specific values for Sim
# Example configuration for Amazon EKS deployment
# values-aws.yaml
#
# When to use: Amazon EKS. Configures EBS GP3 storage, ALB ingress with
# AWS Certificate Manager, IRSA-style ServiceAccount annotations, and
# GPU-instance node selectors for Ollama.
#
# Prerequisites:
# - EKS cluster (Kubernetes 1.25+)
# - EBS CSI driver add-on (aws eks create-addon --addon-name aws-ebs-csi-driver)
# - AWS Load Balancer Controller (for ALB ingress)
# - (recommended) cert-manager OR an ACM certificate ARN for the ingress
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-aws.yaml \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set app.env.CRON_SECRET="$CRON_SECRET" \
# --set postgresql.auth.password="$POSTGRES_PASSWORD"
#
# Choosing a secret strategy
# --------------------------
# Pick one of three modes — see helm/sim/README.md for details.
#
# 1. Inline (default below): set values directly in app.env. The chart
# writes every key into a chart-managed Secret. Simplest, but stores
# plaintext in your values file.
#
# 2. existingSecret: pre-create a Kubernetes Secret out-of-band and
# reference it via app.secrets.existingSecret.name. You are
# responsible for ensuring the Secret contains every key your app
# needs (BETTER_AUTH_SECRET, ENCRYPTION_KEY, INTERNAL_API_SECRET,
# AWS_SECRET_ACCESS_KEY, OPENAI_API_KEY, ...). The chart-managed
# Secret is not rendered in this mode, so anything missing from your
# Secret will be empty at runtime.
#
# 3. External Secrets Operator (ESO): sync from AWS Secrets Manager /
# Parameter Store via the External Secrets Operator. Map every key
# via externalSecrets.remoteRefs.app.<KEY>. The chart fails template
# rendering if a key is set in app.env but not mapped — see the
# commented ESO block at the bottom of this file.
# Global configuration
global:
imageRegistry: "ghcr.io"
storageClass: "gp2" # Use gp2 (default on EKS) or create gp3 StorageClass for better performance
storageClass: "gp3" # gp3 is the recommended EBS type (cheaper + faster than gp2). Requires a gp3 StorageClass — create one if your cluster only ships gp2.
# Main application
app:
@@ -255,4 +296,36 @@ affinity:
serviceAccount:
create: true
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/SimStudioServiceRole"
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789012:role/SimStudioServiceRole"
# -----------------------------------------------------------------------------
# External Secrets Operator (ESO) — recommended for production on AWS
# -----------------------------------------------------------------------------
# To switch from inline secrets to ESO:
# 1. Install external-secrets in the cluster (https://external-secrets.io).
# 2. Create a ClusterSecretStore backed by AWS Secrets Manager or
# SSM Parameter Store and grant IRSA access from this namespace.
# 3. Remove entries from app.env above (or leave them and the chart will
# fail-fast naming each unmapped key).
# 4. Uncomment the block below and replace the paths with the names of the
# secrets in your store.
#
# externalSecrets:
# enabled: true
# secretStoreRef:
# name: "aws-secret-store" # name of your ClusterSecretStore
# kind: "ClusterSecretStore"
# remoteRefs:
# app:
# BETTER_AUTH_SECRET: "sim/app/better-auth-secret"
# ENCRYPTION_KEY: "sim/app/encryption-key"
# INTERNAL_API_SECRET: "sim/app/internal-api-secret"
# CRON_SECRET: "sim/app/cron-secret"
# API_ENCRYPTION_KEY: "sim/app/api-encryption-key"
# # AWS provider keys — required when not using IRSA for S3/Bedrock.
# AWS_ACCESS_KEY_ID: "sim/aws/access-key-id"
# AWS_SECRET_ACCESS_KEY: "sim/aws/secret-access-key"
# # Add any other keys your deployment needs:
# OPENAI_API_KEY: "sim/providers/openai-api-key"
# # ANTHROPIC_API_KEY: "sim/providers/anthropic-api-key"
# postgresql:
# password: "sim/postgresql/password"
+76 -3
View File
@@ -1,5 +1,46 @@
# Azure-specific values for Sim
# Example configuration for Azure AKS deployment
# values-azure.yaml
#
# When to use: Azure Kubernetes Service (AKS). Configures managed-csi /
# managed-csi-premium storage, NGINX ingress, role-based node targeting,
# and NVIDIA GPU node pool tolerations for Ollama.
#
# Prerequisites:
# - AKS cluster (Kubernetes 1.25+)
# - NGINX ingress controller installed
# - (optional) cert-manager for TLS
# - (optional) GPU node pool labeled with role: datalake / accelerator: nvidia
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-azure.yaml \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set app.env.CRON_SECRET="$CRON_SECRET" \
# --set postgresql.auth.password="$POSTGRES_PASSWORD"
#
# Choosing a secret strategy
# --------------------------
# Pick one of three modes — see helm/sim/README.md for details.
#
# 1. Inline (default below): set values directly in app.env. The chart
# writes every key into a chart-managed Secret. Simplest, but stores
# plaintext in your values file.
#
# 2. existingSecret: pre-create a Kubernetes Secret out-of-band and
# reference it via app.secrets.existingSecret.name. You are
# responsible for ensuring the Secret contains every key your app
# needs (BETTER_AUTH_SECRET, ENCRYPTION_KEY, INTERNAL_API_SECRET,
# AZURE_OPENAI_API_KEY, AZURE_ACCOUNT_KEY, ...). The chart-managed
# Secret is not rendered in this mode, so anything missing from your
# Secret will be empty at runtime.
#
# 3. External Secrets Operator (ESO): sync from Azure Key Vault via the
# External Secrets Operator. Map every key via
# externalSecrets.remoteRefs.app.<KEY>. The chart fails template
# rendering if a key is set in app.env but not mapped — see the
# commented ESO block at the bottom of this file.
# Global configuration
global:
@@ -236,4 +277,36 @@ ingressInternal:
# TLS configuration
tls:
enabled: true
secretName: simstudio-internal-tls-secret
secretName: simstudio-internal-tls-secret
# -----------------------------------------------------------------------------
# External Secrets Operator (ESO) — recommended for production on Azure
# -----------------------------------------------------------------------------
# To switch from inline secrets to ESO:
# 1. Install external-secrets in the cluster (https://external-secrets.io).
# 2. Create a ClusterSecretStore backed by Azure Key Vault and grant
# Workload Identity / managed-identity access from this namespace.
# 3. Remove entries from app.env above (or leave them and the chart will
# fail-fast naming each unmapped key).
# 4. Uncomment the block below and replace the paths with the names of the
# secrets in your Key Vault.
#
# externalSecrets:
# enabled: true
# secretStoreRef:
# name: "azure-keyvault-store" # name of your ClusterSecretStore
# kind: "ClusterSecretStore"
# remoteRefs:
# app:
# BETTER_AUTH_SECRET: "sim-better-auth-secret"
# ENCRYPTION_KEY: "sim-encryption-key"
# INTERNAL_API_SECRET: "sim-internal-api-secret"
# CRON_SECRET: "sim-cron-secret"
# API_ENCRYPTION_KEY: "sim-api-encryption-key"
# # Azure provider keys
# AZURE_OPENAI_API_KEY: "sim-azure-openai-api-key"
# AZURE_ANTHROPIC_API_KEY: "sim-azure-anthropic-api-key"
# AZURE_ACCOUNT_KEY: "sim-azure-storage-account-key"
# # Use AZURE_CONNECTION_STRING instead if you prefer a single connection string:
# # AZURE_CONNECTION_STRING: "sim-azure-storage-connection-string"
# postgresql:
# password: "sim-postgresql-password"
+35 -2
View File
@@ -1,3 +1,36 @@
# values-copilot.yaml
#
# When to use: enable the Sim Copilot service (the chat / Mothership backend)
# alongside the main app. Provisions a dedicated Copilot Deployment, its own
# Postgres StatefulSet, and a migration Job for the Copilot schema.
#
# Prerequisites: same as the base install. Merge this file with your main
# values file (e.g. values-production.yaml) using multiple --values flags.
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-production.yaml \
# --values ./helm/sim/examples/values-copilot.yaml \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set app.env.CRON_SECRET="$CRON_SECRET" \
# --set postgresql.auth.password="$POSTGRES_PASSWORD" \
# --set copilot.postgresql.auth.password="$COPILOT_POSTGRES_PASSWORD" \
# --set copilot.server.env.AGENT_API_DB_ENCRYPTION_KEY="$AGENT_API_DB_ENCRYPTION_KEY" \
# --set copilot.server.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set copilot.server.env.LICENSE_KEY="$COPILOT_LICENSE_KEY" \
# --set copilot.server.env.SIM_BASE_URL="https://sim.example.com" \
# --set copilot.server.env.SIM_AGENT_API_KEY="$SIM_AGENT_API_KEY" \
# --set copilot.server.env.REDIS_URL="redis://default:password@redis:6379" \
# --set copilot.server.env.OPENAI_API_KEY_1="$OPENAI_API_KEY"
#
# All copilot.server.env.* keys above are required when copilot.enabled=true,
# plus at least one model provider key (OPENAI_API_KEY_1 or ANTHROPIC_API_KEY_1).
# LICENSE_KEY is issued by the Sim team. SIM_AGENT_API_KEY must equal the
# COPILOT_API_KEY value on the main app side. SIM_BASE_URL is the public app URL.
# Enable the copilot service
copilot:
enabled: true
@@ -6,8 +39,8 @@ copilot:
server:
image:
repository: simstudioai/copilot
tag: latest
pullPolicy: Always
tag: "" # defaults to Chart.AppVersion; override to pin a specific release
pullPolicy: IfNotPresent
replicaCount: 2
+18 -3
View File
@@ -1,5 +1,20 @@
# Development values for Sim
# This configuration is suitable for development and testing
# values-development.yaml
#
# When to use: local development on kind / minikube / Docker Desktop, or any
# throwaway cluster where you don't need HA, TLS, or persistence guarantees.
# Minimal resource requests, single replica, no ingress.
#
# Prerequisites: a working kubectl context.
#
# Install:
# helm install sim-dev ./helm/sim \
# --namespace sim-dev --create-namespace \
# --values ./helm/sim/examples/values-development.yaml \
# --set app.env.BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
# --set app.env.ENCRYPTION_KEY=$(openssl rand -hex 32) \
# --set app.env.INTERNAL_API_SECRET=$(openssl rand -hex 32) \
# --set app.env.CRON_SECRET=$(openssl rand -hex 32) \
# --set postgresql.auth.password=$(openssl rand -base64 24 | tr -d '/+=')
# Global configuration
global:
@@ -114,4 +129,4 @@ podDisruptionBudget:
# Network policies (disabled for development)
networkPolicy:
enabled: false
enabled: false
+19 -4
View File
@@ -1,8 +1,23 @@
# Using pre-existing Kubernetes secrets for Sim
# For GitOps, Sealed Secrets, or manual secret management
# values-existing-secret.yaml
#
# When to use: GitOps workflows where secrets are managed outside Helm via
# Sealed Secrets, SOPS, kubeseal, or manual `kubectl create secret`. The
# chart references your pre-created Kubernetes Secret objects by name
# instead of templating values into a chart-managed Secret.
#
# Prerequisites:
# Create your secrets before installing (see examples at bottom of file)
# Create the Secret objects before `helm install`. Example:
# kubectl create secret generic my-app-secrets --namespace sim \
# --from-literal=BETTER_AUTH_SECRET=$(openssl rand -hex 32) \
# --from-literal=ENCRYPTION_KEY=$(openssl rand -hex 32) \
# --from-literal=INTERNAL_API_SECRET=$(openssl rand -hex 32) \
# --from-literal=CRON_SECRET=$(openssl rand -hex 32)
# Full Secret manifests are at the bottom of this file.
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-existing-secret.yaml
app:
enabled: true
+33 -8
View File
@@ -1,6 +1,27 @@
# External Database Example for Sim
# Use this configuration when connecting to a managed database service
# (AWS RDS, Azure Database, Google Cloud SQL, etc.)
# values-external-db.yaml
#
# When to use: production deployments that use a managed Postgres service
# (AWS RDS, Azure Database for PostgreSQL, Google Cloud SQL, Neon, Supabase)
# instead of the chart's in-cluster Postgres StatefulSet. The in-cluster
# Postgres is disabled (postgresql.enabled=false).
#
# Prerequisites:
# - A reachable managed Postgres instance with pgvector extension available
# - DB credentials provisioned (user, password, database)
# - Network reachability from the cluster (peering, private link, or public
# endpoint with TLS)
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-external-db.yaml \
# --set externalDatabase.host=your-db.example.com \
# --set externalDatabase.username=simstudio_user \
# --set externalDatabase.password="$DB_PASSWORD" \
# --set externalDatabase.database=simstudio_prod \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET"
# Global configuration
global:
@@ -140,12 +161,16 @@ monitoring:
networkPolicy:
enabled: true
# Custom egress rules to allow database connectivity
# Custom egress rules to allow database connectivity to an external DB.
# The chart's app/realtime NetworkPolicies already permit egress to
# `postgresql.service.targetPort` when `postgresql.enabled=true`. For an
# external DB on a different port (or off-cluster), append to extraRules.
egress:
- to: [] # Allow outbound connections to external database
ports:
- protocol: TCP
port: 5432
extraRules:
- to: []
ports:
- protocol: TCP
port: 5432
# Example deployment command with secure secret generation:
# helm install sim ./helm/sim \
+27 -7
View File
@@ -1,9 +1,24 @@
# External Secrets Operator integration for Sim
# Syncs secrets from Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, etc.
# values-external-secrets.yaml
#
# When to use: production deployments where secrets live in an external store
# (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager)
# and you want External Secrets Operator (ESO) to sync them into Kubernetes
# Secrets that the chart consumes.
#
# Prerequisites:
# 1. Install ESO: helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
# 2. Create a SecretStore/ClusterSecretStore for your provider (see examples at bottom of file)
# 1. Install ESO once per cluster:
# helm repo add external-secrets https://charts.external-secrets.io
# helm install external-secrets external-secrets/external-secrets \
# -n external-secrets --create-namespace
# 2. Create a SecretStore or ClusterSecretStore for your provider — see
# example manifests at the bottom of this file.
# 3. Pre-populate your remote store with the secret keys referenced in
# externalSecrets.remoteRefs below.
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-external-secrets.yaml
externalSecrets:
enabled: true
@@ -25,7 +40,12 @@ externalSecrets:
app:
enabled: true
replicaCount: 2
env:
# In ESO mode, app.env may only contain keys that are also mapped under
# externalSecrets.remoteRefs.app (the chart-managed Secret is not rendered,
# so any unmapped value would silently go nowhere). Non-secret URL/config
# belongs in envDefaults — those are inlined on the container as `env:`
# and don't flow through the Secret.
envDefaults:
NEXT_PUBLIC_APP_URL: "https://sim.example.com"
BETTER_AUTH_URL: "https://sim.example.com"
NEXT_PUBLIC_SOCKET_URL: "wss://sim-ws.example.com"
@@ -34,7 +54,7 @@ app:
realtime:
enabled: true
replicaCount: 2
env:
envDefaults:
NEXT_PUBLIC_APP_URL: "https://sim.example.com"
BETTER_AUTH_URL: "https://sim.example.com"
ALLOWED_ORIGINS: "https://sim.example.com"
+76 -3
View File
@@ -1,5 +1,46 @@
# GCP-specific values for Sim
# Example configuration for Google Kubernetes Engine (GKE) deployment
# values-gcp.yaml
#
# When to use: Google Kubernetes Engine (GKE). Configures Persistent Disk
# storage, Google Cloud Load Balancer with managed certs, Workload Identity
# annotations, and Tesla T4/V100 GPU node selectors for Ollama.
#
# Prerequisites:
# - GKE cluster (Kubernetes 1.25+)
# - Workload Identity enabled (for IAM-bound ServiceAccount)
# - (optional) Managed certificate or cert-manager for TLS
# - (optional) GPU node pool with accelerator labels for Ollama
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-gcp.yaml \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set app.env.CRON_SECRET="$CRON_SECRET" \
# --set postgresql.auth.password="$POSTGRES_PASSWORD"
#
# Choosing a secret strategy
# --------------------------
# Pick one of three modes — see helm/sim/README.md for details.
#
# 1. Inline (default below): set values directly in app.env. The chart
# writes every key into a chart-managed Secret. Simplest, but stores
# plaintext in your values file.
#
# 2. existingSecret: pre-create a Kubernetes Secret out-of-band and
# reference it via app.secrets.existingSecret.name. You are
# responsible for ensuring the Secret contains every key your app
# needs (BETTER_AUTH_SECRET, ENCRYPTION_KEY, INTERNAL_API_SECRET,
# OPENAI_API_KEY, ...). The chart-managed Secret is not rendered in
# this mode, so anything missing from your Secret will be empty at
# runtime.
#
# 3. External Secrets Operator (ESO): sync from Google Secret Manager
# via the External Secrets Operator. Map every key via
# externalSecrets.remoteRefs.app.<KEY>. The chart fails template
# rendering if a key is set in app.env but not mapped — see the
# commented ESO block at the bottom of this file.
# Global configuration
global:
@@ -257,4 +298,36 @@ extraVolumes:
extraVolumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
readOnly: true
readOnly: true
# -----------------------------------------------------------------------------
# External Secrets Operator (ESO) — recommended for production on GCP
# -----------------------------------------------------------------------------
# To switch from inline secrets to ESO:
# 1. Install external-secrets in the cluster (https://external-secrets.io).
# 2. Create a ClusterSecretStore backed by Google Secret Manager and bind
# Workload Identity from this namespace's ServiceAccount.
# 3. Remove entries from app.env above (or leave them and the chart will
# fail-fast naming each unmapped key).
# 4. Uncomment the block below and replace the paths with the names of the
# secrets in your project.
#
# externalSecrets:
# enabled: true
# secretStoreRef:
# name: "gcp-secret-store" # name of your ClusterSecretStore
# kind: "ClusterSecretStore"
# remoteRefs:
# app:
# BETTER_AUTH_SECRET: "sim-better-auth-secret"
# ENCRYPTION_KEY: "sim-encryption-key"
# INTERNAL_API_SECRET: "sim-internal-api-secret"
# CRON_SECRET: "sim-cron-secret"
# API_ENCRYPTION_KEY: "sim-api-encryption-key"
# # Provider keys typical for a GCP deployment. Vertex AI auth is
# # usually handled by Workload Identity, so VERTEX_CREDENTIALS is
# # only needed when running outside the GCP credential chain.
# OPENAI_API_KEY: "sim-openai-api-key"
# # ANTHROPIC_API_KEY: "sim-anthropic-api-key"
# # VERTEX_CREDENTIALS: "sim-vertex-credentials-json"
# postgresql:
# password: "sim-postgresql-password"
+24 -3
View File
@@ -1,5 +1,26 @@
# Production values for Sim
# This configuration is suitable for production deployments
# values-production.yaml
#
# When to use: generic production deployment on any cluster. Enables HA
# (replicaCount > 1, PDB), NetworkPolicy, HPA, ServiceMonitor, and
# PSS-restricted security defaults.
#
# Prerequisites:
# - A premium StorageClass with reclaimPolicy: Retain
# - cert-manager (for TLS) or pre-provisioned certs
# - ingress-nginx (or equivalent) for ingress
# - metrics-server in the cluster (HPA)
# - (recommended) Prometheus Operator for ServiceMonitor
# - (recommended) External Secrets Operator — see values-external-secrets.yaml
#
# Install:
# helm install sim ./helm/sim \
# --namespace sim --create-namespace \
# --values ./helm/sim/examples/values-production.yaml \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set app.env.CRON_SECRET="$CRON_SECRET" \
# --set postgresql.auth.password="$POSTGRES_PASSWORD"
# Global configuration
global:
@@ -221,4 +242,4 @@ telemetry:
endpoint: "http://prometheus-server/api/v1/write"
jaeger:
enabled: true
endpoint: "http://jaeger-collector:14250"
endpoint: "http://jaeger-collector:14250"
+18 -3
View File
@@ -1,5 +1,20 @@
# Whitelabeled deployment example for Sim
# This configuration shows how to customize branding for Acme Corp
# values-whitelabeled.yaml
#
# When to use: whitelabeled / branded deployment. Replaces the default Sim
# branding (logo, product name, support URLs) with your own. The example
# below uses "Acme Corp" placeholders — replace with your brand.
#
# Prerequisites: a hosted logo URL and brand assets.
#
# Install:
# helm install acme ./helm/sim \
# --namespace acme --create-namespace \
# --values ./helm/sim/examples/values-whitelabeled.yaml \
# --set app.env.BETTER_AUTH_SECRET="$BETTER_AUTH_SECRET" \
# --set app.env.ENCRYPTION_KEY="$ENCRYPTION_KEY" \
# --set app.env.INTERNAL_API_SECRET="$INTERNAL_API_SECRET" \
# --set app.env.CRON_SECRET="$CRON_SECRET" \
# --set postgresql.auth.password="$POSTGRES_PASSWORD"
# Global configuration
global:
@@ -88,4 +103,4 @@ autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetCPUUtilizationPercentage: 70
+89
View File
@@ -0,0 +1,89 @@
{{- /*
NOTES.txt — printed after every `helm install` or `helm upgrade`.
Keep this short and action-oriented. Users land here and decide what to do next.
*/ -}}
Thank you for installing {{ .Chart.Name }} ({{ .Chart.Name }}-{{ .Chart.Version }}, app {{ .Chart.AppVersion }}).
Your release is named {{ .Release.Name }} in namespace {{ .Release.Namespace }}.
1. Watch the rollout finish:
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-app
{{- if .Values.realtime.enabled }}
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-realtime
{{- end }}
{{- if .Values.copilot.enabled }}
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-copilot
{{- end }}
2. Reach the application:
{{- if and .Values.ingress.enabled .Values.ingress.app.host }}
Sim is exposed at:
{{- if .Values.ingress.tls.enabled }}
https://{{ .Values.ingress.app.host }}
{{- else }}
http://{{ .Values.ingress.app.host }}
{{- end }}
If DNS is not yet wired up, point your hostname at the ingress controller's
external IP/LoadBalancer.
{{- else }}
Ingress is disabled. Use port-forward to reach the app locally:
kubectl --namespace {{ .Release.Namespace }} port-forward svc/{{ include "sim.fullname" . }}-app {{ .Values.app.service.port | default 3000 }}:{{ .Values.app.service.port | default 3000 }}
Then open http://localhost:{{ .Values.app.service.port | default 3000 }}
To expose Sim on the public internet, enable `ingress.enabled=true` and set
`ingress.app.host`. See examples/values-production.yaml.
{{- end }}
3. Required secrets:
Sim requires three application secrets and a Postgres password to start.
{{- if .Values.externalSecrets.enabled }}
Using External Secrets Operator. Verify the ExternalSecret has synced:
kubectl --namespace {{ .Release.Namespace }} get externalsecret
kubectl --namespace {{ .Release.Namespace }} get secret {{ include "sim.fullname" . }}-app-secrets
{{- else if .Values.app.secrets.existingSecret.enabled }}
Using existing Kubernetes Secret: {{ .Values.app.secrets.existingSecret.name }}
{{- else if or (eq (default "" .Values.app.env.BETTER_AUTH_SECRET) "") (eq (default "" .Values.app.env.ENCRYPTION_KEY) "") (eq (default "" .Values.app.env.INTERNAL_API_SECRET) "") }}
WARNING: one or more required secrets is empty. The pods will fail to start
until these are set. Generate values with:
openssl rand -hex 32 # BETTER_AUTH_SECRET, ENCRYPTION_KEY, INTERNAL_API_SECRET, CRON_SECRET
openssl rand -hex 32 # API_ENCRYPTION_KEY (must be 64 hex chars)
Then pass them on `helm upgrade --set app.env.BETTER_AUTH_SECRET=...` or
provision an existing Kubernetes Secret and set
`app.secrets.existingSecret.enabled=true`.
{{- else }}
Secrets are set inline. For production, prefer pre-existing Kubernetes
Secrets or External Secrets Operator — see
examples/values-existing-secret.yaml and examples/values-external-secrets.yaml.
{{- end }}
4. Useful commands:
# Tail app logs
kubectl --namespace {{ .Release.Namespace }} logs -f deployment/{{ include "sim.fullname" . }}-app
# Show the rendered values
helm get values {{ .Release.Name }} --namespace {{ .Release.Namespace }}
# Upgrade after changing values
helm upgrade {{ .Release.Name }} ./helm/sim --namespace {{ .Release.Namespace }} -f your-values.yaml
5. Where to go next:
* Production checklist: helm/sim/README.md (search "Production checklist")
* Troubleshooting: helm/sim/README.md (search "Troubleshooting")
* Docs: https://docs.sim.ai
* Issues: https://github.com/simstudioai/sim/issues
+141 -20
View File
@@ -137,24 +137,41 @@ Create the name of the service account to use
{{- end }}
{{/*
Create image name with registry
Expects context with image object passed as second parameter
Usage: {{ include "sim.image" (dict "context" . "image" .Values.app.image) }}
Create image name with optional registry and digest pinning.
Accepts a context dict with:
imageRoot — the image object (repository, optional tag, optional digest, pullPolicy)
global — .Values.global (for imageRegistry and useRegistryForAllImages)
chartAppVersion — .Chart.AppVersion (used as default tag when imageRoot.tag is empty)
Resolution order:
1. If imageRoot.digest is set, render "<registry?/>repo@<digest>"
2. Else render "<registry?/>repo:<tag>" where tag defaults to chartAppVersion
Usage: {{ include "sim.image" (dict "imageRoot" .Values.app.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
*/}}
{{- define "sim.image" -}}
{{- $imageRoot := .imageRoot -}}
{{- $global := .global -}}
{{- $repository := $imageRoot.repository -}}
{{- $digest := $imageRoot.digest | default "" -}}
{{- $tag := $imageRoot.tag | default "" | toString -}}
{{- if and (eq $tag "") (eq $digest "") -}}
{{- $tag = .chartAppVersion | default "" | toString -}}
{{- end -}}
{{- $registry := "" -}}
{{- $repository := .image.repository -}}
{{- $tag := .image.tag | toString -}}
{{- /* Use global registry for simstudioai images or when explicitly set for all images */ -}}
{{- if .context.Values.global.imageRegistry -}}
{{- if or (hasPrefix "simstudioai/" $repository) .context.Values.global.useRegistryForAllImages -}}
{{- $registry = .context.Values.global.imageRegistry -}}
{{- if and $global $global.imageRegistry -}}
{{- if or (hasPrefix "simstudioai/" $repository) $global.useRegistryForAllImages -}}
{{- $registry = $global.imageRegistry -}}
{{- end -}}
{{- end -}}
{{- $repoPath := $repository -}}
{{- if $registry -}}
{{- printf "%s/%s:%s" $registry $repository $tag }}
{{- $repoPath = printf "%s/%s" $registry $repository -}}
{{- end -}}
{{- if ne $digest "" -}}
{{- printf "%s@%s" $repoPath $digest }}
{{- else -}}
{{- printf "%s:%s" $repository $tag }}
{{- $resolvedTag := required (printf "sim.image: no tag or digest resolvable for %q (set imageRoot.tag, imageRoot.digest, or Chart.AppVersion)" $repository) $tag -}}
{{- printf "%s:%s" $repoPath $resolvedTag }}
{{- end -}}
{{- end }}
@@ -202,9 +219,16 @@ Skip validation when using existing secrets or External Secrets Operator
{{- if and .Values.app.enabled (eq .Values.app.env.ENCRYPTION_KEY "CHANGE-ME-32-CHAR-ENCRYPTION-KEY-FOR-PROD") }}
{{- fail "app.env.ENCRYPTION_KEY must not use the default placeholder value. Generate a secure key with: openssl rand -hex 32" }}
{{- end }}
{{- if and .Values.app.enabled (not .Values.app.env.INTERNAL_API_SECRET) }}
{{- fail "app.env.INTERNAL_API_SECRET is required for production deployment (shared auth between sim-app and sim-realtime pods). Generate one with: openssl rand -hex 32" }}
{{- end }}
{{- if and .Values.realtime.enabled (eq .Values.realtime.env.BETTER_AUTH_SECRET "CHANGE-ME-32-CHAR-SECRET-FOR-PRODUCTION-USE") }}
{{- fail "realtime.env.BETTER_AUTH_SECRET must not use the default placeholder value. Generate a secure secret with: openssl rand -hex 32" }}
{{- end }}
{{- /* CRON_SECRET is required when cronjobs are enabled — pods reference it via secretKeyRef and would fail to start if the key is missing from the Secret. */ -}}
{{- if and .Values.cronjobs.enabled (not .Values.app.env.CRON_SECRET) }}
{{- fail "app.env.CRON_SECRET is required when cronjobs.enabled=true (every cron pod authenticates with this token). Generate one with: openssl rand -hex 32, or set cronjobs.enabled=false." }}
{{- end }}
{{- end }}
{{- /* PostgreSQL password validation - skip if using existing secret or ESO */ -}}
{{- if not (or $useExistingPostgresSecret $useExternalSecrets) }}
@@ -227,6 +251,74 @@ Skip validation when using existing secrets or External Secrets Operator
{{- fail "externalDatabase.password must only contain alphanumeric characters, hyphens, underscores, or periods to ensure DATABASE_URL compatibility." }}
{{- end }}
{{- end }}
{{- /* ESO coverage validation - every key set in app.env / realtime.env must be mapped in externalSecrets.remoteRefs.app */ -}}
{{- include "sim.validateExternalSecretCoverage" . }}
{{- end }}
{{/*
Validate that every key set in app.env / realtime.env is also mapped in
externalSecrets.remoteRefs.app when ESO is enabled. When ESO is on, the
chart-managed Secret is not rendered — anything not mapped via ESO would
be silently missing at runtime.
Chart-computed keys (DATABASE_URL, SOCKET_SERVER_URL, OLLAMA_URL) are
exempt because they're inlined on the container, not sourced from the
Secret.
Fail-fast is only safe for ESO because we can introspect remoteRefs at
template time. For existingSecret we cannot read the user's pre-created
Secret, so coverage there is documented (values.yaml + README) rather
than enforced.
*/}}
{{- define "sim.validateExternalSecretCoverage" -}}
{{- if and .Values.externalSecrets .Values.externalSecrets.enabled -}}
{{- $remoteRefs := default (dict) (default (dict) .Values.externalSecrets.remoteRefs).app -}}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" -}}
{{- $appEnv := default (dict) .Values.app.env -}}
{{/*
Required-key coverage: these are non-optional at runtime. With ESO enabled
the chart-managed Secret is not rendered, so a missing key surfaces as a
runtime CrashLoopBackOff with cryptic env errors. Fail at template time
if the key is neither set in app.env nor mapped via remoteRefs.app.
*/}}
{{- if .Values.app.enabled -}}
{{- $required := list "BETTER_AUTH_SECRET" "ENCRYPTION_KEY" "INTERNAL_API_SECRET" -}}
{{- if .Values.cronjobs.enabled -}}
{{- $required = append $required "CRON_SECRET" -}}
{{- end -}}
{{- range $key := $required -}}
{{- $inEnv := index $appEnv $key -}}
{{- $mapped := index $remoteRefs $key -}}
{{- if and (or (not $inEnv) (eq (toString $inEnv) "") (eq (toString $inEnv) "<nil>")) (not $mapped) -}}
{{- fail (printf "Required key '%s' is missing: externalSecrets.enabled=true but the key is neither set in app.env nor mapped in externalSecrets.remoteRefs.app. Map it via externalSecrets.remoteRefs.app.%s='path/in/store' so it is synced into the app Secret." $key $key) }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if .Values.app.enabled -}}
{{- range $key, $value := default (dict) .Values.app.env -}}
{{- if not (has $key $chartComputed) -}}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") -}}
{{- $mapped := index $remoteRefs $key -}}
{{- if not $mapped -}}
{{- fail (printf "Key '%s' is set in app.env but externalSecrets.enabled=true and externalSecrets.remoteRefs.app.%s is not configured. When ESO is enabled the chart-managed app Secret is not rendered, so the container would start with no value. Either map it via externalSecrets.remoteRefs.app.%s='path/in/store' or remove it from app.env." $key $key $key) }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if .Values.realtime.enabled -}}
{{- range $key, $value := default (dict) .Values.realtime.env -}}
{{- if not (has $key $chartComputed) -}}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") -}}
{{- $mapped := index $remoteRefs $key -}}
{{- if not $mapped -}}
{{- fail (printf "Key '%s' is set in realtime.env but externalSecrets.enabled=true and externalSecrets.remoteRefs.app.%s is not configured. Either map it via externalSecrets.remoteRefs.app.%s='path/in/store' or remove it from realtime.env." $key $key $key) }}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end }}
{{/*
@@ -369,23 +461,39 @@ resources:
{{- end }}
{{/*
Security context
Pod-level security context with Pod Security Standards "restricted" defaults.
User-supplied `.podSecurityContext` values override defaults (user wins).
Usage: {{ include "sim.podSecurityContext" .Values.app | nindent 6 }}
*/}}
{{- define "sim.securityContext" -}}
{{- if .securityContext }}
{{- define "sim.podSecurityContext" -}}
{{- $defaults := dict "runAsNonRoot" true "runAsUser" 1001 "runAsGroup" 1001 "fsGroup" 1001 "seccompProfile" (dict "type" "RuntimeDefault") -}}
{{- $user := default (dict) .podSecurityContext -}}
{{- $merged := mergeOverwrite (deepCopy $defaults) $user -}}
securityContext:
{{- toYaml .securityContext | nindent 2 }}
{{- end }}
{{- toYaml $merged | nindent 2 }}
{{- end }}
{{/*
Pod security context
Container-level security context with Pod Security Standards "restricted" defaults.
User-supplied `.securityContext` values override defaults (user wins).
`readOnlyRootFilesystem` is intentionally NOT defaulted — set it per-workload in values
when the container can tolerate a read-only root (Next.js writes to `.next/cache`,
Postgres writes to `/var/lib/postgresql/data`, so they're left writable by default).
Usage: {{ include "sim.containerSecurityContext" .Values.app | nindent 10 }}
*/}}
{{- define "sim.podSecurityContext" -}}
{{- if .podSecurityContext }}
{{- define "sim.containerSecurityContext" -}}
{{- $defaults := dict "runAsNonRoot" true "allowPrivilegeEscalation" false "capabilities" (dict "drop" (list "ALL")) "seccompProfile" (dict "type" "RuntimeDefault") -}}
{{- $user := default (dict) .securityContext -}}
{{- $merged := mergeOverwrite (deepCopy $defaults) $user -}}
securityContext:
{{- toYaml .podSecurityContext | nindent 2 }}
{{- toYaml $merged | nindent 2 }}
{{- end }}
{{/*
Backwards-compatible alias for container security context.
*/}}
{{- define "sim.securityContext" -}}
{{- include "sim.containerSecurityContext" . }}
{{- end }}
{{/*
@@ -418,6 +526,19 @@ affinity:
{{- end }}
{{- end }}
{{/*
Topology spread constraints — spreads pods across failure domains.
Pass the per-component spec (.Values.app, .Values.realtime, ...). Users supply
the full constraint list including labelSelector; pattern mirrors affinity.
Usage: {{ include "sim.topologySpreadConstraints" .Values.app | nindent 6 }}
*/}}
{{- define "sim.topologySpreadConstraints" -}}
{{- if .topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml .topologySpreadConstraints | nindent 2 }}
{{- end }}
{{- end }}
{{/*
Copilot environment secret name
*/}}
+14 -10
View File
@@ -6,6 +6,7 @@ apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "sim.fullname" $ }}-{{ $jobConfig.name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "sim.labels" $ | nindent 4 }}
app.kubernetes.io/component: cronjob-{{ $jobConfig.name }}
@@ -19,6 +20,7 @@ spec:
{{- end }}
jobTemplate:
spec:
ttlSecondsAfterFinished: {{ $.Values.cronjobs.ttlSecondsAfterFinished | default 600 }}
{{- with $.Values.cronjobs.activeDeadlineSeconds }}
activeDeadlineSeconds: {{ . }}
{{- end }}
@@ -27,23 +29,25 @@ spec:
labels:
{{- include "sim.selectorLabels" $ | nindent 12 }}
app.kubernetes.io/component: cronjob-{{ $jobConfig.name }}
# Stable group label so NetworkPolicy can allow all cron pods → app
# without enumerating each job's component value.
simstudio.ai/component-group: cronjob
spec:
serviceAccountName: {{ include "sim.serviceAccountName" $ }}
automountServiceAccountToken: false
restartPolicy: {{ $.Values.cronjobs.restartPolicy | default "OnFailure" }}
{{- with $.Values.cronjobs.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.podSecurityContext" $.Values.cronjobs | nindent 10 }}
containers:
- name: {{ $jobConfig.name }}
image: "{{ $.Values.cronjobs.image.repository }}:{{ $.Values.cronjobs.image.tag }}"
image: {{ include "sim.image" (dict "imageRoot" $.Values.cronjobs.image "global" $.Values.global "chartAppVersion" $.Chart.AppVersion) }}
imagePullPolicy: {{ $.Values.cronjobs.image.pullPolicy }}
{{- with $.Values.cronjobs.securityContext }}
securityContext:
{{- toYaml . | nindent 14 }}
{{- end }}
{{- include "sim.containerSecurityContext" $.Values.cronjobs | nindent 12 }}
env:
- name: CRON_SECRET
value: {{ $.Values.app.env.CRON_SECRET | quote }}
valueFrom:
secretKeyRef:
name: {{ include "sim.appSecretName" $ }}
key: CRON_SECRET
command:
- /bin/sh
- -c
+57 -6
View File
@@ -8,13 +8,19 @@ metadata:
labels:
{{- include "sim.app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.app.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secrets-app.yaml") . | sha256sum }}
{{- if .Values.branding.enabled }}
checksum/config: {{ include (print $.Template.BasePath "/configmap-branding.yaml") . | sha256sum }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -29,14 +35,16 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.app | nindent 6 }}
{{- include "sim.nodeSelector" .Values.app | nindent 6 }}
{{- include "sim.tolerations" .Values | nindent 6 }}
{{- include "sim.affinity" .Values | nindent 6 }}
{{- include "sim.topologySpreadConstraints" .Values.app | nindent 6 }}
{{- if .Values.migrations.enabled }}
initContainers:
- name: migrations
image: {{ include "sim.image" (dict "context" . "image" .Values.migrations.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.migrations.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.migrations.image.pullPolicy }}
command: ["/bin/sh", "-c"]
args:
@@ -53,16 +61,22 @@ spec:
name: {{ include "sim.externalDbSecretName" . }}
{{- end }}
{{- include "sim.resources" .Values.migrations | nindent 10 }}
{{- include "sim.securityContext" .Values.migrations | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.migrations | nindent 10 }}
{{- end }}
containers:
- name: app
image: {{ include "sim.image" (dict "context" . "image" .Values.app.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.app.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.app.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.app.service.targetPort }}
protocol: TCP
{{- /*
All keys from .Values.app.env are mounted via envFrom (see below).
Chart-computed values and operational tunables (.Values.app.envDefaults)
are inlined here so they don't leak into the chart-managed Secret and
don't need to be mapped when externalSecrets.enabled=true.
*/}}
env:
- name: DATABASE_URL
value: {{ include "sim.databaseUrl" . | quote }}
@@ -70,11 +84,44 @@ spec:
value: {{ include "sim.socketServerUrl" . | quote }}
- name: OLLAMA_URL
value: {{ include "sim.ollamaUrl" . | quote }}
{{- range $key, $value := omit .Values.app.env "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- /*
Skip envDefaults keys that the user has explicitly overridden in app.env
with a non-empty value. K8s `env` takes precedence over `envFrom`, so an
inline default would otherwise mask the user's Secret-bound value.
Also skip envDefaults entirely in existingSecret mode: the pre-existing
Secret is the source of truth, and inlining localhost URL defaults would
silently shadow keys (NEXT_PUBLIC_APP_URL, BETTER_AUTH_URL, etc.) the
user stored in their Secret but left empty in app.env.
*/}}
{{- $appEnv := .Values.app.env | default dict }}
{{- $useExistingSecret := and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
{{- if not $useExistingSecret }}
{{- range $key, $value := .Values.app.envDefaults | default dict }}
{{- $override := index $appEnv $key }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (or (not $override) (eq (toString $override) "") (eq (toString $override) "<nil>")) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- /*
In existingSecret mode the chart-managed Secret is not rendered, so
non-empty values in app.env would otherwise go nowhere — inline them
here so the user's overrides actually reach the pod. Skipped in ESO
mode (ESO is the source of truth; inlining would mask the synced value)
and in inline mode (values flow through the chart-managed Secret).
*/}}
{{- if and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- range $key, $value := $appEnv }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (not (has $key $chartComputed)) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.telemetry.enabled }}
{{- $nodeEnv := default (default "production" (index (.Values.app.envDefaults | default dict) "NODE_ENV")) (index (.Values.app.env | default dict) "NODE_ENV") }}
# OpenTelemetry configuration
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://{{ include "sim.fullname" . }}-otel-collector:4318"
@@ -83,7 +130,7 @@ spec:
- name: OTEL_SERVICE_VERSION
value: {{ .Chart.AppVersion | quote }}
- name: OTEL_RESOURCE_ATTRIBUTES
value: "service.name=sim-app,service.version={{ .Chart.AppVersion }},deployment.environment={{ .Values.app.env.NODE_ENV }}"
value: "service.name=sim-app,service.version={{ .Chart.AppVersion }},deployment.environment={{ $nodeEnv }}"
{{- end }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
@@ -100,6 +147,10 @@ spec:
- secretRef:
name: {{ include "sim.externalDbSecretName" . }}
{{- end }}
{{- if .Values.app.startupProbe }}
startupProbe:
{{- toYaml .Values.app.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.app.livenessProbe }}
livenessProbe:
{{- toYaml .Values.app.livenessProbe | nindent 12 }}
@@ -109,7 +160,7 @@ spec:
{{- toYaml .Values.app.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.app | nindent 10 }}
{{- include "sim.securityContext" .Values.app | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.app | nindent 10 }}
{{- $hasBranding := and .Values.branding.enabled (or .Values.branding.files .Values.branding.binaryFiles) }}
{{- if or $hasBranding .Values.extraVolumeMounts .Values.app.extraVolumeMounts }}
volumeMounts:
+10 -9
View File
@@ -38,6 +38,7 @@ spec:
template:
metadata:
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secrets-copilot.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -53,10 +54,9 @@ spec:
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.copilot.server.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.copilot.server | nindent 6 }}
{{- with .Values.copilot.server.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
@@ -71,7 +71,7 @@ spec:
{{- end }}
containers:
- name: copilot
image: {{ include "sim.image" (dict "context" . "image" .Values.copilot.server.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.copilot.server.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.copilot.server.image.pullPolicy }}
ports:
- name: http
@@ -89,6 +89,10 @@ spec:
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.copilot.server.startupProbe }}
startupProbe:
{{- toYaml .Values.copilot.server.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.copilot.server.livenessProbe }}
livenessProbe:
{{- toYaml .Values.copilot.server.livenessProbe | nindent 12 }}
@@ -101,9 +105,6 @@ spec:
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.server.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.server | nindent 10 }}
{{- end }}
+5 -2
View File
@@ -59,12 +59,14 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.ollama | nindent 6 }}
{{- include "sim.nodeSelector" .Values.ollama | nindent 6 }}
{{- include "sim.tolerations" .Values.ollama | nindent 6 }}
{{- include "sim.affinity" .Values | nindent 6 }}
containers:
- name: ollama
image: {{ include "sim.image" (dict "context" . "image" .Values.ollama.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.ollama.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.ollama.image.pullPolicy }}
command: ["ollama", "serve"]
ports:
@@ -92,11 +94,12 @@ spec:
{{- toYaml .Values.ollama.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.ollama | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.ollama | nindent 10 }}
{{- if or .Values.ollama.persistence.enabled .Values.extraVolumeMounts .Values.ollama.extraVolumeMounts }}
volumeMounts:
{{- if .Values.ollama.persistence.enabled }}
- name: ollama-data
mountPath: /root/.ollama
mountPath: /data
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
+78 -11
View File
@@ -8,13 +8,16 @@ metadata:
labels:
{{- include "sim.realtime.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.realtime.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secrets-app.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -29,30 +32,31 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.realtime | nindent 6 }}
{{- include "sim.nodeSelector" .Values.realtime | nindent 6 }}
{{- include "sim.tolerations" .Values | nindent 6 }}
{{- include "sim.affinity" .Values | nindent 6 }}
{{- include "sim.topologySpreadConstraints" .Values.realtime | nindent 6 }}
containers:
- name: realtime
image: {{ include "sim.image" (dict "context" . "image" .Values.realtime.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.realtime.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.realtime.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.realtime.service.targetPort }}
protocol: TCP
{{- /*
All keys from .Values.realtime.env are mounted via envFrom (see below).
Chart-computed values and operational tunables (.Values.realtime.envDefaults)
are inlined here so they don't leak into the chart-managed Secret and don't
need to be mapped when externalSecrets.enabled=true.
*/}}
env:
- name: DATABASE_URL
value: {{ include "sim.databaseUrl" . | quote }}
{{- if .Values.app.env.REDIS_URL }}
- name: REDIS_URL
value: {{ .Values.app.env.REDIS_URL | quote }}
{{- end }}
{{- range $key, $value := .Values.realtime.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- if .Values.telemetry.enabled }}
{{- $nodeEnv := default (default "production" (index (.Values.realtime.envDefaults | default dict) "NODE_ENV")) (index (.Values.realtime.env | default dict) "NODE_ENV") }}
# OpenTelemetry configuration
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://{{ include "sim.fullname" . }}-otel-collector:4318"
@@ -61,7 +65,66 @@ spec:
- name: OTEL_SERVICE_VERSION
value: {{ .Chart.AppVersion | quote }}
- name: OTEL_RESOURCE_ATTRIBUTES
value: "service.name=sim-realtime,service.version={{ .Chart.AppVersion }},deployment.environment={{ .Values.realtime.env.NODE_ENV }}"
value: "service.name=sim-realtime,service.version={{ .Chart.AppVersion }},deployment.environment={{ $nodeEnv }}"
{{- end }}
{{- /*
Inline realtime.envDefaults, skipping keys explicitly set in realtime.env
OR app.env (K8s `env` overrides `envFrom`, so an inline default would
otherwise mask a Secret-bound value). The chart-managed Secret is shared
between app and realtime via envFrom, so a key set in app.env (e.g.
NEXT_PUBLIC_APP_URL=https://prod.example.com) lands on the realtime pod
too — inlining the localhost envDefault here would mask it.
*/}}
{{- $rtEnv := .Values.realtime.env | default dict }}
{{- $appEnv := .Values.app.env | default dict }}
{{- $useExistingSecret := and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
{{- /*
Skip envDefaults entirely in existingSecret mode — same rationale as
deployment-app.yaml: the pre-existing Secret is the source of truth,
and inlining localhost defaults would shadow URL keys (BETTER_AUTH_URL,
ALLOWED_ORIGINS, NEXT_PUBLIC_APP_URL, etc.) stored in the Secret.
*/}}
{{- if not $useExistingSecret }}
{{- range $key, $value := .Values.realtime.envDefaults | default dict }}
{{- $rtOverride := index $rtEnv $key }}
{{- $appOverride := index $appEnv $key }}
{{- $hasRt := and $rtOverride (ne (toString $rtOverride) "") (ne (toString $rtOverride) "<nil>") }}
{{- $hasApp := and $appOverride (ne (toString $appOverride) "") (ne (toString $appOverride) "<nil>") }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (not $hasRt) (not $hasApp) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- /*
In existingSecret mode the chart-managed Secret is not rendered, so
non-empty values in realtime.env / app.env would otherwise go nowhere
— inline them here so the user's overrides actually reach the pod.
realtime.env wins over app.env on collision (realtime-specific intent).
Skipped in ESO mode and inline mode for the same reasons as the app
deployment.
*/}}
{{- if and .Values.app.secrets.existingSecret.enabled (not .Values.externalSecrets.enabled) }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- /*
Build the effective realtime env from app.env as the base, then
overlay non-empty realtime.env values. Sprig's `merge` keeps the
first source on collision and treats "" as a real value, which
would shadow non-empty app.env entries — so do the overlay
manually with an explicit empty-string check.
*/}}
{{- $effective := deepCopy $appEnv }}
{{- range $key, $value := $rtEnv }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") }}
{{- $_ := set $effective $key $value }}
{{- end }}
{{- end }}
{{- range $key, $value := $effective }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") (not (has $key $chartComputed)) }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
@@ -78,6 +141,10 @@ spec:
- secretRef:
name: {{ include "sim.externalDbSecretName" . }}
{{- end }}
{{- if .Values.realtime.startupProbe }}
startupProbe:
{{- toYaml .Values.realtime.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.realtime.livenessProbe }}
livenessProbe:
{{- toYaml .Values.realtime.livenessProbe | nindent 12 }}
@@ -87,7 +154,7 @@ spec:
{{- toYaml .Values.realtime.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.realtime | nindent 10 }}
{{- include "sim.securityContext" .Values.realtime | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.realtime | nindent 10 }}
{{- if or .Values.extraVolumeMounts .Values.realtime.extraVolumeMounts }}
volumeMounts:
{{- with .Values.extraVolumeMounts }}
+19 -27
View File
@@ -1,6 +1,15 @@
{{- if and .Values.externalSecrets.enabled .Values.app.enabled }}
# ExternalSecret for app credentials (syncs from external secret managers)
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
#
# The data list is generated by iterating every non-empty entry in
# externalSecrets.remoteRefs.app. This lets users sync arbitrary sensitive
# keys (OPENAI_API_KEY, AWS_SECRET_ACCESS_KEY, ...) — not just the legacy
# six (BETTER_AUTH_SECRET, ENCRYPTION_KEY, INTERNAL_API_SECRET, CRON_SECRET,
# API_ENCRYPTION_KEY, REDIS_URL). Each entry's value may be either:
# - a string: treated as the remoteRef.key (legacy form)
# - a map: passed through as the remoteRef block (e.g. {key, property,
# version, decodingStrategy, conversionStrategy, metadataPolicy})
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-app-secrets
@@ -16,34 +25,17 @@ spec:
name: {{ include "sim.fullname" . }}-app-secrets
creationPolicy: Owner
data:
{{- if .Values.externalSecrets.remoteRefs.app.BETTER_AUTH_SECRET }}
- secretKey: BETTER_AUTH_SECRET
{{- range $secretKey, $ref := .Values.externalSecrets.remoteRefs.app }}
{{- if $ref }}
{{- if kindIs "string" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.app.BETTER_AUTH_SECRET }}
key: {{ $ref }}
{{- else if kindIs "map" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
{{- toYaml $ref | nindent 8 }}
{{- end }}
{{- if .Values.externalSecrets.remoteRefs.app.ENCRYPTION_KEY }}
- secretKey: ENCRYPTION_KEY
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.app.ENCRYPTION_KEY }}
{{- end }}
{{- if .Values.externalSecrets.remoteRefs.app.INTERNAL_API_SECRET }}
- secretKey: INTERNAL_API_SECRET
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.app.INTERNAL_API_SECRET }}
{{- end }}
{{- if .Values.externalSecrets.remoteRefs.app.CRON_SECRET }}
- secretKey: CRON_SECRET
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.app.CRON_SECRET }}
{{- end }}
{{- if .Values.externalSecrets.remoteRefs.app.API_ENCRYPTION_KEY }}
- secretKey: API_ENCRYPTION_KEY
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.app.API_ENCRYPTION_KEY }}
{{- end }}
{{- if .Values.externalSecrets.remoteRefs.app.REDIS_URL }}
- secretKey: REDIS_URL
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.app.REDIS_URL }}
{{- end }}
{{- end }}
@@ -1,6 +1,6 @@
{{- if and .Values.externalSecrets.enabled .Values.externalDatabase.enabled .Values.externalSecrets.remoteRefs.externalDatabase.password }}
# ExternalSecret for external database password (syncs from external secret managers)
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-external-db-secret
@@ -1,6 +1,6 @@
{{- if and .Values.externalSecrets.enabled .Values.postgresql.enabled .Values.externalSecrets.remoteRefs.postgresql.password }}
# ExternalSecret for PostgreSQL password (syncs from external secret managers)
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }}
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-postgresql-secret
+7 -14
View File
@@ -24,11 +24,10 @@ spec:
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
restartPolicy: {{ .Values.copilot.migrations.restartPolicy }}
{{- with .Values.copilot.migrations.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "sim.podSecurityContext" .Values.copilot.migrations | nindent 6 }}
{{- with .Values.copilot.server.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
@@ -36,7 +35,7 @@ spec:
{{- if .Values.copilot.postgresql.enabled }}
initContainers:
- name: wait-for-postgres
image: {{ include "sim.image" (dict "context" . "image" .Values.copilot.postgresql.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.copilot.postgresql.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
command:
- /bin/sh
- -c
@@ -49,14 +48,11 @@ spec:
envFrom:
- secretRef:
name: {{ include "sim.fullname" . }}-copilot-postgresql-secret
{{- with .Values.copilot.migrations.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.migrations | nindent 10 }}
{{- end }}
containers:
- name: migrations
image: {{ include "sim.image" (dict "context" . "image" .Values.copilot.migrations.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.copilot.migrations.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.copilot.migrations.image.pullPolicy }}
command: ["/usr/local/bin/migrate"]
envFrom:
@@ -75,9 +71,6 @@ spec:
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.migrations.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.migrations | nindent 10 }}
{{- end }}
+72 -12
View File
@@ -26,9 +26,22 @@ spec:
- protocol: TCP
port: {{ .Values.app.service.targetPort }}
{{- end }}
# Allow ingress from ingress controller
# Allow ingress from cron pods (every cron job curls /api/schedules/execute,
# webhook polls, etc. against the app service)
{{- if .Values.cronjobs.enabled }}
- from:
- podSelector:
matchLabels:
{{- include "sim.selectorLabels" . | nindent 10 }}
simstudio.ai/component-group: cronjob
ports:
- protocol: TCP
port: {{ .Values.app.service.targetPort }}
{{- end }}
# Allow ingress from ingress controller (configurable peers; defaults to any)
{{- if .Values.ingress.enabled }}
- from: []
- from:
{{- toYaml (default (list (dict)) .Values.networkPolicy.ingressFrom) | nindent 6 }}
ports:
- protocol: TCP
port: {{ .Values.app.service.targetPort }}
@@ -68,6 +81,20 @@ spec:
- protocol: TCP
port: {{ .Values.ollama.service.targetPort }}
{{- end }}
# Allow egress to OpenTelemetry collector (OTLP gRPC + HTTP)
{{- if .Values.telemetry.enabled }}
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: telemetry
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
{{- end }}
# Allow DNS resolution
- to: []
ports:
@@ -75,13 +102,19 @@ spec:
port: 53
- protocol: TCP
port: 53
# Allow HTTPS egress for external APIs
- to: []
# Allow HTTPS egress for external APIs (excludes cloud metadata endpoints)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
{{- range (default (list "169.254.169.254/32" "169.254.170.2/32") .Values.networkPolicy.egress.exceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
# Allow custom egress rules
{{- with .Values.networkPolicy.egress }}
{{- with .Values.networkPolicy.egress.extraRules }}
{{- toYaml . | nindent 2 }}
{{- end }}
@@ -111,9 +144,10 @@ spec:
ports:
- protocol: TCP
port: {{ .Values.realtime.service.targetPort }}
# Allow ingress from ingress controller
# Allow ingress from ingress controller (configurable peers; defaults to any)
{{- if .Values.ingress.enabled }}
- from: []
- from:
{{- toYaml (default (list (dict)) .Values.networkPolicy.ingressFrom) | nindent 6 }}
ports:
- protocol: TCP
port: {{ .Values.realtime.service.targetPort }}
@@ -129,6 +163,20 @@ spec:
- protocol: TCP
port: {{ .Values.postgresql.service.targetPort }}
{{- end }}
# Allow egress to OpenTelemetry collector (OTLP gRPC + HTTP)
{{- if .Values.telemetry.enabled }}
- to:
- podSelector:
matchLabels:
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: telemetry
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
{{- end }}
# Allow DNS resolution
- to: []
ports:
@@ -136,13 +184,19 @@ spec:
port: 53
- protocol: TCP
port: 53
# Allow HTTPS egress for external APIs
- to: []
# Allow HTTPS egress for external APIs (excludes cloud metadata endpoints)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
{{- range (default (list "169.254.169.254/32" "169.254.170.2/32") .Values.networkPolicy.egress.exceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
# Allow custom egress rules
{{- with .Values.networkPolicy.egress }}
{{- with .Values.networkPolicy.egress.extraRules }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
@@ -237,8 +291,14 @@ spec:
port: 53
- protocol: TCP
port: 53
# Allow HTTPS egress for model downloads
- to: []
# Allow HTTPS egress for model downloads (excludes cloud metadata endpoints)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
{{- range (default (list "169.254.169.254/32" "169.254.170.2/32") .Values.networkPolicy.egress.exceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
+20 -4
View File
@@ -1,4 +1,19 @@
{{- if and .Values.podDisruptionBudget.enabled .Values.app.enabled }}
{{- /*
Tri-state activation for the app PDB:
- enabled: true → force on
- enabled: false → force off (explicit opt-out, even on HA)
- enabled: null → auto-enable when the workload is HA — either a static
replicaCount > 1, or HPA enabled with minReplicas > 1
(HPA owns spec.replicas so replicaCount alone is not
authoritative under autoscaling).
*/ -}}
{{- $pdbEnabled := .Values.podDisruptionBudget.enabled -}}
{{- $hpaMin := 0 -}}
{{- if .Values.autoscaling.enabled -}}
{{- $hpaMin = int (default 1 .Values.autoscaling.minReplicas) -}}
{{- end -}}
{{- $pdbAuto := and (kindIs "invalid" $pdbEnabled) (or (gt (int .Values.app.replicaCount) 1) (gt $hpaMin 1)) -}}
{{- if and .Values.app.enabled (or (eq $pdbEnabled true) $pdbAuto) }}
{{- with .Values.podDisruptionBudget }}
---
apiVersion: policy/v1
@@ -14,7 +29,7 @@ spec:
{{- else if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- else }}
maxUnavailable: 1
minAvailable: 1
{{- end }}
{{- if .unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ .unhealthyPodEvictionPolicy }}
@@ -24,7 +39,8 @@ spec:
{{- include "sim.app.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- if and .Values.podDisruptionBudget.enabled .Values.realtime.enabled }}
{{- $rtPdbAuto := and (kindIs "invalid" .Values.podDisruptionBudget.enabled) (or (gt (int .Values.realtime.replicaCount) 1) (gt $hpaMin 1)) -}}
{{- if and .Values.realtime.enabled (or (eq .Values.podDisruptionBudget.enabled true) $rtPdbAuto) }}
{{- with .Values.podDisruptionBudget }}
---
apiVersion: policy/v1
@@ -40,7 +56,7 @@ spec:
{{- else if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- else }}
maxUnavailable: 1
minAvailable: 1
{{- end }}
{{- if .unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ .unhealthyPodEvictionPolicy }}
+34 -13
View File
@@ -1,5 +1,14 @@
{{- if and .Values.app.enabled (include "sim.createAppSecrets" .) }}
# Secret for app credentials (authentication, encryption keys)
{{/*
Secret for app + realtime env. Every key in .Values.app.env and
.Values.realtime.env is written here and mounted via envFrom on the
respective Deployments. Chart-computed values (DATABASE_URL,
SOCKET_SERVER_URL, OLLAMA_URL) are omitted — they're injected as inline
env on the container so they reflect chart-time resolution.
Treating all env values as secret-grade avoids maintaining a sensitivity
classifier and prevents accidental leaks when new provider keys are added.
*/}}
apiVersion: v1
kind: Secret
metadata:
@@ -9,22 +18,34 @@ metadata:
{{- include "sim.app.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- if .Values.app.env.BETTER_AUTH_SECRET }}
BETTER_AUTH_SECRET: {{ .Values.app.env.BETTER_AUTH_SECRET | quote }}
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" }}
{{- /*
Intent: app.env is authoritative for shared keys (both pods envFrom this
Secret, so the app container must not be silently overwritten by a
realtime-side value), BUT empty strings must never win.
Sprig `merge` treats "" as a real value, so a default-empty app.env entry
would shadow a non-empty realtime.env entry. Build the effective dict
manually: start from realtime.env, then overlay non-empty app.env values
on top — this gives app.env-wins-on-collision without empty-string
shadowing. Mirrors the pattern used in deployment-realtime.yaml.
*/}}
{{- $appEnv := .Values.app.env | default dict }}
{{- $rtEnv := .Values.realtime.env | default dict }}
{{- $effective := dict }}
{{- range $key, $value := $rtEnv }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") }}
{{- $_ := set $effective $key $value }}
{{- end }}
{{- if .Values.app.env.ENCRYPTION_KEY }}
ENCRYPTION_KEY: {{ .Values.app.env.ENCRYPTION_KEY | quote }}
{{- end }}
{{- if .Values.app.env.INTERNAL_API_SECRET }}
INTERNAL_API_SECRET: {{ .Values.app.env.INTERNAL_API_SECRET | quote }}
{{- range $key, $value := $appEnv }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") }}
{{- $_ := set $effective $key $value }}
{{- end }}
{{- if .Values.app.env.CRON_SECRET }}
CRON_SECRET: {{ .Values.app.env.CRON_SECRET | quote }}
{{- end }}
{{- if .Values.app.env.API_ENCRYPTION_KEY }}
API_ENCRYPTION_KEY: {{ .Values.app.env.API_ENCRYPTION_KEY | quote }}
{{- range $key, $value := $effective }}
{{- if not (has $key $chartComputed) }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- if .Values.app.env.REDIS_URL }}
REDIS_URL: {{ .Values.app.env.REDIS_URL | quote }}
{{- end }}
{{- end }}
+1
View File
@@ -10,4 +10,5 @@ metadata:
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: false
{{- end }}
+20 -1
View File
@@ -42,7 +42,7 @@ spec:
{{- if .Values.postgresql.enabled }}
---
# Service for PostgreSQL
# Service for PostgreSQL (ClusterIP — used by client workloads)
apiVersion: v1
kind: Service
metadata:
@@ -59,6 +59,25 @@ spec:
name: postgresql
selector:
{{- include "sim.postgresql.selectorLabels" . | nindent 4 }}
---
# Headless Service for PostgreSQL StatefulSet (stable per-pod DNS)
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-postgresql-headless
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
clusterIP: None
publishNotReadyAddresses: true
ports:
- port: {{ .Values.postgresql.service.port }}
targetPort: {{ .Values.postgresql.service.targetPort }}
protocol: TCP
name: postgresql
selector:
{{- include "sim.postgresql.selectorLabels" . | nindent 4 }}
{{- end }}
{{- if .Values.ollama.enabled }}
@@ -14,6 +14,7 @@ stringData:
POSTGRES_DB: {{ .Values.copilot.postgresql.auth.database | quote }}
DATABASE_URL: "postgresql://{{ .Values.copilot.postgresql.auth.username }}:{{ .Values.copilot.postgresql.auth.password }}@{{ include "sim.fullname" . }}-copilot-postgresql:{{ .Values.copilot.postgresql.service.port }}/{{ .Values.copilot.postgresql.auth.database }}"
---
# ClusterIP Service used by client workloads
apiVersion: v1
kind: Service
metadata:
@@ -34,6 +35,28 @@ spec:
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: copilot-postgresql
---
# Headless Service for the StatefulSet (stable per-pod DNS)
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-copilot-postgresql-headless
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot-postgresql
spec:
clusterIP: None
publishNotReadyAddresses: true
ports:
- port: {{ .Values.copilot.postgresql.service.port }}
targetPort: {{ .Values.copilot.postgresql.service.targetPort }}
protocol: TCP
name: postgresql
selector:
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: copilot-postgresql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
@@ -43,8 +66,11 @@ metadata:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot-postgresql
spec:
serviceName: {{ include "sim.fullname" . }}-copilot-postgresql
serviceName: {{ include "sim.fullname" . }}-copilot-postgresql-headless
replicas: 1
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
app.kubernetes.io/name: {{ include "sim.name" . }}
@@ -64,17 +90,16 @@ spec:
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- with .Values.copilot.postgresql.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.copilot.postgresql.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "sim.podSecurityContext" .Values.copilot.postgresql | nindent 6 }}
containers:
- name: postgresql
image: {{ include "sim.image" (dict "context" . "image" .Values.copilot.postgresql.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.copilot.postgresql.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.copilot.postgresql.image.pullPolicy }}
ports:
- name: postgresql
@@ -98,10 +123,7 @@ spec:
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.postgresql.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.postgresql | nindent 10 }}
{{- if .Values.copilot.postgresql.persistence.enabled }}
volumeMounts:
- name: data
@@ -90,9 +90,12 @@ metadata:
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
serviceName: {{ include "sim.fullname" . }}-postgresql
serviceName: {{ include "sim.fullname" . }}-postgresql-headless
replicas: 1
minReadySeconds: 10
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
{{- include "sim.postgresql.selectorLabels" . | nindent 6 }}
@@ -113,13 +116,14 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.postgresql | nindent 6 }}
{{- include "sim.nodeSelector" .Values.postgresql | nindent 6 }}
{{- include "sim.tolerations" .Values | nindent 6 }}
{{- include "sim.affinity" .Values | nindent 6 }}
containers:
- name: postgresql
image: {{ include "sim.image" (dict "context" . "image" .Values.postgresql.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.postgresql.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }}
args: ["-c", "config_file=/etc/postgresql/postgresql.conf"]
ports:
@@ -140,7 +144,7 @@ spec:
{{- toYaml .Values.postgresql.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.postgresql | nindent 10 }}
{{- include "sim.securityContext" .Values.postgresql | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.postgresql | nindent 10 }}
volumeMounts:
{{- if .Values.postgresql.persistence.enabled }}
- name: postgresql-data
+1 -1
View File
@@ -131,7 +131,7 @@ spec:
fsGroup: 10001
containers:
- name: otel-collector
image: {{ include "sim.image" (dict "context" . "image" .Values.telemetry.image) }}
image: {{ include "sim.image" (dict "imageRoot" .Values.telemetry.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.telemetry.image.pullPolicy }}
command:
- /otelcol-contrib
@@ -0,0 +1,54 @@
{{- if .Values.tests.enabled }}
apiVersion: v1
kind: Pod
metadata:
name: {{ include "sim.fullname" . }}-test-connection
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: test
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
restartPolicy: Never
{{- with .Values.tests.image.pullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 4 }}
{{- end }}
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: probe
image: "{{ .Values.tests.image.repository }}:{{ .Values.tests.image.tag }}"
imagePullPolicy: {{ .Values.tests.image.pullPolicy }}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
capabilities:
drop:
- ALL
command:
- sh
- -c
- |
set -eu
APP_URL="http://{{ include "sim.fullname" . }}-app:{{ .Values.app.service.port }}/"
echo "Probing $APP_URL"
wget -q -T {{ .Values.tests.timeoutSeconds }} --tries=1 --spider "$APP_URL"
{{- if .Values.realtime.enabled }}
RT_URL="http://{{ include "sim.fullname" . }}-realtime:{{ .Values.realtime.service.port }}/health"
echo "Probing $RT_URL"
wget -q -T {{ .Values.tests.timeoutSeconds }} --tries=1 --spider "$RT_URL"
{{- end }}
echo "OK"
resources:
{{- toYaml .Values.tests.resources | nindent 8 }}
{{- end }}
@@ -0,0 +1,50 @@
suite: chart-computed env keys can't be overridden (round-8 regression net)
release:
name: t
namespace: sim
tests:
- it: app pod uses chart-computed DATABASE_URL even when user tries to override
template: deployment-app.yaml
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
app.env.DATABASE_URL: "should-be-ignored"
postgresql.auth.password: x
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: DATABASE_URL
value: "should-be-ignored"
- it: existingSecret inline path skips DATABASE_URL/SOCKET_SERVER_URL
template: deployment-app.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
app.env.DATABASE_URL: "postgres://evil-1:5432/x"
app.env.SOCKET_SERVER_URL: "https://evil-2.example.com"
postgresql.auth.password: x
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content: { name: DATABASE_URL, value: "postgres://evil-1:5432/x" }
- notContains:
path: spec.template.spec.containers[0].env
content: { name: SOCKET_SERVER_URL, value: "https://evil-2.example.com" }
- it: realtime pod existingSecret inline path skips chart-computed keys (round-8)
template: deployment-realtime.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
app.env.DATABASE_URL: "postgres://evil-1:5432/x"
app.env.SOCKET_SERVER_URL: "https://evil-2.example.com"
postgresql.auth.password: x
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content: { name: SOCKET_SERVER_URL, value: "https://evil-2.example.com" }
+102
View File
@@ -0,0 +1,102 @@
suite: envDefaults — secret-mode-aware inlining (round-9 regression net)
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
tests:
- it: inline mode renders localhost envDefaults on the app pod
template: deployment-app.yaml
set:
<<: *defaults
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: BETTER_AUTH_URL
value: http://localhost:3000
- it: inline mode renders localhost envDefaults on the realtime pod
template: deployment-realtime.yaml
set:
<<: *defaults
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: BETTER_AUTH_URL
value: http://localhost:3000
- it: existingSecret mode skips envDefaults on the app pod
template: deployment-app.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
postgresql.auth.password: x
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: BETTER_AUTH_URL
value: http://localhost:3000
- it: existingSecret mode skips envDefaults on the realtime pod
template: deployment-realtime.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
postgresql.auth.password: x
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: BETTER_AUTH_URL
value: http://localhost:3000
- it: existingSecret mode inlines user-set app.env values on the app pod
template: deployment-app.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
app.env.NEXT_PUBLIC_APP_URL: "https://prod.example.com"
postgresql.auth.password: x
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: NEXT_PUBLIC_APP_URL
value: "https://prod.example.com"
- it: existingSecret mode propagates app.env to the realtime pod (round-7 regression)
template: deployment-realtime.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
app.env.NEXT_PUBLIC_APP_URL: "https://prod.example.com"
postgresql.auth.password: x
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: NEXT_PUBLIC_APP_URL
value: "https://prod.example.com"
- it: realtime.env wins over app.env on the realtime pod in existingSecret mode
template: deployment-realtime.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-secret
app.env.NEXT_PUBLIC_APP_URL: "https://prod.example.com"
realtime.env.NEXT_PUBLIC_APP_URL: "https://realtime.example.com"
postgresql.auth.password: x
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: NEXT_PUBLIC_APP_URL
value: "https://realtime.example.com"
+55
View File
@@ -0,0 +1,55 @@
suite: helm test hook (helm test) connectivity probe
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
tests:
- it: renders the test pod with helm.sh/hook=test by default
template: tests/test-connection.yaml
set:
<<: *defaults
asserts:
- isKind:
of: Pod
- equal:
path: 'metadata.annotations["helm.sh/hook"]'
value: test
- equal:
path: 'metadata.annotations["helm.sh/hook-delete-policy"]'
value: before-hook-creation,hook-succeeded
- equal:
path: spec.restartPolicy
value: Never
- it: applies restricted PSS context to the test pod
template: tests/test-connection.yaml
set:
<<: *defaults
asserts:
- equal:
path: spec.securityContext.runAsNonRoot
value: true
- equal:
path: spec.containers[0].securityContext.allowPrivilegeEscalation
value: false
- equal:
path: spec.containers[0].securityContext.readOnlyRootFilesystem
value: true
- contains:
path: spec.containers[0].securityContext.capabilities.drop
content: ALL
- it: skips rendering when tests.enabled=false
template: tests/test-connection.yaml
set:
<<: *defaults
tests.enabled: false
asserts:
- hasDocuments:
count: 0
+147
View File
@@ -0,0 +1,147 @@
suite: NetworkPolicy — ingressFrom + telemetry egress + egress shape
templates:
- networkpolicy.yaml
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
networkPolicy.enabled: true
ingress.enabled: true
ingress.app.host: a.test
ingress.realtime.host: b.test
tests:
- it: does not render when networkPolicy.enabled=false
set:
<<: *defaults
networkPolicy.enabled: false
asserts:
- hasDocuments: { count: 0 }
- it: default ingressFrom is the explicit any-source peer (single empty peer)
set:
<<: *defaults
documentIndex: 0
asserts:
- equal:
path: spec.ingress[2].from
value:
- {}
- it: ingressFrom is overridable to a namespace selector
set:
<<: *defaults
networkPolicy.ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
documentIndex: 0
asserts:
- equal:
path: spec.ingress[2].from
value:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- it: app NetworkPolicy includes telemetry egress (ports 4317 + 4318) when telemetry enabled (round-5 fix)
set:
<<: *defaults
telemetry.enabled: true
documentIndex: 0
asserts:
- contains:
path: spec.egress
content:
to:
- podSelector:
matchLabels:
app.kubernetes.io/name: sim
app.kubernetes.io/instance: t
app.kubernetes.io/component: telemetry
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
- it: realtime NetworkPolicy includes telemetry egress when telemetry enabled
set:
<<: *defaults
telemetry.enabled: true
documentIndex: 1
asserts:
- contains:
path: spec.egress
content:
to:
- podSelector:
matchLabels:
app.kubernetes.io/name: sim
app.kubernetes.io/instance: t
app.kubernetes.io/component: telemetry
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
- it: app NetworkPolicy allows ingress from cron pods when cronjobs.enabled=true
set:
<<: *defaults
documentIndex: 0
asserts:
- contains:
path: spec.ingress
content:
from:
- podSelector:
matchLabels:
app.kubernetes.io/name: sim
app.kubernetes.io/instance: t
simstudio.ai/component-group: cronjob
ports:
- protocol: TCP
port: 3000
- it: cron→app ingress rule is omitted when cronjobs.enabled=false
set:
<<: *defaults
cronjobs.enabled: false
documentIndex: 0
asserts:
- notContains:
path: spec.ingress
content:
from:
- podSelector:
matchLabels:
app.kubernetes.io/name: sim
app.kubernetes.io/instance: t
simstudio.ai/component-group: cronjob
ports:
- protocol: TCP
port: 3000
- it: egress.extraRules are appended to both app and realtime NetworkPolicies
set:
<<: *defaults
networkPolicy.egress.extraRules:
- to: []
ports:
- protocol: TCP
port: 5432
documentIndex: 0
asserts:
- contains:
path: spec.egress
content:
to: []
ports:
- protocol: TCP
port: 5432
+73
View File
@@ -0,0 +1,73 @@
suite: PDB tri-state and HPA conditional rendering
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
tests:
- it: PDB does not render when replicaCount=1 and HPA is off (tri-state null)
template: poddisruptionbudget.yaml
set:
<<: *defaults
app.replicaCount: 1
realtime.replicaCount: 1
autoscaling.enabled: false
asserts:
- hasDocuments: { count: 0 }
- it: PDB auto-renders when replicaCount>1 (tri-state null)
template: poddisruptionbudget.yaml
set:
<<: *defaults
app.replicaCount: 2
realtime.replicaCount: 2
autoscaling.enabled: false
asserts:
- hasDocuments: { count: 2 }
- it: PDB auto-renders when autoscaling.minReplicas>1 (tri-state null)
template: poddisruptionbudget.yaml
set:
<<: *defaults
app.replicaCount: 1
realtime.replicaCount: 1
autoscaling.enabled: true
autoscaling.minReplicas: 2
asserts:
- hasDocuments: { count: 2 }
- it: PDB explicitly disabled stays off even with multiple replicas
template: poddisruptionbudget.yaml
set:
<<: *defaults
app.replicaCount: 3
podDisruptionBudget.enabled: false
asserts:
- hasDocuments: { count: 0 }
- it: HPA does not render when autoscaling.enabled=false
template: hpa.yaml
set:
<<: *defaults
autoscaling.enabled: false
asserts:
- hasDocuments: { count: 0 }
- it: HPA renders when autoscaling.enabled=true and uses autoscaling/v2
template: hpa.yaml
set:
<<: *defaults
autoscaling.enabled: true
autoscaling.minReplicas: 2
autoscaling.maxReplicas: 10
autoscaling.targetCPUUtilizationPercentage: 70
asserts:
- isKind: { of: HorizontalPodAutoscaler }
- equal: { path: apiVersion, value: autoscaling/v2 }
- equal: { path: spec.minReplicas, value: 2 }
- equal: { path: spec.maxReplicas, value: 10 }
+87
View File
@@ -0,0 +1,87 @@
suite: pod rollout — checksum annotations + topology spread
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
tests:
- it: app pod template has checksum/secret annotation
template: deployment-app.yaml
set:
<<: *defaults
asserts:
- isNotEmpty:
path: spec.template.metadata.annotations["checksum/secret"]
- it: realtime pod template has checksum/secret annotation
template: deployment-realtime.yaml
set:
<<: *defaults
asserts:
- isNotEmpty:
path: spec.template.metadata.annotations["checksum/secret"]
- it: changing a secret value changes the app pod checksum (forces rollout)
template: deployment-app.yaml
set:
<<: *defaults
app.env.BETTER_AUTH_SECRET: original-value
asserts:
- notEqual:
path: spec.template.metadata.annotations["checksum/secret"]
value: ""
- it: topologySpreadConstraints render on the app pod when set
template: deployment-app.yaml
set:
<<: *defaults
app.topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: sim
asserts:
- equal:
path: spec.template.spec.topologySpreadConstraints[0].topologyKey
value: topology.kubernetes.io/zone
- equal:
path: spec.template.spec.topologySpreadConstraints[0].whenUnsatisfiable
value: ScheduleAnyway
- it: topologySpreadConstraints render on the realtime pod when set
template: deployment-realtime.yaml
set:
<<: *defaults
realtime.topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: sim
asserts:
- equal:
path: spec.template.spec.topologySpreadConstraints[0].topologyKey
value: topology.kubernetes.io/zone
- it: app topologySpreadConstraints do not leak onto realtime pod
template: deployment-realtime.yaml
set:
<<: *defaults
app.topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: sim
asserts:
- isNull:
path: spec.template.spec.topologySpreadConstraints
+118
View File
@@ -0,0 +1,118 @@
suite: secret modes — inline / existingSecret / ESO routing
release:
name: t
namespace: sim
tests:
- it: inline mode renders the chart-managed Secret
template: secrets-app.yaml
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
asserts:
- isKind: { of: Secret }
- equal: { path: metadata.name, value: t-sim-app-secrets }
- it: existingSecret mode skips the chart-managed Secret
templates:
- secrets-app.yaml
- external-secret-app.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-existing-secret
postgresql.auth.password: x
asserts:
- hasDocuments: { count: 0 }
- it: ESO mode renders an ExternalSecret instead of the chart-managed Secret
template: external-secret-app.yaml
set:
externalSecrets.enabled: true
externalSecrets.secretStoreRef.name: sim-store
externalSecrets.secretStoreRef.kind: ClusterSecretStore
externalSecrets.remoteRefs.app.BETTER_AUTH_SECRET: path/to/auth
externalSecrets.remoteRefs.app.ENCRYPTION_KEY: path/to/enc
externalSecrets.remoteRefs.app.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.app.CRON_SECRET: path/to/cron
externalSecrets.remoteRefs.postgresql.password: path/to/pgpw
postgresql.auth.password: x
asserts:
- isKind: { of: ExternalSecret }
- equal: { path: metadata.name, value: t-sim-app-secrets }
- equal: { path: spec.secretStoreRef.name, value: sim-store }
- equal: { path: spec.secretStoreRef.kind, value: ClusterSecretStore }
- it: ESO mode skips the chart-managed Secret
template: secrets-app.yaml
set:
externalSecrets.enabled: true
externalSecrets.secretStoreRef.name: sim-store
externalSecrets.remoteRefs.app.BETTER_AUTH_SECRET: path/to/auth
externalSecrets.remoteRefs.app.ENCRYPTION_KEY: path/to/enc
externalSecrets.remoteRefs.app.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.app.CRON_SECRET: path/to/cron
externalSecrets.remoteRefs.postgresql.password: path/to/pgpw
postgresql.auth.password: x
asserts:
- hasDocuments: { count: 0 }
- it: ESO validator fails when app.env contains an unmapped key
set:
externalSecrets.enabled: true
externalSecrets.secretStoreRef.name: sim-store
externalSecrets.remoteRefs.app.BETTER_AUTH_SECRET: path/to/auth
externalSecrets.remoteRefs.app.ENCRYPTION_KEY: path/to/enc
externalSecrets.remoteRefs.app.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.app.CRON_SECRET: path/to/cron
externalSecrets.remoteRefs.postgresql.password: path/to/pgpw
app.env.UNMAPPED_KEY: "would-go-nowhere"
postgresql.auth.password: x
asserts:
- failedTemplate:
errorPattern: "Key 'UNMAPPED_KEY' is set in app.env but externalSecrets.enabled=true"
- it: app deployment envFrom references existingSecret name
template: deployment-app.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-existing-secret
postgresql.auth.password: x
asserts:
- contains:
path: spec.template.spec.containers[0].envFrom
content:
secretRef:
name: my-existing-secret
- it: realtime.env-only value reaches the Secret when app.env entry is empty (cursor bugbot fix)
template: secrets-app.yaml
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
app.env.BETTER_AUTH_URL: ""
realtime.env.BETTER_AUTH_URL: "https://realtime.example.com"
postgresql.auth.password: x
asserts:
- equal:
path: stringData.BETTER_AUTH_URL
value: "https://realtime.example.com"
- it: app.env wins over realtime.env on collision when both are non-empty
template: secrets-app.yaml
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
app.env.NEXT_PUBLIC_APP_URL: "https://app.example.com"
realtime.env.NEXT_PUBLIC_APP_URL: "https://realtime.example.com"
postgresql.auth.password: x
asserts:
- equal:
path: stringData.NEXT_PUBLIC_APP_URL
value: "https://app.example.com"
+33
View File
@@ -0,0 +1,33 @@
suite: smoke — minimal install renders
templates:
- deployment-app.yaml
- deployment-realtime.yaml
- secrets-app.yaml
- services.yaml
release:
name: t
namespace: sim
set:
app.env.BETTER_AUTH_SECRET: test-better-auth-secret-32-bytes
app.env.ENCRYPTION_KEY: test-encryption-key-32-bytes
app.env.INTERNAL_API_SECRET: test-internal-api-secret-32-bytes
app.env.CRON_SECRET: test-cron-secret-32-bytes
postgresql.auth.password: testpassword
tests:
- it: renders the app deployment
template: deployment-app.yaml
asserts:
- isKind: { of: Deployment }
- equal: { path: metadata.name, value: t-sim-app }
- it: renders the realtime deployment
template: deployment-realtime.yaml
asserts:
- isKind: { of: Deployment }
- equal: { path: metadata.name, value: t-sim-realtime }
- it: renders the chart-managed app secret
template: secrets-app.yaml
asserts:
- isKind: { of: Secret }
- equal: { path: metadata.name, value: t-sim-app-secrets }
+85
View File
@@ -0,0 +1,85 @@
suite: validators — required secrets fail clearly
templates:
- deployment-app.yaml
release:
name: t
namespace: sim
tests:
- it: fails when BETTER_AUTH_SECRET is missing
set:
app.env.BETTER_AUTH_SECRET: ""
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
asserts:
- failedTemplate:
errorMessage: "app.env.BETTER_AUTH_SECRET is required for production deployment"
- it: fails when ENCRYPTION_KEY is missing
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: ""
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
asserts:
- failedTemplate:
errorMessage: "app.env.ENCRYPTION_KEY is required for production deployment"
- it: fails when INTERNAL_API_SECRET is missing (1.0.0 requirement)
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: ""
app.env.CRON_SECRET: x
postgresql.auth.password: x
asserts:
- failedTemplate:
errorMessage: "app.env.INTERNAL_API_SECRET is required for production deployment (shared auth between sim-app and sim-realtime pods). Generate one with: openssl rand -hex 32"
- it: fails when cronjobs are enabled but CRON_SECRET is empty
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: ""
cronjobs.enabled: true
postgresql.auth.password: x
asserts:
- failedTemplate:
errorMessage: "app.env.CRON_SECRET is required when cronjobs.enabled=true (every cron pod authenticates with this token). Generate one with: openssl rand -hex 32, or set cronjobs.enabled=false."
- it: fails when postgresql.auth.password is missing
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: ""
asserts:
- failedTemplate:
errorMessage: "postgresql.auth.password is required when using internal PostgreSQL"
- it: rejects postgres passwords with URL-unsafe characters
set:
app.env.BETTER_AUTH_SECRET: x
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: "pa$$word"
asserts:
- failedTemplate:
errorPattern: "postgresql.auth.password must only contain alphanumeric"
- it: rejects placeholder BETTER_AUTH_SECRET
set:
app.env.BETTER_AUTH_SECRET: "CHANGE-ME-32-CHAR-SECRET-FOR-PRODUCTION-USE"
app.env.ENCRYPTION_KEY: x
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: x
asserts:
- failedTemplate:
errorPattern: "must not use the default placeholder value"
+28 -10
View File
@@ -117,8 +117,8 @@
},
"NEXT_PUBLIC_APP_URL": {
"type": "string",
"format": "uri",
"description": "Public application URL"
"anyOf": [{ "format": "uri" }, { "const": "" }],
"description": "Public application URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
},
"INTERNAL_API_BASE_URL": {
"type": "string",
@@ -134,8 +134,8 @@
},
"BETTER_AUTH_URL": {
"type": "string",
"format": "uri",
"description": "Authentication service URL"
"anyOf": [{ "format": "uri" }, { "const": "" }],
"description": "Authentication service URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
},
"SOCKET_SERVER_URL": {
"type": "string",
@@ -301,8 +301,8 @@
},
"NEXT_PUBLIC_SUPPORT_EMAIL": {
"type": "string",
"format": "email",
"description": "Support email address"
"anyOf": [{ "format": "email" }, { "const": "" }],
"description": "Support email address (default help@sim.ai via envDefaults; set explicitly here to override in Secret)"
},
"NEXT_PUBLIC_DOCUMENTATION_URL": {
"type": "string",
@@ -385,13 +385,13 @@
},
"NEXT_PUBLIC_APP_URL": {
"type": "string",
"format": "uri",
"description": "Public application URL"
"anyOf": [{ "format": "uri" }, { "const": "" }],
"description": "Public application URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
},
"BETTER_AUTH_URL": {
"type": "string",
"format": "uri",
"description": "Authentication service URL"
"anyOf": [{ "format": "uri" }, { "const": "" }],
"description": "Authentication service URL (default http://localhost:3000 via envDefaults; set explicitly here to override in Secret)"
},
"ALLOWED_ORIGINS": {
"type": "string",
@@ -691,6 +691,24 @@
}
}
},
"tests": {
"type": "object",
"description": "Helm test hook configuration (helm test)",
"properties": {
"enabled": { "type": "boolean" },
"image": {
"type": "object",
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" },
"pullPolicy": { "type": "string", "enum": ["Always", "IfNotPresent", "Never"] },
"pullSecrets": { "type": "array", "items": { "type": "object" } }
}
},
"timeoutSeconds": { "type": "integer", "minimum": 1 },
"resources": { "type": "object" }
}
},
"sharedStorage": {
"type": "object",
"properties": {
+338 -139
View File
@@ -20,8 +20,11 @@ app:
# Image configuration
image:
repository: simstudioai/simstudio
tag: latest
pullPolicy: Always
# tag defaults to Chart.AppVersion. Override with a release tag (e.g. "0.6.73") or pin via image.digest.
tag: ""
# Optional image digest pin: "sha256:..." — when set, overrides tag.
digest: ""
pullPolicy: IfNotPresent
# Number of replicas
replicaCount: 1
@@ -38,6 +41,10 @@ app:
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
nodeSelector: {}
# Topology spread constraints for app pods (HA across zones / nodes).
# Each entry must include its own labelSelector.
topologySpreadConstraints: []
# Pod security context
podSecurityContext:
fsGroup: 1001
@@ -68,11 +75,12 @@ app:
# Environment variables
env:
# Application URLs — set NEXT_PUBLIC_APP_URL to your public origin (e.g. https://sim.example.com).
# Leaving these at localhost in a production deployment will break sign-up/sign-in for browsers,
# because the auth client and Better Auth `trustedOrigins` use these values.
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
BETTER_AUTH_URL: "http://localhost:3000"
# Application URLs — set NEXT_PUBLIC_APP_URL / BETTER_AUTH_URL to your public origin
# (e.g. https://sim.example.com). Defaults to http://localhost:3000 via app.envDefaults
# for local installs; override here (or via --set) for production. Set values here go
# into the chart-managed Secret and take precedence over envDefaults at runtime.
NEXT_PUBLIC_APP_URL: ""
BETTER_AUTH_URL: ""
INTERNAL_API_BASE_URL: "" # Optional server-side internal base URL for /api self-calls (include http:// or https://); falls back to NEXT_PUBLIC_APP_URL when empty
# TRUSTED_ORIGINS: comma-separated extra public origins to trust for auth (e.g. apex+www, alias hostnames).
# Merged into Better Auth `trustedOrigins` alongside NEXT_PUBLIC_APP_URL. Leave empty when serving from a single origin.
@@ -82,10 +90,6 @@ app:
# page's own origin (assumes the ingress/reverse proxy routes /socket.io to the realtime service).
# Set explicitly only when realtime is on a separate host:port from the app (e.g. wss://socket.example.com).
NEXT_PUBLIC_SOCKET_URL: ""
# Node environment
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# Telemetry & Monitoring
TELEMETRY_ENDPOINT: "" # OTLP endpoint for traces/logs (e.g., "https://otlp-collector:4318/v1/traces")
@@ -101,12 +105,12 @@ app:
CRON_SECRET: "" # OPTIONAL - required only if cronjobs.enabled=true, authenticates scheduled job requests
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32 (outputs 64 hex chars = 32 bytes)
API_ENCRYPTION_KEY: "" # OPTIONAL - encrypts API keys at rest, must be exactly 64 hex characters, if not set keys stored in plain text
# Generate with: openssl rand -hex 32 (produces the required 64-hex-char / 32-byte value).
API_ENCRYPTION_KEY: "" # OPTIONAL - encrypts API keys at rest; if unset, keys are stored in plain text
REDIS_URL: "" # OPTIONAL - Redis connection string for caching/sessions; can also come from app secret or External Secrets
# Email & Communication
EMAIL_VERIFICATION_ENABLED: "false" # Enable email verification for user registration and login (defaults to false)
EMAIL_VERIFICATION_ENABLED: "" # Set to "true" to enable email verification for user registration and login (default "false" via envDefaults)
RESEND_API_KEY: "" # Resend API key for transactional emails
FROM_EMAIL_ADDRESS: "" # Complete from address (e.g., "Sim <noreply@domain.com>" or "DoNotReply@domain.com")
EMAIL_DOMAIN: "" # Domain for sending emails (fallback when FROM_EMAIL_ADDRESS not set)
@@ -121,7 +125,7 @@ app:
# Google Vertex AI Configuration
VERTEX_PROJECT: "" # Google Cloud project ID for Vertex AI
VERTEX_LOCATION: "us-central1" # Google Cloud region for Vertex AI (e.g., "us-central1")
VERTEX_LOCATION: "" # Google Cloud region for Vertex AI (default "us-central1" via envDefaults)
# Azure OpenAI Configuration (leave empty if not using Azure OpenAI)
AZURE_OPENAI_ENDPOINT: "" # Azure OpenAI service endpoint (e.g., https://your-resource.openai.azure.com)
@@ -145,58 +149,9 @@ app:
ANTHROPIC_API_KEY_3: "" # Additional Anthropic API key for load balancing
OLLAMA_URL: "" # Ollama local LLM server URL
ELEVENLABS_API_KEY: "" # ElevenLabs API key for text-to-speech in deployed chat
# Admission Gate Configuration
ADMISSION_GATE_MAX_INFLIGHT: "500" # Max concurrent in-flight execution requests per pod
# Rate Limiting Configuration (per minute)
RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window duration (1 minute)
RATE_LIMIT_FREE_SYNC: "50" # Sync API executions per minute
RATE_LIMIT_FREE_ASYNC: "200" # Async API executions per minute
# Execution Timeout Configuration (in seconds)
# Sync timeouts apply to synchronous API calls
EXECUTION_TIMEOUT_FREE: "300" # Free tier sync timeout (5 minutes)
EXECUTION_TIMEOUT_PRO: "3000" # Pro tier sync timeout (50 minutes)
EXECUTION_TIMEOUT_TEAM: "3000" # Team tier sync timeout (50 minutes)
EXECUTION_TIMEOUT_ENTERPRISE: "3000" # Enterprise tier sync timeout (50 minutes)
# Async timeouts apply to async/background job executions
EXECUTION_TIMEOUT_ASYNC_FREE: "5400" # Free tier async timeout (90 minutes)
EXECUTION_TIMEOUT_ASYNC_PRO: "5400" # Pro tier async timeout (90 minutes)
EXECUTION_TIMEOUT_ASYNC_TEAM: "5400" # Team tier async timeout (90 minutes)
EXECUTION_TIMEOUT_ASYNC_ENTERPRISE: "5400" # Enterprise tier async timeout (90 minutes)
# Table Feature Limits (per workspace, per plan)
# Apply when billing is disabled (free tier defaults) or for billed plans
FREE_TABLES_LIMIT: "3" # Max user tables per workspace on free tier
FREE_TABLE_ROWS_LIMIT: "1000" # Max rows per table on free tier
PRO_TABLES_LIMIT: "25" # Max user tables per workspace on pro tier
PRO_TABLE_ROWS_LIMIT: "5000" # Max rows per table on pro tier
TEAM_TABLES_LIMIT: "100" # Max user tables per workspace on team tier
TEAM_TABLE_ROWS_LIMIT: "10000" # Max rows per table on team tier
ENTERPRISE_TABLES_LIMIT: "10000" # Max user tables per workspace on enterprise tier
ENTERPRISE_TABLE_ROWS_LIMIT: "1000000" # Max rows per table on enterprise tier
# Isolated-VM Worker Pool Configuration
IVM_POOL_SIZE: "4" # Max worker processes in pool
IVM_MAX_CONCURRENT: "10000" # Max concurrent executions globally
IVM_MAX_PER_WORKER: "2500" # Max concurrent executions per worker
IVM_WORKER_IDLE_TIMEOUT_MS: "60000" # Worker idle cleanup timeout (ms)
IVM_QUEUE_TIMEOUT_MS: "300000" # Max queue wait before rejection (ms)
IVM_MAX_QUEUE_SIZE: "10000" # Max queued executions globally
IVM_MAX_ACTIVE_PER_OWNER: "200" # Max concurrent executions per user
IVM_MAX_QUEUED_PER_OWNER: "2000" # Max queued executions per user
IVM_MAX_OWNER_WEIGHT: "5" # Max scheduling weight per user
IVM_DISTRIBUTED_MAX_INFLIGHT_PER_OWNER: "2200" # Max in-flight per user across instances (Redis)
IVM_DISTRIBUTED_LEASE_MIN_TTL_MS: "120000" # Min distributed lease TTL (ms)
IVM_MAX_FETCH_RESPONSE_BYTES: "8388608" # Max fetch response size (8MB)
IVM_MAX_FETCH_RESPONSE_CHARS: "4000000" # Max fetch response chars
IVM_MAX_FETCH_URL_LENGTH: "8192" # Max fetch URL length
IVM_MAX_FETCH_OPTIONS_JSON_CHARS: "262144" # Max fetch options payload (256KB)
IVM_MAX_STDOUT_CHARS: "200000" # Max stdout capture per execution
# UI Branding & Whitelabeling Configuration
NEXT_PUBLIC_BRAND_NAME: "Sim" # Custom brand name
NEXT_PUBLIC_BRAND_NAME: "" # Custom brand name (default "Sim" via envDefaults)
NEXT_PUBLIC_BRAND_LOGO_URL: "" # Custom logo URL — square icon, shown in collapsed sidebar
NEXT_PUBLIC_BRAND_WORDMARK_URL: "" # Custom wordmark URL — wide image, shown in expanded sidebar
NEXT_PUBLIC_BRAND_FAVICON_URL: "" # Custom favicon URL (leave empty for default)
@@ -204,7 +159,7 @@ app:
NEXT_PUBLIC_BRAND_ACCENT_COLOR: "" # Accent color (hex, e.g., "#9333ea")
NEXT_PUBLIC_BRAND_BACKGROUND_COLOR: "" # Background color (hex, e.g., "#ffffff")
NEXT_PUBLIC_CUSTOM_CSS_URL: "" # Custom stylesheet URL (leave empty for none)
NEXT_PUBLIC_SUPPORT_EMAIL: "help@sim.ai" # Support email address
NEXT_PUBLIC_SUPPORT_EMAIL: "" # Support email address (default "help@sim.ai" via envDefaults)
NEXT_PUBLIC_DOCUMENTATION_URL: "" # Documentation URL (leave empty for none)
NEXT_PUBLIC_TERMS_URL: "" # Terms of service URL (leave empty for none)
NEXT_PUBLIC_PRIVACY_URL: "" # Privacy policy URL (leave empty for none)
@@ -226,11 +181,11 @@ app:
# Admin API Configuration
ADMIN_API_KEY: "" # Admin API key for organization/user management (generate with: openssl rand -hex 32)
# Organizations & Permission Groups
ACCESS_CONTROL_ENABLED: "false" # Enable permission groups feature ("true" to enable)
ORGANIZATIONS_ENABLED: "false" # Enable organizations feature ("true" to enable)
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: "false" # Show permission groups UI ("true" to enable)
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: "false" # Show organizations UI ("true" to enable)
# Organizations & Permission Groups (defaults to "false" via envDefaults — set "true" here to enable)
ACCESS_CONTROL_ENABLED: "" # Enable permission groups feature ("true" to enable)
ORGANIZATIONS_ENABLED: "" # Enable organizations feature ("true" to enable)
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: "" # Show permission groups UI ("true" to enable)
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: "" # Show organizations UI ("true" to enable)
# LLM Provider/Model Restrictions (leave empty if not restricting)
BLACKLISTED_PROVIDERS: "" # Comma-separated provider IDs to hide from UI (e.g., "openai,anthropic,google")
@@ -312,6 +267,82 @@ app:
AZURE_STORAGE_OG_IMAGES_CONTAINER_NAME: "" # Azure container for OpenGraph preview images
AZURE_STORAGE_WORKSPACE_LOGOS_CONTAINER_NAME: "" # Azure container for workspace logos
# Operational tunables shipped with the chart. These are rendered as inline `env:` on the
# 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.
envDefaults:
# Application URLs (override in app.env or app.envDefaults for production)
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
BETTER_AUTH_URL: "http://localhost:3000"
# Node environment
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# Email defaults
EMAIL_VERIFICATION_ENABLED: "false"
# Vertex AI defaults
VERTEX_LOCATION: "us-central1"
# Branding defaults
NEXT_PUBLIC_BRAND_NAME: "Sim"
NEXT_PUBLIC_SUPPORT_EMAIL: "help@sim.ai"
# Feature flags (default off — set "true" in app.env or app.envDefaults to enable)
ACCESS_CONTROL_ENABLED: "false"
ORGANIZATIONS_ENABLED: "false"
NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: "false"
NEXT_PUBLIC_ORGANIZATIONS_ENABLED: "false"
# Admission Gate
ADMISSION_GATE_MAX_INFLIGHT: "500" # Max concurrent in-flight execution requests per pod
# Rate Limiting (per minute)
RATE_LIMIT_WINDOW_MS: "60000" # Rate limit window duration (1 minute)
RATE_LIMIT_FREE_SYNC: "50" # Sync API executions per minute
RATE_LIMIT_FREE_ASYNC: "200" # Async API executions per minute
# Execution Timeouts (seconds) — sync timeouts apply to synchronous API calls
EXECUTION_TIMEOUT_FREE: "300" # Free tier sync timeout (5 minutes)
EXECUTION_TIMEOUT_PRO: "3000" # Pro tier sync timeout (50 minutes)
EXECUTION_TIMEOUT_TEAM: "3000" # Team tier sync timeout (50 minutes)
EXECUTION_TIMEOUT_ENTERPRISE: "3000" # Enterprise tier sync timeout (50 minutes)
# Async timeouts apply to async/background job executions
EXECUTION_TIMEOUT_ASYNC_FREE: "5400" # Free tier async timeout (90 minutes)
EXECUTION_TIMEOUT_ASYNC_PRO: "5400" # Pro tier async timeout (90 minutes)
EXECUTION_TIMEOUT_ASYNC_TEAM: "5400" # Team tier async timeout (90 minutes)
EXECUTION_TIMEOUT_ASYNC_ENTERPRISE: "5400" # Enterprise tier async timeout (90 minutes)
# Table Feature Limits (per workspace, per plan)
FREE_TABLES_LIMIT: "3" # Max user tables per workspace on free tier
FREE_TABLE_ROWS_LIMIT: "1000" # Max rows per table on free tier
PRO_TABLES_LIMIT: "25" # Max user tables per workspace on pro tier
PRO_TABLE_ROWS_LIMIT: "5000" # Max rows per table on pro tier
TEAM_TABLES_LIMIT: "100" # Max user tables per workspace on team tier
TEAM_TABLE_ROWS_LIMIT: "10000" # Max rows per table on team tier
ENTERPRISE_TABLES_LIMIT: "10000" # Max user tables per workspace on enterprise tier
ENTERPRISE_TABLE_ROWS_LIMIT: "1000000" # Max rows per table on enterprise tier
# Isolated-VM Worker Pool
IVM_POOL_SIZE: "4" # Max worker processes in pool
IVM_MAX_CONCURRENT: "10000" # Max concurrent executions globally
IVM_MAX_PER_WORKER: "2500" # Max concurrent executions per worker
IVM_WORKER_IDLE_TIMEOUT_MS: "60000" # Worker idle cleanup timeout (ms)
IVM_QUEUE_TIMEOUT_MS: "300000" # Max queue wait before rejection (ms)
IVM_MAX_QUEUE_SIZE: "10000" # Max queued executions globally
IVM_MAX_ACTIVE_PER_OWNER: "200" # Max concurrent executions per user
IVM_MAX_QUEUED_PER_OWNER: "2000" # Max queued executions per user
IVM_MAX_OWNER_WEIGHT: "5" # Max scheduling weight per user
IVM_DISTRIBUTED_MAX_INFLIGHT_PER_OWNER: "2200" # Max in-flight per user across instances (Redis)
IVM_DISTRIBUTED_LEASE_MIN_TTL_MS: "120000" # Min distributed lease TTL (ms)
IVM_MAX_FETCH_RESPONSE_BYTES: "8388608" # Max fetch response size (8MB)
IVM_MAX_FETCH_RESPONSE_CHARS: "4000000" # Max fetch response chars
IVM_MAX_FETCH_URL_LENGTH: "8192" # Max fetch URL length
IVM_MAX_FETCH_OPTIONS_JSON_CHARS: "262144" # Max fetch options payload (256KB)
IVM_MAX_STDOUT_CHARS: "200000" # Max stdout capture per execution
# Service configuration
service:
type: ClusterIP
@@ -319,22 +350,36 @@ app:
targetPort: 3000
# Health checks
# startupProbe shields a slow Next.js cold start (large bundle, 4Gi memory limits) from
# liveness restarts. Once it succeeds, liveness + readiness take over. Total cold-start
# budget: failureThreshold * periodSeconds = 60 * 5s = 300s (5 minutes).
startupProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 60
# Liveness: lenient — only restart on true deadlock. failureThreshold * periodSeconds = 180s.
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 90
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
failureThreshold: 6
# Readiness: fast — shift traffic in/out within ~30s of state change.
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 90
timeoutSeconds: 5
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# Additional volumes for app deployment (e.g., branding assets, custom configs)
@@ -349,8 +394,10 @@ realtime:
# Image configuration
image:
repository: simstudioai/realtime
tag: latest
pullPolicy: Always
# tag defaults to Chart.AppVersion. Override with a release tag or pin via image.digest.
tag: ""
digest: ""
pullPolicy: IfNotPresent
# Number of replicas
replicaCount: 1
@@ -366,21 +413,26 @@ realtime:
# Node selector for pod scheduling (leave empty to allow scheduling on any node)
nodeSelector: {}
# Topology spread constraints for realtime pods (HA across zones / nodes).
# Each entry must include its own labelSelector.
topologySpreadConstraints: []
# Pod security context
podSecurityContext:
fsGroup: 1001
# Container security context
securityContext:
runAsNonRoot: true
runAsUser: 1001
# Environment variables
# Environment variables — Secret-bound. See `envDefaults` below for chart-shipped defaults.
env:
# Application URLs — must mirror the public origin used by the main app.
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
BETTER_AUTH_URL: "http://localhost:3000"
# Defaults to http://localhost:3000 via envDefaults; override here for production.
NEXT_PUBLIC_APP_URL: ""
BETTER_AUTH_URL: ""
# Authentication secret (REQUIRED for production)
# Must match the BETTER_AUTH_SECRET value from the main app configuration
@@ -389,9 +441,16 @@ realtime:
# Cross-Origin Resource Sharing (CORS) allowed origins for the realtime service.
# Set to the same public origin(s) as the main app (comma-separated). The realtime
# service rejects WebSocket upgrades from origins not in this list.
# Defaults to http://localhost:3000 via envDefaults; override here for production.
ALLOWED_ORIGINS: ""
# Operational defaults shipped with the chart. Rendered as inline `env:` on the realtime
# container (NOT written into the Secret, NOT required in externalSecrets.remoteRefs).
# User-set values in `realtime.env` above take precedence.
envDefaults:
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
BETTER_AUTH_URL: "http://localhost:3000"
ALLOWED_ORIGINS: "http://localhost:3000"
# Node environment
NODE_ENV: "production"
# Service configuration
@@ -401,22 +460,34 @@ realtime:
targetPort: 3002
# Health checks
# Startup probe absorbs cold-start time. Total budget: 30 * 5s = 150s.
startupProbe:
httpGet:
path: /health
port: 3002
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
# Liveness: lenient. failureThreshold * periodSeconds = 180s.
livenessProbe:
httpGet:
path: /health
port: 3002
initialDelaySeconds: 10
periodSeconds: 90
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
failureThreshold: 6
# Readiness: fast.
readinessProbe:
httpGet:
path: /health
port: 3002
initialDelaySeconds: 10
periodSeconds: 90
timeoutSeconds: 5
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
# Additional volumes for realtime deployment
@@ -431,8 +502,10 @@ migrations:
# Image configuration
image:
repository: simstudioai/migrations
tag: latest
pullPolicy: Always
# tag defaults to Chart.AppVersion. Override with a release tag or pin via image.digest.
tag: ""
digest: ""
pullPolicy: IfNotPresent
# Resource limits and requests
resources:
@@ -486,13 +559,21 @@ postgresql:
cpu: "500m"
# Pod security context
# PostgreSQL image's `postgres` user is uid/gid 999.
podSecurityContext:
fsGroup: 999
# Container security context
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
# Container security context
# readOnlyRootFilesystem is intentionally false — Postgres writes to /var/lib/postgresql/data
# (mounted from PVC) and needs to manage its own data directory.
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
# Persistence configuration
persistence:
enabled: true
@@ -500,7 +581,7 @@ postgresql:
size: 10Gi
accessModes:
- ReadWriteOnce
# SSL/TLS configuration (enable for production deployments with certificates)
# Requires cert-manager to be installed in the cluster
tls:
@@ -583,8 +664,12 @@ ollama:
# Image configuration
image:
repository: ollama/ollama
tag: latest
pullPolicy: Always
# Pinned to a stable release rather than "latest" to keep deployments reproducible.
# 0.23.2 is the latest stable Ollama as of May 2026
# (https://github.com/ollama/ollama/releases/tag/v0.23.2).
tag: "0.23.2"
digest: ""
pullPolicy: IfNotPresent
# Number of replicas
replicaCount: 1
@@ -620,12 +705,34 @@ ollama:
memory: "4Gi"
cpu: "1000m"
# Pod security context
# The upstream ollama/ollama image is built to run as root and writes models to
# /root/.ollama by default. To meet Pod Security Standards "restricted", we run
# as a non-root user (1001) and rely on fsGroup to make the mounted PVC writable.
# Override OLLAMA_MODELS env (or remount /root/.ollama) if your image expects root.
podSecurityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
# Container security context
# readOnlyRootFilesystem is intentionally false — Ollama writes model blobs to its data dir.
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
# Environment variables
env:
NVIDIA_DRIVER_CAPABILITIES: "all"
OLLAMA_LOAD_TIMEOUT: "-1"
OLLAMA_KEEP_ALIVE: "-1"
OLLAMA_DEBUG: "1"
# The default OLLAMA model dir is /root/.ollama, which a non-root user can't write.
# Point it at a path under the writable PVC mount instead.
HOME: "/tmp"
OLLAMA_MODELS: "/data/models"
# Persistence configuration
persistence:
@@ -683,6 +790,18 @@ ingress:
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# Production recommendations (uncomment & adjust to your environment):
#
# Automatic TLS via cert-manager. Issuer name varies per cluster
# (e.g. letsencrypt-prod, letsencrypt-staging, internal-ca).
# cert-manager.io/cluster-issuer: letsencrypt-prod
#
# Allow larger uploads (knowledge base / file ingestion).
# nginx.ingress.kubernetes.io/proxy-body-size: "50m"
#
# Long timeouts for streaming SSE / LLM responses.
# nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
# nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
# Main application (use / as catch-all)
app:
@@ -749,7 +868,10 @@ serviceAccount:
# The name of the service account to use
name: ""
# Horizontal Pod Autoscaler
# Horizontal Pod Autoscaler for app and realtime deployments.
# When enabled, the chart omits spec.replicas from the Deployment so the HPA
# owns replica count. Requires metrics-server (or a custom metrics adapter
# if you use customMetrics) installed in the cluster.
autoscaling:
enabled: false
minReplicas: 1
@@ -779,22 +901,33 @@ autoscaling:
behavior: {}
# Pod disruption budget
# Note: PDBs only protect against voluntary disruptions (node drains, autoscaler)
# They do NOT affect rolling updates - use deployment.strategy.rollingUpdate for that
# Note: PDBs only protect against voluntary disruptions (node drains, autoscaler).
# They do NOT affect rolling updates — use deployment.strategy.rollingUpdate for that.
#
# Tri-state activation:
# enabled: true → force PDB on (any replicaCount)
# enabled: false → force PDB off (explicit opt-out, even at replicaCount > 1)
# enabled: null → auto-enable when replicaCount > 1 (recommended default)
podDisruptionBudget:
enabled: false
# Use either minAvailable or maxUnavailable (not both)
# Recommendation: Use maxUnavailable as it scales better with HPA
# - minAvailable: minimum pods that must remain available (e.g., 1, "50%")
enabled: null
# Use either minAvailable or maxUnavailable (not both).
# Percentages scale with HPA / replicaCount; absolute numbers do not.
# Default `maxUnavailable: "25%"` allows rolling updates while keeping
# 75% of pods serving — matches the K8s docs guidance for stateless HA
# frontends (https://kubernetes.io/docs/tasks/run-application/configure-pdb/).
# - minAvailable: minimum pods that must remain available (e.g., 1, "90%")
# - maxUnavailable: maximum pods that can be unavailable (e.g., 1, "25%")
minAvailable: null
maxUnavailable: 1
maxUnavailable: "25%"
# unhealthyPodEvictionPolicy: allows eviction of unhealthy pods during node drains
# Options: IfHealthyBudget (default) | AlwaysAllow (recommended for production)
# Set to null to use K8s default (IfHealthyBudget)
unhealthyPodEvictionPolicy: null
# Monitoring configuration
# Prometheus monitoring integration.
# Creates a ServiceMonitor CR scraped by the Prometheus Operator. Requires
# the kube-prometheus-stack (or prometheus-operator) CRDs to be installed
# in the cluster — otherwise the manifest is rejected by the API server.
monitoring:
# ServiceMonitor for Prometheus
serviceMonitor:
@@ -816,15 +949,40 @@ monitoring:
# Relabeling configurations
relabelings: []
# Network policies
# NetworkPolicy for east-west and egress isolation.
# When enabled, the chart creates policies that allow Sim components to talk
# to each other and to required external services (DNS, HTTPS) while blocking
# everything else. The egress block additionally blacklists cloud metadata
# endpoints (169.254.169.254/32, 169.254.170.2/32) by default — extend
# egress.exceptCidrs with your cluster's API server CIDR for tighter isolation.
# Your CNI must support NetworkPolicy (Calico, Cilium, GKE Dataplane V2, etc.).
networkPolicy:
enabled: false
# Custom ingress rules
# NetworkPolicyPeers allowed to reach the app/realtime pods on the
# ingress-controller path. Defaults to `- {}` (any source), which matches the
# common case where the ingress controller is the only thing routing to these
# ports. To restrict ingress to a specific controller namespace, replace with:
# ingressFrom:
# - namespaceSelector:
# matchLabels:
# kubernetes.io/metadata.name: ingress-nginx
ingressFrom:
- {}
# Custom ingress rules appended to the policy
ingress: []
# Custom egress rules
egress: []
# Egress configuration
egress:
# CIDRs excluded from broad HTTPS (443) egress.
# Defaults block AWS/GCP/Azure IMDS (169.254.169.254/32) and ECS task metadata
# (169.254.170.2/32). Add your cluster's API server CIDR for stronger isolation.
exceptCidrs:
- "169.254.169.254/32"
- "169.254.170.2/32"
# Custom egress rules appended to the policy
extraRules: []
# Shared storage for enterprise workflows requiring data sharing between pods
sharedStorage:
@@ -1118,9 +1276,11 @@ copilot:
# Image configuration
image:
repository: simstudioai/copilot
tag: latest
pullPolicy: Always
# tag defaults to Chart.AppVersion. Override with a release tag or pin via image.digest.
tag: ""
digest: ""
pullPolicy: IfNotPresent
# Number of replicas
replicaCount: 1
@@ -1184,23 +1344,33 @@ copilot:
targetPort: 8080
# Health checks
# Startup probe absorbs cold-start time. Total budget: 30 * 5s = 150s.
startupProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 30
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
failureThreshold: 6
# Pod Disruption Budget for high availability
podDisruptionBudget:
@@ -1239,13 +1409,20 @@ copilot:
cpu: "250m"
# Pod security context
# PostgreSQL image's `postgres` user is uid/gid 999.
podSecurityContext:
fsGroup: 999
# Container security context
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
# Container security context
# readOnlyRootFilesystem is intentionally false — Postgres writes to /var/lib/postgresql/data.
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
# Persistence configuration
persistence:
enabled: true
@@ -1253,13 +1430,13 @@ copilot:
size: 10Gi
accessModes:
- ReadWriteOnce
# Service configuration
service:
type: ClusterIP
port: 5432
targetPort: 5432
# Health checks
livenessProbe:
exec:
@@ -1291,9 +1468,11 @@ copilot:
# Image configuration (same as server)
image:
repository: simstudioai/copilot
tag: latest
pullPolicy: Always
# tag defaults to Chart.AppVersion. Override with a release tag or pin via image.digest.
tag: ""
digest: ""
pullPolicy: IfNotPresent
# Resource limits and requests
resources:
limits:
@@ -1324,8 +1503,10 @@ externalSecrets:
# Enable External Secrets Operator integration
enabled: false
# ESO API version - use "v1" for ESO v0.17+ (recommended), "v1beta1" for older versions
apiVersion: "v1"
# ESO API version. Default "v1beta1" — supported by every ESO release from
# v0.7+ (mid-2023) through current. Set to "v1" only when targeting ESO
# v0.17+ clusters where the v1 API has graduated.
apiVersion: "v1beta1"
# How often to sync secrets from the external store
refreshInterval: "1h"
@@ -1412,4 +1593,22 @@ certManager:
# CA ClusterIssuer configuration
# This is the issuer that applications should reference for obtaining certificates
caIssuer:
name: "sim-ca-issuer"
name: "sim-ca-issuer"
# Helm test hook (helm test) connectivity probes.
# Renders a Pod with helm.sh/hook=test that checks the app and realtime
# Services are reachable from inside the cluster. Run with: helm test <release>
tests:
enabled: true
image:
repository: busybox
tag: "1.36"
pullPolicy: IfNotPresent
pullSecrets: []
timeoutSeconds: 5
resources:
limits:
cpu: 100m
memory: 64Mi
requests:
cpu: 10m
memory: 16Mi