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
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:
@@ -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 2–4 are unaffected by ESO.
|
||||
|
||||
**Override precedence.** Values set in `app.env` (Layer 1 overrides) win over `envDefaults` (Layer 2) — so users who already had operational tunables in `app.env` continue to work.
|
||||
|
||||
## 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.
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
@@ -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 '/+=')"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user