diff --git a/.gitignore b/.gitignore index 24a77fce2..fb8679fa7 100644 --- a/.gitignore +++ b/.gitignore @@ -187,6 +187,9 @@ rootfs/ # Docker build artifacts (image tarballs, packaged helm charts) deploy/docker/.build/ +# Helm subchart tarballs (regenerated by `helm dependency build`) +deploy/helm/openshell/charts/ + # SBOM generated output (JSON, CSV) — release artifacts, not committed deploy/sbom/output/ diff --git a/README.md b/README.md index b1d3a2be6..e16a57190 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,6 @@ Docker-backed GPU sandboxes auto-select CDI when available and otherwise fall ba | [Ollama](https://ollama.com/) | [Community](https://github.com/NVIDIA/OpenShell-Community) | Launch with `openshell sandbox create --from ollama`. | | [Pi](https://pi.dev/) | [Community](https://github.com/NVIDIA/OpenShell-Community) | Launch with `openshell sandbox create --from pi`. | - ## Key Commands | Command | Description | diff --git a/deploy/helm/openshell/Chart.lock b/deploy/helm/openshell/Chart.lock new file mode 100644 index 000000000..f1a95f424 --- /dev/null +++ b/deploy/helm/openshell/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: postgresql + repository: oci://registry-1.docker.io/bitnamicharts + version: 18.6.7 +digest: sha256:ad78500c7c3a7ee365fd151890cf3368444d6b167c972052fc245024f5a25d9c +generated: "2026-05-27T17:48:47.648592-04:00" diff --git a/deploy/helm/openshell/Chart.yaml b/deploy/helm/openshell/Chart.yaml index 06608adb3..fbfba7b2c 100644 --- a/deploy/helm/openshell/Chart.yaml +++ b/deploy/helm/openshell/Chart.yaml @@ -11,3 +11,9 @@ type: application # empty), so a released chart automatically pulls the matching gateway and supervisor images. version: 0.0.0 appVersion: "0.0.0" +dependencies: + - name: postgresql + version: 18.6.7 + repository: oci://registry-1.docker.io/bitnamicharts + condition: postgres.deploy + alias: postgres diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 9df0b91a0..2c0db85fd 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -34,7 +34,6 @@ oc adm policy add-scc-to-user privileged -z openshell-sandbox -n openshell # Deploy openshell with overrides to allow SCC assignment of fsGroup and runAsUser for the gateway helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version -n openshell \ - --set pkiInitJob.enabled=false \ --set server.disableTls=true \ --set podSecurityContext.fsGroup=null \ --set securityContext.runAsUser=null @@ -58,6 +57,111 @@ See [`values.yaml`](values.yaml) for source defaults. Selected overlays: - [`ci/values-cert-manager.yaml`](ci/values-cert-manager.yaml) - cert-manager integration - [`ci/values-keycloak.yaml`](ci/values-keycloak.yaml) - Keycloak OIDC integration +### Database backend + +By default, OpenShell uses SQLite: + +```yaml +server: + dbUrl: "sqlite:/var/openshell/openshell.db" +postgres: + enabled: false +``` + +#### Use an existing Kubernetes Secret + +If you already have a Secret containing PostgreSQL credentials (e.g. managed +via GitOps or external-secrets-operator), point the chart at it directly: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.existingSecret=my-pg-credentials +``` + +On OpenShift, append the platform overrides: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.existingSecret=my-pg-credentials \ + --set server.disableTls=true \ + --set podSecurityContext.fsGroup=null \ + --set securityContext.runAsUser=null +``` + +The Secret must contain a `uri` key with the full connection string: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-pg-credentials +type: Opaque +data: + uri: # postgresql://user:pass@host:5432/dbname +``` + +#### Kubernetes + +Enable bundled PostgreSQL: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.deploy=true \ + --set postgres.auth.password=my-secret-password +``` + +Use external PostgreSQL (chart creates the Secret from fields): + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.host=my-postgres.example.com \ + --set postgres.external.port=5432 \ + --set postgres.external.database=openshell \ + --set postgres.external.username=openshell \ + --set postgres.external.password=my-password +``` + +#### OpenShift + +Append these flags to any of the PostgreSQL commands above for OpenShift: + +``` +--set server.disableTls=true \ +--set podSecurityContext.fsGroup=null \ +--set securityContext.runAsUser=null +``` + +Enable bundled PostgreSQL on OpenShift: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.deploy=true \ + --set postgres.auth.password=my-secret-password \ + --set server.disableTls=true \ + --set podSecurityContext.fsGroup=null \ + --set securityContext.runAsUser=null +``` + +Use external PostgreSQL on OpenShift: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.host=my-postgres.example.com \ + --set postgres.external.port=5432 \ + --set postgres.external.database=openshell \ + --set postgres.external.username=openshell \ + --set postgres.external.password=my-password \ + --set server.disableTls=true \ + --set podSecurityContext.fsGroup=null \ + --set securityContext.runAsUser=null +``` + ## PKI bootstrap By default, a pre-install/pre-upgrade hook Job runs `openshell-gateway generate-certs` @@ -111,6 +215,19 @@ cert-manager alternative. | podLabels | object | `{}` | Extra labels to add to the gateway pod. | | podLifecycle.terminationGracePeriodSeconds | int | `5` | Grace period, in seconds, before Kubernetes terminates the gateway pod. | | podSecurityContext.fsGroup | int | `1000` | fsGroup assigned to the gateway pod. | +| postgres.auth.database | string | `"openshell"` | | +| postgres.auth.password | string | `""` | | +| postgres.auth.username | string | `"openshell"` | | +| postgres.deploy | bool | `false` | Deploy the bundled Bitnami PostgreSQL subchart. Set to true to run PostgreSQL alongside the gateway. Leave false when using an external PostgreSQL instance. | +| postgres.enabled | bool | `false` | | +| postgres.external.database | string | `"openshell"` | | +| postgres.external.existingSecret | string | `""` | Name of a pre-existing Opaque Secret containing PostgreSQL credentials. When set, the chart does not create its own db Secret and reads directly from this one. The Secret must contain a `uri` key with the full connection string, e.g. postgresql://user:pass@host:5432/dbname. | +| postgres.external.host | string | `""` | | +| postgres.external.password | string | `""` | | +| postgres.external.port | int | `5432` | | +| postgres.external.username | string | `"openshell"` | | +| postgres.primary.persistence.enabled | bool | `true` | | +| postgres.serviceBindings.enabled | bool | `true` | | | probes.liveness.failureThreshold | int | `3` | Liveness probe failure threshold before the container is restarted. | | probes.liveness.initialDelaySeconds | int | `2` | Liveness probe initial delay, in seconds. | | probes.liveness.periodSeconds | int | `5` | Liveness probe period, in seconds. | @@ -151,6 +268,7 @@ cert-manager alternative. | server.sandboxImagePullPolicy | string | `""` | Kubernetes imagePullPolicy for sandbox pods. Empty = Kubernetes default (Always for :latest, IfNotPresent otherwise). Set to "Always" for dev clusters so new images are picked up without manual eviction. | | server.sandboxJwt.gatewayId | string | `""` | Stable gateway identity embedded in iss/aud of every minted token. Defaults to the release name so HA replicas share identity. | | server.sandboxJwt.k8sSaTokenTtlSecs | int | `3600` | Lifetime (seconds) of the projected ServiceAccount token kubelet writes into each sandbox pod for the IssueSandboxToken bootstrap exchange. Kubelet enforces a minimum of 600s; the driver clamps values outside [600, 86400]. Default 3600 — generous, since the supervisor consumes the token within seconds of pod start. | +| server.sandboxJwt.secretDefaultMode | string | `""` | File mode for the mounted JWT signing key Secret. Default 0400 (owner-read only). Override to 0440 or 0444 if the container UID does not match the volume file owner. | | server.sandboxJwt.signingSecretName | string | `""` | Name of the Opaque Secret holding the signing key material. Empty falls back to the chart fullname with "-jwt-keys" appended. | | server.sandboxJwt.ttlSecs | int | `3600` | Token TTL in seconds. Defaults to 3600 (1h). | | server.sandboxNamespace | string | `""` | Namespace where sandbox pods are created. Defaults to the Helm release namespace (.Release.Namespace) when left empty. | diff --git a/deploy/helm/openshell/README.md.gotmpl b/deploy/helm/openshell/README.md.gotmpl index 9e6a0ec65..6c9733fbc 100644 --- a/deploy/helm/openshell/README.md.gotmpl +++ b/deploy/helm/openshell/README.md.gotmpl @@ -34,7 +34,6 @@ oc adm policy add-scc-to-user privileged -z openshell-sandbox -n openshell # Deploy openshell with overrides to allow SCC assignment of fsGroup and runAsUser for the gateway helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version -n openshell \ - --set pkiInitJob.enabled=false \ --set server.disableTls=true \ --set podSecurityContext.fsGroup=null \ --set securityContext.runAsUser=null @@ -58,6 +57,111 @@ See [`values.yaml`](values.yaml) for source defaults. Selected overlays: - [`ci/values-cert-manager.yaml`](ci/values-cert-manager.yaml) - cert-manager integration - [`ci/values-keycloak.yaml`](ci/values-keycloak.yaml) - Keycloak OIDC integration +### Database backend + +By default, OpenShell uses SQLite: + +```yaml +server: + dbUrl: "sqlite:/var/openshell/openshell.db" +postgres: + enabled: false +``` + +#### Use an existing Kubernetes Secret + +If you already have a Secret containing PostgreSQL credentials (e.g. managed +via GitOps or external-secrets-operator), point the chart at it directly: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.existingSecret=my-pg-credentials +``` + +On OpenShift, append the platform overrides: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.existingSecret=my-pg-credentials \ + --set server.disableTls=true \ + --set podSecurityContext.fsGroup=null \ + --set securityContext.runAsUser=null +``` + +The Secret must contain a `uri` key with the full connection string: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-pg-credentials +type: Opaque +data: + uri: # postgresql://user:pass@host:5432/dbname +``` + +#### Kubernetes + +Enable bundled PostgreSQL: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.deploy=true \ + --set postgres.auth.password=my-secret-password +``` + +Use external PostgreSQL (chart creates the Secret from fields): + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.host=my-postgres.example.com \ + --set postgres.external.port=5432 \ + --set postgres.external.database=openshell \ + --set postgres.external.username=openshell \ + --set postgres.external.password=my-password +``` + +#### OpenShift + +Append these flags to any of the PostgreSQL commands above for OpenShift: + +``` +--set server.disableTls=true \ +--set podSecurityContext.fsGroup=null \ +--set securityContext.runAsUser=null +``` + +Enable bundled PostgreSQL on OpenShift: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.deploy=true \ + --set postgres.auth.password=my-secret-password \ + --set server.disableTls=true \ + --set podSecurityContext.fsGroup=null \ + --set securityContext.runAsUser=null +``` + +Use external PostgreSQL on OpenShift: + +```bash +helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart --version \ + --set postgres.enabled=true \ + --set postgres.external.host=my-postgres.example.com \ + --set postgres.external.port=5432 \ + --set postgres.external.database=openshell \ + --set postgres.external.username=openshell \ + --set postgres.external.password=my-password \ + --set server.disableTls=true \ + --set podSecurityContext.fsGroup=null \ + --set securityContext.runAsUser=null +``` + ## PKI bootstrap By default, a pre-install/pre-upgrade hook Job runs `openshell-gateway generate-certs` diff --git a/deploy/helm/openshell/ci/test-openshift-scenarios.sh b/deploy/helm/openshell/ci/test-openshift-scenarios.sh new file mode 100755 index 000000000..5ff54fbd7 --- /dev/null +++ b/deploy/helm/openshell/ci/test-openshift-scenarios.sh @@ -0,0 +1,199 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Validates all OpenShift database-backend scenarios against a live cluster. +# +# Prerequisites: +# - oc CLI authenticated to an OpenShift cluster +# - helm 3.x installed +# - Chart dependencies built (helm dependency build deploy/helm/openshell) +# +# Usage: +# ./deploy/helm/openshell/ci/test-openshift-scenarios.sh [--chart-path ./deploy/helm/openshell] [--image-tag dev] + +set -euo pipefail + +CHART_PATH="${CHART_PATH:-./deploy/helm/openshell}" +NAMESPACE="openshell" +RELEASE="openshell" +IMAGE_TAG="${IMAGE_TAG:-dev}" +WAIT_TIMEOUT="120s" +PASSED=0 +FAILED=0 +SCENARIOS=() + +while [[ $# -gt 0 ]]; do + case $1 in + --chart-path) CHART_PATH="$2"; shift 2 ;; + --image-tag) IMAGE_TAG="$2"; shift 2 ;; + --namespace) NAMESPACE="$2"; shift 2 ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +# --- helpers ---------------------------------------------------------------- + +log() { echo "==> $*"; } +pass() { log "PASS: $1"; PASSED=$((PASSED + 1)); SCENARIOS+=("PASS $1"); } +fail() { log "FAIL: $1 — $2"; FAILED=$((FAILED + 1)); SCENARIOS+=("FAIL $1: $2"); } + +wait_for_ready() { + local label="$1" timeout="$2" + if oc wait pod -n "$NAMESPACE" -l "$label" --for=condition=Ready --timeout="$timeout" 2>/dev/null; then + return 0 + fi + return 1 +} + +cleanup_release() { + log "Cleaning up release $RELEASE" + helm uninstall "$RELEASE" -n "$NAMESPACE" --wait 2>/dev/null || true + # Wait for pods to terminate + for i in $(seq 1 30); do + if [ -z "$(oc get pods -n "$NAMESPACE" -l "app.kubernetes.io/instance=$RELEASE" --no-headers 2>/dev/null)" ]; then + break + fi + sleep 2 + done + # Clean up PVCs left by StatefulSets + oc delete pvc -n "$NAMESPACE" -l "app.kubernetes.io/instance=$RELEASE" --wait=false 2>/dev/null || true +} + +verify_gateway() { + local scenario="$1" + if wait_for_ready "app.kubernetes.io/name=openshell,app.kubernetes.io/instance=$RELEASE" "$WAIT_TIMEOUT"; then + # Check the pod is actually running (not CrashLoopBackOff) + local phase + phase=$(oc get pod -n "$NAMESPACE" -l "app.kubernetes.io/name=openshell,app.kubernetes.io/instance=$RELEASE" \ + -o jsonpath='{.items[0].status.phase}' 2>/dev/null) + if [ "$phase" = "Running" ]; then + pass "$scenario" + else + fail "$scenario" "pod phase is $phase, expected Running" + fi + else + local status + status=$(oc get pods -n "$NAMESPACE" -l "app.kubernetes.io/name=openshell" --no-headers 2>/dev/null || echo "no pods found") + fail "$scenario" "gateway pod not ready within $WAIT_TIMEOUT ($status)" + fi +} + +# --- setup ------------------------------------------------------------------ + +log "Setting up namespace $NAMESPACE" +oc create ns "$NAMESPACE" 2>/dev/null || true +oc adm policy add-scc-to-user privileged -z "${RELEASE}-sandbox" -n "$NAMESPACE" + +OPENSHIFT_FLAGS=( + --set server.disableTls=true + --set podSecurityContext.fsGroup=null + --set securityContext.runAsUser=null + --set image.tag="$IMAGE_TAG" +) + +# --- scenario 1: SQLite (default, no postgres) ----------------------------- + +SCENARIO="SQLite (default)" +log "Testing: $SCENARIO" +cleanup_release + +helm install "$RELEASE" "$CHART_PATH" -n "$NAMESPACE" \ + "${OPENSHIFT_FLAGS[@]}" + +verify_gateway "$SCENARIO" +cleanup_release + +# --- scenario 2: Bundled PostgreSQL (deploy=true) --------------------------- + +SCENARIO="Bundled PostgreSQL (deploy=true)" +log "Testing: $SCENARIO" +cleanup_release + +helm install "$RELEASE" "$CHART_PATH" -n "$NAMESPACE" \ + "${OPENSHIFT_FLAGS[@]}" \ + --set postgres.enabled=true \ + --set postgres.deploy=true \ + --set postgres.auth.password=test-password + +# Wait for postgres to be ready first +log "Waiting for bundled PostgreSQL..." +wait_for_ready "app.kubernetes.io/name=postgres,app.kubernetes.io/instance=$RELEASE" "$WAIT_TIMEOUT" || true + +verify_gateway "$SCENARIO" +cleanup_release + +# --- scenario 3: External PostgreSQL with existing Secret ------------------- + +SCENARIO="External PostgreSQL (existingSecret)" +log "Testing: $SCENARIO" +cleanup_release + +# Deploy a standalone Bitnami PostgreSQL as the "external" database +EXTERNAL_PG_RELEASE="pg-external" +EXTERNAL_PG_PASSWORD="ext-test-password" +EXTERNAL_PG_DATABASE="openshell" +EXTERNAL_PG_USERNAME="openshell" + +log "Deploying standalone PostgreSQL as external database..." +helm install "$EXTERNAL_PG_RELEASE" oci://registry-1.docker.io/bitnamicharts/postgresql \ + -n "$NAMESPACE" \ + --set auth.username="$EXTERNAL_PG_USERNAME" \ + --set auth.password="$EXTERNAL_PG_PASSWORD" \ + --set auth.database="$EXTERNAL_PG_DATABASE" \ + --set primary.podSecurityContext.fsGroup=null \ + --set primary.containerSecurityContext.runAsUser=null \ + --wait --timeout "$WAIT_TIMEOUT" 2>/dev/null || true + +wait_for_ready "app.kubernetes.io/name=postgresql,app.kubernetes.io/instance=$EXTERNAL_PG_RELEASE" "$WAIT_TIMEOUT" || true + +EXTERNAL_PG_HOST="${EXTERNAL_PG_RELEASE}-postgresql.${NAMESPACE}.svc.cluster.local" +EXTERNAL_PG_URI="postgresql://${EXTERNAL_PG_USERNAME}:${EXTERNAL_PG_PASSWORD}@${EXTERNAL_PG_HOST}:5432/${EXTERNAL_PG_DATABASE}" + +# Create the existing Secret with the expected keys +log "Creating existing Secret with PostgreSQL credentials..." +oc create secret generic my-pg-credentials -n "$NAMESPACE" \ + --from-literal=host="$EXTERNAL_PG_HOST" \ + --from-literal=port="5432" \ + --from-literal=username="$EXTERNAL_PG_USERNAME" \ + --from-literal=password="$EXTERNAL_PG_PASSWORD" \ + --from-literal=database="$EXTERNAL_PG_DATABASE" \ + --from-literal=uri="$EXTERNAL_PG_URI" \ + 2>/dev/null || true + +# Install OpenShell pointing at the existing Secret +helm install "$RELEASE" "$CHART_PATH" -n "$NAMESPACE" \ + "${OPENSHIFT_FLAGS[@]}" \ + --set postgres.enabled=true \ + --set postgres.external.existingSecret=my-pg-credentials + +verify_gateway "$SCENARIO" + +# Cleanup external postgres and secret +cleanup_release +helm uninstall "$EXTERNAL_PG_RELEASE" -n "$NAMESPACE" --wait 2>/dev/null || true +oc delete secret my-pg-credentials -n "$NAMESPACE" 2>/dev/null || true +oc delete pvc -n "$NAMESPACE" -l "app.kubernetes.io/instance=$EXTERNAL_PG_RELEASE" --wait=false 2>/dev/null || true + +# --- teardown --------------------------------------------------------------- + +log "Removing SCC binding and namespace" +oc adm policy remove-scc-from-user privileged -z "${RELEASE}-sandbox" -n "$NAMESPACE" 2>/dev/null || true +oc delete ns "$NAMESPACE" --wait=false 2>/dev/null || true + +# --- summary ---------------------------------------------------------------- + +echo "" +echo "========================================" +echo " Test Summary" +echo "========================================" +for s in "${SCENARIOS[@]}"; do + echo " $s" +done +echo "----------------------------------------" +echo " Passed: $PASSED Failed: $FAILED" +echo "========================================" + +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi diff --git a/deploy/helm/openshell/templates/_helpers.tpl b/deploy/helm/openshell/templates/_helpers.tpl index 3e375a54a..5d2b2803c 100644 --- a/deploy/helm/openshell/templates/_helpers.tpl +++ b/deploy/helm/openshell/templates/_helpers.tpl @@ -102,6 +102,38 @@ Namespace where sandbox pods are created. An explicit {{- .Values.server.sandboxNamespace | default .Release.Namespace -}} {{- end }} +{{/* +Fully qualified name of the PostgreSQL subchart, mirroring the Bitnami +common.names.fullname template so we stay in sync when users set +postgres.fullnameOverride or postgres.nameOverride. +*/}} +{{- define "openshell.postgresFullname" -}} +{{- if .Values.postgres.fullnameOverride -}} +{{- .Values.postgres.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default "postgres" .Values.postgres.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end }} + +{{/* +Name of the Secret holding the PostgreSQL connection URI. +- deploy=true: derive from Bitnami service-binding naming convention +- deploy=false + existingSecret set: use it verbatim +- deploy=false + no existingSecret: use chart-generated "-db" +*/}} +{{- define "openshell.dbSecretName" -}} +{{- if .Values.postgres.deploy -}} +{{- printf "%s-svcbind-custom-user" (include "openshell.postgresFullname" .) -}} +{{- else -}} +{{- .Values.postgres.external.existingSecret | default (printf "%s-db" (include "openshell.fullname" .)) -}} +{{- end -}} +{{- end }} + {{/* gRPC endpoint sandbox pods use to call back into the gateway. An explicit .Values.server.grpcEndpoint is used verbatim. Otherwise it is derived from diff --git a/deploy/helm/openshell/templates/db-secret.yaml b/deploy/helm/openshell/templates/db-secret.yaml new file mode 100644 index 000000000..b7e5bb0e3 --- /dev/null +++ b/deploy/helm/openshell/templates/db-secret.yaml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +{{/* +Chart-managed db Secret for external PostgreSQL only. +When postgres.deploy=true the Bitnami subchart creates the service-binding +secret (with uri key) via postgres.serviceBindings.enabled=true. +When postgres.external.existingSecret is set the user brings their own Secret. +*/}} +{{- if and .Values.postgres.enabled (not .Values.postgres.deploy) (not .Values.postgres.external.existingSecret) }} +{{- $host := required "postgres.external.host is required when postgres.deploy=false and no postgres.external.existingSecret is provided" .Values.postgres.external.host }} +{{- $pw := required "postgres.external.password is required when postgres.deploy=false and no postgres.external.existingSecret is provided" .Values.postgres.external.password }} +{{- $port := toString (int (default 5432 .Values.postgres.external.port)) }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "openshell.dbSecretName" . }} + labels: + {{- include "openshell.labels" . | nindent 4 }} +type: Opaque +data: + host: {{ $host | b64enc | quote }} + port: {{ $port | b64enc | quote }} + username: {{ .Values.postgres.external.username | b64enc | quote }} + password: {{ $pw | b64enc | quote }} + database: {{ .Values.postgres.external.database | b64enc | quote }} + uri: {{ printf "postgresql://%s:%s@%s:%s/%s" (.Values.postgres.external.username | urlquery) ($pw | urlquery) $host $port .Values.postgres.external.database | b64enc | quote }} +{{- end }} diff --git a/deploy/helm/openshell/templates/gateway-config.yaml b/deploy/helm/openshell/templates/gateway-config.yaml index bd74664c5..6a8cbca83 100644 --- a/deploy/helm/openshell/templates/gateway-config.yaml +++ b/deploy/helm/openshell/templates/gateway-config.yaml @@ -8,7 +8,8 @@ at startup. CLI flags and OPENSHELL_* env vars on the StatefulSet container still override anything in this file. One value is intentionally NOT rendered here: - - server.dbUrl → passed via --db-url in the StatefulSet args + - server.dbUrl → passed via OPENSHELL_DB_URL env var (from Secret) + when postgres.enabled=true, or --db-url arg for SQLite */}} apiVersion: v1 kind: ConfigMap diff --git a/deploy/helm/openshell/templates/statefulset.yaml b/deploy/helm/openshell/templates/statefulset.yaml index 5dd4f1caf..53c3c4320 100644 --- a/deploy/helm/openshell/templates/statefulset.yaml +++ b/deploy/helm/openshell/templates/statefulset.yaml @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 - +{{- if and .Values.postgres.deploy (not .Values.postgres.enabled) }} +{{- fail "postgres.deploy=true requires postgres.enabled=true" }} +{{- end }} apiVersion: apps/v1 kind: StatefulSet metadata: @@ -21,6 +23,9 @@ spec: # without this annotation a `helm upgrade` that only mutates the # ConfigMap would leave pods running with stale config. checksum/gateway-config: {{ include (print $.Template.BasePath "/gateway-config.yaml") . | sha256sum }} + {{- if and .Values.postgres.enabled (not .Values.postgres.deploy) (not .Values.postgres.external.existingSecret) }} + checksum/db-secret: {{ include (print $.Template.BasePath "/db-secret.yaml") . | sha256sum }} + {{- end }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} @@ -54,9 +59,18 @@ spec: args: - --config - /etc/openshell/gateway.toml + {{- if not .Values.postgres.enabled }} - --db-url - {{ .Values.server.dbUrl | quote }} + {{- end }} env: + {{- if .Values.postgres.enabled }} + - name: OPENSHELL_DB_URL + valueFrom: + secretKeyRef: + name: {{ include "openshell.dbSecretName" . }} + key: uri + {{- end }} # All gateway settings live in the ConfigMap-backed TOML file # mounted at /etc/openshell/gateway.toml. The only env var below # is a process-level setting consumed by libraries outside @@ -137,7 +151,7 @@ spec: - name: sandbox-jwt secret: secretName: {{ .Values.server.sandboxJwt.signingSecretName | default (printf "%s-jwt-keys" (include "openshell.fullname" .)) }} - defaultMode: 0400 + defaultMode: {{ .Values.server.sandboxJwt.secretDefaultMode | default 0400 }} {{- if not .Values.server.disableTls }} - name: tls-cert secret: diff --git a/deploy/helm/openshell/tests/gateway_config_test.yaml b/deploy/helm/openshell/tests/gateway_config_test.yaml index f17203c6f..8a820c218 100644 --- a/deploy/helm/openshell/tests/gateway_config_test.yaml +++ b/deploy/helm/openshell/tests/gateway_config_test.yaml @@ -5,6 +5,7 @@ suite: gateway TOML config shape templates: - templates/gateway-config.yaml - templates/statefulset.yaml + - templates/db-secret.yaml release: name: openshell namespace: my-namespace @@ -125,3 +126,195 @@ tests: - matchRegex: path: data["gateway.toml"] pattern: 'server_sans\s*=\s*\["openshell", "\*\.dev\.openshell\.localhost"\]' + + - it: passes sqlite db-url via --db-url arg by default + template: templates/statefulset.yaml + asserts: + - contains: + path: spec.template.spec.containers[0].args + content: "sqlite:/var/openshell/openshell.db" + + - it: does not create a db Secret when postgres is disabled + template: templates/db-secret.yaml + asserts: + - hasDocuments: + count: 0 + + - it: does not pass --db-url in args when postgres is enabled + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.deploy: true + postgres.auth.password: test-pw + asserts: + - notContains: + path: spec.template.spec.containers[0].args + content: "--db-url" + + - it: references Bitnami service-binding Secret when postgres.deploy is true + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.deploy: true + postgres.auth.password: test-pw + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OPENSHELL_DB_URL + valueFrom: + secretKeyRef: + name: openshell-postgres-svcbind-custom-user + key: uri + + - it: respects postgres.fullnameOverride for bundled service-binding Secret + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.deploy: true + postgres.auth.password: test-pw + postgres.fullnameOverride: my-pg + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OPENSHELL_DB_URL + valueFrom: + secretKeyRef: + name: my-pg-svcbind-custom-user + key: uri + + - it: respects postgres.nameOverride for bundled service-binding Secret + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.deploy: true + postgres.auth.password: test-pw + postgres.nameOverride: pgdb + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OPENSHELL_DB_URL + valueFrom: + secretKeyRef: + name: openshell-pgdb-svcbind-custom-user + key: uri + + - it: references chart-created Secret for external postgres + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.deploy: false + postgres.external.host: pg.example.com + postgres.external.password: ext-pw + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OPENSHELL_DB_URL + valueFrom: + secretKeyRef: + name: openshell-db + key: uri + + - it: references existing Secret when existingSecret is set + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.external.existingSecret: my-pg-secret + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: OPENSHELL_DB_URL + valueFrom: + secretKeyRef: + name: my-pg-secret + key: uri + + - it: does not create db Secret when postgres.deploy is true + template: templates/db-secret.yaml + set: + postgres.enabled: true + postgres.deploy: true + postgres.auth.password: test-pw + asserts: + - hasDocuments: + count: 0 + + - it: does not create db Secret when existingSecret is set + template: templates/db-secret.yaml + set: + postgres.enabled: true + postgres.external.existingSecret: my-pg-secret + asserts: + - hasDocuments: + count: 0 + + - it: creates db Secret with individual keys for external postgres + template: templates/db-secret.yaml + set: + postgres.enabled: true + postgres.deploy: false + postgres.external.host: external-postgres.example.com + postgres.external.port: 5432 + postgres.external.database: openshell_ext + postgres.external.username: ext_user + postgres.external.password: ext_pass + asserts: + - isKind: + of: Secret + - equal: + path: data.host + decodeBase64: true + value: "external-postgres.example.com" + - equal: + path: data.username + decodeBase64: true + value: "ext_user" + - equal: + path: data.password + decodeBase64: true + value: "ext_pass" + - equal: + path: data.database + decodeBase64: true + value: "openshell_ext" + - equal: + path: data.uri + decodeBase64: true + value: "postgresql://ext_user:ext_pass@external-postgres.example.com:5432/openshell_ext" + + - it: URL-encodes special characters in external credentials + template: templates/db-secret.yaml + set: + postgres.enabled: true + postgres.deploy: false + postgres.external.host: pg.example.com + postgres.external.username: "user@corp" + postgres.external.password: "p@ss:word/secret" + postgres.external.database: mydb + asserts: + - equal: + path: data.uri + decodeBase64: true + value: "postgresql://user%40corp:p%40ss%3Aword%2Fsecret@pg.example.com:5432/mydb" + + - it: fails when postgres.deploy=true but postgres.enabled=false + template: templates/statefulset.yaml + set: + postgres.deploy: true + asserts: + - failedTemplate: + errorMessage: "postgres.deploy=true requires postgres.enabled=true" + + - it: fails when external postgres has no password and no existingSecret + template: templates/statefulset.yaml + set: + postgres.enabled: true + postgres.deploy: false + postgres.external.host: my-host.example.com + asserts: + - failedTemplate: + errorMessage: "postgres.external.password is required when postgres.deploy=false and no postgres.external.existingSecret is provided" diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 2d707168c..d4d45389a 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -206,6 +206,10 @@ server: # values outside [600, 86400]. Default 3600 — generous, since the # supervisor consumes the token within seconds of pod start. k8sSaTokenTtlSecs: 3600 + # -- File mode for the mounted JWT signing key Secret. Default 0400 + # (owner-read only). Override to 0440 or 0444 if the container UID + # does not match the volume file owner. + secretDefaultMode: "" # OIDC (OpenID Connect) configuration for JWT-based authentication. # When issuer is set, the server validates Bearer tokens on gRPC requests. oidc: @@ -231,6 +235,46 @@ server: # issuer uses a non-public CA (e.g. OpenShift ingress, private PKI). caConfigMapName: "" +# Optional PostgreSQL backing store. +# - enabled=false (default): gateway uses server.dbUrl (SQLite by default). +# - enabled=true + deploy=true: deploy bundled PostgreSQL subchart and derive dbUrl. +# - enabled=true + deploy=false: use postgres.external.existingSecret or compose +# from postgres.external.* fields. +postgres: + enabled: false + # -- Deploy the bundled Bitnami PostgreSQL subchart. Set to true to + # run PostgreSQL alongside the gateway. Leave false when using an + # external PostgreSQL instance. + deploy: false + # External mode connection settings (used when deploy=false). + external: + # -- Name of a pre-existing Opaque Secret containing PostgreSQL + # credentials. When set, the chart does not create its own db Secret + # and reads directly from this one. The Secret must contain a `uri` + # key with the full connection string, e.g. + # postgresql://user:pass@host:5432/dbname. + existingSecret: "" + host: "" + port: 5432 + database: openshell + username: openshell + password: "" + # Values below also configure the bundled Bitnami PostgreSQL subchart + # (aliased as "postgres" in Chart.yaml). The subchart uses these to + # initialise the PostgreSQL instance; the gateway uses them to compose + # the connection URL. They must stay in sync. + auth: + username: openshell + password: "" + database: openshell + # Enable Bitnami service-binding Secrets so the gateway can read the + # connection URI directly from the subchart-managed Secret. + serviceBindings: + enabled: true + primary: + persistence: + enabled: true + # NetworkPolicy restricting SSH ingress on sandbox pods to the gateway only. networkPolicy: # -- Create a NetworkPolicy restricting SSH ingress on sandbox pods to the gateway. diff --git a/tasks/helm.toml b/tasks/helm.toml index 31788088a..f25dadb09 100644 --- a/tasks/helm.toml +++ b/tasks/helm.toml @@ -26,6 +26,7 @@ hide = true description = "Lint the openshell Helm chart (defaults + all CI configuration variants)" run = """ set -e + helm dependency build deploy/helm/openshell echo "--- helm lint: defaults ---" echo "values files: deploy/helm/openshell/values.yaml" helm lint deploy/helm/openshell @@ -45,6 +46,7 @@ run = """ if ! helm plugin list | grep -q unittest; then helm plugin install https://github.com/helm-unittest/helm-unittest --verify=false fi + helm dependency build deploy/helm/openshell helm unittest deploy/helm/openshell """