chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 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,149 @@
# 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
**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 24 are unaffected by ESO.
**Override precedence.** Values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work.
## 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.
+29
View File
@@ -0,0 +1,29 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
# 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/
+33
View File
@@ -0,0 +1,33 @@
apiVersion: v2
name: sim
description: A Helm chart for Sim - the open-source AI workspace where teams build, deploy, and manage AI agents
type: application
version: 1.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:
- https://github.com/simstudioai/sim
maintainers:
- name: Sim Team
email: help@sim.ai
url: https://sim.ai
keywords:
- ai
- workflow
- automation
- agents
- nextjs
annotations:
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
+482
View File
@@ -0,0 +1,482 @@
# Sim Helm Chart
Deploy [Sim](https://sim.ai) — the open-source AI workspace where teams build, deploy, and manage AI agents — on Kubernetes.
* **Chart version:** see `Chart.yaml`
* **App version:** tracks the upstream Sim release
* **Kubernetes:** 1.25+
* **License:** Apache-2.0
---
## TL;DR
```bash
# Generate required secrets
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)
export POSTGRES_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
# Install from this repository
helm install sim ./helm/sim \
--namespace sim --create-namespace \
--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"
```
After install, follow the on-screen `NOTES.txt` to reach the app.
---
## Introduction
This chart deploys the Sim platform on a Kubernetes cluster using the Helm package manager. A default install includes:
* **`app`** — the Sim Next.js web application (Deployment).
* **`realtime`** — the WebSocket service for live workflow updates (Deployment).
* **`postgresql`** — an in-cluster `pgvector/pgvector` Postgres (StatefulSet, with a headless Service for stable per-pod DNS).
* **`migrations`** — a Job that applies database migrations on install/upgrade.
* **`cronjobs`** — scheduled jobs for workflow schedule execution, inbox/calendar/drive polling (Gmail, Outlook, Calendar, Drive, Sheets, IMAP, RSS), workspace event polling, subscription renewal, data drains, and connector syncs.
* **`serviceaccount`** — a dedicated ServiceAccount with `automountServiceAccountToken: false`.
Optional components (off by default):
* **`copilot`** — the Sim Copilot service plus its own Postgres StatefulSet.
* **`ollama`** — local LLM inference, with optional NVIDIA GPU support.
* **`pii`** — Presidio PII redaction service (analyzer + anonymizer) for the Guardrails PII block and log redaction. See [PII redaction](#pii-redaction).
* **`telemetry`** — OpenTelemetry Collector wired to Jaeger / Prometheus / OTLP backends.
* **`ingress`** — NGINX-style Ingress for the app and realtime services.
* **`networkPolicy`** — east-west and egress isolation (blocks cloud metadata endpoints by default).
* **`hpa`** — HorizontalPodAutoscaler for `app` and `realtime`.
* **`podDisruptionBudget`** — auto-activates when `replicaCount > 1`.
* **`servicemonitor`** — Prometheus Operator integration.
---
## Prerequisites
| Requirement | Version / Notes |
|---|---|
| Kubernetes | **1.25+** (`Chart.yaml` enforces `kubeVersion: ">=1.25.0-0"`) |
| Helm | **3.8+** |
| StorageClass | A default StorageClass that supports `ReadWriteOnce` PVCs (for Postgres, Ollama). Set `global.storageClass` to pick a non-default class. |
| Ingress controller | Only if `ingress.enabled=true`. The chart's defaults assume `nginx`. |
| cert-manager | Only if you want auto-issued TLS certificates. See [cert-manager docs](https://cert-manager.io/docs/). |
| metrics-server | Only if `autoscaling.enabled=true` (HPA needs metrics). |
| External Secrets Operator | Only if `externalSecrets.enabled=true`. See [ESO docs](https://external-secrets.io/). |
| Prometheus Operator | Only if `monitoring.serviceMonitor.enabled=true`. |
| Namespace PSS labels | Recommended: `pod-security.kubernetes.io/enforce=restricted`. The chart's pod and container security contexts are PSS-restricted by default. |
---
## Generate required secrets
Sim will not start without these. Generate them once and feed them via `--set`, an existing Kubernetes Secret, or External Secrets Operator.
```bash
# Application secrets (32 bytes hex each)
openssl rand -hex 32 # BETTER_AUTH_SECRET - signs auth JWTs
openssl rand -hex 32 # ENCRYPTION_KEY - encrypts sensitive env vars
openssl rand -hex 32 # INTERNAL_API_SECRET - service-to-service auth
openssl rand -hex 32 # CRON_SECRET - required if cronjobs.enabled (default true)
openssl rand -hex 32 # API_ENCRYPTION_KEY - optional; encrypts user API keys at rest
# Postgres password
openssl rand -base64 24 | tr -d '/+='
```
If you set `app.secrets.existingSecret.enabled=true` and point at a pre-created Secret, you do **not** also pass these via `--set` — pick one path.
---
## Installing the chart
### From this repository
```bash
helm install sim ./helm/sim \
--namespace sim --create-namespace \
--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"
```
### With a values file
```bash
helm install sim ./helm/sim \
--namespace sim --create-namespace \
--values my-values.yaml
```
Run `helm template ./helm/sim --values my-values.yaml | less` first to see what will be applied.
### Validate the install
```bash
helm install sim ./helm/sim --dry-run --debug \
--values my-values.yaml \
--set app.env.BETTER_AUTH_SECRET=$(openssl rand -hex 16) \
--set app.env.ENCRYPTION_KEY=$(openssl rand -hex 16) \
--set app.env.INTERNAL_API_SECRET=$(openssl rand -hex 16) \
--set app.env.CRON_SECRET=$(openssl rand -hex 16) \
--set postgresql.auth.password=$(openssl rand -base64 12 | tr -d '/+=')
```
---
## Upgrading
```bash
helm upgrade sim ./helm/sim --namespace sim --values my-values.yaml
```
---
## Uninstalling
```bash
helm uninstall sim --namespace sim
```
**PVCs are not deleted by `helm uninstall`.** If you want to wipe data too:
```bash
# WARNING: this destroys all Postgres, Ollama, and shared-storage data.
kubectl delete pvc --namespace sim \
-l app.kubernetes.io/instance=sim
# Or list and delete by name
kubectl get pvc --namespace sim
kubectl delete pvc <pvc-name> --namespace sim
# Then delete the namespace if you're done with it
kubectl delete namespace sim
```
---
## Examples
Pre-built values files for common scenarios live in `helm/sim/examples/`. Each file has a header explaining when to use it and any prerequisites.
| File | When to use |
|---|---|
| `values-development.yaml` | Local dev / `kind` / `minikube`. Minimal resources, no TLS. |
| `values-production.yaml` | Generic production: HA, network policy, autoscaling, monitoring. |
| `values-aws.yaml` | EKS — EBS GP3 storage, ALB ingress, IRSA-friendly. |
| `values-gcp.yaml` | GKE — Persistent Disk storage, GCP managed certs, Workload Identity. |
| `values-azure.yaml` | AKS — managed-csi storage, NGINX ingress, GPU node pools. |
| `values-external-db.yaml` | Production with a managed Postgres (RDS, Cloud SQL, Azure DB). |
| `values-external-secrets.yaml` | Sync secrets from Vault / AWS SM / Azure KV / GCP SM via External Secrets Operator. |
| `values-existing-secret.yaml` | GitOps / Sealed Secrets / SOPS — reference pre-created Kubernetes Secrets. |
| `values-copilot.yaml` | Enables the Copilot service + its Postgres StatefulSet. |
| `values-whitelabeled.yaml` | Custom branding (logo, name, support links). |
Use one with:
```bash
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 postgresql.auth.password="$POSTGRES_PASSWORD"
```
---
## Parameters
This chart is intentionally configurable. Rather than maintain a hand-curated parameter table (which would drift), read the canonical sources:
```bash
# Print all values with comments and defaults
helm show values ./helm/sim
# Print the JSON Schema (used by `helm install` to validate your values)
cat ./helm/sim/values.schema.json
```
`values.yaml` is heavily commented; each top-level section explains what it controls and which sub-keys are required vs optional. For per-cloud examples and idiomatic overrides, see `examples/`.
---
## Production checklist
Before installing in production, confirm each of the following:
* **High availability** — scale `app.replicaCount > 1`. The chart auto-creates a `PodDisruptionBudget` with `minAvailable: 1`. Set `podDisruptionBudget.maxUnavailable: "25%"` for a more permissive policy or `minAvailable: "50%"` for a stricter one.
* **Pinned images** — override `image.tag` (or `image.digest`) with an explicit version. Do not rely on the chart's default tag in production.
* **Secrets management** — provide secrets via External Secrets Operator (ESO) or pre-created Kubernetes Secrets. Never commit secrets to `values.yaml`.
* **TLS / Ingress** — set the `cert-manager.io/cluster-issuer` annotation on the ingress and tune `proxy-body-size` / `proxy-read-timeout` for your workload. See commented examples in `values.yaml`.
* **Network policy egress** — review `networkPolicy.egressExceptCidrs`. Defaults block cloud metadata endpoints (`169.254.169.254/32`, `169.254.170.2/32`); add your cluster's API server CIDR for stronger isolation. Custom egress rules go in `networkPolicy.egress` (a list).
* **Network policy ingress** — `networkPolicy.ingressFrom` defaults to `[{}]` (an empty peer selector), which allows ingress traffic from **any pod in the cluster**, not just your ingress controller. This is a deliberate simple default, not a locked-down one. On a shared or multi-tenant cluster, scope it down, e.g. to the ingress-nginx namespace:
```yaml
networkPolicy:
ingressFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
```
* **Namespace hardening** — label the install namespace with Pod Security Standards `restricted` enforcement (`pod-security.kubernetes.io/enforce=restricted`). All workloads set `runAsNonRoot`, drop all Linux capabilities, disable privilege escalation, and set `seccompProfile: RuntimeDefault` — the four controls the Restricted profile requires. `readOnlyRootFilesystem` is intentionally **not** defaulted anywhere (Postgres/Ollama genuinely need a writable root; the stateless services — `realtime`, `pii`, `copilot` — could tolerate it but aren't pre-wired with a `/tmp` `emptyDir`). If your policy requires it, set `<component>.securityContext.readOnlyRootFilesystem: true` and mount an `emptyDir` at `/tmp` yourself via `extraVolumes`/`extraVolumeMounts`.
* **Env validation** — keys under `app.env`, `realtime.env`, and `copilot.env` are passed through to the application and validated at startup. The JSON Schema intentionally does not enforce `additionalProperties: false` (would break custom user envs), so typos like `OPENA_API_KEY` (instead of `OPENAI_API_KEY`) surface as missing-key errors at runtime, not at `helm install` time. Review your env block carefully.
* **Set public URLs** — `app.env.NEXT_PUBLIC_APP_URL` and `app.env.BETTER_AUTH_URL` must match your public origin (e.g. `https://sim.example.com`). Leaving them as `localhost` breaks sign-in.
---
## Secrets
The chart supports three ways to provide secrets, in increasing order of production-readiness:
### 1. Inline `--set` (dev / dry-run only)
```bash
helm install sim ./helm/sim --set app.env.BETTER_AUTH_SECRET=...
```
Discouraged for production — values land in `helm get values` output.
### 2. Pre-existing Kubernetes Secret
Create the Secret first, then reference it:
```bash
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
app:
secrets:
existingSecret:
enabled: true
name: sim-app-secrets
postgresql:
auth:
existingSecret:
enabled: true
name: sim-postgres-secret
passwordKey: POSTGRES_PASSWORD
```
See `examples/values-existing-secret.yaml`.
### 3. External Secrets Operator (recommended)
Sync from Azure Key Vault, AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager. Install ESO once, create a `ClusterSecretStore`, then:
```yaml
externalSecrets:
enabled: true
refreshInterval: 1h
secretStoreRef:
name: my-secret-store
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
postgresql:
password: sim/postgresql/password
# Only needed when copilot.enabled=true and copilot.server.secret.create=true.
# Every non-empty copilot.server.env key must have a matching entry here —
# template rendering fails with a clear message naming the missing key otherwise.
copilot:
AGENT_API_DB_ENCRYPTION_KEY: sim/copilot/agent-api-db-encryption-key
INTERNAL_API_SECRET: sim/copilot/internal-api-secret
LICENSE_KEY: sim/copilot/license-key
SIM_BASE_URL: sim/copilot/sim-base-url
SIM_AGENT_API_KEY: sim/copilot/sim-agent-api-key
REDIS_URL: sim/copilot/redis-url
OPENAI_API_KEY_1: sim/copilot/openai-api-key
```
See `examples/values-external-secrets.yaml`.
---
## Persistence
Postgres, Ollama, and any configured `sharedStorage.volumes[]` use PersistentVolumeClaims. PVCs **survive `helm uninstall`** — see [Uninstalling](#uninstalling) for full cleanup.
| Component | Default size | Access mode | Storage class |
|---|---|---|---|
| `postgresql` | 10Gi | `ReadWriteOnce` | `global.storageClass` |
| `copilot.postgresql` | 10Gi | `ReadWriteOnce` | `global.storageClass` |
| `ollama` | 100Gi | `ReadWriteOnce` | `global.storageClass` |
| `sharedStorage.volumes[]` | user-defined | `ReadWriteMany` recommended | `sharedStorage.storageClass` |
For production, use a `StorageClass` with `reclaimPolicy: Retain` on database volumes.
---
## Security
The chart applies [Pod Security Standards `restricted`](https://kubernetes.io/docs/concepts/security/pod-security-standards/) defaults to every workload:
* `runAsNonRoot: true`
* `allowPrivilegeEscalation: false`
* `capabilities.drop: [ALL]`
* `seccompProfile.type: RuntimeDefault`
User-supplied `securityContext` values are merged with the defaults — your values win, but you don't have to repeat the defaults.
Other security features:
* `automountServiceAccountToken: false` on the ServiceAccount **and** every pod.
* Every value in `app.env` and `realtime.env` is written to a chart-managed Secret and mounted via `envFrom: secretRef` — no values are inlined on the container spec. This eliminates a sensitivity classifier (no static list of "secret" keys to maintain) and ensures new provider keys can never accidentally leak into pod manifests. Two categories are inlined on the container instead: chart-computed values (`DATABASE_URL`, `SOCKET_SERVER_URL`, `OLLAMA_URL`, `PII_URL`) and operational defaults under `app.envDefaults` / `realtime.envDefaults` (rate limits, timeouts, IVM tunables, feature-flag defaults, branding defaults, `http://localhost:3000` URL fallbacks). Operational defaults are non-sensitive by design — moving them out of `app.env` keeps the Secret small and means External Secrets Operator users only have to map the keys they actually set, not every chart default. A value placed in `app.env` always wins over the same key in `app.envDefaults` (the template skips the inline default when an override exists).
* Optional `networkPolicy.enabled=true` enforces east-west isolation and blocks cloud metadata endpoints in egress.
---
## Autoscaling
```yaml
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
```
When `autoscaling.enabled=true`, the chart omits `spec.replicas` from the Deployment so the HPA owns replica count. Requires `metrics-server` in the cluster.
---
## Monitoring
```yaml
monitoring:
serviceMonitor:
enabled: true
interval: 30s
```
Requires the Prometheus Operator CRDs. Scrapes `/metrics` on the app and realtime services.
---
## PII redaction
Sim can redact personally identifiable information using a [Presidio](https://microsoft.github.io/presidio/) service (analyzer + anonymizer combined into one image listening on port 5001). Enable it with:
```yaml
pii:
enabled: true
```
When enabled, the chart deploys it as a standalone `<release>-pii` Deployment + Service and **auto-wires** `PII_URL` on the app to the in-cluster service. The service bundles five large spaCy models (en/es/it/pl/fi, ~2.2GB), so the first start takes ~3 minutes while models load — the `startupProbe` allows for this. Size the `pii.resources` for at least ~4Gi memory.
This alone powers the **Guardrails PII block** and on-demand masking. To additionally turn on **automatic log redaction** (the org/workspace data-retention scrub), you must:
```yaml
app:
env:
PII_REDACTION: "true"
# The log-redaction path calls the app's own /api/guardrails/mask-batch,
# which must be reachable from inside the cluster. Set this to the in-cluster
# app Service URL (NOT the public ingress, which usually isn't hairpin-reachable).
INTERNAL_API_BASE_URL: "http://<release>-app.<namespace>.svc.cluster.local:3000"
```
Without a cluster-reachable `INTERNAL_API_BASE_URL` (it falls back to `NEXT_PUBLIC_APP_URL`), the redaction path fails closed — it scrubs affected fields to `[REDACTION_FAILED]` rather than leaking, but redaction won't actually run.
> The PII image is published at `ghcr.io/simstudioai/pii` (multi-arch). If you mirror images into a private registry, retag it alongside the app/realtime/migrations images.
---
## Troubleshooting
### `Error: execution error at (sim/templates/...): app.env.BETTER_AUTH_SECRET is required for production deployment`
You ran `helm install` without setting required secrets. Generate them and pass with `--set`:
```bash
helm install sim ./helm/sim \
--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 postgresql.auth.password=$(openssl rand -base64 24 | tr -d '/+=')
```
### App pods stuck in `CrashLoopBackOff`
```bash
kubectl logs --namespace sim deploy/sim-app --tail 200
```
Common causes:
* `NEXT_PUBLIC_APP_URL` still set to `http://localhost:3000` in a clustered deploy → set it to your public origin.
* `DATABASE_URL` not reachable → check the Postgres pod is running and `postgresql.auth.password` matches.
* Missing migration → check `kubectl logs job/sim-migrations`.
### Image pull errors (`ErrImagePull` / `ImagePullBackOff`)
* You pushed Sim to a private registry but haven't configured pull secrets. Set `global.imagePullSecrets` and `global.imageRegistry`.
* You overrode `image.tag` to a tag that doesn't exist in the registry. `helm get values sim` and verify.
### Postgres pod `Pending`
```bash
kubectl describe pvc --namespace sim
```
Almost always one of:
* No default `StorageClass` → set `global.storageClass`.
* No PV provisioner → install one (e.g. EBS CSI on EKS, `local-path-provisioner` for dev).
* StorageClass exists but doesn't support `ReadWriteOnce` → pick another class.
### Ingress not routing
```bash
kubectl get ingress --namespace sim
kubectl describe ingress --namespace sim
```
* Ingress controller not installed → install `ingress-nginx` or similar.
* `ingress.className` doesn't match your controller → set it to your installed class.
* DNS not pointed at the ingress's external IP / LoadBalancer.
### Get logs from each component
```bash
kubectl --namespace sim logs -f deployment/sim-app
kubectl --namespace sim logs -f deployment/sim-realtime
kubectl --namespace sim logs -f statefulset/sim-postgresql
kubectl --namespace sim logs job/sim-migrations
```
---
## Support
* **Docs:** https://docs.sim.ai
* **GitHub:** https://github.com/simstudioai/sim
* **Issues:** https://github.com/simstudioai/sim/issues
* **Discord:** https://discord.gg/Hr4UWYEcTT
---
## License
Apache-2.0 © Sim. See [LICENSE](../../LICENSE).
+334
View File
@@ -0,0 +1,334 @@
# 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: "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:
enabled: true
replicaCount: 2
# Node selector for application pods
# Uncomment and customize based on your EKS node labels:
# nodeSelector:
# node.kubernetes.io/instance-type: "t3.large"
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
# Production URLs (REQUIRED - update with your actual domain names)
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
# SOCKET_SERVER_URL is auto-detected (uses internal service http://sim-realtime:3002)
NEXT_PUBLIC_SOCKET_URL: "https://simstudio-ws.acme.com" # Public WebSocket URL for browsers
# Security settings (REQUIRED - replace with your own secure secrets)
# Generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "your-secure-production-auth-secret-here"
ENCRYPTION_KEY: "your-secure-production-encryption-key-here"
INTERNAL_API_SECRET: "your-secure-production-internal-api-secret-here"
CRON_SECRET: "your-secure-production-cron-secret-here"
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32
API_ENCRYPTION_KEY: "your-64-char-hex-api-encryption-key-here" # Optional but recommended
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# AWS Bedrock - when using IRSA (see serviceAccount below), the default credential chain
# resolves automatically. Setting this hides the credential fields in the Agent block UI.
NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS: "true" # Uncomment if using Bedrock with IRSA
# AWS S3 Cloud Storage Configuration (RECOMMENDED for production)
# Create S3 buckets in your AWS account and configure IAM permissions
AWS_REGION: "us-west-2"
AWS_ACCESS_KEY_ID: "" # AWS access key (or use IRSA for EKS)
AWS_SECRET_ACCESS_KEY: "" # AWS secret key (or use IRSA for EKS)
S3_BUCKET_NAME: "workspace-files" # Workspace files
S3_KB_BUCKET_NAME: "knowledge-base" # Knowledge base documents
S3_EXECUTION_FILES_BUCKET_NAME: "execution-files" # Workflow execution outputs
S3_CHAT_BUCKET_NAME: "chat-files" # Deployed chat assets
S3_COPILOT_BUCKET_NAME: "copilot-files" # Copilot attachments
S3_PROFILE_PICTURES_BUCKET_NAME: "profile-pictures" # User avatars
S3_OG_IMAGES_BUCKET_NAME: "og-images" # OpenGraph preview images
S3_WORKSPACE_LOGOS_BUCKET_NAME: "workspace-logos" # Workspace logos
# For S3-compatible storage (Cloudflare R2, MinIO, Backblaze B2) instead of AWS S3:
# S3_ENDPOINT: "https://<account>.r2.cloudflarestorage.com" # custom endpoint; set AWS_REGION: "auto" for R2
# S3_FORCE_PATH_STYLE: "true" # required for MinIO/Ceph; omit for AWS S3 and R2
# Realtime service
realtime:
enabled: true
replicaCount: 2
# Node selector for realtime pods
# Uncomment and customize based on your EKS node labels:
# nodeSelector:
# node.kubernetes.io/instance-type: "t3.medium"
resources:
limits:
memory: "4Gi"
cpu: "1000m"
requests:
memory: "2Gi"
cpu: "500m"
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
BETTER_AUTH_SECRET: "your-secure-production-auth-secret-here"
ALLOWED_ORIGINS: "https://simstudio.acme.com"
NODE_ENV: "production"
# Database migrations
migrations:
enabled: true
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
# PostgreSQL database
postgresql:
enabled: true
# Node selector for database pods
# Uncomment and customize (recommended: memory-optimized EC2 instances like r5.large):
# nodeSelector:
# node.kubernetes.io/instance-type: "r5.large"
# Database authentication (REQUIRED - set secure credentials)
auth:
username: postgres
password: "your-secure-postgres-password"
database: simstudio
# Resource allocation optimized for AWS EKS
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
# Persistent storage using AWS EBS volumes
persistence:
enabled: true
storageClass: "gp2" # Use gp2 (default) or create gp3 StorageClass
size: 50Gi
accessModes:
- ReadWriteOnce
# SSL/TLS configuration (requires cert-manager to be installed)
tls:
enabled: false # Set to true if cert-manager is installed
certificatesSecret: postgres-tls-secret
# PostgreSQL performance tuning for AWS infrastructure
config:
maxConnections: 1000
sharedBuffers: "2GB"
maxWalSize: "8GB"
minWalSize: "160MB"
# Ollama AI models with GPU acceleration (AWS EC2 GPU instances)
# Set ollama.enabled: false if you don't need local AI models
ollama:
enabled: false
replicaCount: 1
# GPU node targeting - uncomment and customize for GPU instances
# Recommended: g4dn.xlarge or p3.2xlarge instances
# nodeSelector:
# node.kubernetes.io/instance-type: "g4dn.xlarge"
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"
# GPU resource allocation for AI model serving
gpu:
enabled: true
count: 1
resources:
limits:
memory: "16Gi"
cpu: "4000m"
nvidia.com/gpu: "1"
requests:
memory: "8Gi"
cpu: "2000m"
# High-performance storage for AI models
persistence:
enabled: true
storageClass: "gp2" # Use gp2 (default) or create gp3 StorageClass
size: 100Gi
accessModes:
- ReadWriteOnce
env:
NVIDIA_DRIVER_CAPABILITIES: "all"
OLLAMA_LOAD_TIMEOUT: "-1"
OLLAMA_KEEP_ALIVE: "-1"
OLLAMA_DEBUG: "1"
# Ingress using AWS Application Load Balancer (ALB)
ingress:
enabled: true
className: alb
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/ssl-redirect: "443"
alb.ingress.kubernetes.io/certificate-arn: "arn:aws:acm:us-west-2:123456789012:certificate/your-cert-arn"
# Main application
app:
host: simstudio.acme.com
paths:
- path: /
pathType: Prefix
# Realtime service
realtime:
host: simstudio-ws.acme.com
paths:
- path: /
pathType: Prefix
# TLS configuration
tls:
enabled: true
secretName: simstudio-tls-secret
# Pod disruption budget for high availability
podDisruptionBudget:
enabled: true
minAvailable: null
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
# Network policies
networkPolicy:
enabled: true
# Pod anti-affinity for high availability across AWS Availability Zones
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: ["simstudio"]
topologyKey: kubernetes.io/hostname
- weight: 50
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: ["simstudio"]
topologyKey: topology.kubernetes.io/zone
# Service Account with IAM roles for service account (IRSA) integration
serviceAccount:
create: true
annotations:
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"
+330
View File
@@ -0,0 +1,330 @@
# 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:
imageRegistry: "ghcr.io"
# Use "managed-csi-premium" for Premium SSD, "managed-csi" for Standard SSD
# IMPORTANT: For production, use a StorageClass with reclaimPolicy: Retain
# to protect database volumes from accidental deletion.
storageClass: "managed-csi"
# Main application
app:
enabled: true
replicaCount: 2
# Node selector for application pods
# Uncomment and customize based on your AKS node labels:
# nodeSelector:
# agentpool: "application"
resources:
limits:
memory: "4Gi"
requests:
memory: "2Gi"
cpu: "500m"
# Production URLs (REQUIRED - update with your actual domain names)
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
# SOCKET_SERVER_URL is auto-detected (uses internal service http://sim-realtime:3002)
NEXT_PUBLIC_SOCKET_URL: "https://simstudio-ws.acme.com" # Public WebSocket URL for browsers
# Security settings (REQUIRED - replace with your own secure secrets)
# Generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "your-secure-production-auth-secret-here"
ENCRYPTION_KEY: "your-secure-production-encryption-key-here"
INTERNAL_API_SECRET: "your-secure-production-internal-api-secret-here"
CRON_SECRET: "your-secure-production-cron-secret-here"
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32
API_ENCRYPTION_KEY: "your-64-char-hex-api-encryption-key-here" # Optional but recommended
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# Azure OpenAI / Azure Anthropic (Azure AI Foundry) — set to use server-side credentials.
# When NEXT_PUBLIC_AZURE_CONFIGURED is true, the endpoint/key/version fields are hidden in
# the Agent block UI so users just pick a model and run.
AZURE_OPENAI_ENDPOINT: "" # e.g. https://your-resource.openai.azure.com
AZURE_OPENAI_API_KEY: "" # Azure OpenAI API key
AZURE_OPENAI_API_VERSION: "" # e.g. 2024-07-01-preview
AZURE_ANTHROPIC_ENDPOINT: "" # Azure AI Foundry endpoint for Anthropic models
AZURE_ANTHROPIC_API_KEY: "" # Azure Anthropic API key
AZURE_ANTHROPIC_API_VERSION: "" # Azure Anthropic API version (e.g., 2023-06-01)
NEXT_PUBLIC_AZURE_CONFIGURED: "true" # Set to "true" once credentials are configured above
KB_OPENAI_MODEL_NAME: "" # Azure deployment name serving the KB embedding model (used only when AZURE_OPENAI_* is set)
WAND_OPENAI_MODEL_NAME: "" # Wand generation model deployment name
# Azure Mistral OCR (optional — Azure-hosted document OCR for knowledge base processing)
OCR_AZURE_ENDPOINT: "" # Azure Mistral OCR service endpoint
OCR_AZURE_MODEL_NAME: "" # Azure Mistral OCR model name
OCR_AZURE_API_KEY: "" # Azure Mistral OCR API key
# Email — Azure Communication Services (recommended on Azure; alternative to Resend/SES/SMTP)
AZURE_ACS_CONNECTION_STRING: "" # Azure Communication Services connection string
# Azure Blob Storage Configuration (RECOMMENDED for production)
# Create a storage account and containers in your Azure subscription
AZURE_ACCOUNT_NAME: "simstudiostorageacct" # Azure storage account name
AZURE_ACCOUNT_KEY: "" # Storage account access key
# Or use connection string instead of account name/key:
# AZURE_CONNECTION_STRING: "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net"
AZURE_STORAGE_CONTAINER_NAME: "workspace-files" # Workspace files container
AZURE_STORAGE_KB_CONTAINER_NAME: "knowledge-base" # Knowledge base documents container
AZURE_STORAGE_EXECUTION_FILES_CONTAINER_NAME: "execution-files" # Workflow execution outputs
AZURE_STORAGE_CHAT_CONTAINER_NAME: "chat-files" # Deployed chat assets container
AZURE_STORAGE_COPILOT_CONTAINER_NAME: "copilot-files" # Copilot attachments container
AZURE_STORAGE_PROFILE_PICTURES_CONTAINER_NAME: "profile-pictures" # User avatars container
AZURE_STORAGE_OG_IMAGES_CONTAINER_NAME: "og-images" # OpenGraph preview images container
AZURE_STORAGE_WORKSPACE_LOGOS_CONTAINER_NAME: "workspace-logos" # Workspace logos container
# Realtime service
realtime:
enabled: true
replicaCount: 2
# Node selector for realtime pods
# Uncomment and customize based on your AKS node labels:
# nodeSelector:
# agentpool: "application"
resources:
limits:
memory: "4Gi"
requests:
memory: "1Gi"
cpu: "250m"
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
BETTER_AUTH_SECRET: "your-secure-production-auth-secret-here"
ALLOWED_ORIGINS: "https://simstudio.acme.com"
NODE_ENV: "production"
# Database migrations
migrations:
enabled: true
# PostgreSQL database
postgresql:
enabled: true
# Node selector for database pods
# Uncomment and customize (recommended: memory-optimized VM sizes):
# nodeSelector:
# agentpool: "database"
# Database authentication (REQUIRED - set secure credentials)
auth:
username: postgres
password: "your-secure-postgres-password"
database: simstudio
# Resource allocation for production workloads
resources:
limits:
memory: "2Gi"
requests:
memory: "1Gi"
cpu: "500m"
# Persistent storage using Azure Managed Disk
persistence:
enabled: true
storageClass: "managed-csi"
size: 10Gi
# SSL/TLS configuration (requires cert-manager to be installed)
tls:
enabled: false # Set to true if cert-manager is installed
certificatesSecret: postgres-tls-secret
# PostgreSQL performance tuning for Azure infrastructure
config:
maxConnections: 1000
sharedBuffers: "1280MB"
maxWalSize: "4GB"
minWalSize: "80MB"
# Ollama AI models with GPU acceleration (Azure NC-series VMs)
# Set ollama.enabled: false if you don't need local AI models
ollama:
enabled: false
replicaCount: 1
# GPU node targeting - uncomment and customize for GPU node pools
# Recommended: NC6s_v3 or NC12s_v3 VMs
# nodeSelector:
# agentpool: "gpu"
tolerations:
- key: "sku"
operator: "Equal"
value: "gpu"
effect: "NoSchedule"
# GPU resource allocation for AI model serving
gpu:
enabled: true
count: 1
resources:
limits:
memory: "8Gi"
nvidia.com/gpu: "1"
requests:
memory: "4Gi"
cpu: "1000m"
# High-performance storage for AI models (use managed-csi-premium for GPU workloads)
persistence:
enabled: true
storageClass: "managed-csi-premium"
size: 100Gi
env:
NVIDIA_DRIVER_CAPABILITIES: "all"
OLLAMA_LOAD_TIMEOUT: "-1"
OLLAMA_KEEP_ALIVE: "-1"
OLLAMA_DEBUG: "1"
# Ingress configuration
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# Main application
app:
host: simstudio.acme.com
paths:
- path: /
pathType: Prefix
# Realtime service
realtime:
host: simstudio-ws.acme.com
paths:
- path: /
pathType: Prefix
# TLS configuration
tls:
enabled: true
secretName: simstudio-tls-secret
# Internal Ingress configuration
ingressInternal:
enabled: false
className: azure-application-gateway
annotations:
appgw.ingress.kubernetes.io/use-private-ip: "true"
# Main application
app:
host: simstudio-internal.acme.local
paths:
- path: /
pathType: Prefix
# Realtime service
realtime:
host: simstudio-internal.acme.local
paths:
- path: /socket.io
pathType: Prefix
# TLS configuration
tls:
enabled: true
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"
# OCR_AZURE_API_KEY: "sim-azure-ocr-api-key"
# AZURE_ACS_CONNECTION_STRING: "sim-azure-acs-connection-string"
# # Non-secret config (deployment names, endpoints) — still required here: ESO mode requires
# # every non-empty app.env key to be mapped, even ones that aren't sensitive.
# KB_OPENAI_MODEL_NAME: "sim-kb-openai-model-name"
# WAND_OPENAI_MODEL_NAME: "sim-wand-openai-model-name"
# OCR_AZURE_ENDPOINT: "sim-azure-ocr-endpoint"
# OCR_AZURE_MODEL_NAME: "sim-azure-ocr-model-name"
# postgresql:
# password: "sim-postgresql-password"
+166
View File
@@ -0,0 +1,166 @@
# 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
# Server configuration
server:
image:
repository: simstudioai/copilot
tag: "" # defaults to Chart.AppVersion; override to pin a specific release
pullPolicy: IfNotPresent
replicaCount: 2
# Node scheduling (OPTIONAL)
# By default, copilot runs on the same nodes as the main Sim platform
nodeSelector: {}
# nodeSelector:
# workload-type: copilot
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
# Required secrets (set via values or provide your own secret)
env:
AGENT_API_DB_ENCRYPTION_KEY: "" # openssl rand -hex 32
INTERNAL_API_SECRET: "" # reuse Sim INTERNAL_API_SECRET
LICENSE_KEY: "" # Provided by Sim team
OPENAI_API_KEY_1: "" # At least one provider key required
ANTHROPIC_API_KEY_1: "" # Optional secondary provider
SIM_BASE_URL: "https://sim.example.com" # Base URL for Sim deployment
SIM_AGENT_API_KEY: "" # Must match SIM-side COPILOT_API_KEY
REDIS_URL: "redis://default:password@redis:6379"
# Optional configuration
CORS_ALLOWED_ORIGINS: "https://sim.example.com"
OTEL_EXPORTER_OTLP_ENDPOINT: ""
# Non-secret operational tunables — inlined as plain container env, never go through
# the Secret/ExternalSecret, so they don't need externalSecrets.remoteRefs.copilot entries
envDefaults:
PORT: "8080"
SERVICE_NAME: "copilot"
ENVIRONMENT: "production"
LOG_LEVEL: "info"
# Create a Secret from the values above. Set create=false to reference an existing secret instead.
secret:
create: true
name: ""
annotations: {}
extraEnv: []
extraEnvFrom: []
service:
type: ClusterIP
port: 8080
targetPort: 8080
# Internal PostgreSQL database (disable to use an external database)
postgresql:
enabled: true
image:
repository: postgres
tag: 17-alpine
pullPolicy: IfNotPresent
auth:
username: copilot
password: "" # REQUIRED - set via --set copilot.postgresql.auth.password
database: copilot
nodeSelector: {}
# nodeSelector:
# workload-type: copilot
resources:
limits:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "250m"
persistence:
enabled: true
size: 10Gi
# External database configuration (only used when postgresql.enabled=false)
database:
existingSecretName: ""
secretKey: DATABASE_URL
url: ""
# Migration job
migrations:
enabled: true
resources:
limits:
memory: "512Mi"
cpu: "500m"
requests:
memory: "256Mi"
cpu: "100m"
# Optional: Configure ingress to expose copilot service
# Uncomment if you need external access to copilot
# ingress:
# enabled: true
# className: nginx
# annotations:
# cert-manager.io/cluster-issuer: letsencrypt-prod
# nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# copilot:
# host: copilot.yourdomain.com
# paths:
# - path: /
# pathType: Prefix
# tls:
# enabled: true
# secretName: copilot-tls-secret
# If using private Docker Hub repository
# global:
# imagePullSecrets:
# - name: dockerhub-secret
+132
View File
@@ -0,0 +1,132 @@
# 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:
imageRegistry: "ghcr.io"
# Main application
app:
enabled: true
replicaCount: 1
# Resource allocation for development environment
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
# Development URLs
env:
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
BETTER_AUTH_URL: "http://localhost:3000"
NEXT_PUBLIC_SOCKET_URL: "http://localhost:3002"
# Example secrets for development (replace with secure values for production)
# For production, generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "dev-32-char-auth-secret-not-secure-dev"
ENCRYPTION_KEY: "dev-32-char-encryption-key-not-secure"
INTERNAL_API_SECRET: "dev-32-char-internal-secret-not-secure"
CRON_SECRET: "dev-32-char-cron-secret-not-for-prod"
# Optional: API Key Encryption (leave empty for dev, encrypts API keys at rest)
# For production, generate 64-char hex using: openssl rand -hex 32
API_ENCRYPTION_KEY: "" # Optional - if not set, API keys stored in plain text
# Realtime service
realtime:
enabled: true
replicaCount: 1
# Resource allocation for realtime WebSocket service in development
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
env:
NEXT_PUBLIC_APP_URL: "http://localhost:3000"
BETTER_AUTH_URL: "http://localhost:3000"
BETTER_AUTH_SECRET: "dev-32-char-auth-secret-not-secure-dev"
ALLOWED_ORIGINS: "http://localhost:3000"
# Database migrations
migrations:
enabled: true
# PostgreSQL database
postgresql:
enabled: true
# Simple authentication for development
auth:
username: postgres
password: "postgres"
database: simstudio
# PostgreSQL with pgvector extension for vector operations
image:
repository: pgvector/pgvector
tag: pg17
pullPolicy: IfNotPresent
# Minimal resource allocation for development PostgreSQL
resources:
limits:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "250m"
# Persistence disabled for easier development (data will be lost on restart)
persistence:
enabled: false
# SSL/TLS disabled for local development
tls:
enabled: false
# Minimal PostgreSQL configuration for development
config:
maxConnections: 100
sharedBuffers: "256MB"
maxWalSize: "1GB"
minWalSize: "80MB"
# Ollama AI models (disabled by default for development)
ollama:
enabled: false
# Ingress (disabled for development - use port-forward for local access)
ingress:
enabled: false
# Pod disruption budget (disabled for development)
podDisruptionBudget:
enabled: false
# Network policies (disabled for development)
networkPolicy:
enabled: false
@@ -0,0 +1,68 @@
# 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 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
replicaCount: 2
secrets:
existingSecret:
enabled: true
name: "sim-app-secrets"
env:
NEXT_PUBLIC_APP_URL: "https://sim.example.com"
BETTER_AUTH_URL: "https://sim.example.com"
NEXT_PUBLIC_SOCKET_URL: "wss://sim-ws.example.com"
NODE_ENV: "production"
realtime:
enabled: true
replicaCount: 2
env:
NEXT_PUBLIC_APP_URL: "https://sim.example.com"
BETTER_AUTH_URL: "https://sim.example.com"
ALLOWED_ORIGINS: "https://sim.example.com"
NODE_ENV: "production"
postgresql:
enabled: true
auth:
username: postgres
database: sim
existingSecret:
enabled: true
name: "sim-postgresql-secret"
passwordKey: "POSTGRES_PASSWORD"
# ---
# Create secrets before installing:
# ---
# 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)" \
# --from-literal=API_ENCRYPTION_KEY="$(openssl rand -hex 32)"
# kubectl create secret generic sim-postgresql-secret \
# --namespace sim \
# --from-literal=POSTGRES_PASSWORD="$(openssl rand -base64 16 | tr -d '/+=')"
+187
View File
@@ -0,0 +1,187 @@
# 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:
imageRegistry: "ghcr.io"
# Main application
app:
enabled: true
replicaCount: 2
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
SOCKET_SERVER_URL: "https://simstudio-ws.acme.com"
NEXT_PUBLIC_SOCKET_URL: "https://simstudio-ws.acme.com"
# Security settings (REQUIRED - replace with your own secure secrets)
# Generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "" # Set via --set flag or external secret manager
ENCRYPTION_KEY: "" # Set via --set flag or external secret manager
INTERNAL_API_SECRET: "" # Set via --set flag or external secret manager
CRON_SECRET: "" # Set via --set flag or external secret manager
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32
API_ENCRYPTION_KEY: "" # Optional but recommended - encrypts API keys at rest
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# Realtime service
realtime:
enabled: true
replicaCount: 2
resources:
limits:
memory: "4Gi"
cpu: "1000m"
requests:
memory: "2Gi"
cpu: "500m"
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
BETTER_AUTH_SECRET: "" # Must match main app secret - set via --set flag
ALLOWED_ORIGINS: "https://simstudio.acme.com"
NODE_ENV: "production"
# Database migrations
migrations:
enabled: true
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
# Disable internal PostgreSQL
postgresql:
enabled: false
# Configure external database connection
externalDatabase:
enabled: true
# Database connection details (REQUIRED - configure for your external database)
host: "" # Database hostname (e.g., "postgres.acme.com" or RDS endpoint)
port: 5432
username: "" # Database username (e.g., "simstudio_user")
password: "" # Database password - set via --set flag or external secret
database: "" # Database name (e.g., "simstudio_production")
# SSL mode for database connections (recommended: 'require' for production)
sslMode: "require" # Options: disable, allow, prefer, require, verify-ca, verify-full
# Ollama (optional for AI models)
ollama:
enabled: false
# Ingress configuration
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
app:
host: simstudio.acme.com
paths:
- path: /
pathType: Prefix
realtime:
host: simstudio-ws.acme.com
paths:
- path: /
pathType: Prefix
tls:
enabled: true
secretName: simstudio-tls-secret
# Production-ready features (autoscaling, monitoring, etc.)
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
podDisruptionBudget:
enabled: true
minAvailable: null
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
monitoring:
serviceMonitor:
enabled: true
labels:
monitoring: "prometheus"
interval: 15s
networkPolicy:
enabled: true
# 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:
extraRules:
- to: []
ports:
- protocol: TCP
port: 5432
# Example deployment command with secure secret generation:
# helm install sim ./helm/sim \
# --values ./helm/sim/examples/values-external-db.yaml \
# --set externalDatabase.host="your-db-host.com" \
# --set externalDatabase.username="your-db-user" \
# --set externalDatabase.password="your-db-password" \
# --set externalDatabase.database="your-db-name" \
# --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 app.env.API_ENCRYPTION_KEY="$(openssl rand -hex 32)" \
# --set realtime.env.BETTER_AUTH_SECRET="$(openssl rand -hex 32)"
@@ -0,0 +1,123 @@
# 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 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
apiVersion: "v1"
refreshInterval: "1h"
secretStoreRef:
name: "sim-secret-store"
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"
postgresql:
password: "sim/postgresql/password"
# Only needed when copilot.enabled=true and copilot.server.secret.create=true
# (uncomment and pre-populate the referenced paths in your secret store):
# copilot:
# AGENT_API_DB_ENCRYPTION_KEY: "sim/copilot/agent-api-db-encryption-key"
# INTERNAL_API_SECRET: "sim/copilot/internal-api-secret"
# LICENSE_KEY: "sim/copilot/license-key"
# SIM_BASE_URL: "sim/copilot/sim-base-url"
# SIM_AGENT_API_KEY: "sim/copilot/sim-agent-api-key"
# REDIS_URL: "sim/copilot/redis-url"
# OPENAI_API_KEY_1: "sim/copilot/openai-api-key"
app:
enabled: true
replicaCount: 2
# 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"
NODE_ENV: "production"
realtime:
enabled: true
replicaCount: 2
envDefaults:
NEXT_PUBLIC_APP_URL: "https://sim.example.com"
BETTER_AUTH_URL: "https://sim.example.com"
ALLOWED_ORIGINS: "https://sim.example.com"
NODE_ENV: "production"
postgresql:
enabled: true
auth:
username: postgres
database: sim
# ---
# SecretStore Examples (apply one of these to your cluster before installing)
# ---
# Azure Key Vault (Workload Identity):
# apiVersion: external-secrets.io/v1beta1
# kind: ClusterSecretStore
# metadata:
# name: sim-secret-store
# spec:
# provider:
# azurekv:
# authType: WorkloadIdentity
# vaultUrl: "https://your-keyvault.vault.azure.net"
# serviceAccountRef:
# name: external-secrets-sa
# namespace: external-secrets
# AWS Secrets Manager (IRSA):
# apiVersion: external-secrets.io/v1beta1
# kind: ClusterSecretStore
# metadata:
# name: sim-secret-store
# spec:
# provider:
# aws:
# service: SecretsManager
# region: us-east-1
# role: arn:aws:iam::123456789012:role/external-secrets-role
# HashiCorp Vault (Kubernetes Auth):
# apiVersion: external-secrets.io/v1beta1
# kind: ClusterSecretStore
# metadata:
# name: sim-secret-store
# spec:
# provider:
# vault:
# server: "https://vault.example.com"
# path: "secret"
# version: "v2"
# auth:
# kubernetes:
# mountPath: "kubernetes"
# role: "external-secrets"
+333
View File
@@ -0,0 +1,333 @@
# 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:
imageRegistry: "ghcr.io"
storageClass: "standard-rwo"
# Main application
app:
enabled: true
replicaCount: 2
# Node selector for application pods
# Uncomment and customize based on your GKE node labels:
# nodeSelector:
# cloud.google.com/gke-nodepool: "default-pool"
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
# Production URLs (REQUIRED - update with your actual domain names)
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
# SOCKET_SERVER_URL is auto-detected (uses internal service http://sim-realtime:3002)
NEXT_PUBLIC_SOCKET_URL: "https://simstudio-ws.acme.com" # Public WebSocket URL for browsers
# Security settings (REQUIRED - replace with your own secure secrets)
# Generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "your-secure-production-auth-secret-here"
ENCRYPTION_KEY: "your-secure-production-encryption-key-here"
INTERNAL_API_SECRET: "your-secure-production-internal-api-secret-here"
CRON_SECRET: "your-secure-production-cron-secret-here"
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32
API_ENCRYPTION_KEY: "your-64-char-hex-api-encryption-key-here" # Optional but recommended
NODE_ENV: "production"
NEXT_TELEMETRY_DISABLED: "1"
# GCP-specific environment variables
GOOGLE_CLOUD_PROJECT: "your-project-id"
GOOGLE_CLOUD_REGION: "us-central1"
# Realtime service
realtime:
enabled: true
replicaCount: 2
# Node selector for realtime pods
# Uncomment and customize based on your GKE node labels:
# nodeSelector:
# cloud.google.com/gke-nodepool: "default-pool"
resources:
limits:
memory: "4Gi"
cpu: "1000m"
requests:
memory: "2Gi"
cpu: "500m"
env:
NEXT_PUBLIC_APP_URL: "https://simstudio.acme.com"
BETTER_AUTH_URL: "https://simstudio.acme.com"
BETTER_AUTH_SECRET: "your-secure-production-auth-secret-here"
ALLOWED_ORIGINS: "https://simstudio.acme.com"
NODE_ENV: "production"
# Database migrations
migrations:
enabled: true
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
# PostgreSQL database
postgresql:
enabled: true
# Node selector for database pods
# Uncomment and customize (recommended: memory-optimized machine types):
# nodeSelector:
# cloud.google.com/gke-nodepool: "database-pool"
# Database authentication (REQUIRED - set secure credentials)
auth:
username: postgres
password: "your-secure-postgres-password"
database: simstudio
# Resource allocation optimized for GKE
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
# Persistent storage using Google Cloud Persistent Disk
persistence:
enabled: true
storageClass: "standard-rwo"
size: 50Gi
accessModes:
- ReadWriteOnce
# SSL/TLS configuration (requires cert-manager to be installed)
tls:
enabled: false # Set to true if cert-manager is installed
certificatesSecret: postgres-tls-secret
# PostgreSQL performance tuning for GCP infrastructure
config:
maxConnections: 1000
sharedBuffers: "2GB"
maxWalSize: "8GB"
minWalSize: "160MB"
# Ollama AI models with GPU acceleration (GCP GPU instances)
# Set ollama.enabled: false if you don't need local AI models
ollama:
enabled: false
replicaCount: 1
# GPU node targeting - uncomment and customize for GPU node pools
# Recommended: T4 or V100 GPU instances
# nodeSelector:
# cloud.google.com/gke-nodepool: "gpu-pool"
# cloud.google.com/gke-accelerator: "nvidia-tesla-t4"
tolerations:
- key: "nvidia.com/gpu"
operator: "Equal"
value: "present"
effect: "NoSchedule"
# GPU resource allocation for AI model serving
gpu:
enabled: true
count: 1
resources:
limits:
memory: "16Gi"
cpu: "4000m"
nvidia.com/gpu: "1"
requests:
memory: "8Gi"
cpu: "2000m"
# High-performance SSD storage for AI models
persistence:
enabled: true
storageClass: "premium-rwo"
size: 100Gi
accessModes:
- ReadWriteOnce
env:
NVIDIA_DRIVER_CAPABILITIES: "all"
OLLAMA_LOAD_TIMEOUT: "-1"
OLLAMA_KEEP_ALIVE: "-1"
OLLAMA_DEBUG: "1"
# Ingress using Google Cloud Load Balancer
ingress:
enabled: true
className: gce
annotations:
kubernetes.io/ingress.class: gce
kubernetes.io/ingress.global-static-ip-name: "simstudio-ip"
networking.gke.io/managed-certificates: "simstudio-ssl-cert"
kubernetes.io/ingress.allow-http: "false"
# Main application
app:
host: simstudio.acme.com
paths:
- path: /
pathType: Prefix
# Realtime service
realtime:
host: simstudio-ws.acme.com
paths:
- path: /
pathType: Prefix
# TLS configuration
tls:
enabled: true
secretName: simstudio-tls-secret
# Pod disruption budget for high availability
podDisruptionBudget:
enabled: true
minAvailable: null
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
# Network policies
networkPolicy:
enabled: true
# Pod anti-affinity for high availability across GCP zones
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: ["simstudio"]
topologyKey: kubernetes.io/hostname
- weight: 50
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: ["simstudio"]
topologyKey: topology.gke.io/zone
# Service Account with Workload Identity integration
serviceAccount:
create: true
annotations:
iam.gke.io/gcp-service-account: "simstudio@your-project-id.iam.gserviceaccount.com"
# Additional environment variables for GCP service integration
extraEnvVars:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: "/var/secrets/google/key.json"
# Additional volumes for service account credentials
extraVolumes:
- name: google-cloud-key
secret:
secretName: google-service-account-key
extraVolumeMounts:
- name: google-cloud-key
mountPath: /var/secrets/google
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"
+245
View File
@@ -0,0 +1,245 @@
# 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:
imageRegistry: "ghcr.io"
# For production, use a StorageClass with reclaimPolicy: Retain
storageClass: "managed-csi-premium"
# Main application
app:
enabled: true
replicaCount: 2
resources:
limits:
memory: "8Gi"
cpu: "2000m"
requests:
memory: "6Gi"
cpu: "1000m"
# Production URLs (REQUIRED - update with your actual domain names)
env:
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
BETTER_AUTH_URL: "https://sim.acme.ai"
SOCKET_SERVER_URL: "https://sim-ws.acme.ai"
NEXT_PUBLIC_SOCKET_URL: "https://sim-ws.acme.ai"
# Security settings (REQUIRED - replace with your own secure secrets)
# Generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ENCRYPTION_KEY: "your-production-encryption-key-here"
INTERNAL_API_SECRET: "your-production-internal-api-secret-here"
CRON_SECRET: "your-production-cron-secret-here"
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32
API_ENCRYPTION_KEY: "your-64-char-hex-api-encryption-key-here" # Optional but recommended
# Email verification (set to true if you want to require email verification)
EMAIL_VERIFICATION_ENABLED: "false"
# Optional third-party service integrations (configure as needed)
RESEND_API_KEY: "your-resend-api-key"
GOOGLE_CLIENT_ID: "your-google-client-id"
GOOGLE_CLIENT_SECRET: "your-google-client-secret"
# DISABLE_GOOGLE_AUTH: "true" # Uncomment to hide Google OAuth login
# DISABLE_GITHUB_AUTH: "true" # Uncomment to hide GitHub OAuth login
# Realtime service
realtime:
enabled: true
replicaCount: 2
resources:
limits:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "250m"
env:
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
BETTER_AUTH_URL: "https://sim.acme.ai"
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ALLOWED_ORIGINS: "https://sim.acme.ai"
# Database migrations
migrations:
enabled: true
resources:
limits:
memory: "2Gi"
cpu: "1000m"
requests:
memory: "1Gi"
cpu: "500m"
# PostgreSQL database
postgresql:
enabled: true
# Database authentication (REQUIRED - set secure credentials)
auth:
username: postgres
password: "your-secure-postgres-password"
database: simstudio
# Resource allocation for production workloads
resources:
limits:
memory: "4Gi"
cpu: "2000m"
requests:
memory: "2Gi"
cpu: "1000m"
# Persistent storage configuration
persistence:
enabled: true
storageClass: "managed-csi-premium"
size: 50Gi
# SSL/TLS configuration (recommended for production)
tls:
enabled: true
certificatesSecret: postgres-tls-secret
# PostgreSQL performance configuration for production
config:
maxConnections: 1000
sharedBuffers: "2GB"
maxWalSize: "8GB"
minWalSize: "160MB"
# Ollama AI models (optional - enable if you need local AI model serving)
ollama:
enabled: false
# Ingress configuration
ingress:
enabled: true
className: nginx
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
# Main application
app:
host: sim.acme.ai
paths:
- path: /
pathType: Prefix
# Realtime service
realtime:
host: sim-ws.acme.ai
paths:
- path: /
pathType: Prefix
# TLS configuration
tls:
enabled: true
secretName: sim-tls-secret
# Horizontal Pod Autoscaler (automatically scales pods based on CPU/memory usage)
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 50
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 2
periodSeconds: 60
# Pod disruption budget (ensures minimum availability during cluster maintenance)
podDisruptionBudget:
enabled: true
minAvailable: null
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
# Monitoring integration with Prometheus
monitoring:
serviceMonitor:
enabled: true
labels:
monitoring: "prometheus"
interval: 15s
scrapeTimeout: 10s
# Network policies (restricts pod-to-pod communication for security)
networkPolicy:
enabled: true
# Shared storage for data sharing between pods (enterprise feature)
sharedStorage:
enabled: true
storageClass: "managed-csi-premium"
volumes:
- name: output-share
size: 100Gi
accessModes:
- ReadWriteMany
- name: model-share
size: 200Gi
accessModes:
- ReadWriteMany
# Telemetry and observability (comprehensive monitoring and tracing)
telemetry:
enabled: true
resources:
limits:
memory: "1Gi"
cpu: "500m"
requests:
memory: "512Mi"
cpu: "200m"
# Configure endpoints based on your observability infrastructure
prometheus:
enabled: true
endpoint: "http://prometheus-server/api/v1/write"
jaeger:
enabled: true
endpoint: "http://jaeger-collector:14250"
+106
View File
@@ -0,0 +1,106 @@
# 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:
imageRegistry: "ghcr.io"
storageClass: "managed-csi-premium"
# Main application with custom branding
app:
enabled: true
replicaCount: 1
# Custom branding configuration
env:
# Application URLs (update with your domain)
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
BETTER_AUTH_URL: "https://sim.acme.ai"
SOCKET_SERVER_URL: "https://sim-ws.acme.ai"
NEXT_PUBLIC_SOCKET_URL: "https://sim-ws.acme.ai"
# Security settings (REQUIRED)
# Generate using: openssl rand -hex 32
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ENCRYPTION_KEY: "your-production-encryption-key-here"
INTERNAL_API_SECRET: "your-production-internal-api-secret-here"
CRON_SECRET: "your-production-cron-secret-here"
# Optional: API Key Encryption (RECOMMENDED for production)
# Generate 64-character hex string using: openssl rand -hex 32
API_ENCRYPTION_KEY: "your-64-char-hex-api-encryption-key-here" # Optional but recommended
# UI Branding & Whitelabeling Configuration
NEXT_PUBLIC_BRAND_NAME: "Acme AI Studio"
NEXT_PUBLIC_BRAND_LOGO_URL: "https://acme.com/assets/logo.png"
NEXT_PUBLIC_BRAND_FAVICON_URL: "https://acme.com/assets/favicon.ico"
NEXT_PUBLIC_CUSTOM_CSS_URL: "https://acme.com/assets/theme.css"
NEXT_PUBLIC_SUPPORT_EMAIL: "ai-support@acme.com"
NEXT_PUBLIC_DOCUMENTATION_URL: "https://docs.acme.com/ai-studio"
NEXT_PUBLIC_TERMS_URL: "https://acme.com/terms"
NEXT_PUBLIC_PRIVACY_URL: "https://acme.com/privacy"
# Realtime service
realtime:
enabled: true
replicaCount: 1
env:
NEXT_PUBLIC_APP_URL: "https://sim.acme.ai"
BETTER_AUTH_URL: "https://sim.acme.ai"
BETTER_AUTH_SECRET: "your-production-auth-secret-here"
ALLOWED_ORIGINS: "https://sim.acme.ai"
# PostgreSQL database
postgresql:
enabled: true
auth:
password: "your-secure-db-password-here"
persistence:
enabled: true
size: 20Gi
# Ingress configuration
ingress:
enabled: true
className: "nginx"
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
app:
host: "sim.acme.ai"
paths:
- path: /
pathType: Prefix
realtime:
host: "sim-ws.acme.ai"
paths:
- path: /
pathType: Prefix
tls:
enabled: true
secretName: "sim-acme-tls"
# Auto-scaling
autoscaling:
enabled: true
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
+101
View File
@@ -0,0 +1,101 @@
{{- /*
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 }}
{{- if .Values.pii.enabled }}
kubectl --namespace {{ .Release.Namespace }} rollout status deployment/{{ include "sim.fullname" . }}-pii
{{- 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. Upgrade notes (read before upgrading from a chart version released before this one):
* externalSecrets.apiVersion default is "v1beta1" (was "v1"). v1beta1 is
supported by every ESO release from v0.7+ through current. If you're on
ESO v0.17+ and want the graduated v1 API, set externalSecrets.apiVersion: "v1".
* networkPolicy.egress remains a list of custom egress rules (unchanged).
Cloud-metadata CIDR blocking is now configured via networkPolicy.egressExceptCidrs
(defaults to AWS/GCP/Azure IMDS + ECS task metadata).
6. 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
+683
View File
@@ -0,0 +1,683 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "sim.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "sim.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "sim.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "sim.labels" -}}
helm.sh/chart: {{ include "sim.chart" . }}
{{ include "sim.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.global.commonLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "sim.selectorLabels" -}}
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
App specific labels
*/}}
{{- define "sim.app.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: app
{{- end }}
{{/*
App selector labels
*/}}
{{- define "sim.app.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: app
{{- end }}
{{/*
Realtime specific labels
*/}}
{{- define "sim.realtime.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: realtime
{{- end }}
{{/*
Realtime selector labels
*/}}
{{- define "sim.realtime.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: realtime
{{- end }}
{{/*
PostgreSQL specific labels
*/}}
{{- define "sim.postgresql.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: postgresql
{{- end }}
{{/*
PostgreSQL selector labels
*/}}
{{- define "sim.postgresql.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: postgresql
{{- end }}
{{/*
Ollama specific labels
*/}}
{{- define "sim.ollama.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: ollama
{{- end }}
{{/*
Ollama selector labels
*/}}
{{- define "sim.ollama.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: ollama
{{- end }}
{{/*
PII (Presidio) specific labels
*/}}
{{- define "sim.pii.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: pii
{{- end }}
{{/*
PII (Presidio) selector labels
*/}}
{{- define "sim.pii.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: pii
{{- end }}
{{/*
Migrations specific labels
*/}}
{{- define "sim.migrations.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: migrations
{{- end }}
{{/*
Copilot specific labels
*/}}
{{- define "sim.copilot.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: copilot
{{- end }}
{{/*
Copilot selector labels
*/}}
{{- define "sim.copilot.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: copilot
{{- end }}
{{/*
Copilot PostgreSQL specific labels
*/}}
{{- define "sim.copilotPostgresql.labels" -}}
{{ include "sim.labels" . }}
app.kubernetes.io/component: copilot-postgresql
{{- end }}
{{/*
Copilot PostgreSQL selector labels
*/}}
{{- define "sim.copilotPostgresql.selectorLabels" -}}
{{ include "sim.selectorLabels" . }}
app.kubernetes.io/component: copilot-postgresql
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "sim.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "sim.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
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 := "" -}}
{{- if and $global $global.imageRegistry -}}
{{- if or (hasPrefix "simstudioai/" $repository) $global.useRegistryForAllImages -}}
{{- $registry = $global.imageRegistry -}}
{{- end -}}
{{- end -}}
{{- $repoPath := $repository -}}
{{- if $registry -}}
{{- $repoPath = printf "%s/%s" $registry $repository -}}
{{- end -}}
{{- if ne $digest "" -}}
{{- printf "%s@%s" $repoPath $digest }}
{{- else -}}
{{- $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 }}
{{/*
Database URL for internal PostgreSQL
*/}}
{{- define "sim.databaseUrl" -}}
{{- if .Values.postgresql.enabled }}
{{- $host := printf "%s-postgresql" (include "sim.fullname" .) }}
{{- $port := .Values.postgresql.service.port }}
{{- $username := .Values.postgresql.auth.username }}
{{- $database := .Values.postgresql.auth.database }}
{{- $sslMode := ternary "require" "disable" .Values.postgresql.tls.enabled }}
{{- printf "postgresql://%s:$(POSTGRES_PASSWORD)@%s:%v/%s?sslmode=%s" $username $host $port $database $sslMode }}
{{- else if .Values.externalDatabase.enabled }}
{{- $host := .Values.externalDatabase.host }}
{{- $port := .Values.externalDatabase.port }}
{{- $username := .Values.externalDatabase.username }}
{{- $database := .Values.externalDatabase.database }}
{{- $sslMode := .Values.externalDatabase.sslMode }}
{{- printf "postgresql://%s:$(EXTERNAL_DB_PASSWORD)@%s:%v/%s?sslmode=%s" $username $host $port $database $sslMode }}
{{- end }}
{{- end }}
{{/*
Validate required secrets and reject default placeholder values
Skip validation when using existing secrets or External Secrets Operator
*/}}
{{- define "sim.validateSecrets" -}}
{{- $useExistingAppSecret := and .Values.app.secrets .Values.app.secrets.existingSecret .Values.app.secrets.existingSecret.enabled }}
{{- $useExternalSecrets := and .Values.externalSecrets .Values.externalSecrets.enabled }}
{{- $useExistingPostgresSecret := and .Values.postgresql.auth.existingSecret .Values.postgresql.auth.existingSecret.enabled }}
{{- $useExistingExternalDbSecret := and .Values.externalDatabase.existingSecret .Values.externalDatabase.existingSecret.enabled }}
{{- /* App secrets validation - skip if using existing secret or ESO */ -}}
{{- if not (or $useExistingAppSecret $useExternalSecrets) }}
{{- if and .Values.app.enabled (not .Values.app.env.BETTER_AUTH_SECRET) }}
{{- fail "app.env.BETTER_AUTH_SECRET is required for production deployment" }}
{{- end }}
{{- if and .Values.app.enabled (eq .Values.app.env.BETTER_AUTH_SECRET "CHANGE-ME-32-CHAR-SECRET-FOR-PRODUCTION-USE") }}
{{- fail "app.env.BETTER_AUTH_SECRET must not use the default placeholder value. Generate a secure secret with: openssl rand -hex 32" }}
{{- end }}
{{- if and .Values.app.enabled (not .Values.app.env.ENCRYPTION_KEY) }}
{{- fail "app.env.ENCRYPTION_KEY is required for production deployment" }}
{{- end }}
{{- 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) }}
{{- if and .Values.postgresql.enabled (not .Values.postgresql.auth.password) }}
{{- fail "postgresql.auth.password is required when using internal PostgreSQL" }}
{{- end }}
{{- if and .Values.postgresql.enabled (eq .Values.postgresql.auth.password "CHANGE-ME-SECURE-PASSWORD") }}
{{- fail "postgresql.auth.password must not use the default placeholder value. Set a secure password for production" }}
{{- end }}
{{- if and .Values.postgresql.enabled .Values.postgresql.auth.password (not (regexMatch "^[a-zA-Z0-9._-]+$" .Values.postgresql.auth.password)) }}
{{- fail "postgresql.auth.password must only contain alphanumeric characters, hyphens, underscores, or periods to ensure DATABASE_URL compatibility. Generate with: openssl rand -base64 16 | tr -d '/+='" }}
{{- end }}
{{- end }}
{{- /* External database password validation - skip if using existing secret or ESO */ -}}
{{- if not (or $useExistingExternalDbSecret $useExternalSecrets) }}
{{- if and .Values.externalDatabase.enabled (not .Values.externalDatabase.password) }}
{{- fail "externalDatabase.password is required when using external database" }}
{{- end }}
{{- if and .Values.externalDatabase.enabled .Values.externalDatabase.password (not (regexMatch "^[a-zA-Z0-9._-]+$" .Values.externalDatabase.password)) }}
{{- 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, PII_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" "PII_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 }}
{{/*
Get the app secrets name
Returns the name of the secret containing app credentials (auth, encryption keys)
*/}}
{{- define "sim.appSecretName" -}}
{{- if and .Values.app.secrets .Values.app.secrets.existingSecret .Values.app.secrets.existingSecret.enabled -}}
{{- .Values.app.secrets.existingSecret.name -}}
{{- else -}}
{{- printf "%s-app-secrets" (include "sim.fullname" .) -}}
{{- end -}}
{{- end }}
{{/*
Get the PostgreSQL secret name
Returns the name of the secret containing PostgreSQL password
*/}}
{{- define "sim.postgresqlSecretName" -}}
{{- if and .Values.postgresql.auth.existingSecret .Values.postgresql.auth.existingSecret.enabled -}}
{{- .Values.postgresql.auth.existingSecret.name -}}
{{- else -}}
{{- printf "%s-postgresql-secret" (include "sim.fullname" .) -}}
{{- end -}}
{{- end }}
{{/*
Get the PostgreSQL password key name
Returns the key name in the secret that contains the password
*/}}
{{- define "sim.postgresqlPasswordKey" -}}
{{- if and .Values.postgresql.auth.existingSecret .Values.postgresql.auth.existingSecret.enabled -}}
{{- .Values.postgresql.auth.existingSecret.passwordKey | default "POSTGRES_PASSWORD" -}}
{{- else -}}
{{- print "POSTGRES_PASSWORD" -}}
{{- end -}}
{{- end }}
{{/*
Get the external database secret name
Returns the name of the secret containing external database password
*/}}
{{- define "sim.externalDbSecretName" -}}
{{- if and .Values.externalDatabase.existingSecret .Values.externalDatabase.existingSecret.enabled -}}
{{- .Values.externalDatabase.existingSecret.name -}}
{{- else -}}
{{- printf "%s-external-db-secret" (include "sim.fullname" .) -}}
{{- end -}}
{{- end }}
{{/*
Get the external database password key name
Returns the key name in the secret that contains the password
*/}}
{{- define "sim.externalDbPasswordKey" -}}
{{- if and .Values.externalDatabase.existingSecret .Values.externalDatabase.existingSecret.enabled -}}
{{- .Values.externalDatabase.existingSecret.passwordKey | default "EXTERNAL_DB_PASSWORD" -}}
{{- else -}}
{{- print "EXTERNAL_DB_PASSWORD" -}}
{{- end -}}
{{- end }}
{{/*
Check if app secrets should be created by the chart
Returns true if we should create the app secrets (not using existing or ESO)
*/}}
{{- define "sim.createAppSecrets" -}}
{{- $useExistingAppSecret := and .Values.app.secrets .Values.app.secrets.existingSecret .Values.app.secrets.existingSecret.enabled }}
{{- $useExternalSecrets := and .Values.externalSecrets .Values.externalSecrets.enabled }}
{{- if not (or $useExistingAppSecret $useExternalSecrets) -}}
true
{{- end -}}
{{- end }}
{{/*
Check if PostgreSQL secret should be created by the chart
Returns true if we should create the PostgreSQL secret (not using existing or ESO)
*/}}
{{- define "sim.createPostgresqlSecret" -}}
{{- $useExistingSecret := and .Values.postgresql.auth.existingSecret .Values.postgresql.auth.existingSecret.enabled }}
{{- $useExternalSecrets := and .Values.externalSecrets .Values.externalSecrets.enabled }}
{{- if not (or $useExistingSecret $useExternalSecrets) -}}
true
{{- end -}}
{{- end }}
{{/*
Check if external database secret should be created by the chart
Returns true if we should create the external database secret (not using existing or ESO)
*/}}
{{- define "sim.createExternalDbSecret" -}}
{{- $useExistingSecret := and .Values.externalDatabase.existingSecret .Values.externalDatabase.existingSecret.enabled }}
{{- $useExternalSecrets := and .Values.externalSecrets .Values.externalSecrets.enabled }}
{{- if not (or $useExistingSecret $useExternalSecrets) -}}
true
{{- end -}}
{{- end }}
{{/*
Ollama URL
*/}}
{{- define "sim.ollamaUrl" -}}
{{- if .Values.ollama.enabled }}
{{- $serviceName := printf "%s-ollama" (include "sim.fullname" .) }}
{{- $port := .Values.ollama.service.port }}
{{- printf "http://%s:%v" $serviceName $port }}
{{- else }}
{{- .Values.app.env.OLLAMA_URL | default "http://localhost:11434" }}
{{- end }}
{{- end }}
{{/*
PII (Presidio) service URL
*/}}
{{- define "sim.piiUrl" -}}
{{- if .Values.pii.enabled }}
{{- $serviceName := printf "%s-pii" (include "sim.fullname" .) }}
{{- $port := .Values.pii.service.port }}
{{- printf "http://%s:%v" $serviceName $port }}
{{- else }}
{{- .Values.app.env.PII_URL | default "http://localhost:5001" }}
{{- end }}
{{- end }}
{{/*
Socket Server URL (internal)
*/}}
{{- define "sim.socketServerUrl" -}}
{{- if .Values.realtime.enabled }}
{{- $serviceName := printf "%s-realtime" (include "sim.fullname" .) }}
{{- $port := .Values.realtime.service.port }}
{{- printf "http://%s:%v" $serviceName $port }}
{{- else }}
{{- .Values.app.env.SOCKET_SERVER_URL | default "http://localhost:3002" }}
{{- end }}
{{- end }}
{{/*
Resource limits and requests
*/}}
{{- define "sim.resources" -}}
{{- if .resources }}
resources:
{{- if .resources.limits }}
limits:
{{- toYaml .resources.limits | nindent 4 }}
{{- end }}
{{- if .resources.requests }}
requests:
{{- toYaml .resources.requests | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
{{/*
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.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 $merged | nindent 2 }}
{{- end }}
{{/*
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.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 $merged | nindent 2 }}
{{- end }}
{{/*
Backwards-compatible alias for container security context.
*/}}
{{- define "sim.securityContext" -}}
{{- include "sim.containerSecurityContext" . }}
{{- end }}
{{/*
Node selector
*/}}
{{- define "sim.nodeSelector" -}}
{{- if .nodeSelector }}
nodeSelector:
{{- toYaml .nodeSelector | nindent 2 }}
{{- end }}
{{- end }}
{{/*
Tolerations
*/}}
{{- define "sim.tolerations" -}}
{{- if .tolerations }}
tolerations:
{{- toYaml .tolerations | nindent 2 }}
{{- end }}
{{- end }}
{{/*
Affinity
*/}}
{{- define "sim.affinity" -}}
{{- if .affinity }}
affinity:
{{- toYaml .affinity | nindent 2 }}
{{- 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
*/}}
{{- define "sim.copilot.envSecretName" -}}
{{- if and .Values.copilot.server.secret.name (ne .Values.copilot.server.secret.name "") -}}
{{- .Values.copilot.server.secret.name -}}
{{- else -}}
{{- printf "%s-copilot-env" (include "sim.fullname" .) -}}
{{- end -}}
{{- end }}
{{/*
Copilot database secret name
*/}}
{{- define "sim.copilot.databaseSecretName" -}}
{{- if .Values.copilot.postgresql.enabled -}}
{{- printf "%s-copilot-postgresql-secret" (include "sim.fullname" .) -}}
{{- else if and .Values.copilot.database.existingSecretName (ne .Values.copilot.database.existingSecretName "") -}}
{{- .Values.copilot.database.existingSecretName -}}
{{- else -}}
{{- printf "%s-copilot-database-secret" (include "sim.fullname" .) -}}
{{- end -}}
{{- end }}
{{/*
Copilot database secret key
*/}}
{{- define "sim.copilot.databaseSecretKey" -}}
{{- default "DATABASE_URL" .Values.copilot.database.secretKey -}}
{{- end }}
{{/*
Validate Copilot configuration
*/}}
{{- define "sim.copilot.validate" -}}
{{- if .Values.copilot.enabled -}}
{{- if and (not .Values.copilot.server.secret.create) (or (not .Values.copilot.server.secret.name) (eq .Values.copilot.server.secret.name "")) -}}
{{- fail "copilot.server.secret.name must be provided when copilot.server.secret.create=false" -}}
{{- end -}}
{{- if .Values.copilot.server.secret.create -}}
{{- $env := .Values.copilot.server.env -}}
{{- $useExternalSecrets := and .Values.externalSecrets .Values.externalSecrets.enabled -}}
{{- $remoteRefs := default (dict) (default (dict) .Values.externalSecrets.remoteRefs).copilot -}}
{{- $required := list "AGENT_API_DB_ENCRYPTION_KEY" "INTERNAL_API_SECRET" "LICENSE_KEY" "SIM_BASE_URL" "SIM_AGENT_API_KEY" "REDIS_URL" -}}
{{- range $key := $required -}}
{{- $inEnv := and $env (index $env $key) (ne (index $env $key) "") -}}
{{- $mapped := index $remoteRefs $key -}}
{{- if not (or $inEnv (and $useExternalSecrets $mapped)) -}}
{{- if $useExternalSecrets -}}
{{- fail (printf "Required key '%s' is missing: externalSecrets.enabled=true but the key is neither set in copilot.server.env nor mapped in externalSecrets.remoteRefs.copilot. Map it via externalSecrets.remoteRefs.copilot.%s='path/in/store' so it is synced into the copilot Secret." $key $key) -}}
{{- else -}}
{{- fail (printf "copilot.server.env.%s is required when copilot is enabled" $key) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- if $useExternalSecrets -}}
{{- range $key, $value := $env -}}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") -}}
{{- if not (index $remoteRefs $key) -}}
{{- fail (printf "Key '%s' is set in copilot.server.env but externalSecrets.enabled=true and externalSecrets.remoteRefs.copilot.%s is not configured. When ESO is enabled the chart-managed copilot Secret is not rendered, so the container would start with no value. Either map it via externalSecrets.remoteRefs.copilot.%s='path/in/store' or remove it from copilot.server.env." $key $key $key) -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $hasOpenAI := or (and $env (ne (default "" (index $env "OPENAI_API_KEY_1")) "")) (and $useExternalSecrets (index $remoteRefs "OPENAI_API_KEY_1")) -}}
{{- $hasAnthropic := or (and $env (ne (default "" (index $env "ANTHROPIC_API_KEY_1")) "")) (and $useExternalSecrets (index $remoteRefs "ANTHROPIC_API_KEY_1")) -}}
{{- if not (or $hasOpenAI $hasAnthropic) -}}
{{- fail "Set at least one of copilot.server.env.OPENAI_API_KEY_1 or copilot.server.env.ANTHROPIC_API_KEY_1 (or map one via externalSecrets.remoteRefs.copilot when externalSecrets.enabled=true)" -}}
{{- end -}}
{{- end -}}
{{- if .Values.copilot.postgresql.enabled -}}
{{- if or (not .Values.copilot.postgresql.auth.password) (eq .Values.copilot.postgresql.auth.password "") -}}
{{- fail "copilot.postgresql.auth.password is required when copilot.postgresql.enabled=true" -}}
{{- end -}}
{{- else -}}
{{- if and (or (not .Values.copilot.database.existingSecretName) (eq .Values.copilot.database.existingSecretName "")) (or (not .Values.copilot.database.url) (eq .Values.copilot.database.url "")) -}}
{{- fail "Provide copilot.database.existingSecretName or copilot.database.url when copilot.postgresql.enabled=false" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end }}
@@ -0,0 +1,84 @@
{{- if .Values.certManager.enabled }}
{{- /*
cert-manager Issuer Bootstrap Pattern
PREREQUISITE: cert-manager must be installed in your cluster before enabling this.
The root CA Certificate is created in the namespace specified by certManager.rootCA.namespace
(defaults to "cert-manager"). Ensure this namespace exists and cert-manager is running there.
Install cert-manager: https://cert-manager.io/docs/installation/
This implements the recommended pattern from cert-manager documentation:
1. A self-signed ClusterIssuer (for bootstrapping the root CA only)
2. A root CA Certificate (self-signed, used to sign other certificates)
3. A CA ClusterIssuer (uses the root CA to sign certificates)
Reference: https://cert-manager.io/docs/configuration/selfsigned/
*/ -}}
---
# 1. Self-Signed ClusterIssuer (Bootstrap Only)
# This issuer is used ONLY to create the root CA certificate.
# It should NOT be used directly for application certificates.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: {{ .Values.certManager.selfSignedIssuer.name }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: cert-manager
spec:
selfSigned: {}
---
# 2. Root CA Certificate
# This certificate is signed by the self-signed issuer and becomes the root of trust.
# The secret created here will be used by the CA issuer to sign certificates.
# NOTE: This must be created in the cert-manager namespace (or the namespace specified
# in certManager.rootCA.namespace). Ensure cert-manager is installed there first.
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ .Values.certManager.rootCA.certificateName }}
namespace: {{ .Values.certManager.rootCA.namespace | default "cert-manager" }} # Must match cert-manager's cluster-resource-namespace
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: cert-manager
spec:
isCA: true
commonName: {{ .Values.certManager.rootCA.commonName }}
secretName: {{ .Values.certManager.rootCA.secretName }}
duration: {{ .Values.certManager.rootCA.duration | default "87600h" }}
renewBefore: {{ .Values.certManager.rootCA.renewBefore | default "2160h" }}
privateKey:
algorithm: {{ .Values.certManager.rootCA.privateKey.algorithm | default "RSA" }}
size: {{ .Values.certManager.rootCA.privateKey.size | default 4096 }}
subject:
organizations:
{{- if .Values.certManager.rootCA.subject.organizations }}
{{- toYaml .Values.certManager.rootCA.subject.organizations | nindent 6 }}
{{- else }}
- {{ .Release.Name }}
{{- end }}
issuerRef:
name: {{ .Values.certManager.selfSignedIssuer.name }}
kind: ClusterIssuer
group: cert-manager.io
---
# 3. CA ClusterIssuer
# This is the issuer that should be used by applications to obtain certificates.
# It signs certificates using the root CA created above.
# NOTE: This issuer may briefly show "not ready" on first install while cert-manager
# processes the Certificate above and creates the secret. It will auto-reconcile.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: {{ .Values.certManager.caIssuer.name }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: cert-manager
spec:
ca:
secretName: {{ .Values.certManager.rootCA.secretName }}
{{- end }}
@@ -0,0 +1,35 @@
{{- if and .Values.postgresql.enabled .Values.postgresql.tls.enabled }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "sim.fullname" . }}-postgresql-tls-certificate
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
secretName: {{ .Values.postgresql.tls.certificatesSecret }}
duration: {{ .Values.postgresql.tls.duration | default "87600h" }} # Default: 10 years
renewBefore: {{ .Values.postgresql.tls.renewBefore | default "2160h" }} # Default: 90 days before expiry
isCA: false
privateKey:
algorithm: {{ .Values.postgresql.tls.privateKey.algorithm | default "RSA" }}
size: {{ .Values.postgresql.tls.privateKey.size | default 4096 }}
{{- if .Values.postgresql.tls.rotationPolicy }}
rotationPolicy: {{ .Values.postgresql.tls.rotationPolicy }}
{{- end }}
usages:
- server auth
- client auth
dnsNames:
- {{ include "sim.fullname" . }}-postgresql
- {{ include "sim.fullname" . }}-postgresql.{{ .Release.Namespace }}.svc.cluster.local
{{- with .Values.postgresql.tls.additionalDnsNames }}
{{- toYaml . | nindent 2 }}
{{- end }}
issuerRef:
name: {{ .Values.postgresql.tls.issuerRef.name }}
kind: {{ .Values.postgresql.tls.issuerRef.kind | default "ClusterIssuer" }}
{{- if .Values.postgresql.tls.issuerRef.group }}
group: {{ .Values.postgresql.tls.issuerRef.group }}
{{- end }}
{{- end }}
@@ -0,0 +1,25 @@
{{- if and .Values.branding.enabled (or .Values.branding.files .Values.branding.binaryFiles) }}
---
# Branding ConfigMap
# Mounts custom branding assets (logos, CSS, etc.) into the application
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "sim.fullname" . }}-branding
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: branding
{{- if .Values.branding.files }}
data:
{{- range $key, $value := .Values.branding.files }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- if .Values.branding.binaryFiles }}
binaryData:
{{- range $key, $value := .Values.branding.binaryFiles }}
{{ $key }}: {{ $value }}
{{- end }}
{{- end }}
{{- end }}
+98
View File
@@ -0,0 +1,98 @@
{{- if .Values.cronjobs.enabled }}
{{- range $jobKey, $jobConfig := .Values.cronjobs.jobs }}
{{- if $jobConfig.enabled }}
---
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 }}
spec:
schedule: {{ $jobConfig.schedule | quote }}
concurrencyPolicy: {{ $jobConfig.concurrencyPolicy | default "Forbid" }}
successfulJobsHistoryLimit: {{ $jobConfig.successfulJobsHistoryLimit | default 3 }}
failedJobsHistoryLimit: {{ $jobConfig.failedJobsHistoryLimit | default 1 }}
{{- with $.Values.cronjobs.startingDeadlineSeconds }}
startingDeadlineSeconds: {{ . }}
{{- end }}
jobTemplate:
spec:
ttlSecondsAfterFinished: {{ $.Values.cronjobs.ttlSecondsAfterFinished | default 600 }}
{{- with $.Values.cronjobs.activeDeadlineSeconds }}
activeDeadlineSeconds: {{ . }}
{{- end }}
template:
metadata:
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" }}
{{- include "sim.podSecurityContext" $.Values.cronjobs | nindent 10 }}
containers:
- name: {{ $jobConfig.name }}
image: {{ include "sim.image" (dict "imageRoot" $.Values.cronjobs.image "global" $.Values.global "chartAppVersion" $.Chart.AppVersion) }}
imagePullPolicy: {{ $.Values.cronjobs.image.pullPolicy }}
{{- include "sim.containerSecurityContext" $.Values.cronjobs | nindent 12 }}
env:
- name: CRON_SECRET
valueFrom:
secretKeyRef:
name: {{ include "sim.appSecretName" $ }}
key: CRON_SECRET
command:
- /bin/sh
- -c
args:
- |
echo "Starting cron job: {{ $jobConfig.name }}"
echo "Making HTTP request to {{ $jobConfig.path }}"
# Determine the service URL (use internal service regardless of ingress)
SERVICE_URL="http://{{ include "sim.fullname" $ }}-app:{{ $.Values.app.service.port }}"
# Make the HTTP request with timeout and retry logic
for i in $(seq 1 3); do
echo "Attempt $i/3"
if curl -f -s -S --max-time 60 --retry 2 --retry-delay 5 \
-H "Content-Type: application/json" \
-H "User-Agent: Kubernetes-CronJob/{{ $jobConfig.name }}" \
-H "Authorization: Bearer ${CRON_SECRET}" \
"$SERVICE_URL{{ $jobConfig.path }}"; then
echo "Success: HTTP request completed"
exit 0
fi
echo "Attempt $i failed, retrying..."
sleep 10
done
echo "Error: All attempts failed"
exit 1
resources:
{{- toYaml $.Values.cronjobs.resources | nindent 14 }}
{{- with $.Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $.Values.app.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $.Values.affinity }}
affinity:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with $.Values.tolerations }}
tolerations:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+196
View File
@@ -0,0 +1,196 @@
{{- if .Values.app.enabled }}
{{- include "sim.validateSecrets" . }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-app
namespace: {{ .Release.Namespace }}
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 }}
labels:
{{- include "sim.app.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- 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 "imageRoot" .Values.migrations.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.migrations.image.pullPolicy }}
command: ["/bin/sh", "-c"]
args:
- |
cd /app/packages/db
export DATABASE_URL="{{ include "sim.databaseUrl" . }}"
bun run db:migrate
envFrom:
{{- if .Values.postgresql.enabled }}
- secretRef:
name: {{ include "sim.postgresqlSecretName" . }}
{{- else if .Values.externalDatabase.enabled }}
- secretRef:
name: {{ include "sim.externalDbSecretName" . }}
{{- end }}
{{- include "sim.resources" .Values.migrations | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.migrations | nindent 10 }}
{{- end }}
containers:
- name: app
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 }}
- name: SOCKET_SERVER_URL
value: {{ include "sim.socketServerUrl" . | quote }}
- name: OLLAMA_URL
value: {{ include "sim.ollamaUrl" . | quote }}
- name: PII_URL
value: {{ include "sim.piiUrl" . | quote }}
{{- /*
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" "PII_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"
- name: OTEL_SERVICE_NAME
value: sim-app
- name: OTEL_SERVICE_VERSION
value: {{ .Chart.AppVersion | quote }}
- name: OTEL_RESOURCE_ATTRIBUTES
value: "service.name=sim-app,service.version={{ .Chart.AppVersion }},deployment.environment={{ $nodeEnv }}"
{{- end }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
{{- end }}
envFrom:
# App secrets (authentication, encryption keys)
- secretRef:
name: {{ include "sim.appSecretName" . }}
# Database secrets
{{- if .Values.postgresql.enabled }}
- secretRef:
name: {{ include "sim.postgresqlSecretName" . }}
{{- else if .Values.externalDatabase.enabled }}
- 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 }}
{{- end }}
{{- if .Values.app.readinessProbe }}
readinessProbe:
{{- toYaml .Values.app.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .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:
{{- if $hasBranding }}
- name: branding
mountPath: {{ .Values.branding.mountPath | default "/app/public/branding" }}
readOnly: true
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.app.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- $hasBranding := and .Values.branding.enabled (or .Values.branding.files .Values.branding.binaryFiles) }}
{{- if or $hasBranding .Values.extraVolumes .Values.app.extraVolumes }}
volumes:
{{- if $hasBranding }}
- name: branding
configMap:
name: {{ include "sim.fullname" . }}-branding
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.app.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+155
View File
@@ -0,0 +1,155 @@
{{- if .Values.copilot.enabled }}
{{- include "sim.copilot.validate" . }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-copilot
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilot.labels" . | nindent 4 }}
spec:
type: {{ .Values.copilot.server.service.type }}
ports:
- port: {{ .Values.copilot.server.service.port }}
targetPort: {{ .Values.copilot.server.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "sim.copilot.selectorLabels" . | nindent 4 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-copilot
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilot.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.copilot.server.replicaCount }}
selector:
matchLabels:
{{- include "sim.copilot.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- /*
Hash both the inline Secret and the ExternalSecret template — whichever mode is
active, only one renders non-empty content, but concatenating both means a mode
switch or a remoteRefs.copilot mapping change still changes the checksum and
forces a rollout. This can't see the live value ESO syncs from the external
store (Helm only has the ExternalSecret manifest at render time, not the
store's payload) — that's an inherent ESO limitation, not something a Helm
checksum can close; ESO's own reconciliation handles picking up rotated values.
*/}}
checksum/secret: {{ printf "%s%s" (include (print $.Template.BasePath "/secrets-copilot.yaml") .) (include (print $.Template.BasePath "/external-secret-copilot.yaml") .) | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sim.copilot.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- 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 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: copilot
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
containerPort: {{ .Values.copilot.server.service.targetPort }}
protocol: TCP
envFrom:
- secretRef:
name: {{ include "sim.copilot.envSecretName" . }}
- secretRef:
name: {{ include "sim.copilot.databaseSecretName" . }}
{{- with .Values.copilot.server.extraEnvFrom }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- /*
copilot.server.env flows through envFrom (the Secret/ExternalSecret) above.
copilot.server.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 — same rationale as app.envDefaults. Skipped
entirely in existingSecret mode: the pre-created Secret is the source of
truth, and inlining defaults would silently shadow keys (PORT, LOG_LEVEL,
etc.) the user stored there via envFrom, since explicit env wins over
envFrom. Keyed purely on copilot.server.secret.create, NOT the global
externalSecrets.enabled flag — envFrom always points at the user-provided
Secret name whenever secret.create=false, regardless of whether ESO is used
for other components, so the global flag is irrelevant here.
*/}}
{{- $useExistingSecret := not .Values.copilot.server.secret.create }}
{{- if or (not $useExistingSecret) .Values.copilot.server.extraEnv }}
env:
{{- $copilotEnv := .Values.copilot.server.env | default dict }}
{{- if not $useExistingSecret }}
{{- range $key, $value := .Values.copilot.server.envDefaults | default dict }}
{{- $override := index $copilotEnv $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 }}
{{- with .Values.copilot.server.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- 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 }}
{{- end }}
{{- if .Values.copilot.server.readinessProbe }}
readinessProbe:
{{- toYaml .Values.copilot.server.readinessProbe | nindent 12 }}
{{- end }}
{{- with .Values.copilot.server.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.server | nindent 10 }}
{{- if or .Values.extraVolumeMounts .Values.copilot.server.extraVolumeMounts }}
volumeMounts:
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.server.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if or .Values.extraVolumes .Values.copilot.server.extraVolumes }}
volumes:
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.copilot.server.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+125
View File
@@ -0,0 +1,125 @@
{{- if .Values.ollama.enabled }}
---
# PersistentVolumeClaim for Ollama data
{{- if .Values.ollama.persistence.enabled }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "sim.fullname" . }}-ollama-data
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.ollama.labels" . | nindent 4 }}
spec:
{{- if .Values.ollama.persistence.storageClass }}
{{- if (eq "-" .Values.ollama.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.ollama.persistence.storageClass | quote }}
{{- end }}
{{- else if .Values.global.storageClass }}
storageClassName: {{ .Values.global.storageClass | quote }}
{{- end }}
accessModes:
{{- range .Values.ollama.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.ollama.persistence.size | quote }}
{{- end }}
---
# Deployment for Ollama
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-ollama
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.ollama.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.ollama.replicaCount }}
selector:
matchLabels:
{{- include "sim.ollama.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sim.ollama.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- 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 "imageRoot" .Values.ollama.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.ollama.image.pullPolicy }}
command: ["ollama", "serve"]
ports:
- name: http
containerPort: {{ .Values.ollama.service.targetPort }}
protocol: TCP
env:
{{- range $key, $value := .Values.ollama.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.ollama.startupProbe }}
startupProbe:
{{- toYaml .Values.ollama.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.ollama.livenessProbe }}
livenessProbe:
{{- toYaml .Values.ollama.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.ollama.readinessProbe }}
readinessProbe:
{{- 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: /data
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.ollama.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if or .Values.ollama.persistence.enabled .Values.extraVolumes .Values.ollama.extraVolumes }}
volumes:
{{- if .Values.ollama.persistence.enabled }}
- name: ollama-data
persistentVolumeClaim:
claimName: {{ include "sim.fullname" . }}-ollama-data
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.ollama.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+93
View File
@@ -0,0 +1,93 @@
{{- if .Values.pii.enabled }}
---
# Deployment for the Presidio PII redaction service (analyzer + anonymizer combined)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-pii
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.pii.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.pii.replicaCount }}
selector:
matchLabels:
{{- include "sim.pii.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sim.pii.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.pii | nindent 6 }}
{{- include "sim.nodeSelector" .Values.pii | nindent 6 }}
{{- include "sim.tolerations" .Values | nindent 6 }}
{{- include "sim.affinity" .Values | nindent 6 }}
{{- include "sim.topologySpreadConstraints" .Values.pii | nindent 6 }}
containers:
- name: pii
image: {{ include "sim.image" (dict "imageRoot" .Values.pii.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.pii.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.pii.service.targetPort }}
protocol: TCP
env:
- name: PII_ENGINE
value: {{ .Values.pii.engine | quote }}
{{- if .Values.pii.device }}
- name: PII_DEVICE
value: {{ .Values.pii.device | quote }}
{{- end }}
{{- range $key, $value := omit (.Values.pii.env | default dict) "PII_ENGINE" "PII_DEVICE" }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- with .Values.extraEnvVars }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.pii.startupProbe }}
startupProbe:
{{- toYaml .Values.pii.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.pii.livenessProbe }}
livenessProbe:
{{- toYaml .Values.pii.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.pii.readinessProbe }}
readinessProbe:
{{- toYaml .Values.pii.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.pii | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.pii | nindent 10 }}
{{- if or .Values.extraVolumeMounts .Values.pii.extraVolumeMounts }}
volumeMounts:
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.pii.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if or .Values.extraVolumes .Values.pii.extraVolumes }}
volumes:
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.pii.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
+176
View File
@@ -0,0 +1,176 @@
{{- if .Values.realtime.enabled }}
{{- include "sim.validateSecrets" . }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-realtime
namespace: {{ .Release.Namespace }}
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 }}
labels:
{{- include "sim.realtime.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- 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 "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.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"
- name: OTEL_SERVICE_NAME
value: sim-realtime
- name: OTEL_SERVICE_VERSION
value: {{ .Chart.AppVersion | quote }}
- name: OTEL_RESOURCE_ATTRIBUTES
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" "PII_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 }}
{{- end }}
envFrom:
# App secrets (authentication keys shared with main app)
- secretRef:
name: {{ include "sim.appSecretName" . }}
# Database secrets
{{- if .Values.postgresql.enabled }}
- secretRef:
name: {{ include "sim.postgresqlSecretName" . }}
{{- else if .Values.externalDatabase.enabled }}
- 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 }}
{{- end }}
{{- if .Values.realtime.readinessProbe }}
readinessProbe:
{{- toYaml .Values.realtime.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.realtime | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.realtime | nindent 10 }}
{{- if or .Values.extraVolumeMounts .Values.realtime.extraVolumeMounts }}
volumeMounts:
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.realtime.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- end }}
{{- if or .Values.extraVolumes .Values.realtime.extraVolumes }}
volumes:
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.realtime.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,14 @@
{{- if and .Values.externalDatabase.enabled (include "sim.createExternalDbSecret" .) }}
---
# Secret for external database credentials
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sim.fullname" . }}-external-db-secret
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
type: Opaque
data:
EXTERNAL_DB_PASSWORD: {{ .Values.externalDatabase.password | b64enc }}
{{- end }}
@@ -0,0 +1,41 @@
{{- if and .Values.externalSecrets.enabled .Values.app.enabled }}
# ExternalSecret for app credentials (syncs from external secret managers)
#
# 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
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.app.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ required "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" .Values.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" }}
target:
name: {{ include "sim.fullname" . }}-app-secrets
creationPolicy: Owner
data:
{{- range $secretKey, $ref := .Values.externalSecrets.remoteRefs.app }}
{{- if $ref }}
{{- if kindIs "string" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
key: {{ $ref }}
{{- else if kindIs "map" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
{{- toYaml $ref | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,38 @@
{{- if and .Values.externalSecrets.enabled .Values.copilot.enabled .Values.copilot.server.secret.create }}
# ExternalSecret for copilot server credentials (syncs from external secret managers)
#
# Mirrors external-secret-app.yaml: the data list is generated by iterating every
# non-empty entry in externalSecrets.remoteRefs.copilot. sim.copilot.validate fails
# template rendering if a required key (or any non-empty copilot.server.env key) is
# missing a matching remoteRefs.copilot entry, so a misconfiguration surfaces at
# `helm template` time instead of a container starting with an empty value.
apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.copilot.envSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilot.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ required "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" .Values.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" }}
target:
name: {{ include "sim.copilot.envSecretName" . }}
creationPolicy: Owner
data:
{{- range $secretKey, $ref := .Values.externalSecrets.remoteRefs.copilot }}
{{- if $ref }}
{{- if kindIs "string" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
key: {{ $ref }}
{{- else if kindIs "map" $ref }}
- secretKey: {{ $secretKey }}
remoteRef:
{{- toYaml $ref | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,23 @@
{{- 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 "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-external-db-secret
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: external-database
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ required "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" .Values.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" }}
target:
name: {{ include "sim.fullname" . }}-external-db-secret
creationPolicy: Owner
data:
- secretKey: EXTERNAL_DB_PASSWORD
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.externalDatabase.password }}
{{- end }}
@@ -0,0 +1,22 @@
{{- 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 "v1beta1" }}
kind: ExternalSecret
metadata:
name: {{ include "sim.fullname" . }}-postgresql-secret
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
refreshInterval: {{ .Values.externalSecrets.refreshInterval | quote }}
secretStoreRef:
name: {{ required "externalSecrets.secretStoreRef.name is required when externalSecrets.enabled=true" .Values.externalSecrets.secretStoreRef.name }}
kind: {{ .Values.externalSecrets.secretStoreRef.kind | default "ClusterSecretStore" }}
target:
name: {{ include "sim.fullname" . }}-postgresql-secret
creationPolicy: Owner
data:
- secretKey: POSTGRES_PASSWORD
remoteRef:
key: {{ .Values.externalSecrets.remoteRefs.postgresql.password }}
{{- end }}
+115
View File
@@ -0,0 +1,115 @@
{{- if and .Values.ollama.enabled .Values.ollama.gpu.enabled }}
---
# 1. ConfigMap for NVIDIA Device Plugin Configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "sim.fullname" . }}-nvidia-device-plugin-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: nvidia-device-plugin
data:
config.yaml: |
version: v1
flags:
{{- if eq .Values.ollama.gpu.strategy "mig" }}
migStrategy: "single"
{{- else }}
migStrategy: "none"
{{- end }}
failOnInitError: false
plugin:
passDeviceSpecs: true
deviceListStrategy: envvar
{{- if eq .Values.ollama.gpu.strategy "time-slicing" }}
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: {{ .Values.ollama.gpu.timeSlicingReplicas | default 5 }}
{{- end }}
---
# 2. NVIDIA Device Plugin DaemonSet for GPU support
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "sim.fullname" . }}-nvidia-device-plugin
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: nvidia-device-plugin
spec:
selector:
matchLabels:
{{- include "sim.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: nvidia-device-plugin
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
{{- include "sim.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: nvidia-device-plugin
spec:
tolerations:
# Allow scheduling on GPU nodes
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
- key: sku
operator: Equal
value: gpu
effect: NoSchedule
nodeSelector:
# Only schedule on nodes with NVIDIA GPUs
accelerator: nvidia
priorityClassName: system-node-critical
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
- name: dev
hostPath:
path: /dev
- name: sys
hostPath:
path: /sys
# Volume to mount the ConfigMap
- name: nvidia-device-plugin-config
configMap:
name: {{ include "sim.fullname" . }}-nvidia-device-plugin-config
containers:
- name: nvidia-device-plugin
image: nvcr.io/nvidia/k8s-device-plugin:v0.18.2
imagePullPolicy: Always
args:
- "--config-file=/etc/device-plugin/config.yaml"
{{- if eq .Values.ollama.gpu.strategy "mig" }}
env:
- name: NVIDIA_MIG_MONITOR_DEVICES
value: all
{{- end }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: dev
mountPath: /dev
- name: sys
mountPath: /sys
readOnly: true
- name: nvidia-device-plugin-config
mountPath: /etc/device-plugin/
readOnly: true
resources:
requests:
cpu: 50m
memory: 20Mi
limits:
cpu: 50m
memory: 50Mi
{{- end }}
+85
View File
@@ -0,0 +1,85 @@
{{- if .Values.autoscaling.enabled }}
---
# HorizontalPodAutoscaler for main application
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "sim.fullname" . }}-app
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.app.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "sim.fullname" . }}-app
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- with .Values.autoscaling.customMetrics }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.autoscaling.behavior }}
behavior:
{{- toYaml .Values.autoscaling.behavior | nindent 4 }}
{{- end }}
{{- end }}
{{- if and .Values.autoscaling.enabled .Values.realtime.enabled }}
---
# HorizontalPodAutoscaler for realtime service
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "sim.fullname" . }}-realtime
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.realtime.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "sim.fullname" . }}-realtime
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- with .Values.autoscaling.customMetrics }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.autoscaling.behavior }}
behavior:
{{- toYaml .Values.autoscaling.behavior | nindent 4 }}
{{- end }}
{{- end }}
+129
View File
@@ -0,0 +1,129 @@
{{- if .Values.ingressInternal.enabled }}
{{- $appActive := .Values.app.enabled -}}
{{- $realtimeActive := .Values.realtime.enabled -}}
{{- $hasCopilotIngress := and .Values.copilot.enabled .Values.ingressInternal.copilot -}}
{{- $realtimeHasOwnRule := and $realtimeActive (or (not $appActive) (ne .Values.ingressInternal.realtime.host .Values.ingressInternal.app.host)) -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "sim.fullname" . }}-ingress-internal
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
{{- with .Values.ingressInternal.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingressInternal.className }}
ingressClassName: {{ .Values.ingressInternal.className }}
{{- end }}
{{- if .Values.ingressInternal.tls.enabled }}
tls:
- hosts:
{{- if $appActive }}
- {{ .Values.ingressInternal.app.host | quote }}
{{- end }}
{{- if $realtimeHasOwnRule }}
- {{ .Values.ingressInternal.realtime.host | quote }}
{{- end }}
{{- if $hasCopilotIngress }}
{{- $copilotHostCovered := false }}
{{- if and $appActive (eq .Values.ingressInternal.copilot.host .Values.ingressInternal.app.host) }}
{{- $copilotHostCovered = true }}
{{- end }}
{{- if and $realtimeHasOwnRule (eq .Values.ingressInternal.copilot.host .Values.ingressInternal.realtime.host) }}
{{- $copilotHostCovered = true }}
{{- end }}
{{- if not $copilotHostCovered }}
- {{ .Values.ingressInternal.copilot.host | quote }}
{{- end }}
{{- end }}
secretName: {{ .Values.ingressInternal.tls.secretName }}
{{- end }}
rules:
{{- if $appActive }}
- host: {{ .Values.ingressInternal.app.host | quote }}
http:
paths:
{{- if and $realtimeActive (eq .Values.ingressInternal.realtime.host .Values.ingressInternal.app.host) }}
{{- range .Values.ingressInternal.realtime.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-realtime
port:
number: {{ $.Values.realtime.service.port }}
{{- end }}
{{- end }}
{{- if and $hasCopilotIngress (eq .Values.ingressInternal.copilot.host .Values.ingressInternal.app.host) }}
{{- range .Values.ingressInternal.copilot.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-copilot
port:
number: {{ $.Values.copilot.server.service.port }}
{{- end }}
{{- end }}
{{- range .Values.ingressInternal.app.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-app
port:
number: {{ $.Values.app.service.port }}
{{- end }}
{{- end }}
{{- if $realtimeHasOwnRule }}
- host: {{ .Values.ingressInternal.realtime.host | quote }}
http:
paths:
{{- range .Values.ingressInternal.realtime.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-realtime
port:
number: {{ $.Values.realtime.service.port }}
{{- end }}
{{- if and $hasCopilotIngress (eq .Values.ingressInternal.copilot.host .Values.ingressInternal.realtime.host) }}
{{- range .Values.ingressInternal.copilot.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-copilot
port:
number: {{ $.Values.copilot.server.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- if $hasCopilotIngress }}
{{- $copilotServed := false }}
{{- if and $appActive (eq .Values.ingressInternal.copilot.host .Values.ingressInternal.app.host) }}
{{- $copilotServed = true }}
{{- end }}
{{- if and $realtimeHasOwnRule (eq .Values.ingressInternal.copilot.host .Values.ingressInternal.realtime.host) }}
{{- $copilotServed = true }}
{{- end }}
{{- if not $copilotServed }}
- host: {{ .Values.ingressInternal.copilot.host | quote }}
http:
paths:
{{- range .Values.ingressInternal.copilot.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-copilot
port:
number: {{ $.Values.copilot.server.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
+129
View File
@@ -0,0 +1,129 @@
{{- if .Values.ingress.enabled }}
{{- $appActive := .Values.app.enabled -}}
{{- $realtimeActive := .Values.realtime.enabled -}}
{{- $hasCopilotIngress := and .Values.copilot.enabled .Values.ingress.copilot -}}
{{- $realtimeHasOwnRule := and $realtimeActive (or (not $appActive) (ne .Values.ingress.realtime.host .Values.ingress.app.host)) -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "sim.fullname" . }}-ingress
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls.enabled }}
tls:
- hosts:
{{- if $appActive }}
- {{ .Values.ingress.app.host | quote }}
{{- end }}
{{- if $realtimeHasOwnRule }}
- {{ .Values.ingress.realtime.host | quote }}
{{- end }}
{{- if $hasCopilotIngress }}
{{- $copilotHostCovered := false }}
{{- if and $appActive (eq .Values.ingress.copilot.host .Values.ingress.app.host) }}
{{- $copilotHostCovered = true }}
{{- end }}
{{- if and $realtimeHasOwnRule (eq .Values.ingress.copilot.host .Values.ingress.realtime.host) }}
{{- $copilotHostCovered = true }}
{{- end }}
{{- if not $copilotHostCovered }}
- {{ .Values.ingress.copilot.host | quote }}
{{- end }}
{{- end }}
secretName: {{ .Values.ingress.tls.secretName }}
{{- end }}
rules:
{{- if $appActive }}
- host: {{ .Values.ingress.app.host | quote }}
http:
paths:
{{- if and $realtimeActive (eq .Values.ingress.realtime.host .Values.ingress.app.host) }}
{{- range .Values.ingress.realtime.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-realtime
port:
number: {{ $.Values.realtime.service.port }}
{{- end }}
{{- end }}
{{- if and $hasCopilotIngress (eq .Values.ingress.copilot.host .Values.ingress.app.host) }}
{{- range .Values.ingress.copilot.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-copilot
port:
number: {{ $.Values.copilot.server.service.port }}
{{- end }}
{{- end }}
{{- range .Values.ingress.app.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-app
port:
number: {{ $.Values.app.service.port }}
{{- end }}
{{- end }}
{{- if $realtimeHasOwnRule }}
- host: {{ .Values.ingress.realtime.host | quote }}
http:
paths:
{{- range .Values.ingress.realtime.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-realtime
port:
number: {{ $.Values.realtime.service.port }}
{{- end }}
{{- if and $hasCopilotIngress (eq .Values.ingress.copilot.host .Values.ingress.realtime.host) }}
{{- range .Values.ingress.copilot.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-copilot
port:
number: {{ $.Values.copilot.server.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- if $hasCopilotIngress }}
{{- $copilotServed := false }}
{{- if and $appActive (eq .Values.ingress.copilot.host .Values.ingress.app.host) }}
{{- $copilotServed = true }}
{{- end }}
{{- if and $realtimeHasOwnRule (eq .Values.ingress.copilot.host .Values.ingress.realtime.host) }}
{{- $copilotServed = true }}
{{- end }}
{{- if not $copilotServed }}
- host: {{ .Values.ingress.copilot.host | quote }}
http:
paths:
{{- range .Values.ingress.copilot.paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "sim.fullname" $ }}-copilot
port:
number: {{ $.Values.copilot.server.service.port }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,76 @@
{{- if and .Values.copilot.enabled .Values.copilot.migrations.enabled }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "sim.fullname" . }}-copilot-migrations
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: copilot-migrations
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
backoffLimit: {{ .Values.copilot.migrations.backoffLimit }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ include "sim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/component: copilot-migrations
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
restartPolicy: {{ .Values.copilot.migrations.restartPolicy }}
{{- include "sim.podSecurityContext" .Values.copilot.migrations | nindent 6 }}
{{- with .Values.copilot.server.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.copilot.postgresql.enabled }}
initContainers:
- name: wait-for-postgres
image: {{ include "sim.image" (dict "imageRoot" .Values.copilot.postgresql.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
command:
- /bin/sh
- -c
- |
until pg_isready -h {{ include "sim.fullname" . }}-copilot-postgresql -p {{ .Values.copilot.postgresql.service.port }} -U {{ .Values.copilot.postgresql.auth.username }}; do
echo "Waiting for Copilot PostgreSQL to be ready..."
sleep 2
done
echo "Copilot PostgreSQL is ready!"
envFrom:
- secretRef:
name: {{ include "sim.fullname" . }}-copilot-postgresql-secret
{{- include "sim.containerSecurityContext" .Values.copilot.migrations | nindent 10 }}
{{- end }}
containers:
- name: migrations
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:
- secretRef:
name: {{ include "sim.copilot.envSecretName" . }}
- secretRef:
name: {{ include "sim.copilot.databaseSecretName" . }}
{{- with .Values.copilot.server.extraEnvFrom }}
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.server.extraEnv }}
env:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.copilot.migrations.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.migrations | nindent 10 }}
{{- end }}
+437
View File
@@ -0,0 +1,437 @@
{{- if .Values.networkPolicy.enabled }}
---
# Network Policy for main application
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-app
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.app.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow ingress from realtime service
{{- if .Values.realtime.enabled }}
- from:
- podSelector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.app.service.targetPort }}
{{- end }}
# 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:
{{- toYaml (default (list (dict)) .Values.networkPolicy.ingressFrom) | nindent 6 }}
ports:
- protocol: TCP
port: {{ .Values.app.service.targetPort }}
{{- end }}
# Allow custom ingress rules
{{- with .Values.networkPolicy.ingress }}
{{- toYaml . | nindent 2 }}
{{- end }}
egress:
# Allow egress to PostgreSQL
{{- if .Values.postgresql.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "sim.postgresql.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.postgresql.service.targetPort }}
{{- end }}
# Allow egress to realtime service
{{- if .Values.realtime.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.realtime.service.targetPort }}
{{- end }}
# Allow egress to Ollama
{{- if .Values.ollama.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "sim.ollama.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.ollama.service.targetPort }}
{{- end }}
# Allow egress to the PII (Presidio) service
{{- if .Values.pii.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "sim.pii.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.pii.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:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 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.egressExceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
# Allow custom egress rules
{{- with .Values.networkPolicy.egress }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- if .Values.realtime.enabled }}
---
# Network Policy for realtime service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-realtime
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.realtime.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow ingress from main application
- from:
- podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.realtime.service.targetPort }}
# Allow ingress from ingress controller (configurable peers; defaults to any)
{{- if .Values.ingress.enabled }}
- from:
{{- toYaml (default (list (dict)) .Values.networkPolicy.ingressFrom) | nindent 6 }}
ports:
- protocol: TCP
port: {{ .Values.realtime.service.targetPort }}
{{- end }}
egress:
# Allow egress to PostgreSQL
{{- if .Values.postgresql.enabled }}
- to:
- podSelector:
matchLabels:
{{- include "sim.postgresql.selectorLabels" . | nindent 10 }}
ports:
- 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:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 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.egressExceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
# Allow custom egress rules
{{- with .Values.networkPolicy.egress }}
{{- toYaml . | nindent 2 }}
{{- end }}
{{- end }}
{{- if .Values.postgresql.enabled }}
---
# Network Policy for PostgreSQL
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "sim.postgresql.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow ingress from main application
- from:
- podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.postgresql.service.targetPort }}
# Allow ingress from realtime service
{{- if .Values.realtime.enabled }}
- from:
- podSelector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.postgresql.service.targetPort }}
{{- end }}
# Allow ingress from migrations job
{{- if .Values.migrations.enabled }}
- from:
- podSelector:
matchLabels:
{{- include "sim.migrations.labels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.postgresql.service.targetPort }}
{{- end }}
egress:
# Allow minimal egress (for health checks, etc.)
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- end }}
{{- if .Values.ollama.enabled }}
---
# Network Policy for Ollama
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-ollama
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.ollama.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "sim.ollama.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow ingress from main application
- from:
- podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.ollama.service.targetPort }}
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# 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.egressExceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
{{- end }}
{{- if .Values.pii.enabled }}
---
# Network Policy for the PII (Presidio) service
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-pii
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.pii.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels:
{{- include "sim.pii.selectorLabels" . | nindent 6 }}
policyTypes:
- Ingress
- Egress
ingress:
# Allow ingress from main application
- from:
- podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: {{ .Values.pii.service.targetPort }}
egress:
# Allow DNS resolution. Models are baked into the image, so no external egress.
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
{{- end }}
{{- if .Values.telemetry.enabled }}
---
# Network Policy for OpenTelemetry Collector
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "sim.fullname" . }}-otel-collector
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: telemetry
spec:
podSelector:
matchLabels:
{{- include "sim.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: telemetry
policyTypes:
- Ingress
- Egress
ingress:
# OTLP from app
- from:
- podSelector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
# OTLP from realtime
{{- if .Values.realtime.enabled }}
- from:
- podSelector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 10 }}
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
{{- end }}
# OTLP from copilot
{{- if .Values.copilot.enabled }}
- from:
- podSelector:
matchLabels:
{{- include "sim.selectorLabels" . | nindent 10 }}
app.kubernetes.io/component: copilot
ports:
- protocol: TCP
port: 4317
- protocol: TCP
port: 4318
{{- end }}
egress:
# DNS
- to: []
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# HTTPS for forwarding to external observability backends (Datadog, Honeycomb, etc.)
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
{{- range (default (list "169.254.169.254/32" "169.254.170.2/32") .Values.networkPolicy.egressExceptCidrs) }}
- {{ . | quote }}
{{- end }}
ports:
- protocol: TCP
port: 443
{{- end }}
{{- /*
Copilot + copilot-postgresql intentionally do NOT ship dedicated NetworkPolicies.
Copilot requires REDIS_URL (external Redis on a non-443 port), and the chart
cannot know the user's Redis host/port at render time — a default egress rule
would silently block Redis on most installs. Users running networkPolicy.enabled=true
with copilot enabled should add their own NPs (or extend networkPolicy.egress
with the appropriate egress rules).
*/}}
{{- end }}
@@ -0,0 +1,94 @@
{{- /*
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
kind: PodDisruptionBudget
metadata:
name: {{ include "sim.fullname" $ }}-app-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "sim.app.labels" $ | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- else if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- else }}
minAvailable: 1
{{- end }}
{{- if .unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ .unhealthyPodEvictionPolicy }}
{{- end }}
selector:
matchLabels:
{{- include "sim.app.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- $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
kind: PodDisruptionBudget
metadata:
name: {{ include "sim.fullname" $ }}-realtime-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "sim.realtime.labels" $ | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- else if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- else }}
minAvailable: 1
{{- end }}
{{- if .unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ .unhealthyPodEvictionPolicy }}
{{- end }}
selector:
matchLabels:
{{- include "sim.realtime.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
{{- if and .Values.copilot.enabled .Values.copilot.server.podDisruptionBudget.enabled }}
{{- with .Values.copilot.server.podDisruptionBudget }}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "sim.fullname" $ }}-copilot-pdb
namespace: {{ $.Release.Namespace }}
labels:
{{- include "sim.copilot.labels" $ | nindent 4 }}
spec:
{{- if .minAvailable }}
minAvailable: {{ .minAvailable }}
{{- else if .maxUnavailable }}
maxUnavailable: {{ .maxUnavailable }}
{{- else }}
maxUnavailable: 1
{{- end }}
{{- if .unhealthyPodEvictionPolicy }}
unhealthyPodEvictionPolicy: {{ .unhealthyPodEvictionPolicy }}
{{- end }}
selector:
matchLabels:
{{- include "sim.copilot.selectorLabels" $ | nindent 6 }}
{{- end }}
{{- end }}
+51
View File
@@ -0,0 +1,51 @@
{{- if and .Values.app.enabled (include "sim.createAppSecrets" .) }}
{{/*
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, PII_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:
name: {{ include "sim.fullname" . }}-app-secrets
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.app.labels" . | nindent 4 }}
type: Opaque
stringData:
{{- $chartComputed := list "DATABASE_URL" "SOCKET_SERVER_URL" "OLLAMA_URL" "PII_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 }}
{{- end }}
{{- range $key, $value := $appEnv }}
{{- if and (ne (toString $value) "") (ne (toString $value) "<nil>") }}
{{- $_ := set $effective $key $value }}
{{- end }}
{{- end }}
{{- range $key, $value := $effective }}
{{- if not (has $key $chartComputed) }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- end }}
+35
View File
@@ -0,0 +1,35 @@
{{- if and .Values.copilot.enabled .Values.copilot.server.secret.create (not (and .Values.externalSecrets .Values.externalSecrets.enabled)) }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sim.copilot.envSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilot.labels" . | nindent 4 }}
{{- with .Values.copilot.server.secret.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
type: Opaque
stringData:
{{- range $key, $value := .Values.copilot.server.env }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
{{- if and .Values.copilot.enabled (not .Values.copilot.postgresql.enabled) (or (not .Values.copilot.database.existingSecretName) (eq .Values.copilot.database.existingSecretName "")) (ne .Values.copilot.database.url "") }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sim.copilot.databaseSecretName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilot.labels" . | nindent 4 }}
{{- with .Values.copilot.server.secret.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
type: Opaque
stringData:
{{ include "sim.copilot.databaseSecretKey" . }}: {{ required "copilot.database.url is required when using an external database" .Values.copilot.database.url | quote }}
{{- end }}
+14
View File
@@ -0,0 +1,14 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "sim.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: false
{{- end }}
+79
View File
@@ -0,0 +1,79 @@
{{- if .Values.monitoring.serviceMonitor.enabled }}
---
# ServiceMonitor for main application
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "sim.fullname" . }}-app
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.app.labels" . | nindent 4 }}
{{- with .Values.monitoring.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.monitoring.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "sim.app.selectorLabels" . | nindent 6 }}
endpoints:
- port: http
path: {{ .Values.monitoring.serviceMonitor.path }}
interval: {{ .Values.monitoring.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.monitoring.serviceMonitor.scrapeTimeout }}
{{- with .Values.monitoring.serviceMonitor.metricRelabelings }}
metricRelabelings:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.monitoring.serviceMonitor.relabelings }}
relabelings:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- if .Values.monitoring.serviceMonitor.targetLabels }}
targetLabels:
{{- toYaml .Values.monitoring.serviceMonitor.targetLabels | nindent 4 }}
{{- end }}
{{- end }}
{{- if and .Values.monitoring.serviceMonitor.enabled .Values.realtime.enabled }}
---
# ServiceMonitor for realtime service
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "sim.fullname" . }}-realtime
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.realtime.labels" . | nindent 4 }}
{{- with .Values.monitoring.serviceMonitor.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.monitoring.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "sim.realtime.selectorLabels" . | nindent 6 }}
endpoints:
- port: http
path: {{ .Values.monitoring.serviceMonitor.path }}
interval: {{ .Values.monitoring.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.monitoring.serviceMonitor.scrapeTimeout }}
{{- with .Values.monitoring.serviceMonitor.metricRelabelings }}
metricRelabelings:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.monitoring.serviceMonitor.relabelings }}
relabelings:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- if .Values.monitoring.serviceMonitor.targetLabels }}
targetLabels:
{{- toYaml .Values.monitoring.serviceMonitor.targetLabels | nindent 4 }}
{{- end }}
{{- end }}
+123
View File
@@ -0,0 +1,123 @@
{{- if .Values.app.enabled }}
---
# Service for main application
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-app
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.app.labels" . | nindent 4 }}
spec:
type: {{ .Values.app.service.type }}
ports:
- port: {{ .Values.app.service.port }}
targetPort: {{ .Values.app.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "sim.app.selectorLabels" . | nindent 4 }}
{{- end }}
{{- if .Values.realtime.enabled }}
---
# Service for realtime server
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-realtime
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.realtime.labels" . | nindent 4 }}
spec:
type: {{ .Values.realtime.service.type }}
ports:
- port: {{ .Values.realtime.service.port }}
targetPort: {{ .Values.realtime.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "sim.realtime.selectorLabels" . | nindent 4 }}
{{- end }}
{{- if .Values.postgresql.enabled }}
---
# Service for PostgreSQL (ClusterIP — used by client workloads)
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
type: {{ .Values.postgresql.service.type }}
ports:
- port: {{ .Values.postgresql.service.port }}
targetPort: {{ .Values.postgresql.service.targetPort }}
protocol: TCP
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 }}
---
# Service for Ollama
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-ollama
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.ollama.labels" . | nindent 4 }}
spec:
type: {{ .Values.ollama.service.type }}
ports:
- port: {{ .Values.ollama.service.port }}
targetPort: {{ .Values.ollama.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "sim.ollama.selectorLabels" . | nindent 4 }}
{{- end }}
{{- if .Values.pii.enabled }}
---
# Service fronting the Presidio PII redaction deployment
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-pii
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.pii.labels" . | nindent 4 }}
spec:
type: {{ .Values.pii.service.type }}
ports:
- port: {{ .Values.pii.service.port }}
targetPort: {{ .Values.pii.service.targetPort }}
protocol: TCP
name: http
selector:
{{- include "sim.pii.selectorLabels" . | nindent 4 }}
{{- end }}
+48
View File
@@ -0,0 +1,48 @@
{{- if .Values.sharedStorage.enabled }}
{{- range .Values.sharedStorage.volumes }}
---
# Shared Storage PVC for {{ .name }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ include "sim.fullname" $ }}-{{ .name }}
namespace: {{ $.Release.Namespace }}
labels:
{{- include "sim.labels" $ | nindent 4 }}
sim.ai/volume-type: shared-storage
sim.ai/volume-name: {{ .name }}
{{- with .annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .storageClass }}
{{- if (eq "-" .storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .storageClass | quote }}
{{- end }}
{{- else if $.Values.sharedStorage.storageClass }}
storageClassName: {{ $.Values.sharedStorage.storageClass | quote }}
{{- else if $.Values.global.storageClass }}
storageClassName: {{ $.Values.global.storageClass | quote }}
{{- end }}
accessModes:
{{- if .accessModes }}
{{- range .accessModes }}
- {{ . | quote }}
{{- end }}
{{- else }}
{{- range $.Values.sharedStorage.defaultAccessModes }}
- {{ . | quote }}
{{- end }}
{{- end }}
resources:
requests:
storage: {{ .size | quote }}
{{- if .selector }}
selector:
{{- toYaml .selector | nindent 4 }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,154 @@
{{- if and .Values.copilot.enabled .Values.copilot.postgresql.enabled }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sim.fullname" . }}-copilot-postgresql-secret
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilotPostgresql.labels" . | nindent 4 }}
type: Opaque
stringData:
POSTGRES_USER: {{ .Values.copilot.postgresql.auth.username | quote }}
POSTGRES_PASSWORD: {{ required "copilot.postgresql.auth.password is required when copilot is enabled" .Values.copilot.postgresql.auth.password | quote }}
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:
name: {{ include "sim.fullname" . }}-copilot-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilotPostgresql.labels" . | nindent 4 }}
spec:
type: {{ .Values.copilot.postgresql.service.type }}
ports:
- port: {{ .Values.copilot.postgresql.service.port }}
targetPort: {{ .Values.copilot.postgresql.service.targetPort }}
protocol: TCP
name: postgresql
selector:
{{- include "sim.copilotPostgresql.selectorLabels" . | nindent 4 }}
---
# 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.copilotPostgresql.labels" . | nindent 4 }}
spec:
clusterIP: None
publishNotReadyAddresses: true
ports:
- port: {{ .Values.copilot.postgresql.service.port }}
targetPort: {{ .Values.copilot.postgresql.service.targetPort }}
protocol: TCP
name: postgresql
selector:
{{- include "sim.copilotPostgresql.selectorLabels" . | nindent 4 }}
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "sim.fullname" . }}-copilot-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.copilotPostgresql.labels" . | nindent 4 }}
spec:
# Must remain {{ include "sim.fullname" . }}-copilot-postgresql (not the
# -headless name) — spec.serviceName is immutable on a StatefulSet, and
# the prior chart shipped with this value. Same rationale as the main
# postgresql STS; see statefulset-postgresql.yaml for details.
serviceName: {{ include "sim.fullname" . }}-copilot-postgresql
replicas: 1
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
{{- include "sim.copilotPostgresql.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "sim.copilotPostgresql.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- with .Values.copilot.postgresql.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- include "sim.podSecurityContext" .Values.copilot.postgresql | nindent 6 }}
containers:
- name: postgresql
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
containerPort: {{ .Values.copilot.postgresql.service.targetPort }}
protocol: TCP
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
envFrom:
- secretRef:
name: {{ include "sim.fullname" . }}-copilot-postgresql-secret
{{- if .Values.copilot.postgresql.startupProbe }}
startupProbe:
{{- toYaml .Values.copilot.postgresql.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.copilot.postgresql.livenessProbe }}
livenessProbe:
{{- toYaml .Values.copilot.postgresql.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.copilot.postgresql.readinessProbe }}
readinessProbe:
{{- toYaml .Values.copilot.postgresql.readinessProbe | nindent 12 }}
{{- end }}
{{- with .Values.copilot.postgresql.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- include "sim.containerSecurityContext" .Values.copilot.postgresql | nindent 10 }}
{{- if .Values.copilot.postgresql.persistence.enabled }}
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
subPath: pgdata
{{- end }}
{{- if .Values.copilot.postgresql.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: data
labels:
{{- include "sim.copilotPostgresql.labels" . | nindent 10 }}
spec:
accessModes:
{{- range .Values.copilot.postgresql.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
{{- if .Values.copilot.postgresql.persistence.storageClass }}
{{- if (eq "-" .Values.copilot.postgresql.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.copilot.postgresql.persistence.storageClass | quote }}
{{- end }}
{{- else if .Values.global.storageClass }}
storageClassName: {{ .Values.global.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.copilot.postgresql.persistence.size | quote }}
{{- end }}
{{- end }}
@@ -0,0 +1,210 @@
{{- if .Values.postgresql.enabled }}
---
# ConfigMap for PostgreSQL configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "sim.fullname" . }}-postgresql-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
data:
postgresql.conf: |
hba_file = '/etc/postgresql/pg_hba.conf'
listen_addresses = '0.0.0.0'
max_connections = {{ .Values.postgresql.config.maxConnections }}
tcp_keepalives_idle = 60
tcp_keepalives_interval = 5
tcp_keepalives_count = 3
authentication_timeout = 1min
password_encryption = scram-sha-256
{{- if .Values.postgresql.tls.enabled }}
ssl = on
ssl_cert_file = '/etc/postgresql/tls/tls.crt'
ssl_key_file = '/etc/postgresql/tls/tls.key'
{{- else }}
ssl = off
{{- end }}
shared_buffers = {{ .Values.postgresql.config.sharedBuffers }}
dynamic_shared_memory_type = posix
max_wal_size = {{ .Values.postgresql.config.maxWalSize }}
min_wal_size = {{ .Values.postgresql.config.minWalSize }}
log_timezone = 'Etc/UTC'
idle_in_transaction_session_timeout = 50000000
datestyle = 'iso, mdy'
timezone = 'Etc/UTC'
lc_messages = 'en_US.utf8'
lc_monetary = 'en_US.utf8'
lc_numeric = 'en_US.utf8'
lc_time = 'en_US.utf8'
default_text_search_config = 'pg_catalog.english'
pg_hba.conf: |
# Secure authentication for all connections
local all all scram-sha-256
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
host all all all scram-sha-256
# Replication connections also require authentication
local replication all scram-sha-256
host replication all 127.0.0.1/32 scram-sha-256
host replication all ::1/128 scram-sha-256
---
# ConfigMap for PostgreSQL environment variables
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "sim.fullname" . }}-postgresql-env
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
data:
POSTGRES_DB: {{ .Values.postgresql.auth.database | quote }}
POSTGRES_USER: {{ .Values.postgresql.auth.username | quote }}
PGDATA: "/var/lib/postgresql/data/pgdata"
{{- if (include "sim.createPostgresqlSecret" .) }}
---
# Secret for PostgreSQL password
apiVersion: v1
kind: Secret
metadata:
name: {{ include "sim.fullname" . }}-postgresql-secret
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
type: Opaque
data:
POSTGRES_PASSWORD: {{ .Values.postgresql.auth.password | b64enc }}
{{- end }}
---
# StatefulSet for PostgreSQL
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "sim.fullname" . }}-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.postgresql.labels" . | nindent 4 }}
spec:
# Must remain {{ include "sim.fullname" . }}-postgresql (not the -headless
# name) — spec.serviceName is immutable on a StatefulSet, and the prior
# chart shipped with this value. Changing it would break `helm upgrade` for
# every existing install with `Forbidden: updates to statefulset spec ...`.
# The headless Service in services.yaml is added alongside, not as a swap.
serviceName: {{ include "sim.fullname" . }}-postgresql
replicas: 1
minReadySeconds: 10
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
selector:
matchLabels:
{{- include "sim.postgresql.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "sim.postgresql.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- 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 "imageRoot" .Values.postgresql.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.postgresql.image.pullPolicy }}
args: ["-c", "config_file=/etc/postgresql/postgresql.conf"]
ports:
- name: postgresql
containerPort: {{ .Values.postgresql.service.targetPort }}
protocol: TCP
envFrom:
- configMapRef:
name: {{ include "sim.fullname" . }}-postgresql-env
- secretRef:
name: {{ include "sim.postgresqlSecretName" . }}
{{- if .Values.postgresql.startupProbe }}
startupProbe:
{{- toYaml .Values.postgresql.startupProbe | nindent 12 }}
{{- end }}
{{- if .Values.postgresql.livenessProbe }}
livenessProbe:
{{- toYaml .Values.postgresql.livenessProbe | nindent 12 }}
{{- end }}
{{- if .Values.postgresql.readinessProbe }}
readinessProbe:
{{- toYaml .Values.postgresql.readinessProbe | nindent 12 }}
{{- end }}
{{- include "sim.resources" .Values.postgresql | nindent 10 }}
{{- include "sim.containerSecurityContext" .Values.postgresql | nindent 10 }}
volumeMounts:
{{- if .Values.postgresql.persistence.enabled }}
- name: postgresql-data
mountPath: /var/lib/postgresql/data
subPath: pgdata
{{- end }}
- name: postgresql-config
mountPath: "/etc/postgresql"
{{- if .Values.postgresql.tls.enabled }}
- name: postgresql-tls
mountPath: "/etc/postgresql/tls"
readOnly: true
{{- end }}
{{- with .Values.extraVolumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: postgresql-config
configMap:
name: {{ include "sim.fullname" . }}-postgresql-config
{{- if .Values.postgresql.tls.enabled }}
- name: postgresql-tls
secret:
secretName: {{ .Values.postgresql.tls.certificatesSecret }}
defaultMode: 0600
{{- end }}
{{- with .Values.extraVolumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.postgresql.persistence.enabled }}
volumeClaimTemplates:
- metadata:
name: postgresql-data
labels:
{{- include "sim.postgresql.labels" . | nindent 10 }}
spec:
{{- if .Values.postgresql.persistence.storageClass }}
{{- if (eq "-" .Values.postgresql.persistence.storageClass) }}
storageClassName: ""
{{- else }}
storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }}
{{- end }}
{{- else if .Values.global.storageClass }}
storageClassName: {{ .Values.global.storageClass | quote }}
{{- end }}
accessModes:
{{- range .Values.postgresql.persistence.accessModes }}
- {{ . | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgresql.persistence.size | quote }}
{{- end }}
{{- end }}
+223
View File
@@ -0,0 +1,223 @@
{{- if .Values.telemetry.enabled }}
---
# OpenTelemetry Collector Configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "sim.fullname" . }}-otel-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: telemetry
data:
otel-config.yaml: |
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
prometheus:
config:
scrape_configs:
- job_name: 'sim-app'
static_configs:
- targets: ['{{ include "sim.fullname" . }}-app:{{ .Values.app.service.port }}']
- job_name: 'sim-realtime'
static_configs:
- targets: ['{{ include "sim.fullname" . }}-realtime:{{ .Values.realtime.service.port }}']
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
limit_mib: 512
exporters:
{{- if .Values.telemetry.jaeger.enabled }}
jaeger:
endpoint: {{ .Values.telemetry.jaeger.endpoint }}
tls:
insecure: {{ not .Values.telemetry.jaeger.tls.enabled }}
{{- end }}
{{- if .Values.telemetry.prometheus.enabled }}
prometheusremotewrite:
endpoint: {{ .Values.telemetry.prometheus.endpoint }}
headers:
Authorization: {{ .Values.telemetry.prometheus.auth | quote }}
{{- end }}
{{- if .Values.telemetry.otlp.enabled }}
otlp:
endpoint: {{ .Values.telemetry.otlp.endpoint }}
tls:
insecure: {{ not .Values.telemetry.otlp.tls.enabled }}
{{- end }}
logging:
loglevel: info
extensions:
health_check:
endpoint: 0.0.0.0:13133
pprof:
endpoint: 0.0.0.0:1777
zpages:
endpoint: 0.0.0.0:55679
service:
extensions: [health_check, pprof, zpages]
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters:
- logging
{{- if .Values.telemetry.jaeger.enabled }}
- jaeger
{{- end }}
{{- if .Values.telemetry.otlp.enabled }}
- otlp
{{- end }}
metrics:
receivers: [otlp, prometheus]
processors: [memory_limiter, batch]
exporters:
- logging
{{- if .Values.telemetry.prometheus.enabled }}
- prometheusremotewrite
{{- end }}
{{- if .Values.telemetry.otlp.enabled }}
- otlp
{{- end }}
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters:
- logging
{{- if .Values.telemetry.otlp.enabled }}
- otlp
{{- end }}
---
# OpenTelemetry Collector Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "sim.fullname" . }}-otel-collector
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: telemetry
spec:
replicas: {{ .Values.telemetry.replicaCount }}
selector:
matchLabels:
{{- include "sim.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: telemetry
template:
metadata:
labels:
{{- include "sim.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: telemetry
spec:
{{- with .Values.global.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "sim.serviceAccountName" . }}
automountServiceAccountToken: false
{{- include "sim.podSecurityContext" .Values.telemetry | nindent 6 }}
containers:
- name: otel-collector
image: {{ include "sim.image" (dict "imageRoot" .Values.telemetry.image "global" .Values.global "chartAppVersion" .Chart.AppVersion) }}
imagePullPolicy: {{ .Values.telemetry.image.pullPolicy }}
command:
- /otelcol-contrib
- --config=/etc/otel-collector-config/otel-config.yaml
ports:
- name: otlp-grpc
containerPort: 4317
protocol: TCP
- name: otlp-http
containerPort: 4318
protocol: TCP
- name: health
containerPort: 13133
protocol: TCP
- name: pprof
containerPort: 1777
protocol: TCP
- name: zpages
containerPort: 55679
protocol: TCP
env:
- name: GOGC
value: "80"
volumeMounts:
- name: otel-config
mountPath: /etc/otel-collector-config
readOnly: true
livenessProbe:
httpGet:
path: /
port: health
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: health
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
{{- toYaml .Values.telemetry.resources | nindent 12 }}
{{- include "sim.containerSecurityContext" .Values.telemetry | nindent 10 }}
volumes:
- name: otel-config
configMap:
name: {{ include "sim.fullname" . }}-otel-config
{{- with .Values.telemetry.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.telemetry.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.telemetry.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
---
# OpenTelemetry Collector Service
apiVersion: v1
kind: Service
metadata:
name: {{ include "sim.fullname" . }}-otel-collector
namespace: {{ .Release.Namespace }}
labels:
{{- include "sim.labels" . | nindent 4 }}
app.kubernetes.io/component: telemetry
spec:
type: {{ .Values.telemetry.service.type }}
ports:
- name: otlp-grpc
port: 4317
targetPort: otlp-grpc
protocol: TCP
- name: otlp-http
port: 4318
targetPort: otlp-http
protocol: TCP
- name: health
port: 13133
targetPort: health
protocol: TCP
selector:
{{- include "sim.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: telemetry
{{- end }}
@@ -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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
app.env.DATABASE_URL: "should-be-ignored"
postgresql.auth.password: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content: { name: SOCKET_SERVER_URL, value: "https://evil-2.example.com" }
@@ -0,0 +1,349 @@
suite: copilot secret modes — inline / existingSecret / ESO routing (best-practices fix regression net)
release:
name: t
namespace: sim
tests:
- it: checksum/secret changes when an ESO remoteRefs.copilot mapping changes, forcing a rollout (Cursor round 4 finding)
template: deployment-copilot.yaml
documentSelector:
path: kind
value: Deployment
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
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
externalSecrets.remoteRefs.copilot.AGENT_API_DB_ENCRYPTION_KEY: path/to/agentdbkey
externalSecrets.remoteRefs.copilot.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.copilot.LICENSE_KEY: path/to/license-v1
externalSecrets.remoteRefs.copilot.SIM_BASE_URL: path/to/baseurl
externalSecrets.remoteRefs.copilot.SIM_AGENT_API_KEY: path/to/agentkey
externalSecrets.remoteRefs.copilot.REDIS_URL: path/to/redis
externalSecrets.remoteRefs.copilot.OPENAI_API_KEY_1: path/to/openai
asserts:
- isNotEmpty:
path: spec.template.metadata.annotations["checksum/secret"]
- matchRegex:
path: spec.template.metadata.annotations["checksum/secret"]
pattern: "^[a-f0-9]{64}$"
- it: ESO mode with only the required copilot secrets mapped renders cleanly (envDefaults regression net — Greptile round 1 finding)
template: deployment-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
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
# Only the genuinely-secret keys are mapped — PORT/SERVICE_NAME/ENVIRONMENT/LOG_LEVEL
# live in copilot.server.envDefaults now, not copilot.server.env, so they must NOT
# need a remoteRefs.copilot entry.
externalSecrets.remoteRefs.copilot.AGENT_API_DB_ENCRYPTION_KEY: path/to/agentdbkey
externalSecrets.remoteRefs.copilot.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.copilot.LICENSE_KEY: path/to/license
externalSecrets.remoteRefs.copilot.SIM_BASE_URL: path/to/baseurl
externalSecrets.remoteRefs.copilot.SIM_AGENT_API_KEY: path/to/agentkey
externalSecrets.remoteRefs.copilot.REDIS_URL: path/to/redis
externalSecrets.remoteRefs.copilot.OPENAI_API_KEY_1: path/to/openai
documentSelector:
path: kind
value: Deployment
asserts:
- isKind: { of: Deployment }
- contains:
path: spec.template.spec.containers[0].env
content:
name: PORT
value: "8080"
- contains:
path: spec.template.spec.containers[0].env
content:
name: SERVICE_NAME
value: copilot
- it: inline mode renders the chart-managed copilot Secret
template: secrets-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: true
copilot.postgresql.auth.password: xxxxxxxx
copilot.server.env.AGENT_API_DB_ENCRYPTION_KEY: x
copilot.server.env.INTERNAL_API_SECRET: x
copilot.server.env.LICENSE_KEY: x
copilot.server.env.SIM_BASE_URL: x
copilot.server.env.SIM_AGENT_API_KEY: x
copilot.server.env.REDIS_URL: x
copilot.server.env.OPENAI_API_KEY_1: x
asserts:
- hasDocuments: { count: 1 }
- isKind: { of: Secret }
- equal: { path: metadata.name, value: t-sim-copilot-env }
- it: existingSecret mode does not shadow the pre-created Secret's values with envDefaults (Greptile + Cursor round 2 finding)
template: deployment-copilot.yaml
documentSelector:
path: kind
value: Deployment
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
copilot.server.secret.create: false
copilot.server.secret.name: my-existing-copilot-secret
asserts:
- notExists:
path: spec.template.spec.containers[0].env
- it: existingSecret mode still renders extraEnv even though envDefaults are skipped
template: deployment-copilot.yaml
documentSelector:
path: kind
value: Deployment
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
copilot.server.secret.create: false
copilot.server.secret.name: my-existing-copilot-secret
copilot.server.extraEnv:
- name: CUSTOM_VAR
value: custom-value
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: CUSTOM_VAR
value: custom-value
- notContains:
path: spec.template.spec.containers[0].env
content:
name: PORT
value: "8080"
- it: copilot's own existingSecret is still the source of truth even when externalSecrets.enabled=true globally for other components (Greptile round 3 finding)
template: deployment-copilot.yaml
documentSelector:
path: kind
value: Deployment
set:
app.env.BETTER_AUTH_SECRET: ""
app.env.ENCRYPTION_KEY: ""
app.env.INTERNAL_API_SECRET: ""
app.env.CRON_SECRET: ""
postgresql.auth.password: ""
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
copilot.server.secret.create: false
copilot.server.secret.name: my-existing-copilot-secret
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
asserts:
- notExists:
path: spec.template.spec.containers[0].env
- equal:
path: spec.template.spec.containers[0].envFrom[0].secretRef.name
value: my-existing-copilot-secret
- it: existingSecret mode skips the chart-managed copilot Secret and the ExternalSecret
templates:
- secrets-copilot.yaml
- external-secret-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: true
copilot.postgresql.auth.password: xxxxxxxx
copilot.server.secret.create: false
copilot.server.secret.name: my-existing-copilot-secret
asserts:
- hasDocuments: { count: 0 }
- it: ESO mode renders an ExternalSecret instead of the chart-managed copilot Secret
template: external-secret-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
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
externalSecrets.remoteRefs.copilot.PORT: path/to/port
externalSecrets.remoteRefs.copilot.SERVICE_NAME: path/to/name
externalSecrets.remoteRefs.copilot.ENVIRONMENT: path/to/env
externalSecrets.remoteRefs.copilot.LOG_LEVEL: path/to/log
externalSecrets.remoteRefs.copilot.AGENT_API_DB_ENCRYPTION_KEY: path/to/agentdbkey
externalSecrets.remoteRefs.copilot.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.copilot.LICENSE_KEY: path/to/license
externalSecrets.remoteRefs.copilot.SIM_BASE_URL: path/to/baseurl
externalSecrets.remoteRefs.copilot.SIM_AGENT_API_KEY: path/to/agentkey
externalSecrets.remoteRefs.copilot.REDIS_URL: path/to/redis
externalSecrets.remoteRefs.copilot.OPENAI_API_KEY_1: path/to/openai
asserts:
- isKind: { of: ExternalSecret }
- equal: { path: metadata.name, value: t-sim-copilot-env }
- equal: { path: spec.secretStoreRef.name, value: sim-store }
- it: ESO mode skips the chart-managed copilot Secret
template: secrets-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: true
copilot.postgresql.auth.password: xxxxxxxx
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
externalSecrets.remoteRefs.copilot.PORT: path/to/port
externalSecrets.remoteRefs.copilot.SERVICE_NAME: path/to/name
externalSecrets.remoteRefs.copilot.ENVIRONMENT: path/to/env
externalSecrets.remoteRefs.copilot.LOG_LEVEL: path/to/log
externalSecrets.remoteRefs.copilot.AGENT_API_DB_ENCRYPTION_KEY: path/to/agentdbkey
externalSecrets.remoteRefs.copilot.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.copilot.LICENSE_KEY: path/to/license
externalSecrets.remoteRefs.copilot.SIM_BASE_URL: path/to/baseurl
externalSecrets.remoteRefs.copilot.SIM_AGENT_API_KEY: path/to/agentkey
externalSecrets.remoteRefs.copilot.REDIS_URL: path/to/redis
externalSecrets.remoteRefs.copilot.OPENAI_API_KEY_1: path/to/openai
asserts:
- hasDocuments: { count: 0 }
- it: ESO validator fails when a required copilot key is neither in env nor mapped
template: deployment-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
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
asserts:
- failedTemplate:
errorPattern: "Required key 'AGENT_API_DB_ENCRYPTION_KEY' is missing"
- it: ESO validator fails when copilot.server.env contains an unmapped key
template: deployment-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
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
externalSecrets.remoteRefs.copilot.PORT: path/to/port
externalSecrets.remoteRefs.copilot.SERVICE_NAME: path/to/name
externalSecrets.remoteRefs.copilot.ENVIRONMENT: path/to/env
externalSecrets.remoteRefs.copilot.LOG_LEVEL: path/to/log
externalSecrets.remoteRefs.copilot.AGENT_API_DB_ENCRYPTION_KEY: path/to/agentdbkey
externalSecrets.remoteRefs.copilot.INTERNAL_API_SECRET: path/to/iapi
externalSecrets.remoteRefs.copilot.LICENSE_KEY: path/to/license
externalSecrets.remoteRefs.copilot.SIM_BASE_URL: path/to/baseurl
externalSecrets.remoteRefs.copilot.SIM_AGENT_API_KEY: path/to/agentkey
externalSecrets.remoteRefs.copilot.REDIS_URL: path/to/redis
externalSecrets.remoteRefs.copilot.OPENAI_API_KEY_1: path/to/openai
copilot.server.env.UNMAPPED_KEY: "would-go-nowhere"
asserts:
- failedTemplate:
errorPattern: "Key 'UNMAPPED_KEY' is set in copilot.server.env but externalSecrets.enabled=true"
- it: non-ESO mode still requires OPENAI_API_KEY_1 or ANTHROPIC_API_KEY_1 directly in env
template: deployment-copilot.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
copilot.enabled: true
copilot.postgresql.enabled: false
copilot.database.url: "postgresql://x:x@x:5432/x"
copilot.server.env.AGENT_API_DB_ENCRYPTION_KEY: x
copilot.server.env.INTERNAL_API_SECRET: x
copilot.server.env.LICENSE_KEY: x
copilot.server.env.SIM_BASE_URL: x
copilot.server.env.SIM_AGENT_API_KEY: x
copilot.server.env.REDIS_URL: x
asserts:
- failedTemplate:
errorPattern: "Set at least one of copilot.server.env.OPENAI_API_KEY_1 or copilot.server.env.ANTHROPIC_API_KEY_1"
+101
View File
@@ -0,0 +1,101 @@
suite: envDefaults — secret-mode-aware inlining (round-9 regression net)
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
tests:
- it: 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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: NEXT_PUBLIC_APP_URL
value: "https://realtime.example.com"
+54
View File
@@ -0,0 +1,54 @@
suite: helm test hook (helm test) connectivity probe
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
tests:
- it: 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
+178
View File
@@ -0,0 +1,178 @@
suite: NetworkPolicy — ingressFrom + telemetry egress + egress shape
templates:
- networkpolicy.yaml
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
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: telemetry collector NetworkPolicy renders when telemetry.enabled=true
set:
<<: *defaults
telemetry.enabled: true
documentIndex: 3
asserts:
- equal:
path: kind
value: NetworkPolicy
- equal:
path: metadata.name
value: t-sim-otel-collector
- it: networkPolicy.egress (custom rules) are appended to the app NetworkPolicy
set:
<<: *defaults
networkPolicy.egress:
- to: []
ports:
- protocol: TCP
port: 5432
documentIndex: 0
asserts:
- contains:
path: spec.egress
content:
to: []
ports:
- protocol: TCP
port: 5432
- it: networkPolicy.egress (custom rules) are appended to the realtime NetworkPolicy
set:
<<: *defaults
networkPolicy.egress:
- to: []
ports:
- protocol: TCP
port: 5432
documentIndex: 1
asserts:
- contains:
path: spec.egress
content:
to: []
ports:
- protocol: TCP
port: 5432
+72
View File
@@ -0,0 +1,72 @@
suite: PDB tri-state and HPA conditional rendering
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
tests:
- it: 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 }
+185
View File
@@ -0,0 +1,185 @@
suite: pii — optional Presidio service + PII_URL wiring
release:
name: t
namespace: sim
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
tests:
- it: does not render the pii deployment when disabled
template: deployment-pii.yaml
asserts:
- hasDocuments: { count: 0 }
- it: renders the pii deployment + service when enabled
set:
pii.enabled: true
templates:
- deployment-pii.yaml
- services.yaml
asserts:
- template: deployment-pii.yaml
isKind: { of: Deployment }
- template: deployment-pii.yaml
equal: { path: metadata.name, value: t-sim-pii }
- template: deployment-pii.yaml
equal:
path: spec.template.spec.containers[0].ports[0].containerPort
value: 5001
- it: pii pod defaults to the spacy engine
template: deployment-pii.yaml
set:
pii.enabled: true
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_ENGINE
value: "spacy"
- notContains:
path: spec.template.spec.containers[0].env
content:
name: PII_DEVICE
value: "cpu"
- it: pii.engine and pii.device render through to the container env
template: deployment-pii.yaml
set:
pii.enabled: true
pii.engine: gliner
pii.device: cuda
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_ENGINE
value: "gliner"
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_DEVICE
value: "cuda"
- it: user-set pii.env cannot override the chart-owned engine keys
template: deployment-pii.yaml
set:
pii.enabled: true
pii.env:
PII_ENGINE: evil
PII_DEVICE: evil
PII_GLINER_MODEL: acme/custom-model
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: PII_ENGINE
value: "evil"
- notContains:
path: spec.template.spec.containers[0].env
content:
name: PII_DEVICE
value: "evil"
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_GLINER_MODEL
value: "acme/custom-model"
- it: app pod gets chart-computed PII_URL pointing at the in-cluster service
template: deployment-app.yaml
set:
pii.enabled: true
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_URL
value: "http://t-sim-pii:5001"
- it: app pod gets the localhost PII_URL fallback when service disabled
template: deployment-app.yaml
asserts:
- contains:
path: spec.template.spec.containers[0].env
content:
name: PII_URL
value: "http://localhost:5001"
- it: PII_URL is excluded from the chart-managed app secret
template: secrets-app.yaml
set:
pii.enabled: true
app.env.PII_URL: "http://should-not-leak:5001"
asserts:
- notExists:
path: stringData.PII_URL
- it: user-set PII_URL never overrides the chart-computed inline value
template: deployment-app.yaml
set:
pii.enabled: true
app.env.PII_URL: "http://evil-pii:5001"
asserts:
- notContains:
path: spec.template.spec.containers[0].env
content:
name: PII_URL
value: "http://evil-pii:5001"
- it: app NetworkPolicy allows egress to the PII service
template: networkpolicy.yaml
set:
networkPolicy.enabled: true
pii.enabled: true
documentSelector:
path: metadata.name
value: t-sim-app
asserts:
- contains:
path: spec.egress
content:
to:
- podSelector:
matchLabels:
app.kubernetes.io/name: sim
app.kubernetes.io/instance: t
app.kubernetes.io/component: pii
ports:
- protocol: TCP
port: 5001
- it: renders a dedicated NetworkPolicy for the PII service
template: networkpolicy.yaml
set:
networkPolicy.enabled: true
pii.enabled: true
documentSelector:
path: metadata.name
value: t-sim-pii
asserts:
- isKind: { of: NetworkPolicy }
- equal:
path: spec.podSelector.matchLabels["app.kubernetes.io/component"]
value: pii
- it: pii pod inherits global tolerations (not component-scoped)
template: deployment-pii.yaml
set:
pii.enabled: true
tolerations:
- key: dedicated
operator: Equal
value: pii
effect: NoSchedule
asserts:
- contains:
path: spec.template.spec.tolerations
content:
key: dedicated
operator: Equal
value: pii
effect: NoSchedule
+86
View File
@@ -0,0 +1,86 @@
suite: pod rollout — checksum annotations + topology spread
release:
name: t
namespace: sim
defaults: &defaults
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
tests:
- it: 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: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
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
@@ -0,0 +1,59 @@
suite: values.schema.json — secret length enforcement (blocker fix regression net)
release:
name: t
namespace: sim
tests:
- it: rejects a BETTER_AUTH_SECRET shorter than 32 characters
set:
app.env.BETTER_AUTH_SECRET: short
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
asserts:
- failedTemplate:
errorPattern: "BETTER_AUTH_SECRET.*minLength"
- it: rejects an ENCRYPTION_KEY shorter than 32 characters
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: short
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
asserts:
- failedTemplate:
errorPattern: "ENCRYPTION_KEY.*minLength"
- it: rejects a postgresql.auth.password shorter than 8 characters
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: short
asserts:
- failedTemplate:
errorPattern: "password.*minLength"
- it: accepts an empty BETTER_AUTH_SECRET (deferred to existingSecret/ESO)
templates:
- secrets-app.yaml
set:
app.secrets.existingSecret.enabled: true
app.secrets.existingSecret.name: my-existing-secret
postgresql.auth.password: xxxxxxxx
asserts:
- hasDocuments: { count: 0 }
- it: accepts a valid 32-character BETTER_AUTH_SECRET and ENCRYPTION_KEY
template: deployment-app.yaml
set:
app.env.BETTER_AUTH_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
asserts:
- isKind: { of: Deployment }
+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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
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: xxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
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: xxxxxxxx
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-long
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 }
@@ -0,0 +1,53 @@
suite: telemetry — Restricted Pod Security Standard hardening (best-practices fix regression net)
release:
name: t
namespace: sim
templates:
- telemetry.yaml
tests:
- it: pod-level security context preserves the collector's original UID/GID/fsGroup and adds seccompProfile
set:
telemetry.enabled: true
documentSelector:
path: kind
value: Deployment
asserts:
- equal:
path: spec.template.spec.securityContext.runAsUser
value: 10001
- equal:
path: spec.template.spec.securityContext.runAsGroup
value: 10001
- equal:
path: spec.template.spec.securityContext.fsGroup
value: 10001
- equal:
path: spec.template.spec.securityContext.runAsNonRoot
value: true
- equal:
path: spec.template.spec.securityContext.seccompProfile.type
value: RuntimeDefault
- equal:
path: spec.template.spec.automountServiceAccountToken
value: false
- it: container-level security context matches the Restricted profile (same as every other workload)
set:
telemetry.enabled: true
documentSelector:
path: kind
value: Deployment
asserts:
- equal:
path: spec.template.spec.containers[0].securityContext.allowPrivilegeEscalation
value: false
- equal:
path: spec.template.spec.containers[0].securityContext.capabilities.drop[0]
value: ALL
- equal:
path: spec.template.spec.containers[0].securityContext.runAsNonRoot
value: true
- equal:
path: spec.template.spec.containers[0].securityContext.seccompProfile.type
value: RuntimeDefault
+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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: ""
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: ""
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: ""
cronjobs.enabled: true
postgresql.auth.password: xxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.ENCRYPTION_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
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: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
app.env.INTERNAL_API_SECRET: x
app.env.CRON_SECRET: x
postgresql.auth.password: xxxxxxxx
asserts:
- failedTemplate:
errorPattern: "must not use the default placeholder value"
File diff suppressed because it is too large Load Diff
+1840
View File
File diff suppressed because it is too large Load Diff