diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml new file mode 100644 index 00000000000..11944217283 --- /dev/null +++ b/.github/workflows/helm.yml @@ -0,0 +1,157 @@ +name: Helm Chart + +on: + push: + branches: [main, staging, dev] + paths: + - 'helm/sim/**' + - '.github/workflows/helm.yml' + pull_request: + branches: [main, staging, dev] + paths: + - 'helm/sim/**' + - '.github/workflows/helm.yml' + +concurrency: + group: helm-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + chart: + name: Lint, test, and validate chart + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + with: + version: v3.16.4 + + - name: Helm lint + run: helm lint helm/sim --values helm/sim/ci/default-values.yaml + + - name: Helm unit tests + run: | + # Official helm-unittest image, pinned by immutable digest (tag 3.17.3-0.8.2). + # Run as the runner's UID so the container can write into the bind + # mount (it creates tests/__snapshot__), with a writable HOME for helm. + docker run --rm --user "$(id -u):$(id -g)" -e HOME=/tmp \ + -v "$PWD/helm/sim:/apps" \ + helmunittest/helm-unittest@sha256:b653db7d5665bc6cec677b15c5eaa1c0377c0de8ac4eb1df58b924478baa21e1 . + + - name: Install kubeconform + run: | + curl -sSL -o /tmp/kubeconform.tar.gz \ + https://github.com/yannh/kubeconform/releases/download/v0.6.7/kubeconform-linux-amd64.tar.gz + echo "95f14e87aa28c09d5941f11bd024c1d02fdc0303ccaa23f61cef67bc92619d73 /tmp/kubeconform.tar.gz" | sha256sum -c - + tar -xzf /tmp/kubeconform.tar.gz -C /tmp kubeconform + + - name: Render and validate manifests (default configuration) + run: | + helm template sim helm/sim --namespace sim \ + --values helm/sim/ci/default-values.yaml \ + | /tmp/kubeconform -strict -summary \ + -kubernetes-version 1.29.0 \ + -schema-location default \ + -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' + + - name: Render and validate manifests (all components enabled) + run: | + helm template sim helm/sim --namespace sim \ + --values helm/sim/ci/full-values.yaml \ + | /tmp/kubeconform -strict -summary \ + -kubernetes-version 1.29.0 \ + -schema-location default \ + -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' + + - name: Render every example values file + run: | + set -euo pipefail + for f in helm/sim/examples/values-*.yaml; do + echo "--- $f" + # Examples intentionally omit secrets (their headers document the + # required --set flags), so supply the CI dummies alongside each. + helm template sim helm/sim --namespace sim \ + --values "$f" \ + --values helm/sim/ci/default-values.yaml \ + --set copilot.postgresql.auth.password=ci-dummy-password \ + --set copilot.server.env.AGENT_API_DB_ENCRYPTION_KEY=cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici \ + --set copilot.server.env.INTERNAL_API_SECRET=cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici \ + --set copilot.server.env.LICENSE_KEY=ci-dummy-license \ + --set copilot.server.env.SIM_BASE_URL=https://ci.example.com \ + --set copilot.server.env.SIM_AGENT_API_KEY=ci-dummy-agent-key \ + --set copilot.server.env.REDIS_URL=redis://ci-redis:6379 \ + --set copilot.server.env.OPENAI_API_KEY_1=ci-dummy-openai-key \ + --set externalDatabase.password=ci-dummy-password > /dev/null + done + + version-bump: + name: Chart version bumped + if: github.event_name == 'pull_request' + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }} + timeout-minutes: 5 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + - name: Require a Chart.yaml version bump when chart content changes + run: | + set -euo pipefail + base="origin/${{ github.base_ref }}" + git fetch origin "${{ github.base_ref }}" + merge_base=$(git merge-base "$base" HEAD) + changed=$(git diff --name-only "$merge_base" HEAD) + if echo "$changed" | grep -q '^helm/sim/'; then + base_version=$(git show "$merge_base:helm/sim/Chart.yaml" | awk '/^version:/ {print $2}') + head_version=$(awk '/^version:/ {print $2}' helm/sim/Chart.yaml) + echo "base=$base_version head=$head_version" + if [ "$base_version" = "$head_version" ]; then + echo "::error::helm/sim/** changed but Chart.yaml version did not (still $head_version). Bump it per SemVer." + exit 1 + fi + else + echo "No chart changes; skipping." + fi + + install: + name: Install on kind and run helm test + needs: chart + runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }} + timeout-minutes: 25 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + + - name: Set up Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 + with: + version: v3.16.4 + + - name: Create kind cluster + uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1 + with: + version: v0.24.0 + + - name: Install chart + run: | + helm install sim helm/sim \ + --namespace sim --create-namespace \ + --values helm/sim/ci/default-values.yaml \ + --values helm/sim/ci/kind-values.yaml \ + --wait --timeout 15m + + - name: Diagnostics on failure + if: failure() + run: | + kubectl -n sim get pods -o wide || true + kubectl -n sim get events --sort-by=.lastTimestamp | tail -40 || true + kubectl -n sim describe pods | tail -100 || true + kubectl -n sim logs deploy/sim-app -c migrations --tail=50 || true + kubectl -n sim logs deploy/sim-app --tail=80 || true + + - name: Run helm test + run: helm test sim --namespace sim --timeout 5m diff --git a/helm/sim/Chart.yaml b/helm/sim/Chart.yaml index c4c8b2ca7c1..9550ef6d9c1 100644 --- a/helm/sim/Chart.yaml +++ b/helm/sim/Chart.yaml @@ -2,8 +2,8 @@ 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.1.0 -appVersion: "0.6.73" +version: 1.2.0 +appVersion: "v0.7.44" kubeVersion: ">=1.25.0-0" home: https://sim.ai icon: https://raw.githubusercontent.com/simstudioai/sim/main/apps/sim/public/logo/primary/primary.svg diff --git a/helm/sim/README.md b/helm/sim/README.md index 5b02a288567..e48eaeb4220 100644 --- a/helm/sim/README.md +++ b/helm/sim/README.md @@ -467,6 +467,13 @@ kubectl --namespace sim logs deploy/sim-app -c migrations --- +## Upgrading to 1.2.0 + +* `appVersion` (the default image tag when `image.tag` is unset) is now `v0.7.44` — the previous `0.6.73` referenced a tag that does not exist on GHCR, so an unpinned default install could not pull images. Production installs should still pin `image.tag` explicitly. +* `externalSecrets.apiVersion` now defaults to `"v1"` — current External Secrets Operator releases no longer serve `v1beta1` (removed upstream in 2026). Set `externalSecrets.apiVersion: "v1beta1"` only if you still run ESO < 0.17. +* `values.schema.json` now declares every top-level key and rejects unknown top-level keys, so a typo like `networkPolciy:` fails fast at install time instead of being silently ignored. If an upgrade suddenly fails schema validation, check your values file for stray top-level keys. +* The opt-in telemetry collector no longer ships a Prometheus scrape config for the app/realtime services (they expose no `/metrics` endpoint); OTLP ingestion is unchanged. + ## Upgrading to 1.1.0 No action is required for working configurations. Notes: diff --git a/helm/sim/ci/default-values.yaml b/helm/sim/ci/default-values.yaml new file mode 100644 index 00000000000..f27cd58a49b --- /dev/null +++ b/helm/sim/ci/default-values.yaml @@ -0,0 +1,12 @@ +# CI-only values: the minimum required secrets so `helm lint`/`helm template` +# render the DEFAULT configuration. Dummy values — never use in a deployment. +app: + env: + BETTER_AUTH_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + INTERNAL_API_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + CRON_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + +postgresql: + auth: + password: "ci-dummy-password" diff --git a/helm/sim/ci/full-values.yaml b/helm/sim/ci/full-values.yaml new file mode 100644 index 00000000000..ca980273414 --- /dev/null +++ b/helm/sim/ci/full-values.yaml @@ -0,0 +1,72 @@ +# CI-only values: every optional component enabled so the full template surface +# renders and validates. Dummy values — never use in a deployment. +app: + env: + BETTER_AUTH_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + INTERNAL_API_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + CRON_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + API_ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + +postgresql: + auth: + password: "ci-dummy-password" + tls: + enabled: true + +certManager: + enabled: true + +ingress: + enabled: true + app: + host: ci.example.com + paths: [{ path: /, pathType: Prefix }] + realtime: + host: ci-ws.example.com + paths: [{ path: /, pathType: Prefix }] + tls: + enabled: true + +networkPolicy: + enabled: true + +autoscaling: + enabled: true + +monitoring: + serviceMonitor: + enabled: true + +telemetry: + enabled: true + jaeger: + enabled: true + prometheus: + enabled: true + otlp: + enabled: true + +sharedStorage: + enabled: true + +ollama: + enabled: true + +pii: + enabled: true + +copilot: + enabled: true + postgresql: + auth: + password: "ci-dummy-password" + server: + env: + AGENT_API_DB_ENCRYPTION_KEY: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + INTERNAL_API_SECRET: "cicicicicicicicicicicicicicicicicicicicicicicicicicicicicicicici" + LICENSE_KEY: "ci-dummy-license" + SIM_BASE_URL: "https://ci.example.com" + SIM_AGENT_API_KEY: "ci-dummy-agent-key" + REDIS_URL: "redis://ci-redis:6379" + OPENAI_API_KEY_1: "ci-dummy-openai-key" diff --git a/helm/sim/ci/kind-values.yaml b/helm/sim/ci/kind-values.yaml new file mode 100644 index 00000000000..cf3185d63d6 --- /dev/null +++ b/helm/sim/ci/kind-values.yaml @@ -0,0 +1,39 @@ +# CI-only overlay for the kind install test: shrink resource requests so the +# default configuration schedules on a small CI runner. Layered on top of +# ci/default-values.yaml. Dummy sizing — never use in a deployment. +app: + resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1000m + memory: 2Gi + +realtime: + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + +migrations: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 1Gi + +postgresql: + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi + persistence: + size: 2Gi diff --git a/helm/sim/templates/NOTES.txt b/helm/sim/templates/NOTES.txt index 29be6c39bd6..c3f3e211f8a 100644 --- a/helm/sim/templates/NOTES.txt +++ b/helm/sim/templates/NOTES.txt @@ -86,9 +86,9 @@ Your release is named {{ .Release.Name }} in namespace {{ .Release.Namespace }}. 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". + * externalSecrets.apiVersion defaults to "v1". Current ESO releases no + longer serve v1beta1 (the compatibility path was removed in 2026) — set + externalSecrets.apiVersion: "v1beta1" only if you still run ESO < 0.17. * 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). diff --git a/helm/sim/templates/external-secret-app.yaml b/helm/sim/templates/external-secret-app.yaml index 826291e4eba..ea82d6a1f6e 100644 --- a/helm/sim/templates/external-secret-app.yaml +++ b/helm/sim/templates/external-secret-app.yaml @@ -9,7 +9,7 @@ # - 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" }} +apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }} kind: ExternalSecret metadata: name: {{ include "sim.fullname" . }}-app-secrets diff --git a/helm/sim/templates/external-secret-copilot.yaml b/helm/sim/templates/external-secret-copilot.yaml index a7b78da0310..93cf7fb3bc8 100644 --- a/helm/sim/templates/external-secret-copilot.yaml +++ b/helm/sim/templates/external-secret-copilot.yaml @@ -6,7 +6,7 @@ # 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" }} +apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }} kind: ExternalSecret metadata: name: {{ include "sim.copilot.envSecretName" . }} diff --git a/helm/sim/templates/external-secret-external-db.yaml b/helm/sim/templates/external-secret-external-db.yaml index 1d2ac705e82..d1cde291aac 100644 --- a/helm/sim/templates/external-secret-external-db.yaml +++ b/helm/sim/templates/external-secret-external-db.yaml @@ -1,6 +1,6 @@ {{- if and .Values.externalSecrets.enabled .Values.externalDatabase.enabled .Values.externalSecrets.remoteRefs.externalDatabase.password }} # ExternalSecret for external database password (syncs from external secret managers) -apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }} +apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }} kind: ExternalSecret metadata: name: {{ include "sim.fullname" . }}-external-db-secret diff --git a/helm/sim/templates/external-secret-postgresql.yaml b/helm/sim/templates/external-secret-postgresql.yaml index 7f55bff5424..d8796077d92 100644 --- a/helm/sim/templates/external-secret-postgresql.yaml +++ b/helm/sim/templates/external-secret-postgresql.yaml @@ -1,6 +1,6 @@ {{- if and .Values.externalSecrets.enabled .Values.postgresql.enabled .Values.externalSecrets.remoteRefs.postgresql.password }} # ExternalSecret for PostgreSQL password (syncs from external secret managers) -apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1beta1" }} +apiVersion: external-secrets.io/{{ .Values.externalSecrets.apiVersion | default "v1" }} kind: ExternalSecret metadata: name: {{ include "sim.fullname" . }}-postgresql-secret diff --git a/helm/sim/templates/job-copilot-migrations.yaml b/helm/sim/templates/job-copilot-migrations.yaml index 60a2bbe663f..862ef02e919 100644 --- a/helm/sim/templates/job-copilot-migrations.yaml +++ b/helm/sim/templates/job-copilot-migrations.yaml @@ -45,6 +45,13 @@ spec: sleep 2 done echo "Copilot PostgreSQL is ready!" + resources: + requests: + cpu: 10m + memory: 16Mi + limits: + cpu: 100m + memory: 64Mi envFrom: - secretRef: name: {{ include "sim.fullname" . }}-copilot-postgresql-secret diff --git a/helm/sim/templates/telemetry.yaml b/helm/sim/templates/telemetry.yaml index 761c5155784..bd0627d4284 100644 --- a/helm/sim/templates/telemetry.yaml +++ b/helm/sim/templates/telemetry.yaml @@ -18,16 +18,6 @@ data: 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 @@ -83,7 +73,7 @@ data: - otlp {{- end }} metrics: - receivers: [otlp, prometheus] + receivers: [otlp] processors: [memory_limiter, batch] exporters: - logging diff --git a/helm/sim/tests/schema-secret-length_test.yaml b/helm/sim/tests/schema-secret-length_test.yaml index b2255d93853..f9b24c9b13c 100644 --- a/helm/sim/tests/schema-secret-length_test.yaml +++ b/helm/sim/tests/schema-secret-length_test.yaml @@ -13,7 +13,7 @@ tests: postgresql.auth.password: xxxxxxxx asserts: - failedTemplate: - errorPattern: "BETTER_AUTH_SECRET.*minLength" + errorPattern: "BETTER_AUTH_SECRET.*(minLength|greater than or equal to 32)" - it: rejects an ENCRYPTION_KEY shorter than 32 characters set: @@ -24,7 +24,7 @@ tests: postgresql.auth.password: xxxxxxxx asserts: - failedTemplate: - errorPattern: "ENCRYPTION_KEY.*minLength" + errorPattern: "ENCRYPTION_KEY.*(minLength|greater than or equal to 32)" - it: rejects a postgresql.auth.password shorter than 8 characters set: @@ -35,7 +35,7 @@ tests: postgresql.auth.password: short asserts: - failedTemplate: - errorPattern: "password.*minLength" + errorPattern: "password.*(minLength|greater than or equal to 8)" - it: accepts an empty BETTER_AUTH_SECRET (deferred to existingSecret/ESO) templates: diff --git a/helm/sim/tests/smoke_test.yaml b/helm/sim/tests/smoke_test.yaml index 4b95fd12f2d..23c035c0acf 100644 --- a/helm/sim/tests/smoke_test.yaml +++ b/helm/sim/tests/smoke_test.yaml @@ -33,3 +33,13 @@ tests: asserts: - isKind: { of: Secret } - equal: { path: metadata.name, value: t-sim-app-secrets } + + - it: accepts the standard Helm naming overrides under the strict schema + set: + nameOverride: acme + fullnameOverride: acme-sim + asserts: + - matchRegex: + path: metadata.name + pattern: "^acme-sim" + template: deployment-app.yaml diff --git a/helm/sim/values.schema.json b/helm/sim/values.schema.json index fc26f3c48c8..9088f9bd6f1 100644 --- a/helm/sim/values.schema.json +++ b/helm/sim/values.schema.json @@ -1190,8 +1190,81 @@ } } } + }, + "nameOverride": { + "type": "string", + "description": "Override the chart name used in resource names" + }, + "fullnameOverride": { + "type": "string", + "description": "Override the fully qualified release name used in resource names" + }, + "affinity": { + "type": "object", + "description": "Pod affinity/anti-affinity rules applied to app and realtime pods" + }, + "branding": { + "type": "object", + "description": "Whitelabel branding configuration (logos, names, colors)" + }, + "certManager": { + "type": "object", + "description": "Chart-managed cert-manager issuers (self-signed bootstrap + CA) for postgresql.tls" + }, + "cronjobs": { + "type": "object", + "description": "Scheduled jobs (schedule execution, polling, reconciliation)" + }, + "extraEnvVars": { + "type": "array", + "description": "Additional environment variables added to app and realtime containers" + }, + "extraVolumeMounts": { + "type": "array", + "description": "Additional volume mounts added to app and realtime containers" + }, + "extraVolumes": { + "type": "array", + "description": "Additional volumes added to app and realtime pods" + }, + "ingressInternal": { + "type": "object", + "description": "Secondary internal ingress configuration" + }, + "migrations": { + "type": "object", + "description": "Database migrations init-container configuration" + }, + "monitoring": { + "type": "object", + "description": "Prometheus Operator ServiceMonitor configuration" + }, + "networkPolicy": { + "type": "object", + "description": "NetworkPolicy toggles and custom ingress/egress rules" + }, + "podAnnotations": { + "type": "object", + "description": "Annotations added to app and realtime pods" + }, + "podDisruptionBudget": { + "type": "object", + "description": "PodDisruptionBudget configuration for app and realtime" + }, + "podLabels": { + "type": "object", + "description": "Labels added to app and realtime pods" + }, + "serviceAccount": { + "type": "object", + "description": "ServiceAccount creation, name, and annotations (e.g. Workload Identity)" + }, + "tolerations": { + "type": "array", + "description": "Tolerations applied to app and realtime pods" } }, + "additionalProperties": false, "allOf": [ { "if": { diff --git a/helm/sim/values.yaml b/helm/sim/values.yaml index 314c4edb38f..5c62ee6def7 100644 --- a/helm/sim/values.yaml +++ b/helm/sim/values.yaml @@ -20,7 +20,7 @@ app: # Image configuration image: repository: simstudioai/simstudio - # tag defaults to Chart.AppVersion. Override with a release tag (e.g. "0.6.73") or pin via image.digest. + # tag defaults to Chart.AppVersion. Override with a release tag (e.g. "v0.7.44") or pin via image.digest. tag: "" # Optional image digest pin: "sha256:..." — when set, overrides tag. digest: "" @@ -1729,9 +1729,10 @@ externalSecrets: enabled: false # ESO API version. Default "v1beta1" — supported by every ESO release from - # v0.7+ (mid-2023) through current. Set to "v1" only when targeting ESO - # v0.17+ clusters where the v1 API has graduated. - apiVersion: "v1beta1" + # Current ESO releases serve only external-secrets.io/v1 (the v1beta1 + # compatibility path was removed in 2026). Set to "v1beta1" only for + # clusters still running ESO < 0.17. + apiVersion: "v1" # How often to sync secrets from the external store refreshInterval: "1h"