mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-25 06:42:52 +08:00
feat(helm): migrate gateway configuration to gatewayConfig (#3384)
* feat(helm): add generic gateway TOML serializer Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * docs(architecture): define Helm gateway configuration boundary Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * feat(helm): define default gateway configuration map Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * refactor(helm): render gateway config from values map Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * test(helm): cover generic gateway TOML rendering Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * feat(helm): protect gateway config secret boundary Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * docs(helm): classify legacy gateway configuration values Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * refactor(helm): derive dual-use resources from gateway config Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * test(helm): migrate gateway config scenarios Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * refactor(helm): derive credential resources from gateway config Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * refactor(helm): migrate gateway config overlays Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * refactor(helm): remove gateway configuration shadow values Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * refactor(helm): align runtime config with resources Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * docs(helm): add gateway config migration guide Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * test(helm): validate rendered gateway config Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * test(helm): cover config resource coherence Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): make Kubernetes E2E deployable Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(e2e): pass host aliases through gateway config Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * docs(e2e): reference gateway config host alias Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): enforce gateway resource ownership Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * test(e2e): cover chart host gateway input Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): address gateway config review findings Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): repair RFC 0012 migration integration Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(e2e): align Kubernetes parity with RFC 0012 Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): complete gateway config migration Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * chore(helm): add SPDX header to TOML template * fix(helm): preserve legacy gateway configuration aliases Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): complete legacy gateway config compatibility Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * feat(helm): support gateway config arrays of tables * fix(helm): preserve sandbox identity aliases * fix(helm): address gateway config review findings Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> * fix(helm): preserve gateway config migration settings Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * test(helm): validate extension arrays in CI values Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * fix(helm): restore unconditional SPDX headers Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * fix(helm): restore route SPDX header placement Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * docs(helm): simplify configuration and migration examples Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * docs(helm): remove redundant flag explanations Signed-off-by: Taylor Mutch <taylormutch@gmail.com> * docs(helm): move chart configuration guidance to Kubernetes setup Signed-off-by: Taylor Mutch <taylormutch@gmail.com> --------- Signed-off-by: Gaizka Menendez Hernandez <gmenende@redhat.com> Signed-off-by: Taylor Mutch <taylormutch@gmail.com> Co-authored-by: Taylor Mutch <taylormutch@gmail.com>
This commit is contained in:
co-authored by
Taylor Mutch
parent
aad551d3f7
commit
fe3942f2da
@@ -763,6 +763,17 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_rate_limits_reject_negative_values_during_toml_parsing() {
|
||||
for field in ["grpc_rate_limit_requests", "grpc_rate_limit_window_seconds"] {
|
||||
let tmp = write_tmp(&format!("[openshell.gateway]\n{field} = -1\n"));
|
||||
assert!(
|
||||
matches!(load(tmp.path()), Err(ConfigFileError::Parse { .. })),
|
||||
"{field} must reject negative values because gateway rate limits are unsigned"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_compute_driver_is_singular() {
|
||||
let file: ConfigFile = toml::from_str(
|
||||
|
||||
@@ -240,9 +240,37 @@ database. The chart creates a retained Kubernetes Secret with the shared
|
||||
key-encryption key and injects that key into every gateway pod, so the same
|
||||
default works for single-replica and external database-backed HA deployments.
|
||||
|
||||
Use `kubernetes-secrets` or `vault` instead when credentials should live in a
|
||||
cluster or external secret backend. Enabling one external credential driver
|
||||
disables the default credential-storage key-encryption key Secret and env injection.
|
||||
Use `gatewayConfig` to select `kubernetes-secrets` or `vault` when credentials
|
||||
should live in a cluster or external secret backend. Selecting an external
|
||||
credential driver disables the default credential-storage key-encryption-key
|
||||
Secret and environment injection. The map is rendered directly as gateway TOML,
|
||||
so it uses the gateway's snake_case field names:
|
||||
|
||||
```yaml
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- vault
|
||||
openshell.credential_drivers.vault:
|
||||
address: https://vault.vault.svc.cluster.local:8200
|
||||
mount: secret
|
||||
kv_version: "2"
|
||||
auth_method: kubernetes
|
||||
role: openshell-gateway
|
||||
```
|
||||
|
||||
> `gatewayConfig` must contain only non-secret values. Helm serializes unknown
|
||||
> fields generically and cannot determine whether an arbitrary string, such as
|
||||
> `api_token`, is confidential. Do not put passwords, tokens, private keys,
|
||||
> database URLs, or other secret material in this map. Use Secret-backed
|
||||
> environment variables, files, volumes, or gateway credential drivers instead.
|
||||
> The chart rejects known unsafe forms such as `database_url`, inline URL
|
||||
> credentials, and PEM private keys; it is not a general secret scanner.
|
||||
|
||||
For the Kubernetes Secret driver, use
|
||||
`openshell.credential_drivers.kubernetes-secrets.namespace` in the same map.
|
||||
The chart derives any required RBAC from the selected driver; use a dedicated
|
||||
namespace to limit access to OpenShell-managed Secrets.
|
||||
|
||||
#### OpenShift
|
||||
|
||||
@@ -318,12 +346,16 @@ JWT signing Secret.
|
||||
|
||||
## SPIFFE/SPIRE provider token grants
|
||||
|
||||
Set `server.providerTokenGrants.spiffe.enabled=true` to let the gateway and
|
||||
sandbox supervisors use SPIFFE JWT-SVIDs for dynamic provider token grants. The
|
||||
chart keeps supervisor-to-gateway authentication on gateway-minted sandbox JWTs,
|
||||
mounts the SPIFFE CSI socket into the gateway pod, exports
|
||||
`OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`, and passes the socket path to
|
||||
the Kubernetes driver so sandbox pods can mount the same socket.
|
||||
Set `gatewayConfig.openshell.drivers.kubernetes.provider_spiffe_workload_api_socket_path`
|
||||
to let sandbox supervisors use SPIFFE JWT-SVIDs for dynamic provider token
|
||||
grants. The chart keeps supervisor-to-gateway authentication on gateway-minted
|
||||
sandbox JWTs and passes the configured socket path to the Kubernetes driver.
|
||||
|
||||
```yaml
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
provider_spiffe_workload_api_socket_path: /spiffe-workload-api/spire-agent.sock
|
||||
```
|
||||
|
||||
For local development, uncomment the SPIRE Helm releases in `skaffold.yaml` and
|
||||
add `ci/values-spire.yaml` to the OpenShell release values files.
|
||||
@@ -353,12 +385,14 @@ discovery endpoint or its TLS CA.
|
||||
| certManager.serverDnsNames | list | `["openshell","openshell.openshell.svc","openshell.openshell.svc.cluster.local","localhost","openshell.localhost","*.openshell.localhost","host.docker.internal"]` | DNS SANs on the cert-manager-issued server certificate. |
|
||||
| certManager.serverIpAddresses | list | `["127.0.0.1"]` | IP SANs on the cert-manager-issued server certificate. |
|
||||
| certManager.serverIssuerRef | object | `{"group":"","kind":"","name":""}` | Override the issuerRef for the external server Certificate (e.g. a real LetsEncrypt/ACME ClusterIssuer for a publicly-trusted cert on an external hostname). When set, the chart creates a second server certificate from this issuer with only the hostnames in serverDnsNames; the internal server certificate is always signed by the chart's own CA. Leave name empty to use the chart CA for all server certificates (default). Requires certManager.enabled=true. |
|
||||
| credentialDrivers.vault.caConfigMapName | string | `""` | ConfigMap containing the private Vault/OpenBao CA certificate under the ca.crt key. Helm mounts it only when the Vault driver is selected. |
|
||||
| fullnameOverride | string | `""` | Override the full generated resource name. |
|
||||
| gateway.image.digest | string | `""` | Gateway image digest. When set, this takes precedence over tag. |
|
||||
| gateway.image.pullPolicy | string | `nil` | Gateway image pull policy. Empty uses global.image.pullPolicy. |
|
||||
| gateway.image.registry | string | `""` | Gateway image registry. Empty uses global.image.registry. |
|
||||
| gateway.image.repository | string | `"openshell/gateway"` | Gateway image repository. |
|
||||
| gateway.image.tag | string | `""` | Gateway image tag. Defaults to the chart appVersion when empty. |
|
||||
| gatewayConfig | object | `{"openshell":{"version":2}}` | Non-secret gateway application configuration. Top-level keys name TOML tables and are rendered into the mounted gateway.toml file. Kubernetes resource inputs remain outside this map; template expressions derive the corresponding runtime values from their resource owner. |
|
||||
| global.image.pullPolicy | string | `"IfNotPresent"` | Shared OpenShell image pull policy. Individual image pull policies take precedence. |
|
||||
| global.image.registry | string | `"ghcr.io/nvidia"` | Shared OpenShell image registry. Individual image registries take precedence. |
|
||||
| global.image.tag | string | `""` | Shared OpenShell image tag. Defaults to the chart appVersion when empty. |
|
||||
|
||||
@@ -241,9 +241,37 @@ database. The chart creates a retained Kubernetes Secret with the shared
|
||||
key-encryption key and injects that key into every gateway pod, so the same
|
||||
default works for single-replica and external database-backed HA deployments.
|
||||
|
||||
Use `kubernetes-secrets` or `vault` instead when credentials should live in a
|
||||
cluster or external secret backend. Enabling one external credential driver
|
||||
disables the default credential-storage key-encryption key Secret and env injection.
|
||||
Use `gatewayConfig` to select `kubernetes-secrets` or `vault` when credentials
|
||||
should live in a cluster or external secret backend. Selecting an external
|
||||
credential driver disables the default credential-storage key-encryption-key
|
||||
Secret and environment injection. The map is rendered directly as gateway TOML,
|
||||
so it uses the gateway's snake_case field names:
|
||||
|
||||
```yaml
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- vault
|
||||
openshell.credential_drivers.vault:
|
||||
address: https://vault.vault.svc.cluster.local:8200
|
||||
mount: secret
|
||||
kv_version: "2"
|
||||
auth_method: kubernetes
|
||||
role: openshell-gateway
|
||||
```
|
||||
|
||||
> `gatewayConfig` must contain only non-secret values. Helm serializes unknown
|
||||
> fields generically and cannot determine whether an arbitrary string, such as
|
||||
> `api_token`, is confidential. Do not put passwords, tokens, private keys,
|
||||
> database URLs, or other secret material in this map. Use Secret-backed
|
||||
> environment variables, files, volumes, or gateway credential drivers instead.
|
||||
> The chart rejects known unsafe forms such as `database_url`, inline URL
|
||||
> credentials, and PEM private keys; it is not a general secret scanner.
|
||||
|
||||
For the Kubernetes Secret driver, use
|
||||
`openshell.credential_drivers.kubernetes-secrets.namespace` in the same map.
|
||||
The chart derives any required RBAC from the selected driver; use a dedicated
|
||||
namespace to limit access to OpenShell-managed Secrets.
|
||||
|
||||
#### OpenShift
|
||||
|
||||
@@ -319,12 +347,16 @@ JWT signing Secret.
|
||||
|
||||
## SPIFFE/SPIRE provider token grants
|
||||
|
||||
Set `server.providerTokenGrants.spiffe.enabled=true` to let the gateway and
|
||||
sandbox supervisors use SPIFFE JWT-SVIDs for dynamic provider token grants. The
|
||||
chart keeps supervisor-to-gateway authentication on gateway-minted sandbox JWTs,
|
||||
mounts the SPIFFE CSI socket into the gateway pod, exports
|
||||
`OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`, and passes the socket path to
|
||||
the Kubernetes driver so sandbox pods can mount the same socket.
|
||||
Set `gatewayConfig.openshell.drivers.kubernetes.provider_spiffe_workload_api_socket_path`
|
||||
to let sandbox supervisors use SPIFFE JWT-SVIDs for dynamic provider token
|
||||
grants. The chart keeps supervisor-to-gateway authentication on gateway-minted
|
||||
sandbox JWTs and passes the configured socket path to the Kubernetes driver.
|
||||
|
||||
```yaml
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
provider_spiffe_workload_api_socket_path: /spiffe-workload-api/spire-agent.sock
|
||||
```
|
||||
|
||||
For local development, uncomment the SPIRE Helm releases in `skaffold.yaml` and
|
||||
add `ci/values-spire.yaml` to the OpenShell release values files.
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
# The Kubernetes corporate-proxy e2e wrapper supplies the generated proxy URL
|
||||
# and creates `openshell-e2e-proxy-auth` before Helm installs the gateway.
|
||||
upstreamProxy:
|
||||
authSecret:
|
||||
name: openshell-e2e-proxy-auth
|
||||
key: proxy-auth
|
||||
authAllowInsecure: true
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
# The e2e wrapper replaces this endpoint with its dynamically allocated
|
||||
# port. Keep the overlay valid when rendered independently as well.
|
||||
https_proxy: http://host.openshell.internal:8080
|
||||
proxy_auth_secret_name: openshell-e2e-proxy-auth
|
||||
proxy_auth_secret_key: proxy-auth
|
||||
proxy_auth_allow_insecure: true
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
# Use with:
|
||||
# skaffold run -p credential-driver-kubernetes-secrets
|
||||
#
|
||||
server:
|
||||
credentialDrivers:
|
||||
kubernetesSecrets:
|
||||
enabled: true
|
||||
namespace: openshell
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- kubernetes-secrets
|
||||
openshell.credential_drivers.kubernetes-secrets:
|
||||
namespace: openshell
|
||||
|
||||
@@ -8,15 +8,19 @@
|
||||
#
|
||||
# The profile assumes another process has already deployed a Vault-compatible
|
||||
# backend. Local e2e validation deploys OpenBao in the `openbao` namespace with
|
||||
# TLS enabled, publishes its private CA in the `openbao-ca` ConfigMap, and
|
||||
# creates an `openbao-0` DNS alias matching the OpenBao dev certificate. It also
|
||||
# configures a Kubernetes auth role named `openshell-gateway` bound to the
|
||||
# OpenShell gateway ServiceAccount in the `openshell` namespace.
|
||||
# a Kubernetes auth role named `openshell-gateway` bound to the OpenShell
|
||||
# gateway ServiceAccount in the `openshell` namespace.
|
||||
|
||||
server:
|
||||
credentialDrivers:
|
||||
vault:
|
||||
enabled: true
|
||||
address: https://openbao-0:8200
|
||||
caConfigMapName: openbao-ca
|
||||
role: openshell-gateway
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- vault
|
||||
openshell.credential_drivers.vault:
|
||||
# The local E2E fixture exposes this alias in the gateway namespace. It
|
||||
# also matches the DNS SAN in OpenBao's development TLS certificate.
|
||||
address: https://openbao-0:8200
|
||||
role: openshell-gateway
|
||||
|
||||
credentialDrivers:
|
||||
vault:
|
||||
caConfigMapName: openbao-ca
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Render/preflight example for operator-run extensions. Top-level YAML lists
|
||||
# emit one TOML [[table]] per entry, preserving the list and explicit order.
|
||||
# The CI parser check does not connect to these example endpoints. Deploy the
|
||||
# services separately before using this overlay with a running gateway.
|
||||
gatewayConfig:
|
||||
openshell.gateway.interceptors:
|
||||
- name: governance
|
||||
grpc_endpoint: http://governance.ns.svc.cluster.local:18081
|
||||
allow_insecure_transport: true
|
||||
order: 0
|
||||
failure_policy: fail_closed
|
||||
- name: audit
|
||||
grpc_endpoint: http://audit.ns.svc.cluster.local:18081
|
||||
allow_insecure_transport: true
|
||||
order: 1
|
||||
failure_policy: fail_open
|
||||
openshell.supervisor.middleware:
|
||||
- name: content-guard
|
||||
grpc_endpoint: http://content-guard.ns.svc.cluster.local:18082
|
||||
allow_insecure_transport: true
|
||||
max_payload_bytes: 262144
|
||||
timeout: 500ms
|
||||
- name: request-audit
|
||||
grpc_endpoint: http://request-audit.ns.svc.cluster.local:18082
|
||||
allow_insecure_transport: true
|
||||
max_payload_bytes: 262144
|
||||
timeout: 1s
|
||||
@@ -28,6 +28,8 @@ grpcRoute:
|
||||
server:
|
||||
# Envoy terminates TLS at the edge; the gateway listens plaintext behind it.
|
||||
disableTls: true
|
||||
oidc:
|
||||
|
||||
gatewayConfig:
|
||||
openshell.gateway.oidc:
|
||||
issuer: "https://keycloak.example.com/realms/openshell"
|
||||
audience: "openshell-cli"
|
||||
|
||||
@@ -21,18 +21,23 @@
|
||||
# CLI token acquisition: keep a port-forward running while using openshell login:
|
||||
# kubectl -n keycloak port-forward svc/keycloak 9090:80
|
||||
|
||||
server:
|
||||
oidc:
|
||||
gatewayConfig:
|
||||
openshell.gateway.oidc:
|
||||
# Must match KC_HOSTNAME set by keycloak:k8s:setup (in-cluster service hostname).
|
||||
issuer: "https://keycloak.keycloak.svc.cluster.local:443/realms/openshell"
|
||||
caConfigMapName: "openshell-keycloak-ca"
|
||||
# Must match the client ID in the imported realm (openshell-cli).
|
||||
audience: "openshell-cli"
|
||||
# Short TTL for dev so JWKS key rotation is picked up quickly.
|
||||
# Use 3600 (default) in production.
|
||||
jwksTtl: 60
|
||||
jwks_ttl_secs: 60
|
||||
# Keycloak puts realm roles at realm_access.roles in the JWT.
|
||||
rolesClaim: "realm_access.roles"
|
||||
roles_claim: "realm_access.roles"
|
||||
# Leave both empty for authentication-only mode (any valid token is accepted).
|
||||
adminRole: "openshell-admin"
|
||||
userRole: "openshell-user"
|
||||
admin_role: "openshell-admin"
|
||||
user_role: "openshell-user"
|
||||
|
||||
# This is a Kubernetes ConfigMap mounted by the chart, not a gateway TOML
|
||||
# field, so it remains a chart-owned resource reference.
|
||||
server:
|
||||
oidc:
|
||||
caConfigMapName: "openshell-keycloak-ca"
|
||||
|
||||
@@ -25,21 +25,22 @@
|
||||
# is true and mTLS is MANDATORY at the TLS handshake — a caller with only the
|
||||
# Route URL and no client certificate is rejected before any RPC. The passthrough
|
||||
# Route terminates TLS at the gateway pod, so this holds end-to-end.
|
||||
# `allowUnauthenticatedUsers` only promotes the already cert-verified caller to a
|
||||
# `allow_unauthenticated_users` only promotes the already cert-verified caller to a
|
||||
# dev principal at the app layer (mtls_auth is unsupported with the Kubernetes
|
||||
# driver). Both are required together; the client certificate is the access gate.
|
||||
gateway:
|
||||
image:
|
||||
pullPolicy: Always
|
||||
|
||||
supervisor:
|
||||
image:
|
||||
pullPolicy: Always
|
||||
|
||||
server:
|
||||
disableTls: false
|
||||
auth:
|
||||
allowUnauthenticatedUsers: true
|
||||
|
||||
gatewayConfig:
|
||||
openshell.gateway.auth:
|
||||
allow_unauthenticated_users: true
|
||||
openshell.drivers.kubernetes:
|
||||
sandbox_runtime_image_pull_policy: always
|
||||
supervisor_image_pull_policy: always
|
||||
|
||||
openshiftRoute:
|
||||
enabled: true
|
||||
|
||||
@@ -2,6 +2,14 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Merge with values.yaml for Skaffold-driven local image builds (see skaffold.yaml).
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
image_pull_policy: if_not_present
|
||||
openshell.gateway.otlp:
|
||||
endpoint: http://openshell-collector.observability.svc.cluster.local:4317
|
||||
openshell.gateway.auth:
|
||||
allow_unauthenticated_users: true
|
||||
|
||||
server:
|
||||
otlp:
|
||||
endpoint: http://openshell-collector.observability.svc.cluster.local:4317
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# OpenShell overlay for local SPIRE-backed provider token grants.
|
||||
server:
|
||||
providerTokenGrants:
|
||||
spiffe:
|
||||
enabled: true
|
||||
workloadApiSocketPath: /spiffe-workload-api/spire-agent.sock
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
provider_spiffe_workload_api_socket_path: /spiffe-workload-api/spire-agent.sock
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
#
|
||||
# E2E overlay: deploy the gateway in managed workspace mode.
|
||||
# Sandbox namespaces are auto-created as openshell-{gateway_id}-{workspace}.
|
||||
server:
|
||||
sandboxImagePullSecrets:
|
||||
- name: e2e-regcred
|
||||
drivers:
|
||||
kubernetes:
|
||||
workspaceMode: "managed"
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
image_pull_secrets:
|
||||
- e2e-regcred
|
||||
workspace_mode: managed
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#
|
||||
# E2E overlay: deploy the gateway in operator workspace mode.
|
||||
# Namespaces must be pre-provisioned and labeled before sandbox creation.
|
||||
server:
|
||||
drivers:
|
||||
kubernetes:
|
||||
workspaceMode: "operator"
|
||||
operatorNamespaceLabel: "openshell.ai/e2e-operator-workspace=true"
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
workspace_mode: operator
|
||||
operator_namespace_label: "openshell.ai/e2e-operator-workspace=true"
|
||||
|
||||
@@ -148,6 +148,7 @@ deploy:
|
||||
supervisor.image.tag: '{{.IMAGE_TAG_openshell_supervisor}}'
|
||||
sandboxRuntime.image.repository: '{{.IMAGE_REPO_openshell_sandbox}}'
|
||||
sandboxRuntime.image.tag: '{{.IMAGE_TAG_openshell_sandbox}}'
|
||||
gatewayConfig.openshell\.drivers\.kubernetes.supervisor_image: '{{.IMAGE_REPO_openshell_supervisor}}:{{.IMAGE_TAG_openshell_supervisor}}'
|
||||
profiles:
|
||||
# Full HA test path: installs Envoy Gateway and layers both HA replicas and
|
||||
# Gateway API routing values onto the OpenShell release.
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
Gateway pod template shared by the StatefulSet and Deployment workload shapes.
|
||||
*/}}
|
||||
{{- define "openshell.gatewayPodTemplate" -}}
|
||||
{{- $gatewayConfig := .Values.gatewayConfig | default dict -}}
|
||||
{{- $oidcRuntimeConfig := get $gatewayConfig "openshell.gateway.oidc" | default dict -}}
|
||||
{{- if not (get $oidcRuntimeConfig "issuer") -}}{{- $oidcRuntimeConfig = .Values.server.oidc | default dict -}}{{- end -}}
|
||||
{{- $kubernetesRuntimeConfig := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- $spiffeSocketPath := get $kubernetesRuntimeConfig "provider_spiffe_workload_api_socket_path" -}}
|
||||
{{- $hasExternalCredentialDriver := or (eq (include "openshell.credentialDriverEnabled" (list . "kubernetes-secrets")) "true") (eq (include "openshell.credentialDriverEnabled" (list . "vault")) "true") -}}
|
||||
{{- $vaultCredentialDriverEnabled := eq (include "openshell.credentialDriverEnabled" (list . "vault")) "true" -}}
|
||||
{{- $credentialDrivers := .Values.credentialDrivers | default dict -}}
|
||||
{{- $vaultResources := get $credentialDrivers "vault" | default dict -}}
|
||||
{{- $legacyCredentialDrivers := .Values.server.credentialDrivers | default dict -}}
|
||||
{{- $legacyVaultResources := get $legacyCredentialDrivers "vault" | default dict -}}
|
||||
{{- $vaultCaConfigMapName := get $vaultResources "caConfigMapName" | default (get $legacyVaultResources "caConfigMapName") -}}
|
||||
metadata:
|
||||
annotations:
|
||||
# Roll the gateway workload when the rendered gateway TOML changes - the
|
||||
@@ -96,7 +108,7 @@ spec:
|
||||
value: /etc/openshell-tls/peer-client/tls.key
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if not (or .Values.server.credentialDrivers.kubernetesSecrets.enabled .Values.server.credentialDrivers.vault.enabled) }}
|
||||
{{- if not $hasExternalCredentialDriver }}
|
||||
- name: {{ include "openshell.credentialStorageKeyEncryptionKeyEnvName" . }}
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
@@ -114,7 +126,7 @@ spec:
|
||||
# mounted at /etc/openshell/gateway.toml. Secret-bearing settings use
|
||||
# env vars that the TOML references by name. Some process-level
|
||||
# settings consumed by libraries outside gateway code also remain here.
|
||||
{{- if and .Values.server.oidc.issuer .Values.server.oidc.caConfigMapName }}
|
||||
{{- if and (get $oidcRuntimeConfig "issuer") .Values.server.oidc.caConfigMapName }}
|
||||
# OIDC issuer custom-CA: rustls/reqwest read SSL_CERT_FILE for
|
||||
# outbound TLS verification. This is a process-level env var
|
||||
# consumed by the TLS stack itself, not by gateway code, so it
|
||||
@@ -124,9 +136,9 @@ spec:
|
||||
{{- end }}
|
||||
- name: OPENSHELL_TELEMETRY_ENABLED
|
||||
value: {{ .Values.server.telemetryEnabled | quote }}
|
||||
{{- if .Values.server.providerTokenGrants.spiffe.enabled }}
|
||||
{{- if $spiffeSocketPath }}
|
||||
- name: OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET
|
||||
value: {{ .Values.server.providerTokenGrants.spiffe.workloadApiSocketPath | quote }}
|
||||
value: {{ $spiffeSocketPath | quote }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
{{- if eq (include "openshell.workloadKind" .) "statefulset" }}
|
||||
@@ -164,12 +176,12 @@ spec:
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and .Values.server.oidc.issuer .Values.server.oidc.caConfigMapName }}
|
||||
{{- if and (get $oidcRuntimeConfig "issuer") .Values.server.oidc.caConfigMapName }}
|
||||
- name: oidc-ca
|
||||
mountPath: /etc/openshell-tls/oidc-ca
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if and .Values.server.credentialDrivers.vault.enabled .Values.server.credentialDrivers.vault.caConfigMapName }}
|
||||
{{- if and $vaultCredentialDriverEnabled $vaultCaConfigMapName }}
|
||||
- name: vault-ca
|
||||
mountPath: /etc/openshell-tls/vault-ca
|
||||
readOnly: true
|
||||
@@ -179,9 +191,9 @@ spec:
|
||||
mountPath: /etc/openshell-tls/proxy-ca
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if .Values.server.providerTokenGrants.spiffe.enabled }}
|
||||
{{- if $spiffeSocketPath }}
|
||||
- name: spiffe-workload-api
|
||||
mountPath: {{ dir .Values.server.providerTokenGrants.spiffe.workloadApiSocketPath | quote }}
|
||||
mountPath: {{ dir $spiffeSocketPath | quote }}
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- with .Values.server.extraVolumeMounts }}
|
||||
@@ -265,15 +277,15 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if and .Values.server.oidc.issuer .Values.server.oidc.caConfigMapName }}
|
||||
{{- if and (get $oidcRuntimeConfig "issuer") .Values.server.oidc.caConfigMapName }}
|
||||
- name: oidc-ca
|
||||
configMap:
|
||||
name: {{ .Values.server.oidc.caConfigMapName }}
|
||||
{{- end }}
|
||||
{{- if and .Values.server.credentialDrivers.vault.enabled .Values.server.credentialDrivers.vault.caConfigMapName }}
|
||||
{{- if and $vaultCredentialDriverEnabled $vaultCaConfigMapName }}
|
||||
- name: vault-ca
|
||||
configMap:
|
||||
name: {{ .Values.server.credentialDrivers.vault.caConfigMapName }}
|
||||
name: {{ $vaultCaConfigMapName }}
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
@@ -283,12 +295,10 @@ spec:
|
||||
configMap:
|
||||
name: {{ .Values.upstreamProxy.caBundle.configMapName | quote }}
|
||||
items:
|
||||
# The mounted filename stays fixed so the rendered proxy_ca_bundle
|
||||
# path does not depend on the operator's ConfigMap key.
|
||||
- key: {{ .Values.upstreamProxy.caBundle.key | default "ca.crt" | quote }}
|
||||
path: ca.crt
|
||||
{{- end }}
|
||||
{{- if .Values.server.providerTokenGrants.spiffe.enabled }}
|
||||
{{- if $spiffeSocketPath }}
|
||||
- name: spiffe-workload-api
|
||||
csi:
|
||||
driver: csi.spiffe.io
|
||||
|
||||
@@ -218,7 +218,7 @@ defaults.
|
||||
{{- if .Values.server.disableTls -}}
|
||||
{{- else if not .Values.server.tls.enableMtls -}}
|
||||
{{- else if eq .Values.server.tls.clientCaSecretName "" -}}
|
||||
{{- else if or .Values.server.tls.clientCaSecretName (and .Values.pkiInitJob.enabled (not .Values.certManager.enabled)) (and .Values.certManager.enabled .Values.certManager.clientCaFromServerTlsSecret) -}}
|
||||
{{- else -}}
|
||||
true
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -258,15 +258,16 @@ into per-generation Secrets in workspace namespaces, as a JSON array. Empty in
|
||||
shared workspace mode.
|
||||
*/}}
|
||||
{{- define "openshell.workspaceSecretSourceNames" -}}
|
||||
{{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" -}}
|
||||
{{- $kubernetesConfig := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- $workspaceMode := get $kubernetesConfig "workspace_mode" | default "shared" -}}
|
||||
{{- $names := list -}}
|
||||
{{- if and (ne $workspaceMode "shared") (not .Values.server.disableTls) -}}
|
||||
{{- $names = append $names .Values.server.tls.clientTlsSecretName -}}
|
||||
{{- end -}}
|
||||
{{- if eq $workspaceMode "managed" -}}
|
||||
{{- range .Values.server.sandboxImagePullSecrets -}}
|
||||
{{- if .name -}}
|
||||
{{- $names = append $names .name -}}
|
||||
{{- range (get $kubernetesConfig "image_pull_secrets" | default list) -}}
|
||||
{{- if . -}}
|
||||
{{- $names = append $names . -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -277,7 +278,26 @@ shared workspace mode.
|
||||
Namespace where Kubernetes Secret-backed provider credentials live.
|
||||
*/}}
|
||||
{{- define "openshell.credentialKubernetesSecretsNamespace" -}}
|
||||
{{- .Values.server.credentialDrivers.kubernetesSecrets.namespace | default .Release.Namespace -}}
|
||||
{{- $gatewayConfig := .Values.gatewayConfig | default dict -}}
|
||||
{{- $config := get $gatewayConfig "openshell.credential_drivers.kubernetes-secrets" | default dict -}}
|
||||
{{- $legacy := .Values.server.credentialDrivers.kubernetesSecrets | default dict -}}
|
||||
{{- get $config "namespace" | default (get $legacy "namespace") | default .Release.Namespace -}}
|
||||
{{- end }}
|
||||
|
||||
{{/* Whether a credential driver is enabled in the generic gateway config. */}}
|
||||
{{- define "openshell.credentialDriverEnabled" -}}
|
||||
{{- $root := index . 0 -}}
|
||||
{{- $driver := index . 1 -}}
|
||||
{{- $gatewayConfig := $root.Values.gatewayConfig | default dict -}}
|
||||
{{- $gateway := get $gatewayConfig "openshell.gateway" | default dict -}}
|
||||
{{- $configuredDrivers := get $gateway "credential_drivers" -}}
|
||||
{{- if and (hasKey $gateway "credential_drivers") (ne $configuredDrivers nil) -}}
|
||||
{{- if has $driver ($configuredDrivers | default list) -}}true{{- end -}}
|
||||
{{- else if eq $driver "kubernetes-secrets" -}}
|
||||
{{- if $root.Values.server.credentialDrivers.kubernetesSecrets.enabled -}}true{{- end -}}
|
||||
{{- else if eq $driver "vault" -}}
|
||||
{{- if $root.Values.server.credentialDrivers.vault.enabled -}}true{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
@@ -408,6 +428,134 @@ Booleans are rejected because they would otherwise convert to 1 or 0.
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Validate a non-empty, user-provided Kubernetes Secret name. Secret data never
|
||||
passes through Helm values into gateway.toml; only this reference is rendered.
|
||||
*/}}
|
||||
{{- define "openshell.validateSecretReference" -}}
|
||||
{{- $path := index . 0 -}}
|
||||
{{- $name := index . 1 -}}
|
||||
{{- if and (ne $name nil) (ne $name "") -}}
|
||||
{{- if not (kindIs "string" $name) -}}
|
||||
{{- fail (printf "%s must be a Kubernetes Secret name, got %s" $path (kindOf $name)) -}}
|
||||
{{- end -}}
|
||||
{{- if gt (len $name) 253 -}}
|
||||
{{- fail (printf "%s must be no more than 253 characters" $path) -}}
|
||||
{{- end -}}
|
||||
{{- if not (regexMatch "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$" $name) -}}
|
||||
{{- fail (printf "%s must be a valid Kubernetes Secret name" $path) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Return the effective Kubernetes driver configuration as YAML. Schema-v2
|
||||
gatewayConfig fields take precedence; deprecated 0.1.x aliases fill only
|
||||
absent fields so every chart consumer observes the same configuration.
|
||||
*/}}
|
||||
{{- define "openshell.effectiveKubernetesConfig" -}}
|
||||
{{- $gatewayConfig := deepCopy (.Values.gatewayConfig | default dict) -}}
|
||||
{{- $kubernetes := get $gatewayConfig "openshell.drivers.kubernetes" | default dict -}}
|
||||
{{- if hasKey $gatewayConfig "openshell.drivers.kubernetes.resource_admission" -}}
|
||||
{{- $_ := set $kubernetes "resource_admission" (get $gatewayConfig "openshell.drivers.kubernetes.resource_admission") -}}
|
||||
{{- end -}}
|
||||
{{- $legacySandbox := .Values.sandboxRuntime.image | default dict -}}
|
||||
{{- if and (include "openshell.sandboxRuntimeImageOverrideEnabled" .) (not (hasKey $kubernetes "sandbox_runtime_image")) -}}
|
||||
{{- $_ := set $kubernetes "sandbox_runtime_image" (include "openshell.sandboxRuntimeImage" .) -}}
|
||||
{{- end -}}
|
||||
{{- $legacySandboxImage := .Values.sandbox.image | default dict -}}
|
||||
{{- $legacySandboxPullPolicy := get $legacySandboxImage "pullPolicy" -}}
|
||||
{{- if and $legacySandboxPullPolicy (not (hasKey $kubernetes "image_pull_policy")) -}}
|
||||
{{- $_ := set $kubernetes "image_pull_policy" (include "openshell.canonicalImagePullPolicy" $legacySandboxPullPolicy) -}}
|
||||
{{- end -}}
|
||||
{{- $legacySandboxRuntimePullPolicy := get $legacySandbox "pullPolicy" | default .Values.global.image.pullPolicy -}}
|
||||
{{- if and $legacySandboxRuntimePullPolicy (not (hasKey $kubernetes "sandbox_runtime_image_pull_policy")) -}}
|
||||
{{- $_ := set $kubernetes "sandbox_runtime_image_pull_policy" (include "openshell.canonicalImagePullPolicy" $legacySandboxRuntimePullPolicy) -}}
|
||||
{{- end -}}
|
||||
{{- $legacySupervisor := .Values.supervisor.image | default dict -}}
|
||||
{{- $legacySupervisorImage := include "openshell.supervisorImage" . -}}
|
||||
{{- $defaultSupervisorImage := printf "ghcr.io/nvidia/openshell/supervisor:%s" .Chart.AppVersion -}}
|
||||
{{- if and (ne $legacySupervisorImage $defaultSupervisorImage) (not (hasKey $kubernetes "supervisor_image")) -}}
|
||||
{{- $_ := set $kubernetes "supervisor_image" $legacySupervisorImage -}}
|
||||
{{- end -}}
|
||||
{{- $legacySupervisorPullPolicy := get $legacySupervisor "pullPolicy" | default .Values.global.image.pullPolicy -}}
|
||||
{{- if and $legacySupervisorPullPolicy (not (hasKey $kubernetes "supervisor_image_pull_policy")) -}}
|
||||
{{- $_ := set $kubernetes "supervisor_image_pull_policy" (include "openshell.canonicalImagePullPolicy" $legacySupervisorPullPolicy) -}}
|
||||
{{- end -}}
|
||||
{{- $legacyRuntime := .Values.supervisor.sandboxRuntime | default dict -}}
|
||||
{{- $runtimeConfig := get $kubernetes "sandbox_runtime" | default dict -}}
|
||||
{{- if and (ne (int (get $legacyRuntime "boundaryPort" | default 5500)) 5500) (not (hasKey $runtimeConfig "boundary_port")) -}}
|
||||
{{- $_ := set $runtimeConfig "boundary_port" (int (get $legacyRuntime "boundaryPort")) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $kubernetes "sandbox_runtime" $runtimeConfig -}}
|
||||
{{- $legacyProxy := .Values.upstreamProxy | default dict -}}
|
||||
{{- range $legacyKey, $runtimeKey := dict "url" "https_proxy" "noProxy" "no_proxy" "authAllowInsecure" "proxy_auth_allow_insecure" "connectByHostname" "proxy_connect_by_hostname" -}}
|
||||
{{- if and (get $legacyProxy $legacyKey) (not (hasKey $kubernetes $runtimeKey)) -}}
|
||||
{{- $_ := set $kubernetes $runtimeKey (get $legacyProxy $legacyKey) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $legacyProxyAuth := get $legacyProxy "authSecret" | default dict -}}
|
||||
{{- if and (get $legacyProxyAuth "name") (not (hasKey $kubernetes "proxy_auth_secret_name")) -}}{{- $_ := set $kubernetes "proxy_auth_secret_name" (get $legacyProxyAuth "name") -}}{{- end -}}
|
||||
{{- if and (get $legacyProxyAuth "key") (not (hasKey $kubernetes "proxy_auth_secret_key")) -}}{{- $_ := set $kubernetes "proxy_auth_secret_key" (get $legacyProxyAuth "key") -}}{{- end -}}
|
||||
{{- $legacyProxyCa := get $legacyProxy "caBundle" | default dict -}}
|
||||
{{- if and (get $legacyProxyCa "configMapName") (not (hasKey $kubernetes "proxy_ca_bundle")) -}}
|
||||
{{- $_ := set $kubernetes "proxy_ca_bundle" "/etc/openshell-tls/proxy-ca/ca.crt" -}}
|
||||
{{- end -}}
|
||||
{{- $legacyKubernetes := .Values.server.drivers.kubernetes | default dict -}}
|
||||
{{- $legacyServer := .Values.server | default dict -}}
|
||||
{{- if not (hasKey $kubernetes "allow_driver_config") -}}{{- $_ := set $kubernetes "allow_driver_config" (get $legacyKubernetes "allowDriverConfig" | default false) -}}{{- end -}}
|
||||
{{- if and (ne (get $legacyKubernetes "workspaceMode" | default "shared") "shared") (not (hasKey $kubernetes "workspace_mode")) -}}
|
||||
{{- $_ := set $kubernetes "workspace_mode" (get $legacyKubernetes "workspaceMode") -}}
|
||||
{{- end -}}
|
||||
{{- range $legacyKey, $runtimeKey := dict "operatorNamespaceLabel" "operator_namespace_label" "operatorNamespaceFile" "operator_namespace_file" -}}
|
||||
{{- if and (get $legacyKubernetes $legacyKey) (not (hasKey $kubernetes $runtimeKey)) -}}
|
||||
{{- $_ := set $kubernetes $runtimeKey (get $legacyKubernetes $legacyKey) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "resource_admission") -}}
|
||||
{{- $legacyAdmission := get $legacyKubernetes "resourceAdmission" | default dict -}}
|
||||
{{- $admission := dict -}}
|
||||
{{- if hasKey $legacyAdmission "enabled" -}}{{- $_ := set $admission "enabled" (get $legacyAdmission "enabled") -}}{{- end -}}
|
||||
{{- if and (hasKey $legacyAdmission "requiredLabels") (ne (get $legacyAdmission "requiredLabels") nil) -}}{{- $_ := set $admission "required_labels" (deepCopy (get $legacyAdmission "requiredLabels")) -}}{{- end -}}
|
||||
{{- $_ := set $kubernetes "resource_admission" $admission -}}
|
||||
{{- end -}}
|
||||
{{- if and (get $legacyServer "enableUserNamespaces") (not (hasKey $kubernetes "enable_user_namespaces")) -}}{{- $_ := set $kubernetes "enable_user_namespaces" true -}}{{- end -}}
|
||||
{{- if and (get $legacyServer "hostGatewayIP") (not (hasKey $kubernetes "host_gateway_ip")) -}}{{- $_ := set $kubernetes "host_gateway_ip" (get $legacyServer "hostGatewayIP") -}}{{- end -}}
|
||||
{{/* Namespace and ServiceAccount refer to chart-created resources. */}}
|
||||
{{- $_ := set $kubernetes "namespace" (include "openshell.sandboxNamespace" .) -}}
|
||||
{{- if not (hasKey $kubernetes "default_image") -}}{{- $_ := set $kubernetes "default_image" (include "openshell.sandboxImage" .) -}}{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "gateway_id") -}}{{- $_ := set $kubernetes "gateway_id" (get (.Values.server.sandboxJwt | default dict) "gatewayId" | default (include "openshell.fullname" .)) -}}{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "grpc_endpoint") -}}{{- $_ := set $kubernetes "grpc_endpoint" (include "openshell.grpcEndpoint" .) -}}{{- end -}}
|
||||
{{- $_ := set $kubernetes "service_account_name" (include "openshell.sandboxServiceAccountName" .) -}}
|
||||
{{- if not (hasKey $kubernetes "sa_token_ttl_secs") -}}{{- $_ := set $kubernetes "sa_token_ttl_secs" (get (.Values.server.sandboxJwt | default dict) "k8sSaTokenTtlSecs" | default 3600) -}}{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "image_pull_secrets") -}}
|
||||
{{- $imagePullSecrets := list -}}{{- range (get $legacyServer "sandboxImagePullSecrets" | default list) }}{{- if .name }}{{- $imagePullSecrets = append $imagePullSecrets .name }}{{- end }}{{- end -}}
|
||||
{{- if $imagePullSecrets }}{{- $_ := set $kubernetes "image_pull_secrets" $imagePullSecrets -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $legacyKey, $runtimeKey := dict "workspaceDefaultStorageSize" "workspace_default_storage_size" "workspaceStorageClass" "workspace_storage_class" "defaultRuntimeClassName" "default_runtime_class_name" -}}
|
||||
{{- if and (get $legacyServer $legacyKey) (not (hasKey $kubernetes $runtimeKey)) -}}{{- $_ := set $kubernetes $runtimeKey (get $legacyServer $legacyKey) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "sandbox_uid") -}}
|
||||
{{- $sandboxUid := include "openshell.sandboxId" (dict "name" "server.sandboxUid" "value" (get $legacyServer "sandboxUid")) -}}
|
||||
{{- if ne $sandboxUid "" -}}{{- $_ := set $kubernetes "sandbox_uid" (int64 $sandboxUid) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "sandbox_gid") -}}
|
||||
{{- $sandboxGid := include "openshell.sandboxId" (dict "name" "server.sandboxGid" "value" (get $legacyServer "sandboxGid")) -}}
|
||||
{{- if ne $sandboxGid "" -}}{{- $_ := set $kubernetes "sandbox_gid" (int64 $sandboxGid) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $legacySpiffe := .Values.server.providerTokenGrants.spiffe | default dict -}}
|
||||
{{- if and (get $legacySpiffe "enabled") (not (hasKey $kubernetes "provider_spiffe_workload_api_socket_path")) -}}
|
||||
{{- $_ := set $kubernetes "provider_spiffe_workload_api_socket_path" (get $legacySpiffe "workloadApiSocketPath") -}}
|
||||
{{- end -}}
|
||||
{{/* Keep the rendered default configuration stable without making defaults look
|
||||
like user-supplied schema-v2 fields during compatibility resolution. */}}
|
||||
{{- if not (hasKey $kubernetes "workspace_mode") -}}{{- $_ := set $kubernetes "workspace_mode" "shared" -}}{{- end -}}
|
||||
{{- if not (hasKey $kubernetes "supervisor_image") -}}{{- $_ := set $kubernetes "supervisor_image" (include "openshell.supervisorImage" .) -}}{{- end -}}
|
||||
{{- if not (hasKey $runtimeConfig "boundary_port") -}}{{- $_ := set $runtimeConfig "boundary_port" 5500 -}}{{- end -}}
|
||||
{{- $_ := set $kubernetes "sandbox_runtime" $runtimeConfig -}}
|
||||
{{- toYaml $kubernetes -}}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Validate chart values that Helm would otherwise accept silently.
|
||||
*/}}
|
||||
@@ -419,6 +567,9 @@ Validate chart values that Helm would otherwise accept silently.
|
||||
{{- if and (hasKey .Values "postgres") (kindIs "map" .Values.postgres) (hasKey .Values.postgres "enabled") -}}
|
||||
{{- fail "postgres.enabled was removed; the OpenShell chart no longer deploys PostgreSQL. Provision PostgreSQL separately and set server.externalDbSecret to a Secret containing a PostgreSQL URI." -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.certManager.serverIssuerRef.name (not .Values.certManager.enabled) -}}
|
||||
{{- fail "certManager.serverIssuerRef.name is set but certManager.enabled is false — the external server certificate, its Secret mount, and the gateway TLS configuration all require cert-manager to be enabled. Set certManager.enabled=true or remove certManager.serverIssuerRef.name." -}}
|
||||
{{- end -}}
|
||||
{{- if not (or (eq $workloadKind "statefulset") (eq $workloadKind "deployment")) -}}
|
||||
{{- fail "workload.kind must be one of: statefulset, deployment." -}}
|
||||
{{- end -}}
|
||||
@@ -440,19 +591,27 @@ Validate chart values that Helm would otherwise accept silently.
|
||||
{{- fail (printf "grpcRoute.replicaRouting.enabled supports %s of at most 15; a GRPCRoute holds at most 16 rules." $maxReplicasSource) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" -}}
|
||||
{{- if not (has $workspaceMode (list "shared" "managed" "operator")) -}}
|
||||
{{- fail "server.drivers.kubernetes.workspaceMode must be one of: shared, managed, operator." -}}
|
||||
{{- end -}}
|
||||
{{- $credentialDrivers := list -}}
|
||||
{{- if .Values.server.credentialDrivers.kubernetesSecrets.enabled -}}
|
||||
{{- $credentialDrivers = append $credentialDrivers "kubernetes-secrets" -}}
|
||||
{{- end -}}
|
||||
{{- if .Values.server.credentialDrivers.vault.enabled -}}
|
||||
{{- $credentialDrivers = append $credentialDrivers "vault" -}}
|
||||
{{- include "openshell.validateSecretReference" (list "server.externalDbSecret" .Values.server.externalDbSecret) -}}
|
||||
{{- include "openshell.validateSecretReference" (list "server.credentialStorage.existingSecret" .Values.server.credentialStorage.existingSecret) -}}
|
||||
{{- include "openshell.validateSecretReference" (list "server.sandboxJwt.signingSecretName" .Values.server.sandboxJwt.signingSecretName) -}}
|
||||
{{- include "openshell.validateSecretReference" (list "server.tls.certSecretName" .Values.server.tls.certSecretName) -}}
|
||||
{{- include "openshell.validateSecretReference" (list "upstreamProxy.authSecret.name" .Values.upstreamProxy.authSecret.name) -}}
|
||||
{{- $gatewayConfig := .Values.gatewayConfig | default dict -}}
|
||||
{{- $gateway := get $gatewayConfig "openshell.gateway" | default dict -}}
|
||||
{{- $credentialDrivers := get $gateway "credential_drivers" -}}
|
||||
{{- if and (hasKey $gateway "credential_drivers") (ne $credentialDrivers nil) -}}
|
||||
{{- if eq (len $credentialDrivers) 0 -}}
|
||||
{{- fail "gatewayConfig.openshell.gateway.credential_drivers must select exactly one backend or be omitted/null to use the chart default" -}}
|
||||
{{- end -}}
|
||||
{{- if gt (len $credentialDrivers) 1 -}}
|
||||
{{- fail "only one external server.credentialDrivers backend can be enabled at a time." -}}
|
||||
{{- fail "gatewayConfig.openshell.gateway.credential_drivers may select only one backend" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $kubernetesConfig := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- include "openshell.validateSecretReference" (list "gatewayConfig.openshell.drivers.kubernetes.proxy_auth_secret_name" (get $kubernetesConfig "proxy_auth_secret_name")) -}}
|
||||
{{- $workspaceMode := get $kubernetesConfig "workspace_mode" | default "shared" -}}
|
||||
{{- if not (has $workspaceMode (list "shared" "managed" "operator")) -}}
|
||||
{{- fail "gatewayConfig.openshell.drivers.kubernetes.workspace_mode must be one of: shared, managed, operator." -}}
|
||||
{{- end -}}
|
||||
{{- if kindIs "invalid" .Values.server.tls.clientCaSecretName -}}
|
||||
{{- fail "server.tls.clientCaSecretName cannot be null; omit the key to use the chart default (openshell-server-client-ca), or set to \"\" to disable client certificate verification for HTTPS-only mode" -}}
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
{{/*
|
||||
Render gatewayConfig as TOML.
|
||||
|
||||
The chart deliberately treats the top-level keys as TOML table names. Nested
|
||||
maps are TOML inline tables, and maps in arrays are inline-table array items.
|
||||
This keeps the YAML-to-TOML boundary generic: adding a non-secret gateway
|
||||
field must not require a Helm template change.
|
||||
*/}}
|
||||
|
||||
{{/* Render a TOML key. Bare keys keep ordinary output readable. */}}
|
||||
{{- define "openshell.toml.key" -}}
|
||||
{{- $key := . | toString -}}
|
||||
{{- if regexMatch "^[A-Za-z0-9_-]+$" $key -}}
|
||||
{{- $key -}}
|
||||
{{- else -}}
|
||||
{{- $key | quote -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Render a scalar. Strings are serialized literally. */}}
|
||||
{{- define "openshell.toml.scalar" -}}
|
||||
{{- $value := index . 1 -}}
|
||||
{{- if kindIs "string" $value -}}
|
||||
{{- if regexMatch "-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----" $value -}}
|
||||
{{- fail "gatewayConfig must not contain an inline private key; provide it through a Secret-backed file mount" -}}
|
||||
{{- end -}}
|
||||
{{- if regexMatch "^[A-Za-z][A-Za-z0-9+.-]*://[^/@[:space:]]*@" $value -}}
|
||||
{{- fail "gatewayConfig must not contain inline URL credentials; provide them through a Secret-backed environment variable, file, or volume" -}}
|
||||
{{- end -}}
|
||||
{{- $value | quote -}}
|
||||
{{- else if or
|
||||
(kindIs "bool" $value)
|
||||
(kindIs "int" $value)
|
||||
(kindIs "int8" $value)
|
||||
(kindIs "int16" $value)
|
||||
(kindIs "int32" $value)
|
||||
(kindIs "int64" $value)
|
||||
(kindIs "uint" $value)
|
||||
(kindIs "uint8" $value)
|
||||
(kindIs "uint16" $value)
|
||||
(kindIs "uint32" $value)
|
||||
(kindIs "uint64" $value)
|
||||
(kindIs "float32" $value)
|
||||
(kindIs "float64" $value) -}}
|
||||
{{- $value | toJson -}}
|
||||
{{- else -}}
|
||||
{{- fail (printf "gatewayConfig values must be strings, booleans, numbers, maps, or arrays; got %s" (kindOf $value)) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Render a TOML inline table, omitting YAML null values. */}}
|
||||
{{- define "openshell.toml.inlineTable" -}}
|
||||
{{- $root := index . 0 -}}
|
||||
{{- $table := index . 1 -}}
|
||||
{{- $entries := list -}}
|
||||
{{- range $key := keys $table | sortAlpha -}}
|
||||
{{- $value := get $table $key -}}
|
||||
{{- if ne $value nil -}}
|
||||
{{- if eq $key "database_url" -}}
|
||||
{{- fail "gatewayConfig must not contain database_url; provide database credentials through the chart's Secret-backed OPENSHELL_DB_URL environment variable" -}}
|
||||
{{- end -}}
|
||||
{{- $entry := printf "%s = %s" (include "openshell.toml.key" $key) (include "openshell.toml.value" (list $root $value)) -}}
|
||||
{{- $entries = append $entries $entry -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- printf "{ %s }" (join ", " $entries) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Render an array. Maps become TOML inline-table entries. */}}
|
||||
{{- define "openshell.toml.array" -}}
|
||||
{{- $root := index . 0 -}}
|
||||
{{- $array := index . 1 -}}
|
||||
{{- $entries := list -}}
|
||||
{{- range $value := $array -}}
|
||||
{{- if eq $value nil -}}
|
||||
{{- fail "gatewayConfig arrays cannot contain null values" -}}
|
||||
{{- end -}}
|
||||
{{- $entries = append $entries (include "openshell.toml.value" (list $root $value)) -}}
|
||||
{{- end -}}
|
||||
{{- printf "[%s]" (join ", " $entries) -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Render any supported YAML value as TOML. */}}
|
||||
{{- define "openshell.toml.value" -}}
|
||||
{{- $root := index . 0 -}}
|
||||
{{- $value := index . 1 -}}
|
||||
{{- if kindIs "map" $value -}}
|
||||
{{- include "openshell.toml.inlineTable" (list $root $value) -}}
|
||||
{{- else if kindIs "slice" $value -}}
|
||||
{{- include "openshell.toml.array" (list $root $value) -}}
|
||||
{{- else -}}
|
||||
{{- include "openshell.toml.scalar" (list $root $value) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* Render the top-level gatewayConfig map as deterministic TOML tables.
|
||||
Top-level lists represent TOML arrays of tables and preserve their YAML order. */}}
|
||||
{{- define "openshell.gatewayConfigToml" -}}
|
||||
{{- $root := . -}}
|
||||
{{- $config := deepCopy (.Values.gatewayConfig | default dict) -}}
|
||||
{{- $legacyServer := .Values.server | default dict -}}
|
||||
{{- $gateway := get $config "openshell.gateway" | default dict -}}
|
||||
{{- if not (hasKey $gateway "name") -}}{{- $_ := set $gateway "name" (get $legacyServer "name" | default (include "openshell.fullname" .)) -}}{{- end -}}
|
||||
{{/* The Service and probes own listener ports, so keep runtime listeners aligned. */}}
|
||||
{{- $_ := set $gateway "bind_address" (printf "0.0.0.0:%v" .Values.service.port) -}}
|
||||
{{- if .Values.service.healthPort -}}{{- $_ := set $gateway "health_bind_address" (printf "0.0.0.0:%v" .Values.service.healthPort) -}}{{- else -}}{{- $_ := unset $gateway "health_bind_address" -}}{{- end -}}
|
||||
{{- if .Values.service.metricsPort -}}{{- $_ := set $gateway "metrics_bind_address" (printf "0.0.0.0:%v" .Values.service.metricsPort) -}}{{- else -}}{{- $_ := unset $gateway "metrics_bind_address" -}}{{- end -}}
|
||||
{{- range $legacyKey, $runtimeKey := dict "logLevel" "log_level" "enableLoopbackServiceHttp" "enable_loopback_service_http" "enableWebsocketTunnel" "enable_websocket_tunnel" "policyValidationFailureMode" "policy_validation_failure_mode" -}}
|
||||
{{- if not (hasKey $gateway $runtimeKey) -}}{{- $_ := set $gateway $runtimeKey (get $legacyServer $legacyKey) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (hasKey $gateway "compute_driver") -}}{{- $_ := set $gateway "compute_driver" "kubernetes" -}}{{- end -}}
|
||||
{{- $serverDnsNames := .Values.pkiInitJob.serverDnsNames | default list -}}
|
||||
{{- if .Values.certManager.enabled -}}{{- $serverDnsNames = .Values.certManager.serverDnsNames | default list -}}{{- end -}}
|
||||
{{- if and $serverDnsNames (not (hasKey $gateway "server_sans")) -}}
|
||||
{{- $_ := set $gateway "server_sans" (deepCopy $serverDnsNames) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway" $gateway -}}
|
||||
{{- $gatewayJwt := get $config "openshell.gateway.gateway_jwt" | default dict -}}
|
||||
{{- $legacyJwt := get $legacyServer "sandboxJwt" | default dict -}}
|
||||
{{- range $key, $value := dict "signing_key_path" "/etc/openshell-jwt/signing.pem" "public_key_path" "/etc/openshell-jwt/public.pem" "kid_path" "/etc/openshell-jwt/kid" -}}
|
||||
{{- if not (hasKey $gatewayJwt $key) -}}{{- $_ := set $gatewayJwt $key $value -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if not (hasKey $gatewayJwt "gateway_id") -}}{{- $_ := set $gatewayJwt "gateway_id" (get $legacyJwt "gatewayId" | default (include "openshell.fullname" .)) -}}{{- end -}}
|
||||
{{- if not (hasKey $gatewayJwt "ttl_secs") -}}{{- $_ := set $gatewayJwt "ttl_secs" (get $legacyJwt "ttlSecs" | default 3600) -}}{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway.gateway_jwt" $gatewayJwt -}}
|
||||
{{- $kubernetesCompat := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes" $kubernetesCompat -}}
|
||||
{{- $legacyDrivers := get $legacyServer "drivers" | default dict -}}
|
||||
{{- $legacyKubernetes := get $legacyDrivers "kubernetes" | default dict -}}
|
||||
{{- if hasKey $kubernetesCompat "resource_admission" -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes.resource_admission" (deepCopy (get $kubernetesCompat "resource_admission")) -}}
|
||||
{{- else -}}
|
||||
{{- $legacyAdmission := get $legacyKubernetes "resourceAdmission" | default dict -}}
|
||||
{{- $admission := dict -}}
|
||||
{{- if hasKey $legacyAdmission "enabled" -}}{{- $_ := set $admission "enabled" (get $legacyAdmission "enabled") -}}{{- end -}}
|
||||
{{- if and (hasKey $legacyAdmission "requiredLabels") (ne (get $legacyAdmission "requiredLabels") nil) -}}{{- $_ := set $admission "required_labels" (deepCopy (get $legacyAdmission "requiredLabels")) -}}{{- end -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes.resource_admission" $admission -}}
|
||||
{{- end -}}
|
||||
{{- $_ := unset $kubernetesCompat "resource_admission" -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes" $kubernetesCompat -}}
|
||||
{{- if not (hasKey $config "openshell.drivers.kubernetes.managed_ssh_ingress") -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes.managed_ssh_ingress" (dict "enabled" .Values.networkPolicy.enabled "gateway_namespace" .Release.Namespace "gateway_pod_selector" (dict "app.kubernetes.io/name" (include "openshell.name" .) "app.kubernetes.io/instance" .Release.Name)) -}}
|
||||
{{- end -}}
|
||||
{{- $legacyOidc := get $legacyServer "oidc" | default dict -}}
|
||||
{{- $oidcConfig := get $config "openshell.gateway.oidc" | default dict -}}
|
||||
{{- if or (hasKey $config "openshell.gateway.oidc") (get $legacyOidc "issuer") -}}
|
||||
{{- if get $legacyOidc "issuer" -}}
|
||||
{{- if not (hasKey $oidcConfig "issuer") -}}{{- $_ := set $oidcConfig "issuer" (get $legacyOidc "issuer") -}}{{- end -}}
|
||||
{{- if not (hasKey $oidcConfig "dangerously_allow_insecure_http") -}}{{- $_ := set $oidcConfig "dangerously_allow_insecure_http" (get $legacyOidc "dangerouslyAllowInsecureHttp") -}}{{- end -}}
|
||||
{{- if not (hasKey $oidcConfig "jwks_allowed_origins") -}}{{- $_ := set $oidcConfig "jwks_allowed_origins" (get $legacyOidc "jwksAllowedOrigins") -}}{{- end -}}
|
||||
{{- if not (hasKey $oidcConfig "audience") -}}{{- $_ := set $oidcConfig "audience" (get $legacyOidc "audience") -}}{{- end -}}
|
||||
{{- if not (hasKey $oidcConfig "jwks_ttl_secs") -}}{{- $_ := set $oidcConfig "jwks_ttl_secs" (get $legacyOidc "jwksTtl") -}}{{- end -}}
|
||||
{{- if and (get $legacyOidc "rolesClaim") (not (hasKey $oidcConfig "roles_claim")) -}}{{- $_ := set $oidcConfig "roles_claim" (get $legacyOidc "rolesClaim") -}}{{- end -}}
|
||||
{{- if and (get $legacyOidc "adminRole") (not (hasKey $oidcConfig "admin_role")) -}}{{- $_ := set $oidcConfig "admin_role" (get $legacyOidc "adminRole") -}}{{- end -}}
|
||||
{{- if and (get $legacyOidc "userRole") (not (hasKey $oidcConfig "user_role")) -}}{{- $_ := set $oidcConfig "user_role" (get $legacyOidc "userRole") -}}{{- end -}}
|
||||
{{- if and (get $legacyOidc "scopesClaim") (not (hasKey $oidcConfig "scopes_claim")) -}}{{- $_ := set $oidcConfig "scopes_claim" (get $legacyOidc "scopesClaim") -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway.oidc" $oidcConfig -}}
|
||||
{{- end -}}
|
||||
{{- $legacyOtlp := get $legacyServer "otlp" | default dict -}}
|
||||
{{- $otlpConfig := get $config "openshell.gateway.otlp" | default dict -}}
|
||||
{{- if or (hasKey $config "openshell.gateway.otlp") (get $legacyOtlp "endpoint") -}}
|
||||
{{- if get $legacyOtlp "endpoint" -}}
|
||||
{{- if not (hasKey $otlpConfig "endpoint") -}}{{- $_ := set $otlpConfig "endpoint" (get $legacyOtlp "endpoint") -}}{{- end -}}
|
||||
{{- if not (hasKey $otlpConfig "service_name") -}}{{- $_ := set $otlpConfig "service_name" (get $legacyOtlp "serviceName") -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway.otlp" $otlpConfig -}}
|
||||
{{- end -}}
|
||||
{{- $legacyAuth := get $legacyServer "auth" | default dict -}}
|
||||
{{- if and (get $legacyAuth "allowUnauthenticatedUsers") (not (hasKey $config "openshell.gateway.auth")) -}}{{- $_ := set $config "openshell.gateway.auth" (dict "allow_unauthenticated_users" true) -}}{{- end -}}
|
||||
{{- $legacyOcsf := get $legacyServer "ocsfLog" | default dict -}}
|
||||
{{- $ocsfConfig := get $config "openshell.gateway.ocsf_log" | default dict -}}
|
||||
{{- if or (hasKey $config "openshell.gateway.ocsf_log") (get $legacyOcsf "enabled") -}}
|
||||
{{- if get $legacyOcsf "enabled" -}}
|
||||
{{- if not (hasKey $ocsfConfig "path") -}}{{- $path := get $legacyOcsf "path" -}}{{- if not $path -}}{{- fail "server.ocsfLog.path must be set when server.ocsfLog.enabled is true" -}}{{- end -}}{{- $_ := set $ocsfConfig "path" $path -}}{{- end -}}
|
||||
{{- if not (hasKey $ocsfConfig "rotation") -}}{{- $rotation := get $legacyOcsf "rotation" | default "daily" -}}{{- if not (has $rotation (list "daily" "never")) -}}{{- fail "server.ocsfLog.rotation must be daily or never" -}}{{- end -}}{{- $_ := set $ocsfConfig "rotation" $rotation -}}{{- end -}}
|
||||
{{- if not (hasKey $ocsfConfig "queue_capacity") -}}{{- $queueCapacity := 10000 -}}{{- if and (hasKey $legacyOcsf "queueCapacity") (ne (get $legacyOcsf "queueCapacity") nil) -}}{{- $queueCapacity = get $legacyOcsf "queueCapacity" -}}{{- end -}}{{- if lt (int $queueCapacity) 1 -}}{{- fail "server.ocsfLog.queueCapacity and queueMaxBytes must be positive" -}}{{- end -}}{{- $_ := set $ocsfConfig "queue_capacity" (int $queueCapacity) -}}{{- end -}}
|
||||
{{- if not (hasKey $ocsfConfig "queue_max_bytes") -}}{{- $queueMaxBytes := 16777216 -}}{{- if and (hasKey $legacyOcsf "queueMaxBytes") (ne (get $legacyOcsf "queueMaxBytes") nil) -}}{{- $queueMaxBytes = get $legacyOcsf "queueMaxBytes" -}}{{- end -}}{{- if lt (int $queueMaxBytes) 1 -}}{{- fail "server.ocsfLog.queueCapacity and queueMaxBytes must be positive" -}}{{- end -}}{{- $_ := set $ocsfConfig "queue_max_bytes" (int $queueMaxBytes) -}}{{- end -}}
|
||||
{{- if not (hasKey $ocsfConfig "schema_version") -}}{{- $schemaVersion := get $legacyOcsf "schemaVersion" | default "" -}}{{- if not (has $schemaVersion (list "" "1.1" "1.3")) -}}{{- fail "server.ocsfLog.schemaVersion must be empty, 1.1, or 1.3" -}}{{- end -}}{{- if $schemaVersion -}}{{- $_ := set $ocsfConfig "schema_version" $schemaVersion -}}{{- end -}}{{- end -}}
|
||||
{{- if and (eq (get $ocsfConfig "rotation") "daily") (not (hasKey $ocsfConfig "max_files")) -}}{{- $maxFiles := 7 -}}{{- if and (hasKey $legacyOcsf "maxFiles") (ne (get $legacyOcsf "maxFiles") nil) -}}{{- $maxFiles = get $legacyOcsf "maxFiles" -}}{{- end -}}{{- if lt (int $maxFiles) 1 -}}{{- fail "server.ocsfLog.maxFiles must be positive when rotation is daily" -}}{{- end -}}{{- $_ := set $ocsfConfig "max_files" (int $maxFiles) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway.ocsf_log" $ocsfConfig -}}
|
||||
{{- end -}}
|
||||
{{- $legacyRateLimit := get $legacyServer "grpcRateLimit" | default dict -}}
|
||||
{{- $rateRequests := int (get $legacyRateLimit "requests" | default 0) -}}
|
||||
{{- $rateWindow := int (get $legacyRateLimit "windowSeconds" | default 0) -}}
|
||||
{{- if or (lt $rateRequests 0) (lt $rateWindow 0) -}}{{- fail "server.grpcRateLimit.requests and server.grpcRateLimit.windowSeconds must not be negative; they map to unsigned gateway settings" -}}{{- end -}}
|
||||
{{- if and (gt $rateRequests 0) (gt $rateWindow 0) -}}
|
||||
{{- if not (hasKey $gateway "grpc_rate_limit_requests") -}}{{- $_ := set $gateway "grpc_rate_limit_requests" $rateRequests -}}{{- end -}}
|
||||
{{- if not (hasKey $gateway "grpc_rate_limit_window_seconds") -}}{{- $_ := set $gateway "grpc_rate_limit_window_seconds" $rateWindow -}}{{- end -}}
|
||||
{{- else if or (gt $rateRequests 0) (gt $rateWindow 0) -}}{{- fail "server.grpcRateLimit requires both requests and windowSeconds to be positive to enable rate limiting, or both 0/unset to disable it" -}}{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway" $gateway -}}
|
||||
{{- $legacyCredentialDrivers := .Values.server.credentialDrivers | default dict -}}
|
||||
{{- $gatewayForCredentials := get $config "openshell.gateway" | default dict -}}
|
||||
{{- $rawConfiguredCredentialDrivers := get $gatewayForCredentials "credential_drivers" -}}
|
||||
{{- $hasConfiguredCredentialDrivers := and (hasKey $gatewayForCredentials "credential_drivers") (ne $rawConfiguredCredentialDrivers nil) -}}
|
||||
{{- $configuredCredentialDrivers := $rawConfiguredCredentialDrivers | default list -}}
|
||||
{{- if and $hasConfiguredCredentialDrivers (eq (len $configuredCredentialDrivers) 0) -}}
|
||||
{{- fail "gatewayConfig.openshell.gateway.credential_drivers must select exactly one backend or be omitted/null to use the chart default" -}}
|
||||
{{- end -}}
|
||||
{{- if gt (len $configuredCredentialDrivers) 1 -}}
|
||||
{{- fail "gatewayConfig.openshell.gateway.credential_drivers may select only one backend" -}}
|
||||
{{- end -}}
|
||||
{{- $legacyKubernetesSecrets := get $legacyCredentialDrivers "kubernetesSecrets" | default dict -}}
|
||||
{{- $legacyVault := get $legacyCredentialDrivers "vault" | default dict -}}
|
||||
{{- if and $legacyKubernetesSecrets.enabled $legacyVault.enabled -}}
|
||||
{{- fail "only one external server.credentialDrivers backend can be enabled at a time" -}}
|
||||
{{- end -}}
|
||||
{{- if and (not $hasConfiguredCredentialDrivers) (eq (len $configuredCredentialDrivers) 0) -}}
|
||||
{{- if $legacyKubernetesSecrets.enabled -}}
|
||||
{{- $_ := set $gatewayForCredentials "credential_drivers" (list "kubernetes-secrets") -}}
|
||||
{{- else if $legacyVault.enabled -}}
|
||||
{{- $_ := set $gatewayForCredentials "credential_drivers" (list "vault") -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway" $gatewayForCredentials -}}
|
||||
{{- $effectiveCredentialDrivers := get $gatewayForCredentials "credential_drivers" | default list -}}
|
||||
{{- if has "kubernetes-secrets" $effectiveCredentialDrivers -}}
|
||||
{{- $kubernetesSecretsConfig := get $config "openshell.credential_drivers.kubernetes-secrets" | default dict -}}
|
||||
{{- if not (hasKey $kubernetesSecretsConfig "namespace") -}}
|
||||
{{- $_ := set $kubernetesSecretsConfig "namespace" (include "openshell.credentialKubernetesSecretsNamespace" .) -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.credential_drivers.kubernetes-secrets" $kubernetesSecretsConfig -}}
|
||||
{{- end -}}
|
||||
{{- if and $legacyVault.enabled (has "vault" $effectiveCredentialDrivers) -}}
|
||||
{{- $vaultConfig := get $config "openshell.credential_drivers.vault" | default dict -}}
|
||||
{{- range $legacyKey, $runtimeKey := dict "address" "address" "authMethod" "auth_method" "role" "role" -}}
|
||||
{{- if not (hasKey $vaultConfig $runtimeKey) -}}{{- $_ := set $vaultConfig $runtimeKey (get $legacyVault $legacyKey) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $legacyKey, $runtimeKey := dict "mount" "mount" "kvVersion" "kv_version" "kubernetesAuthMount" "kubernetes_auth_mount" "serviceAccountTokenPath" "service_account_token_path" "tokenPath" "token_path" "timeoutSecs" "timeout_secs" -}}
|
||||
{{- if and (get $legacyVault $legacyKey) (not (hasKey $vaultConfig $runtimeKey)) -}}{{- $_ := set $vaultConfig $runtimeKey (get $legacyVault $legacyKey) -}}{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.credential_drivers.vault" $vaultConfig -}}
|
||||
{{- end -}}
|
||||
{{/* External credential drivers own their storage. Do not configure the
|
||||
chart-managed encrypted database store when any driver is selected: its KEK
|
||||
environment variable is intentionally not mounted in that mode. */}}
|
||||
{{- $configuredGateway := get $config "openshell.gateway" | default dict -}}
|
||||
{{- $configuredCredentialDrivers := get $configuredGateway "credential_drivers" | default list -}}
|
||||
{{- if gt (len $configuredCredentialDrivers) 0 -}}
|
||||
{{- $_ := unset $config "openshell.gateway.credential_storage" -}}
|
||||
{{- else if not (hasKey $config "openshell.gateway.credential_storage") -}}
|
||||
{{- $_ := set $config "openshell.gateway.credential_storage" (dict "key_encryption_key_env" (include "openshell.credentialStorageKeyEncryptionKeyEnvName" .)) -}}
|
||||
{{- end -}}
|
||||
{{/* Kubernetes packaging owns host aliases. Do not permit a second runtime
|
||||
source to make sandbox callback hostnames disagree with the pod spec. */}}
|
||||
{{- $kubernetes := get $config "openshell.drivers.kubernetes" | default dict -}}
|
||||
{{- if .Values.server.hostGatewayIP -}}
|
||||
{{- $_ := set $kubernetes "host_gateway_ip" .Values.server.hostGatewayIP -}}
|
||||
{{- else -}}
|
||||
{{- $_ := unset $kubernetes "host_gateway_ip" -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes" $kubernetes -}}
|
||||
|
||||
{{/* A Vault CA is a Kubernetes resource reference, not a free-form runtime
|
||||
path. Derive its mounted path only from the chart-owned ConfigMap reference. */}}
|
||||
{{- $credentialDrivers := .Values.credentialDrivers | default dict -}}
|
||||
{{- $vaultResources := get $credentialDrivers "vault" | default dict -}}
|
||||
{{- $legacyVaultResources := get $legacyCredentialDrivers "vault" | default dict -}}
|
||||
{{- $vaultCaConfigMapName := get $vaultResources "caConfigMapName" | default (get $legacyVaultResources "caConfigMapName") -}}
|
||||
{{- if hasKey $config "openshell.credential_drivers.vault" -}}
|
||||
{{- $vaultConfig := get $config "openshell.credential_drivers.vault" | default dict -}}
|
||||
{{- $_ := unset $vaultConfig "ca_bundle" -}}
|
||||
{{- if and (eq (include "openshell.credentialDriverEnabled" (list . "vault")) "true") $vaultCaConfigMapName -}}
|
||||
{{- $_ := set $vaultConfig "ca_bundle" "/etc/openshell-tls/vault-ca/ca.crt" -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.credential_drivers.vault" $vaultConfig -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/* TLS resources, mounts, and their corresponding runtime fields have one
|
||||
owner: server.*. Override any gatewayConfig copies before serializing TOML. */}}
|
||||
{{- $gateway := get $config "openshell.gateway" | default dict -}}
|
||||
{{- $_ := set $gateway "disable_tls" .Values.server.disableTls -}}
|
||||
{{- $_ := set $config "openshell.gateway" $gateway -}}
|
||||
{{- if .Values.server.disableTls -}}
|
||||
{{- $_ := unset $config "openshell.gateway.tls" -}}
|
||||
{{- $_ := unset $kubernetes "client_tls_secret_name" -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes" $kubernetes -}}
|
||||
{{- else -}}
|
||||
{{- if .Values.server.tls.clientTlsSecretName -}}
|
||||
{{- $_ := set $kubernetes "client_tls_secret_name" .Values.server.tls.clientTlsSecretName -}}
|
||||
{{- else -}}
|
||||
{{- $_ := unset $kubernetes "client_tls_secret_name" -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.drivers.kubernetes" $kubernetes -}}
|
||||
{{- $gatewayTls := get $config "openshell.gateway.tls" | default dict -}}
|
||||
{{- $_ := set $gatewayTls "cert_path" "/etc/openshell-tls/server/tls.crt" -}}
|
||||
{{- $_ := set $gatewayTls "key_path" "/etc/openshell-tls/server/tls.key" -}}
|
||||
{{- if eq (include "openshell.gatewayClientCaEnabled" .) "true" -}}
|
||||
{{- $_ := set $gatewayTls "client_ca_path" "/etc/openshell-tls/client-ca/ca.crt" -}}
|
||||
{{- else -}}
|
||||
{{- $_ := unset $gatewayTls "client_ca_path" -}}
|
||||
{{- end -}}
|
||||
{{- if .Values.certManager.serverIssuerRef.name -}}
|
||||
{{- $_ := set $gatewayTls "external_cert_path" "/etc/openshell-tls/server-external/tls.crt" -}}
|
||||
{{- $_ := set $gatewayTls "external_key_path" "/etc/openshell-tls/server-external/tls.key" -}}
|
||||
{{- $_ := set $gatewayTls "external_server_names" (deepCopy (.Values.certManager.serverDnsNames | default list)) -}}
|
||||
{{- else -}}
|
||||
{{- $_ := unset $gatewayTls "external_cert_path" -}}
|
||||
{{- $_ := unset $gatewayTls "external_key_path" -}}
|
||||
{{- $_ := unset $gatewayTls "external_server_names" -}}
|
||||
{{- end -}}
|
||||
{{- $_ := set $config "openshell.gateway.tls" $gatewayTls -}}
|
||||
{{- end -}}
|
||||
{{/* A nested YAML map and its dotted top-level table would serialize to
|
||||
conflicting TOML definitions. Fail before rendering an invalid ConfigMap. */}}
|
||||
{{- range $tableName := keys $config | sortAlpha -}}
|
||||
{{- $fields := get $config $tableName -}}
|
||||
{{- if kindIs "map" $fields -}}
|
||||
{{- range $fieldName := keys $fields -}}
|
||||
{{- $nestedTableName := printf "%s.%s" $tableName $fieldName -}}
|
||||
{{- if hasKey $config $nestedTableName -}}
|
||||
{{- fail (printf "gatewayConfig defines %q both as a nested map and a dotted top-level table; use only the dotted top-level key" $nestedTableName) -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- range $tableName := keys $config | sortAlpha -}}
|
||||
{{- $fields := get $config $tableName -}}
|
||||
{{- if ne $fields nil -}}
|
||||
{{- if and (not (kindIs "map" $fields)) (not (kindIs "slice" $fields)) -}}
|
||||
{{- fail (printf "gatewayConfig table %q must be a map, got %s" $tableName (kindOf $fields)) -}}
|
||||
{{- end -}}
|
||||
{{- $header := list -}}
|
||||
{{- $segments := splitList "." $tableName -}}
|
||||
{{- range $index, $segment := $segments -}}
|
||||
{{- if eq $segment "" -}}
|
||||
{{- fail (printf "gatewayConfig table %q contains an empty TOML key segment" $tableName) -}}
|
||||
{{- end -}}
|
||||
{{- $header = append $header (include "openshell.toml.key" $segment) -}}
|
||||
{{- end -}}
|
||||
{{- if kindIs "map" $fields -}}
|
||||
{{ printf "[%s]\n" (join "." $header) }}
|
||||
{{- range $fieldName := keys $fields | sortAlpha }}
|
||||
{{- $value := get $fields $fieldName -}}
|
||||
{{- if ne $value nil }}
|
||||
{{- if eq $fieldName "database_url" -}}
|
||||
{{- fail "gatewayConfig must not contain database_url; provide database credentials through the chart's Secret-backed OPENSHELL_DB_URL environment variable" -}}
|
||||
{{- end -}}
|
||||
{{ printf "%s = %s\n" (include "openshell.toml.key" $fieldName) (include "openshell.toml.value" (list $root $value)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else -}}
|
||||
{{- range $index, $entry := $fields -}}
|
||||
{{- if not (kindIs "map" $entry) -}}
|
||||
{{- fail (printf "gatewayConfig array-of-tables %q entry %d must be a map, got %s" $tableName $index (kindOf $entry)) -}}
|
||||
{{- end -}}
|
||||
{{ printf "[[%s]]\n" (join "." $header) }}
|
||||
{{- range $fieldName := keys $entry | sortAlpha }}
|
||||
{{- $value := get $entry $fieldName -}}
|
||||
{{- if ne $value nil }}
|
||||
{{- if eq $fieldName "database_url" -}}
|
||||
{{- fail "gatewayConfig must not contain database_url; provide database credentials through the chart's Secret-backed OPENSHELL_DB_URL environment variable" -}}
|
||||
{{- end -}}
|
||||
{{ printf "%s = %s\n" (include "openshell.toml.key" $fieldName) (include "openshell.toml.value" (list $root $value)) }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
@@ -2,7 +2,15 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
{{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" }}
|
||||
{{ $kubernetesConfig := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- $workspaceMode := get $kubernetesConfig "workspace_mode" | default "shared" }}
|
||||
{{- $allowDriverConfig := get $kubernetesConfig "allow_driver_config" | default false -}}
|
||||
{{- $managedSshIngressEnabled := .Values.networkPolicy.enabled -}}
|
||||
{{- $gatewayConfig := .Values.gatewayConfig | default dict -}}
|
||||
{{- $managedSshIngress := get $gatewayConfig "openshell.drivers.kubernetes.managed_ssh_ingress" | default dict -}}
|
||||
{{- if hasKey $managedSshIngress "enabled" -}}
|
||||
{{- $managedSshIngressEnabled = get $managedSshIngress "enabled" -}}
|
||||
{{- end -}}
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
@@ -16,7 +24,7 @@ rules:
|
||||
- apiGroups: ["scheduling.k8s.io"]
|
||||
resources: ["priorityclasses"]
|
||||
verbs: ["get"]
|
||||
{{- if and (ne $workspaceMode "shared") .Values.server.drivers.kubernetes.allowDriverConfig }}
|
||||
{{- if and (ne $workspaceMode "shared") $allowDriverConfig }}
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get"]
|
||||
@@ -99,15 +107,11 @@ rules:
|
||||
verbs: ["create", "get"]
|
||||
{{- end }}
|
||||
{{- if eq $workspaceMode "managed" }}
|
||||
# Operator-mode namespaces receive Secret permissions from the openshell-workspace
|
||||
# chart Role instead.
|
||||
# Operator-mode namespaces receive Secret permissions from the
|
||||
# openshell-workspace chart Role instead.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
# Bootstrap and image-pull Secrets are generation-scoped. Recovery deletes
|
||||
# them by exact name.
|
||||
verbs: ["create", "delete"]
|
||||
{{- end }}
|
||||
{{- if eq $workspaceMode "managed" }}
|
||||
# ServiceAccount creation in managed namespaces.
|
||||
- apiGroups:
|
||||
- ""
|
||||
@@ -116,7 +120,7 @@ rules:
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
{{- if .Values.networkPolicy.enabled }}
|
||||
{{- if $managedSshIngressEnabled }}
|
||||
# Apply gateway-only SSH ingress isolation in managed namespaces.
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
{{- $credentialSecrets := .Values.server.credentialDrivers.kubernetesSecrets -}}
|
||||
{{- if and $credentialSecrets.enabled $credentialSecrets.createNamespace }}
|
||||
{{- if and (eq (include "openshell.credentialDriverEnabled" (list . "kubernetes-secrets")) "true") $credentialSecrets.createNamespace }}
|
||||
{{- $namespace := include "openshell.credentialKubernetesSecretsNamespace" . -}}
|
||||
{{- if eq $namespace .Release.Namespace -}}
|
||||
{{- fail "server.credentialDrivers.kubernetesSecrets.createNamespace requires server.credentialDrivers.kubernetesSecrets.namespace to name a namespace other than the release namespace" -}}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{{- if and .Values.server.credentialDrivers.kubernetesSecrets.enabled .Values.server.credentialDrivers.kubernetesSecrets.rbac.create }}
|
||||
{{- $credentialSecrets := .Values.server.credentialDrivers.kubernetesSecrets | default dict -}}
|
||||
{{- $credentialSecretsRbac := get $credentialSecrets "rbac" | default dict -}}
|
||||
{{- $createRbac := true -}}
|
||||
{{- if hasKey $credentialSecretsRbac "create" -}}
|
||||
{{- $createRbac = get $credentialSecretsRbac "create" -}}
|
||||
{{- end -}}
|
||||
{{- if and (eq (include "openshell.credentialDriverEnabled" (list . "kubernetes-secrets")) "true") $createRbac }}
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
@@ -13,7 +19,7 @@ metadata:
|
||||
# runtime. Kubernetes RBAC does not support label-based or prefix-based
|
||||
# filtering for resourceNames. To limit blast radius, deploy the gateway
|
||||
# with a dedicated namespace for credential Secrets
|
||||
# (server.credentialDrivers.kubernetesSecrets.namespace).
|
||||
# (openshell.credential_drivers.kubernetes-secrets.namespace).
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{{- if and .Values.server.credentialDrivers.kubernetesSecrets.enabled .Values.server.credentialDrivers.kubernetesSecrets.rbac.create }}
|
||||
{{- $credentialSecrets := .Values.server.credentialDrivers.kubernetesSecrets | default dict -}}
|
||||
{{- $credentialSecretsRbac := get $credentialSecrets "rbac" | default dict -}}
|
||||
{{- $createRbac := true -}}
|
||||
{{- if hasKey $credentialSecretsRbac "create" -}}
|
||||
{{- $createRbac = get $credentialSecretsRbac "create" -}}
|
||||
{{- end -}}
|
||||
{{- if and (eq (include "openshell.credentialDriverEnabled" (list . "kubernetes-secrets")) "true") $createRbac }}
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
{{- if not (or .Values.server.credentialDrivers.kubernetesSecrets.enabled .Values.server.credentialDrivers.vault.enabled) }}
|
||||
{{- if not (or (eq (include "openshell.credentialDriverEnabled" (list . "kubernetes-secrets")) "true") (eq (include "openshell.credentialDriverEnabled" (list . "vault")) "true")) }}
|
||||
{{- if not .Values.server.credentialStorage.existingSecret }}
|
||||
{{- $secretName := include "openshell.credentialStorageKeyEncryptionKeySecretName" . -}}
|
||||
{{- $secretKey := include "openshell.credentialStorageKeyEncryptionKeySecretKey" . -}}
|
||||
|
||||
@@ -7,23 +7,10 @@ The gateway reads `/etc/openshell/gateway.toml` (mounted from this ConfigMap)
|
||||
at startup. CLI flags and OPENSHELL_* env vars on the gateway workload container
|
||||
still override anything in this file.
|
||||
|
||||
One value is intentionally NOT rendered here:
|
||||
- server.dbUrl → passed via OPENSHELL_DB_URL env var (from Secret)
|
||||
when server.externalDbSecret is set, otherwise
|
||||
--db-url arg for SQLite
|
||||
gatewayConfig is the complete non-secret application configuration boundary.
|
||||
Database URLs and other credentials remain injected through Secret-backed
|
||||
environment variables, files, or volumes outside this ConfigMap.
|
||||
*/}}
|
||||
{{- $credentialDrivers := list -}}
|
||||
{{- $otlp := .Values.server.otlp | default dict -}}
|
||||
{{- $ocsfLog := .Values.server.ocsfLog | default dict -}}
|
||||
{{- if .Values.server.credentialDrivers.kubernetesSecrets.enabled -}}
|
||||
{{- $credentialDrivers = append $credentialDrivers "kubernetes-secrets" -}}
|
||||
{{- end -}}
|
||||
{{- if .Values.server.credentialDrivers.vault.enabled -}}
|
||||
{{- $credentialDrivers = append $credentialDrivers "vault" -}}
|
||||
{{- end -}}
|
||||
{{- if and .Values.certManager.serverIssuerRef.name (not .Values.certManager.enabled) }}
|
||||
{{- fail "certManager.serverIssuerRef.name is set but certManager.enabled is false \u2014 the external server certificate, its Secret mount, and the gateway TLS configuration all require cert-manager to be enabled. Set certManager.enabled=true or remove certManager.serverIssuerRef.name." }}
|
||||
{{- end }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
@@ -32,286 +19,4 @@ metadata:
|
||||
{{- include "openshell.labels" . | nindent 4 }}
|
||||
data:
|
||||
gateway.toml: |
|
||||
[openshell]
|
||||
version = 2
|
||||
|
||||
[openshell.gateway]
|
||||
name = {{ .Values.server.name | default (include "openshell.fullname" .) | quote }}
|
||||
bind_address = "0.0.0.0:{{ .Values.service.port }}"
|
||||
{{- if .Values.service.healthPort }}
|
||||
health_bind_address = "0.0.0.0:{{ .Values.service.healthPort }}"
|
||||
{{- end }}
|
||||
{{- if .Values.service.metricsPort }}
|
||||
metrics_bind_address = "0.0.0.0:{{ .Values.service.metricsPort }}"
|
||||
{{- end }}
|
||||
log_level = {{ .Values.server.logLevel | quote }}
|
||||
compute_driver = "kubernetes"
|
||||
{{- if $credentialDrivers }}
|
||||
credential_drivers = [{{- range $i, $driver := $credentialDrivers }}{{ if $i }}, {{ end }}{{ $driver | quote }}{{- end }}]
|
||||
{{- end }}
|
||||
{{- $policyValidationFailureMode := .Values.server.policyValidationFailureMode }}
|
||||
{{- if not (has $policyValidationFailureMode (list "fail_closed" "retain_last_valid")) }}
|
||||
{{- fail "server.policyValidationFailureMode must be fail_closed or retain_last_valid" }}
|
||||
{{- end }}
|
||||
policy_validation_failure_mode = {{ $policyValidationFailureMode | quote }}
|
||||
{{- if .Values.server.disableTls }}
|
||||
disable_tls = true
|
||||
{{- end }}
|
||||
enable_loopback_service_http = {{ .Values.server.enableLoopbackServiceHttp }}
|
||||
enable_websocket_tunnel = {{ .Values.server.enableWebsocketTunnel }}
|
||||
{{- $sans := list -}}
|
||||
{{- if and .Values.certManager.enabled .Values.certManager.serverDnsNames }}
|
||||
{{- $sans = .Values.certManager.serverDnsNames }}
|
||||
{{- else if and .Values.pkiInitJob.enabled .Values.pkiInitJob.serverDnsNames }}
|
||||
{{- $sans = .Values.pkiInitJob.serverDnsNames }}
|
||||
{{- end }}
|
||||
{{- if $sans }}
|
||||
server_sans = [{{- range $i, $san := $sans }}{{ if $i }}, {{ end }}{{ $san | quote }}{{- end }}]
|
||||
{{- end }}
|
||||
{{- $rlRequests := int .Values.server.grpcRateLimit.requests }}
|
||||
{{- $rlWindowSeconds := int .Values.server.grpcRateLimit.windowSeconds }}
|
||||
{{- if or (lt $rlRequests 0) (lt $rlWindowSeconds 0) }}
|
||||
{{- fail "server.grpcRateLimit.requests and server.grpcRateLimit.windowSeconds must not be negative; they map to unsigned gateway settings" }}
|
||||
{{- end }}
|
||||
{{- if and (gt $rlRequests 0) (gt $rlWindowSeconds 0) }}
|
||||
grpc_rate_limit_requests = {{ $rlRequests }}
|
||||
grpc_rate_limit_window_seconds = {{ $rlWindowSeconds }}
|
||||
{{- else if or (gt $rlRequests 0) (gt $rlWindowSeconds 0) }}
|
||||
{{- fail "server.grpcRateLimit requires both requests and windowSeconds to be positive to enable rate limiting, or both 0/unset to disable it" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if $otlp.endpoint }}
|
||||
|
||||
[openshell.gateway.otlp]
|
||||
endpoint = {{ $otlp.endpoint | quote }}
|
||||
{{- if $otlp.serviceName }}
|
||||
service_name = {{ $otlp.serviceName | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if $ocsfLog.enabled }}
|
||||
{{- if not $ocsfLog.path -}}
|
||||
{{- fail "server.ocsfLog.path must be set when server.ocsfLog.enabled is true" -}}
|
||||
{{- end }}
|
||||
|
||||
{{- $ocsfRotation := $ocsfLog.rotation | default "daily" -}}
|
||||
{{- if not (has $ocsfRotation (list "daily" "never")) -}}
|
||||
{{- fail "server.ocsfLog.rotation must be daily or never" -}}
|
||||
{{- end }}
|
||||
{{- $ocsfQueueCapacity := int (ternary 10000 $ocsfLog.queueCapacity (kindIs "invalid" $ocsfLog.queueCapacity)) -}}
|
||||
{{- $ocsfQueueMaxBytes := int (ternary 16777216 $ocsfLog.queueMaxBytes (kindIs "invalid" $ocsfLog.queueMaxBytes)) -}}
|
||||
{{- if or (lt $ocsfQueueCapacity 1) (lt $ocsfQueueMaxBytes 1) -}}
|
||||
{{- fail "server.ocsfLog.queueCapacity and queueMaxBytes must be positive" -}}
|
||||
{{- end }}
|
||||
[openshell.gateway.ocsf_log]
|
||||
path = {{ $ocsfLog.path | quote }}
|
||||
{{- $ocsfSchemaVersion := $ocsfLog.schemaVersion | default "" -}}
|
||||
{{- if not (has $ocsfSchemaVersion (list "" "1.1" "1.3")) -}}
|
||||
{{- fail "server.ocsfLog.schemaVersion must be empty, 1.1, or 1.3" -}}
|
||||
{{- end }}
|
||||
{{- if $ocsfSchemaVersion }}
|
||||
schema_version = {{ $ocsfSchemaVersion | quote }}
|
||||
{{- end }}
|
||||
rotation = {{ $ocsfRotation | quote }}
|
||||
{{- if eq $ocsfRotation "daily" }}
|
||||
{{- $ocsfMaxFiles := int (ternary 7 $ocsfLog.maxFiles (kindIs "invalid" $ocsfLog.maxFiles)) -}}
|
||||
{{- if lt $ocsfMaxFiles 1 -}}
|
||||
{{- fail "server.ocsfLog.maxFiles must be positive when rotation is daily" -}}
|
||||
{{- end }}
|
||||
max_files = {{ $ocsfMaxFiles }}
|
||||
{{- end }}
|
||||
queue_capacity = {{ $ocsfQueueCapacity }}
|
||||
queue_max_bytes = {{ $ocsfQueueMaxBytes }}
|
||||
{{- end }}
|
||||
|
||||
{{- if not .Values.server.disableTls }}
|
||||
|
||||
[openshell.gateway.tls]
|
||||
cert_path = "/etc/openshell-tls/server/tls.crt"
|
||||
key_path = "/etc/openshell-tls/server/tls.key"
|
||||
{{- if eq (include "openshell.gatewayClientCaEnabled" .) "true" }}
|
||||
client_ca_path = "/etc/openshell-tls/client-ca/ca.crt"
|
||||
{{- end }}
|
||||
{{- if .Values.certManager.serverIssuerRef.name }}
|
||||
external_cert_path = "/etc/openshell-tls/server-external/tls.crt"
|
||||
external_key_path = "/etc/openshell-tls/server-external/tls.key"
|
||||
external_server_names = [{{- range $i, $name := .Values.certManager.serverDnsNames }}{{ if $i }}, {{ end }}{{ $name | quote }}{{- end }}]
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.server.auth.allowUnauthenticatedUsers }}
|
||||
|
||||
[openshell.gateway.auth]
|
||||
allow_unauthenticated_users = true
|
||||
{{- end }}
|
||||
|
||||
[openshell.gateway.gateway_jwt]
|
||||
signing_key_path = "/etc/openshell-jwt/signing.pem"
|
||||
public_key_path = "/etc/openshell-jwt/public.pem"
|
||||
kid_path = "/etc/openshell-jwt/kid"
|
||||
gateway_id = {{ .Values.server.sandboxJwt.gatewayId | default (include "openshell.fullname" .) | quote }}
|
||||
ttl_secs = {{ .Values.server.sandboxJwt.ttlSecs | default 3600 }}
|
||||
|
||||
{{- if .Values.server.oidc.issuer }}
|
||||
|
||||
[openshell.gateway.oidc]
|
||||
issuer = {{ .Values.server.oidc.issuer | quote }}
|
||||
dangerously_allow_insecure_http = {{ .Values.server.oidc.dangerouslyAllowInsecureHttp }}
|
||||
jwks_allowed_origins = {{ .Values.server.oidc.jwksAllowedOrigins | toJson }}
|
||||
audience = {{ .Values.server.oidc.audience | quote }}
|
||||
jwks_ttl_secs = {{ .Values.server.oidc.jwksTtl }}
|
||||
{{- if .Values.server.oidc.rolesClaim }}
|
||||
roles_claim = {{ .Values.server.oidc.rolesClaim | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.oidc.adminRole }}
|
||||
admin_role = {{ .Values.server.oidc.adminRole | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.oidc.userRole }}
|
||||
user_role = {{ .Values.server.oidc.userRole | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.oidc.scopesClaim }}
|
||||
scopes_claim = {{ .Values.server.oidc.scopesClaim | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
[openshell.drivers.kubernetes]
|
||||
allow_driver_config = {{ .Values.server.drivers.kubernetes.allowDriverConfig }}
|
||||
namespace = {{ include "openshell.sandboxNamespace" . | quote }}
|
||||
default_image = {{ include "openshell.sandboxImage" . | quote }}
|
||||
{{- if include "openshell.sandboxRuntimeImageOverrideEnabled" . }}
|
||||
sandbox_runtime_image = {{ include "openshell.sandboxRuntimeImage" . | quote }}
|
||||
{{- end }}
|
||||
supervisor_image = {{ include "openshell.supervisorImage" . | quote }}
|
||||
{{- if .Values.server.hostGatewayIP }}
|
||||
host_gateway_ip = {{ .Values.server.hostGatewayIP | quote }}
|
||||
{{- end }}
|
||||
{{- if not .Values.server.disableTls }}
|
||||
client_tls_secret_name = {{ .Values.server.tls.clientTlsSecretName | quote }}
|
||||
{{- end }}
|
||||
workspace_mode = {{ .Values.server.drivers.kubernetes.workspaceMode | default "shared" | quote }}
|
||||
gateway_id = {{ .Values.server.sandboxJwt.gatewayId | default (include "openshell.fullname" .) | quote }}
|
||||
grpc_endpoint = {{ include "openshell.grpcEndpoint" . | quote }}
|
||||
service_account_name = {{ include "openshell.sandboxServiceAccountName" . | quote }}
|
||||
{{- if .Values.server.enableUserNamespaces }}
|
||||
enable_user_namespaces = true
|
||||
{{- end }}
|
||||
{{- if .Values.server.drivers.kubernetes.operatorNamespaceLabel }}
|
||||
operator_namespace_label = {{ .Values.server.drivers.kubernetes.operatorNamespaceLabel | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.drivers.kubernetes.operatorNamespaceFile }}
|
||||
operator_namespace_file = {{ .Values.server.drivers.kubernetes.operatorNamespaceFile | quote }}
|
||||
{{- end }}
|
||||
sa_token_ttl_secs = {{ .Values.server.sandboxJwt.k8sSaTokenTtlSecs | default 3600 }}
|
||||
{{- if .Values.upstreamProxy.url }}
|
||||
https_proxy = {{ .Values.upstreamProxy.url | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.upstreamProxy.noProxy }}
|
||||
no_proxy = {{ .Values.upstreamProxy.noProxy | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.upstreamProxy.authSecret.name }}
|
||||
proxy_auth_secret_name = {{ .Values.upstreamProxy.authSecret.name | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.upstreamProxy.authSecret.key }}
|
||||
proxy_auth_secret_key = {{ .Values.upstreamProxy.authSecret.key | quote }}
|
||||
{{- end }}
|
||||
{{- if and .Values.upstreamProxy.authSecret.name .Values.upstreamProxy.authSecret.key }}
|
||||
proxy_auth_allow_insecure = {{ .Values.upstreamProxy.authAllowInsecure }}
|
||||
{{- end }}
|
||||
{{- if .Values.upstreamProxy.connectByHostname }}
|
||||
proxy_connect_by_hostname = true
|
||||
{{- end }}
|
||||
{{- if .Values.upstreamProxy.caBundle.configMapName }}
|
||||
proxy_ca_bundle = "/etc/openshell-tls/proxy-ca/ca.crt"
|
||||
{{- end }}
|
||||
{{- if .Values.server.providerTokenGrants.spiffe.enabled }}
|
||||
provider_spiffe_workload_api_socket_path = {{ .Values.server.providerTokenGrants.spiffe.workloadApiSocketPath | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.sandbox.image.pullPolicy }}
|
||||
image_pull_policy = {{ include "openshell.canonicalImagePullPolicy" .Values.sandbox.image.pullPolicy | quote }}
|
||||
{{- end }}
|
||||
{{- $sandboxImagePullSecretNames := list -}}
|
||||
{{- range .Values.server.sandboxImagePullSecrets }}
|
||||
{{- if .name }}
|
||||
{{- $sandboxImagePullSecretNames = append $sandboxImagePullSecretNames .name }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if $sandboxImagePullSecretNames }}
|
||||
image_pull_secrets = [{{- range $i, $name := $sandboxImagePullSecretNames }}{{ if $i }}, {{ end }}{{ $name | quote }}{{- end }}]
|
||||
{{- end }}
|
||||
{{- if .Values.server.workspaceDefaultStorageSize }}
|
||||
workspace_default_storage_size = {{ .Values.server.workspaceDefaultStorageSize | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.workspaceStorageClass }}
|
||||
workspace_storage_class = {{ .Values.server.workspaceStorageClass | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.defaultRuntimeClassName }}
|
||||
default_runtime_class_name = {{ .Values.server.defaultRuntimeClassName | quote }}
|
||||
{{- end }}
|
||||
{{- with include "openshell.sandboxId" (dict "name" "server.sandboxUid" "value" .Values.server.sandboxUid) }}
|
||||
sandbox_uid = {{ . }}
|
||||
{{- end }}
|
||||
{{- with include "openshell.sandboxId" (dict "name" "server.sandboxGid" "value" .Values.server.sandboxGid) }}
|
||||
sandbox_gid = {{ . }}
|
||||
{{- end }}
|
||||
{{- if (.Values.supervisor.image.pullPolicy | default .Values.global.image.pullPolicy) }}
|
||||
supervisor_image_pull_policy = {{ include "openshell.canonicalImagePullPolicy" (.Values.supervisor.image.pullPolicy | default .Values.global.image.pullPolicy) | quote }}
|
||||
{{- end }}
|
||||
{{- if (.Values.sandboxRuntime.image.pullPolicy | default .Values.global.image.pullPolicy) }}
|
||||
sandbox_runtime_image_pull_policy = {{ include "openshell.canonicalImagePullPolicy" (.Values.sandboxRuntime.image.pullPolicy | default .Values.global.image.pullPolicy) | quote }}
|
||||
{{- end }}
|
||||
|
||||
[openshell.drivers.kubernetes.resource_admission]
|
||||
enabled = {{ .Values.server.drivers.kubernetes.resourceAdmission.enabled }}
|
||||
{{- if ne .Values.server.drivers.kubernetes.resourceAdmission.requiredLabels nil }}
|
||||
[openshell.drivers.kubernetes.resource_admission.required_labels]
|
||||
{{- range $key, $value := .Values.server.drivers.kubernetes.resourceAdmission.requiredLabels }}
|
||||
{{ $key | quote }} = {{ $value | quote }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
[openshell.drivers.kubernetes.managed_ssh_ingress]
|
||||
enabled = {{ .Values.networkPolicy.enabled }}
|
||||
gateway_namespace = {{ .Release.Namespace | quote }}
|
||||
gateway_pod_selector = { "app.kubernetes.io/name" = {{ include "openshell.name" . | quote }}, "app.kubernetes.io/instance" = {{ .Release.Name | quote }} }
|
||||
|
||||
[openshell.drivers.kubernetes.sandbox_runtime]
|
||||
boundary_port = {{ .Values.supervisor.sandboxRuntime.boundaryPort | default 5500 }}
|
||||
|
||||
{{- if not $credentialDrivers }}
|
||||
|
||||
[openshell.gateway.credential_storage]
|
||||
key_encryption_key_env = {{ include "openshell.credentialStorageKeyEncryptionKeyEnvName" . | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.server.credentialDrivers.kubernetesSecrets.enabled }}
|
||||
|
||||
[openshell.credential_drivers.kubernetes-secrets]
|
||||
namespace = {{ include "openshell.credentialKubernetesSecretsNamespace" . | quote }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values.server.credentialDrivers.vault.enabled }}
|
||||
|
||||
[openshell.credential_drivers.vault]
|
||||
address = {{ .Values.server.credentialDrivers.vault.address | quote }}
|
||||
{{- if .Values.server.credentialDrivers.vault.caConfigMapName }}
|
||||
ca_bundle = "/etc/openshell-tls/vault-ca/ca.crt"
|
||||
{{- end }}
|
||||
mount = {{ .Values.server.credentialDrivers.vault.mount | quote }}
|
||||
kv_version = {{ .Values.server.credentialDrivers.vault.kvVersion | quote }}
|
||||
auth_method = {{ .Values.server.credentialDrivers.vault.authMethod | quote }}
|
||||
{{- if .Values.server.credentialDrivers.vault.role }}
|
||||
role = {{ .Values.server.credentialDrivers.vault.role | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.credentialDrivers.vault.kubernetesAuthMount }}
|
||||
kubernetes_auth_mount = {{ .Values.server.credentialDrivers.vault.kubernetesAuthMount | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.credentialDrivers.vault.serviceAccountTokenPath }}
|
||||
service_account_token_path = {{ .Values.server.credentialDrivers.vault.serviceAccountTokenPath | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.credentialDrivers.vault.tokenPath }}
|
||||
token_path = {{ .Values.server.credentialDrivers.vault.tokenPath | quote }}
|
||||
{{- end }}
|
||||
{{- if .Values.server.credentialDrivers.vault.timeoutSecs }}
|
||||
timeout_secs = {{ .Values.server.credentialDrivers.vault.timeoutSecs }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ include "openshell.gatewayConfigToml" . | nindent 4 }}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" -}}
|
||||
{{- $kubernetesConfig := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- $workspaceMode := get $kubernetesConfig "workspace_mode" | default "shared" -}}
|
||||
{{- $allowDriverConfig := get $kubernetesConfig "allow_driver_config" | default false -}}
|
||||
{{- if and (eq $workspaceMode "shared") (include "openshell.workspaceResourcesEnabled" .) (include "openshell.rbacCreate" .) }}
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
@@ -10,7 +12,7 @@ metadata:
|
||||
labels:
|
||||
{{- include "openshell.labels" . | nindent 4 }}
|
||||
rules:
|
||||
{{- if .Values.server.drivers.kubernetes.allowDriverConfig }}
|
||||
{{- if $allowDriverConfig }}
|
||||
# Metadata-only admission of caller-selected PVCs requires get permission.
|
||||
- apiGroups: [""]
|
||||
resources: ["persistentvolumeclaims"]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{{- $workspaceMode := .Values.server.drivers.kubernetes.workspaceMode | default "shared" -}}
|
||||
{{- $kubernetesConfig := include "openshell.effectiveKubernetesConfig" . | fromYaml -}}
|
||||
{{- $workspaceMode := get $kubernetesConfig "workspace_mode" | default "shared" -}}
|
||||
{{- if and (eq $workspaceMode "shared") (include "openshell.workspaceResourcesEnabled" .) (include "openshell.rbacCreate" .) }}
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -79,6 +79,21 @@ tests:
|
||||
resources: ["networkpolicies"]
|
||||
verbs: ["get", "create", "patch", "update"]
|
||||
|
||||
- it: gives schema-v2 managed SSH ingress precedence over the legacy alias
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: managed
|
||||
networkPolicy.enabled: true
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes.managed_ssh_ingress:
|
||||
enabled: false
|
||||
asserts:
|
||||
- notContains:
|
||||
path: rules
|
||||
content:
|
||||
apiGroups: ["networking.k8s.io"]
|
||||
resources: ["networkpolicies"]
|
||||
verbs: ["get", "create", "patch", "update"]
|
||||
|
||||
- it: omits credential Secret access in managed mode
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: managed
|
||||
@@ -126,7 +141,9 @@ tests:
|
||||
|
||||
- it: grants managed sandbox-runtime companion permissions
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: managed
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
workspace_mode: managed
|
||||
asserts:
|
||||
- contains:
|
||||
path: rules
|
||||
@@ -149,7 +166,9 @@ tests:
|
||||
|
||||
- it: grants operator sandbox-runtime companion permissions
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: operator
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
workspace_mode: operator
|
||||
asserts:
|
||||
- contains:
|
||||
path: rules
|
||||
|
||||
@@ -80,6 +80,44 @@ tests:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '\[openshell\.gateway\.credential_storage\]'
|
||||
|
||||
- it: fills an absent schema-v2 Kubernetes Secrets namespace from the legacy alias
|
||||
template: templates/gateway-config.yaml
|
||||
set:
|
||||
server.credentialDrivers.kubernetesSecrets.namespace: provider-secrets
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- kubernetes-secrets
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.credential_drivers\.kubernetes-secrets\].*?namespace\s*=\s*"provider-secrets"'
|
||||
|
||||
- it: preserves the canonical credential namespace when the legacy alias selects the driver
|
||||
set:
|
||||
server.credentialDrivers.kubernetesSecrets.enabled: true
|
||||
server.credentialDrivers.kubernetesSecrets.namespace: legacy-secrets
|
||||
gatewayConfig:
|
||||
openshell.credential_drivers.kubernetes-secrets:
|
||||
namespace: provider-secrets
|
||||
asserts:
|
||||
- template: templates/gateway-config.yaml
|
||||
matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: 'credential_drivers\s*=\s*\["kubernetes-secrets"\]'
|
||||
- template: templates/gateway-config.yaml
|
||||
matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.credential_drivers\.kubernetes-secrets\]\s*namespace\s*=\s*"provider-secrets"'
|
||||
- template: templates/credential-secrets-role.yaml
|
||||
equal:
|
||||
path: metadata.namespace
|
||||
value: provider-secrets
|
||||
- template: templates/credential-secrets-rolebinding.yaml
|
||||
equal:
|
||||
path: metadata.namespace
|
||||
value: provider-secrets
|
||||
|
||||
- it: renders Vault credential driver config
|
||||
template: templates/gateway-config.yaml
|
||||
set:
|
||||
@@ -93,10 +131,31 @@ tests:
|
||||
pattern: 'credential_drivers\s*=\s*\["vault"\]'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.credential_drivers\.vault\].*?address\s*=\s*"https://vault\.vault\.svc\.cluster\.local:8200".*?ca_bundle\s*=\s*"/etc/openshell-tls/vault-ca/ca\.crt".*?auth_method\s*=\s*"kubernetes".*?role\s*=\s*"openshell-gateway"'
|
||||
pattern: '(?ms)\[openshell\.credential_drivers\.vault\].*?address\s*=\s*"https://vault\.vault\.svc\.cluster\.local:8200".*?auth_method\s*=\s*"kubernetes".*?ca_bundle\s*=\s*"/etc/openshell-tls/vault-ca/ca\.crt".*?role\s*=\s*"openshell-gateway"'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: 'transport\s*=\s*"in_tree"'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '\[openshell\.gateway\.credential_storage\]'
|
||||
|
||||
- it: fills absent schema-v2 Vault settings from legacy values without overwriting schema-v2 fields
|
||||
template: templates/gateway-config.yaml
|
||||
set:
|
||||
server.credentialDrivers.vault.enabled: true
|
||||
server.credentialDrivers.vault.address: https://legacy-vault.example:8200
|
||||
server.credentialDrivers.vault.authMethod: kubernetes
|
||||
server.credentialDrivers.vault.role: legacy-role
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- vault
|
||||
openshell.credential_drivers.vault:
|
||||
role: schema-v2-role
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.credential_drivers\.vault\].*?address\s*=\s*"https://legacy-vault\.example:8200".*?auth_method\s*=\s*"kubernetes".*?role\s*=\s*"schema-v2-role"'
|
||||
|
||||
- it: rejects multiple enabled credential drivers
|
||||
template: templates/statefulset.yaml
|
||||
@@ -109,6 +168,29 @@ tests:
|
||||
- failedTemplate:
|
||||
errorPattern: "only one external server.credentialDrivers backend can be enabled at a time"
|
||||
|
||||
- it: rejects multiple schema-v2 credential drivers
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- kubernetes-secrets
|
||||
- vault
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "gatewayConfig.openshell.gateway.credential_drivers may select only one backend"
|
||||
|
||||
- it: rejects an explicit empty schema-v2 driver list
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
server.credentialDrivers.vault.enabled: true
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers: []
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "gatewayConfig.openshell.gateway.credential_drivers must select exactly one backend or be omitted/null to use the chart default"
|
||||
|
||||
- it: mounts the Vault CA ConfigMap into the gateway
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
@@ -174,6 +256,24 @@ tests:
|
||||
path: subjects[0].namespace
|
||||
value: my-namespace
|
||||
|
||||
- it: honors the legacy Kubernetes Secrets RBAC opt-out
|
||||
template: templates/credential-secrets-role.yaml
|
||||
set:
|
||||
server.credentialDrivers.kubernetesSecrets.enabled: true
|
||||
server.credentialDrivers.kubernetesSecrets.rbac.create: false
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: omits the Kubernetes Secrets RoleBinding when RBAC is opted out
|
||||
template: templates/credential-secrets-rolebinding.yaml
|
||||
set:
|
||||
server.credentialDrivers.kubernetesSecrets.enabled: true
|
||||
server.credentialDrivers.kubernetesSecrets.rbac.create: false
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: allows default credential storage on a Deployment with an external database
|
||||
template: templates/deployment.yaml
|
||||
set:
|
||||
|
||||
@@ -32,6 +32,24 @@ tests:
|
||||
path: metadata.annotations["helm.sh/resource-policy"]
|
||||
value: keep
|
||||
|
||||
- it: creates a retained credential namespace when schema-v2 selects the driver
|
||||
set:
|
||||
server.credentialDrivers.kubernetesSecrets.namespace: provider-secrets
|
||||
server.credentialDrivers.kubernetesSecrets.createNamespace: true
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
credential_drivers:
|
||||
- kubernetes-secrets
|
||||
asserts:
|
||||
- isKind:
|
||||
of: Namespace
|
||||
- equal:
|
||||
path: metadata.name
|
||||
value: provider-secrets
|
||||
- equal:
|
||||
path: metadata.annotations["helm.sh/resource-policy"]
|
||||
value: keep
|
||||
|
||||
- it: requires a namespace other than the release namespace
|
||||
set:
|
||||
server.credentialDrivers.kubernetesSecrets.enabled: true
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Inputs used by deploy/helm/test-gateway-config-parser.sh. The driver table is
|
||||
# intentionally opaque to the gateway loader, which makes it a safe way to
|
||||
# exercise every TOML shape supported by the chart serializer.
|
||||
gatewayConfig:
|
||||
openshell.drivers.parser-validation:
|
||||
rendered_name: '{{ .Release.Namespace }}/{{ .Release.Name }}'
|
||||
boolean_value: true
|
||||
integer_value: 42
|
||||
float_value: 1.5
|
||||
empty_value: ""
|
||||
scalar_array:
|
||||
- first
|
||||
- second
|
||||
inline_map:
|
||||
alpha: false
|
||||
zebra: 2
|
||||
map_array:
|
||||
- name: first
|
||||
enabled: true
|
||||
- name: second
|
||||
enabled: false
|
||||
escaped_string: 'quotes " and backslash \\ and a newline
|
||||
are preserved'
|
||||
omitted_value: null
|
||||
@@ -0,0 +1,491 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
suite: gatewayConfig TOML serializer
|
||||
templates:
|
||||
- templates/gateway-config.yaml
|
||||
release:
|
||||
name: serializer-test
|
||||
namespace: serializer-namespace
|
||||
|
||||
tests:
|
||||
- it: gives gatewayConfig precedence over a deprecated proxy alias
|
||||
set:
|
||||
upstreamProxy.url: http://legacy-proxy.example:8080
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
https_proxy: http://schema-v2-proxy.example:8443
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^https_proxy = "http://schema-v2-proxy\.example:8443"$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: legacy-proxy
|
||||
|
||||
- it: derives runtime listener ports from chart-owned Service ports
|
||||
set:
|
||||
service.port: 19080
|
||||
service.healthPort: 19081
|
||||
service.metricsPort: 19090
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
bind_address: 0.0.0.0:8080
|
||||
health_bind_address: 0.0.0.0:8081
|
||||
metrics_bind_address: 0.0.0.0:9090
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^bind_address = "0\.0\.0\.0:19080"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^health_bind_address = "0\.0\.0\.0:19081"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^metrics_bind_address = "0\.0\.0\.0:19090"$'
|
||||
|
||||
- it: aligns runtime namespace and ServiceAccount with chart-owned resources
|
||||
set:
|
||||
server.sandboxNamespace: chart-sandboxes
|
||||
sandboxServiceAccount.create: false
|
||||
sandboxServiceAccount.name: chart-sandbox
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
namespace: unrelated-namespace
|
||||
service_account_name: unrelated-service-account
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^namespace = "chart-sandboxes"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^service_account_name = "chart-sandbox"$'
|
||||
|
||||
- it: gives gatewayConfig precedence over deprecated image, runtime, and CA aliases
|
||||
set:
|
||||
sandboxRuntime.image.repository: legacy.example/sandbox
|
||||
sandboxRuntime.image.tag: legacy
|
||||
supervisor.image.repository: legacy.example/supervisor
|
||||
supervisor.image.tag: legacy
|
||||
supervisor.sandboxRuntime.boundaryPort: 6600
|
||||
upstreamProxy.caBundle.configMapName: legacy-proxy-ca
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
sandbox_runtime_image: schema.example/sandbox:2
|
||||
supervisor_image: schema.example/supervisor:2
|
||||
sandbox_runtime:
|
||||
boundary_port: 7700
|
||||
proxy_ca_bundle: /schema-v2/ca.crt
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^sandbox_runtime_image = "schema\.example/sandbox:2"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^supervisor_image = "schema\.example/supervisor:2"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^proxy_ca_bundle = "/schema-v2/ca\.crt"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^sandbox_runtime = \{ boundary_port = 7700 \}$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: legacy\.example|6600|proxy-ca
|
||||
|
||||
- it: does not serialize the removed NetworkPolicy acknowledgement alias
|
||||
set:
|
||||
supervisor.sandboxRuntime.networkPolicyEnforced: true
|
||||
supervisor.sandboxRuntime.boundaryPort: 6600
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^sandbox_runtime = \{ boundary_port = 6600 \}$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: network_policy_enforced
|
||||
|
||||
- it: translates a deprecated workspace mode when schema-v2 keeps its default
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: managed
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^workspace_mode = "managed"$'
|
||||
|
||||
- it: gives an explicit schema-v2 workspace mode precedence over the deprecated alias
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: managed
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
workspace_mode: shared
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^workspace_mode = "shared"$'
|
||||
|
||||
- it: gives explicit schema-v2 gateway settings precedence over legacy aliases
|
||||
set:
|
||||
server.name: legacy-gateway
|
||||
server.enableWebsocketTunnel: false
|
||||
gatewayConfig:
|
||||
openshell.gateway:
|
||||
name: schema-v2-gateway
|
||||
enable_websocket_tunnel: true
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^name = "schema-v2-gateway"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^enable_websocket_tunnel = true$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: legacy-gateway
|
||||
|
||||
- it: translates legacy OIDC roles and scopes into schema-v2 field names
|
||||
set:
|
||||
server.oidc.issuer: https://issuer.example/realms/openshell
|
||||
server.oidc.audience: openshell-cli
|
||||
server.oidc.rolesClaim: custom.roles
|
||||
server.oidc.adminRole: platform-admin
|
||||
server.oidc.userRole: platform-user
|
||||
server.oidc.scopesClaim: permissions
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.gateway\.oidc\].*?admin_role = "platform-admin"'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.gateway\.oidc\].*?roles_claim = "custom\.roles"'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.gateway\.oidc\].*?scopes_claim = "permissions"'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.gateway\.oidc\].*?user_role = "platform-user"'
|
||||
|
||||
- it: fills absent schema-v2 OIDC fields from legacy values without overwriting schema-v2 fields
|
||||
set:
|
||||
server.oidc.issuer: https://legacy.example/realms/openshell
|
||||
server.oidc.rolesClaim: legacy.roles
|
||||
server.oidc.adminRole: legacy-admin
|
||||
server.oidc.userRole: legacy-user
|
||||
server.oidc.scopesClaim: legacy.scopes
|
||||
gatewayConfig:
|
||||
openshell.gateway.oidc:
|
||||
issuer: https://schema-v2.example/realms/openshell
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^issuer = "https://schema-v2\.example/realms/openshell"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^roles_claim = "legacy\.roles"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^admin_role = "legacy-admin"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^user_role = "legacy-user"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^scopes_claim = "legacy\.scopes"$'
|
||||
|
||||
- it: fills an absent schema-v2 OTLP endpoint from legacy values
|
||||
set:
|
||||
server.otlp.endpoint: https://legacy-otlp.example:4317
|
||||
server.otlp.serviceName: legacy-service
|
||||
gatewayConfig:
|
||||
openshell.gateway.otlp:
|
||||
service_name: schema-v2-service
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^endpoint = "https://legacy-otlp\.example:4317"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^service_name = "schema-v2-service"$'
|
||||
|
||||
- it: fills an absent schema-v2 OCSF path from legacy values
|
||||
set:
|
||||
server.ocsfLog.enabled: true
|
||||
server.ocsfLog.path: /var/log/openshell/legacy.jsonl
|
||||
gatewayConfig:
|
||||
openshell.gateway.ocsf_log:
|
||||
rotation: never
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^path = "/var/log/openshell/legacy\.jsonl"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^rotation = "never"$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^max_files ='
|
||||
|
||||
- it: omits empty optional OIDC role fields translated from legacy values
|
||||
set:
|
||||
server.oidc.issuer: https://issuer.example/realms/openshell
|
||||
server.oidc.audience: openshell-cli
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^issuer = "https://issuer\.example/realms/openshell"$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^roles_claim ='
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^admin_role ='
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^user_role ='
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^scopes_claim ='
|
||||
|
||||
- it: preserves explicit empty schema-v2 OIDC role fields
|
||||
set:
|
||||
server.oidc.issuer: https://legacy.example/realms/openshell
|
||||
server.oidc.adminRole: legacy-admin
|
||||
gatewayConfig:
|
||||
openshell.gateway.oidc:
|
||||
issuer: https://schema-v2.example/realms/openshell
|
||||
roles_claim: ""
|
||||
admin_role: ""
|
||||
user_role: ""
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^issuer = "https://schema-v2\.example/realms/openshell"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^roles_claim = ""$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^admin_role = ""$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^user_role = ""$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: legacy-admin
|
||||
|
||||
- it: translates legacy resource admission labels into snake_case
|
||||
set:
|
||||
server.drivers.kubernetes.resourceAdmission.requiredLabels:
|
||||
platform.example.com/approved: "true"
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^required_labels = \{ "platform\.example\.com/approved" = "true" \}$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '"platform.example.com/approved" = "true"'
|
||||
|
||||
- it: gives schema-v2 resource admission precedence over the legacy alias
|
||||
set:
|
||||
server.drivers.kubernetes.resourceAdmission.requiredLabels:
|
||||
platform.example.com/source: legacy
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
resource_admission:
|
||||
required_labels:
|
||||
platform.example.com/source: schema-v2
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '"platform.example.com/source" = "schema-v2"'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '"platform.example.com/source" = "legacy"'
|
||||
|
||||
- it: preserves the dotted schema-v2 resource admission table over legacy defaults
|
||||
set:
|
||||
server.drivers.kubernetes.resourceAdmission.enabled: true
|
||||
server.drivers.kubernetes.resourceAdmission.requiredLabels:
|
||||
platform.example.com/source: legacy
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes.resource_admission:
|
||||
enabled: false
|
||||
required_labels:
|
||||
platform.example.com/source: schema-v2
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^\[openshell\.drivers\.kubernetes\.resource_admission\]\nenabled = false\nrequired_labels = \{ "platform\.example\.com/source" = "schema-v2" \}$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '"platform.example.com/source" = "legacy"'
|
||||
|
||||
- it: leaves the workload image pull policy unset when only the global policy is configured
|
||||
set:
|
||||
global.image.pullPolicy: Always
|
||||
asserts:
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^image_pull_policy ='
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^sandbox_runtime_image_pull_policy = "always"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^supervisor_image_pull_policy = "always"$'
|
||||
|
||||
- it: translates an explicit legacy workload image pull policy
|
||||
set:
|
||||
sandbox.image.pullPolicy: Always
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^image_pull_policy = "always"$'
|
||||
|
||||
- it: gives the canonical workload image pull policy precedence over the legacy alias
|
||||
set:
|
||||
sandbox.image.pullPolicy: Always
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
image_pull_policy: never
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^image_pull_policy = "never"$'
|
||||
|
||||
- it: renders all supported YAML shapes with deterministic TOML output
|
||||
set:
|
||||
gatewayConfig:
|
||||
example.config:
|
||||
string_value: plain text
|
||||
boolean_value: true
|
||||
integer_value: 42
|
||||
float_value: 1.5
|
||||
scalar_array:
|
||||
- first
|
||||
- second
|
||||
nested_map:
|
||||
zebra: 2
|
||||
alpha: false
|
||||
map_array:
|
||||
- name: first
|
||||
enabled: true
|
||||
- name: second
|
||||
enabled: false
|
||||
omitted_value: null
|
||||
empty_value: ""
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)^\[example\.config\]\n.*?boolean_value = true\n.*?empty_value = ""\n.*?float_value = 1\.5\n.*?integer_value = 42\n.*?map_array = \[\{ enabled = true, name = "first" \}, \{ enabled = false, name = "second" \}\]\n.*?nested_map = \{ alpha = false, zebra = 2 \}\n.*?scalar_array = \["first", "second"\]\n.*?string_value = "plain text"$'
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: omitted_value
|
||||
|
||||
- it: renders top-level lists as ordered TOML arrays of tables
|
||||
set:
|
||||
gatewayConfig:
|
||||
openshell.gateway.interceptors:
|
||||
- name: governance
|
||||
grpc_endpoint: http://governance.ns.svc.cluster.local:18081
|
||||
order: 0
|
||||
failure_policy: fail_closed
|
||||
- name: audit
|
||||
grpc_endpoint: http://audit.ns.svc.cluster.local:18081
|
||||
order: 1
|
||||
failure_policy: fail_open
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)^\[\[openshell\.gateway\.interceptors\]\]\nfailure_policy = "fail_closed"\ngrpc_endpoint = "http://governance\.ns\.svc\.cluster\.local:18081"\nname = "governance"\norder = 0\n\[\[openshell\.gateway\.interceptors\]\]\nfailure_policy = "fail_open"\ngrpc_endpoint = "http://audit\.ns\.svc\.cluster\.local:18081"\nname = "audit"\norder = 1$'
|
||||
|
||||
- it: renders the CI extension lists as separate ordered interceptor and middleware tables
|
||||
values:
|
||||
- ../ci/values-gateway-extensions.yaml
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^\[\[openshell\.gateway\.interceptors\]\]\nallow_insecure_transport = true\nfailure_policy = "fail_closed"\ngrpc_endpoint = "http://governance\.ns\.svc\.cluster\.local:18081"\nname = "governance"\norder = 0\n\[\[openshell\.gateway\.interceptors\]\]\nallow_insecure_transport = true\nfailure_policy = "fail_open"\ngrpc_endpoint = "http://audit\.ns\.svc\.cluster\.local:18081"\nname = "audit"\norder = 1$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^\[\[openshell\.supervisor\.middleware\]\]\nallow_insecure_transport = true\ngrpc_endpoint = "http://content-guard\.ns\.svc\.cluster\.local:18082"\nmax_payload_bytes = 262144\nname = "content-guard"\ntimeout = "500ms"\n\[\[openshell\.supervisor\.middleware\]\]\nallow_insecure_transport = true\ngrpc_endpoint = "http://request-audit\.ns\.svc\.cluster\.local:18082"\nmax_payload_bytes = 262144\nname = "request-audit"\ntimeout = "1s"$'
|
||||
|
||||
- it: rejects non-map entries in a top-level array of tables
|
||||
set:
|
||||
gatewayConfig:
|
||||
example.entries:
|
||||
- valid: true
|
||||
- invalid
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: 'gatewayConfig array-of-tables "example.entries" entry 1 must be a map, got string'
|
||||
|
||||
- it: rejects a nested map that collides with a dotted top-level table
|
||||
set:
|
||||
gatewayConfig:
|
||||
openshell:
|
||||
gateway:
|
||||
log_level: debug
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: 'gatewayConfig defines "openshell.gateway" both as a nested map and a dotted top-level table; use only the dotted top-level key'
|
||||
|
||||
- it: serializes strings literally without evaluating Helm expressions
|
||||
set:
|
||||
gatewayConfig:
|
||||
example:
|
||||
release_template: '{{ .Release.Namespace }}/{{ .Release.Name }}'
|
||||
helm_function: '{{ randAlphaNum 8 }}'
|
||||
lookup_expression: '{{ lookup "v1" "Secret" "default" "example" }}'
|
||||
boolean_value: true
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^release_template = "\{\{ \.Release\.Namespace \}\}/\{\{ \.Release\.Name \}\}"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^helm_function = "\{\{ randAlphaNum 8 \}\}"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^lookup_expression = "\{\{ lookup \\"v1\\" \\"Secret\\" \\"default\\" \\"example\\" \}\}"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^boolean_value = true$'
|
||||
|
||||
- it: quotes non-bare TOML keys safely
|
||||
set:
|
||||
gatewayConfig:
|
||||
example:
|
||||
"field with spaces": value
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^"field with spaces" = "value"$'
|
||||
|
||||
- it: rejects a top-level table that is not a map
|
||||
set:
|
||||
gatewayConfig:
|
||||
example: invalid
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: 'gatewayConfig table "example" must be a map, got string'
|
||||
|
||||
- it: rejects null members in arrays
|
||||
set:
|
||||
gatewayConfig:
|
||||
example:
|
||||
values:
|
||||
- valid
|
||||
- null
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: gatewayConfig arrays cannot contain null values
|
||||
|
||||
- it: rejects table names with empty TOML key segments
|
||||
set:
|
||||
gatewayConfig:
|
||||
"example..config":
|
||||
value: valid
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: 'gatewayConfig table "example..config" contains an empty TOML key segment'
|
||||
@@ -359,7 +359,7 @@ tests:
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.gateway\.ocsf_log\].*?path\s*=\s*"/var/openshell/gateway-ocsf\.jsonl".*?schema_version\s*=\s*"1\.3".*?rotation\s*=\s*"daily".*?max_files\s*=\s*14.*?queue_capacity\s*=\s*20000.*?queue_max_bytes\s*=\s*33554432'
|
||||
pattern: '(?ms)\[openshell\.gateway\.ocsf_log\].*?max_files\s*=\s*14.*?path\s*=\s*"/var/openshell/gateway-ocsf\.jsonl".*?queue_capacity\s*=\s*20000.*?queue_max_bytes\s*=\s*33554432.*?rotation\s*=\s*"daily".*?schema_version\s*=\s*"1\.3"'
|
||||
|
||||
- it: omits OCSF retention when rotation is disabled
|
||||
template: templates/gateway-config.yaml
|
||||
@@ -502,7 +502,10 @@ tests:
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)supervisor_image_pull_policy\s*=\s*"if_not_present".*?sandbox_runtime_image_pull_policy\s*=\s*"if_not_present"'
|
||||
pattern: '(?m)^supervisor_image_pull_policy\s*=\s*"if_not_present"$'
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?m)^sandbox_runtime_image_pull_policy\s*=\s*"if_not_present"$'
|
||||
|
||||
- it: renders canonical image pull policies in the Kubernetes driver table
|
||||
template: templates/gateway-config.yaml
|
||||
@@ -549,7 +552,7 @@ tests:
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.drivers\.kubernetes\].*?namespace\s*=\s*"my-namespace".*?default_image\s*=.*?supervisor_image\s*=.*?host_gateway_ip\s*=\s*"10\.0\.0\.1".*?client_tls_secret_name\s*=.*?service_account_name\s*=\s*"openshell-sandbox".*?enable_user_namespaces\s*=\s*true.*?sa_token_ttl_secs\s*='
|
||||
pattern: '(?ms)\[openshell\.drivers\.kubernetes\].*?client_tls_secret_name\s*=.*?default_image\s*=.*?enable_user_namespaces\s*=\s*true.*?host_gateway_ip\s*=\s*"10\.0\.0\.1".*?namespace\s*=\s*"my-namespace".*?sa_token_ttl_secs\s*=.*?service_account_name\s*=\s*"openshell-sandbox".*?supervisor_image\s*='
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?ms)\[openshell\.gateway\][^\[]*?(sandbox_namespace|default_image|supervisor_image|client_tls_secret_name|service_account_name|host_gateway_ip|enable_user_namespaces|sa_token_ttl_secs)\s*='
|
||||
@@ -969,6 +972,17 @@ tests:
|
||||
path: data["gateway.toml"]
|
||||
pattern: 'server_sans\s*=\s*\["openshell", "\*\.dev\.openshell\.localhost"\]'
|
||||
|
||||
- it: preserves pkiInitJob serverDnsNames as the runtime SAN fallback
|
||||
set:
|
||||
certManager.enabled: false
|
||||
pkiInitJob.serverDnsNames:
|
||||
- "*.apps.example.com"
|
||||
template: templates/gateway-config.yaml
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: 'server_sans\s*=\s*\["\*\.apps\.example\.com"\]'
|
||||
|
||||
- it: passes sqlite db-url via --db-url arg by default
|
||||
template: templates/statefulset.yaml
|
||||
asserts:
|
||||
@@ -1320,7 +1334,7 @@ tests:
|
||||
asserts:
|
||||
- matchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: "(?ms)name\\s*=\\s*\\\"openshell\\\".*?\\[openshell\\.drivers\\.kubernetes\\].*?namespace\\s*=\\s*\\\"my-namespace\\\".*?grpc_endpoint\\s*=\\s*\\\"https://openshell\\.my-namespace\\.svc\\.cluster\\.local:8080\\\""
|
||||
pattern: "(?ms)\\[openshell\\.drivers\\.kubernetes\\].*?grpc_endpoint\\s*=\\s*\\\"https://openshell\\.my-namespace\\.svc\\.cluster\\.local:8080\\\".*?namespace\\s*=\\s*\\\"my-namespace\\\""
|
||||
|
||||
- it: uses an explicit server grpc endpoint verbatim
|
||||
template: templates/gateway-config.yaml
|
||||
|
||||
@@ -34,3 +34,18 @@ tests:
|
||||
asserts:
|
||||
- notExists:
|
||||
path: spec.template.spec.securityContext
|
||||
|
||||
- it: derives host aliases from server.hostGatewayIP and not gatewayConfig
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
server.hostGatewayIP: 10.23.45.67
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
host_gateway_ip: 192.0.2.1
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.hostAliases[0].ip
|
||||
value: 10.23.45.67
|
||||
- contains:
|
||||
path: spec.template.spec.hostAliases[0].hostnames
|
||||
content: host.openshell.internal
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
suite: gateway ConfigMap secret boundary
|
||||
templates:
|
||||
- templates/gateway-config.yaml
|
||||
- templates/statefulset.yaml
|
||||
|
||||
tests:
|
||||
- it: rejects a database URL in gatewayConfig before creating the ConfigMap
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig.example.database_url: postgresql://gateway:password@database:5432/openshell
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: "gatewayConfig must not contain database_url; provide database credentials through the chart's Secret-backed OPENSHELL_DB_URL environment variable"
|
||||
|
||||
- it: rejects an inline PEM private key before creating the ConfigMap
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig.example.private_key: "-----BEGIN PRIVATE KEY-----"
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: gatewayConfig must not contain an inline private key; provide it through a Secret-backed file mount
|
||||
|
||||
- it: rejects inline credentials embedded in a URL
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig.example.endpoint: https://gateway:password@database.example.com
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: gatewayConfig must not contain inline URL credentials; provide them through a Secret-backed environment variable, file, or volume
|
||||
|
||||
- it: rejects URL credentials with an empty username
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig.example.endpoint: https://:password@database.example.com
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: gatewayConfig must not contain inline URL credentials; provide them through a Secret-backed environment variable, file, or volume
|
||||
|
||||
- it: rejects URL userinfo without a password
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig.example.endpoint: https://username@database.example.com
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: gatewayConfig must not contain inline URL credentials; provide them through a Secret-backed environment variable, file, or volume
|
||||
|
||||
- it: keeps external database credentials out of gateway.toml
|
||||
template: templates/gateway-config.yaml
|
||||
set:
|
||||
server.externalDbSecret: gateway-database
|
||||
asserts:
|
||||
- notMatchRegex:
|
||||
path: data["gateway.toml"]
|
||||
pattern: '(?i)postgresql://|gateway-database|password'
|
||||
|
||||
- it: references external database credentials through an environment variable
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
server.externalDbSecret: gateway-database
|
||||
asserts:
|
||||
- contains:
|
||||
path: spec.template.spec.containers[0].env
|
||||
content:
|
||||
name: OPENSHELL_DB_URL
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: gateway-database
|
||||
key: uri
|
||||
|
||||
- it: rejects malformed Secret references
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
server.credentialStorage.existingSecret: contains spaces
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: server.credentialStorage.existingSecret must be a valid Kubernetes Secret name
|
||||
|
||||
- it: rejects an invalid schema-v2 proxy authentication Secret reference
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
proxy_auth_secret_name: contains spaces
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: gatewayConfig.openshell.drivers.kubernetes.proxy_auth_secret_name must be a valid Kubernetes Secret name
|
||||
|
||||
- it: rejects Secret references longer than the Kubernetes limit
|
||||
template: templates/statefulset.yaml
|
||||
set:
|
||||
server.credentialStorage.existingSecret: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
asserts:
|
||||
- failedTemplate:
|
||||
errorMessage: server.credentialStorage.existingSecret must be no more than 253 characters
|
||||
@@ -105,6 +105,22 @@ tests:
|
||||
resources: ["persistentvolumeclaims"]
|
||||
verbs: ["get"]
|
||||
|
||||
- it: omits shared-workspace resources for the deprecated managed workspace mode
|
||||
template: templates/role.yaml
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: managed
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: omits shared-workspace resources for the deprecated operator workspace mode
|
||||
template: templates/rolebinding.yaml
|
||||
set:
|
||||
server.drivers.kubernetes.workspaceMode: operator
|
||||
asserts:
|
||||
- hasDocuments:
|
||||
count: 0
|
||||
|
||||
- it: uses explicit sandboxNamespace for sandbox RoleBinding
|
||||
template: templates/rolebinding.yaml
|
||||
set:
|
||||
|
||||
@@ -16,15 +16,15 @@ tests:
|
||||
pkiInitJob.enabled: true
|
||||
certManager.enabled: false
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].name
|
||||
value: tls-client-ca
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].secret.secretName
|
||||
value: openshell-server-tls
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].secret.items[0].key
|
||||
value: ca.crt
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: tls-client-ca
|
||||
secret:
|
||||
secretName: openshell-server-tls
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
|
||||
- it: shares the cert-manager server TLS ca.crt when clientCaFromServerTlsSecret is true
|
||||
template: templates/statefulset.yaml
|
||||
@@ -32,15 +32,15 @@ tests:
|
||||
certManager.enabled: true
|
||||
certManager.clientCaFromServerTlsSecret: true
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].name
|
||||
value: tls-client-ca
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].secret.secretName
|
||||
value: openshell-server-tls
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].secret.items[0].key
|
||||
value: ca.crt
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: tls-client-ca
|
||||
secret:
|
||||
secretName: openshell-server-tls
|
||||
items:
|
||||
- key: ca.crt
|
||||
path: ca.crt
|
||||
|
||||
# Regression: with cert-manager enabled and pkiInitJob left at its default
|
||||
# `true`, the client CA condition must honor certManager precedence and NOT
|
||||
@@ -55,14 +55,12 @@ tests:
|
||||
pkiInitJob.enabled: true
|
||||
server.tls.clientCaSecretName: openshell-ca-tls
|
||||
asserts:
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].name
|
||||
value: tls-client-ca
|
||||
- equal:
|
||||
path: spec.template.spec.volumes[5].secret.secretName
|
||||
value: openshell-ca-tls
|
||||
- notExists:
|
||||
path: spec.template.spec.volumes[5].secret.items
|
||||
- contains:
|
||||
path: spec.template.spec.volumes
|
||||
content:
|
||||
name: tls-client-ca
|
||||
secret:
|
||||
secretName: openshell-ca-tls
|
||||
|
||||
- it: omits client CA volume and mount when mTLS is disabled
|
||||
template: templates/statefulset.yaml
|
||||
@@ -94,9 +92,6 @@ tests:
|
||||
pkiInitJob.enabled: true
|
||||
server.tls.clientCaSecretName: ""
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.template.spec.volumes
|
||||
count: 4
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content:
|
||||
@@ -113,9 +108,6 @@ tests:
|
||||
certManager.enabled: false
|
||||
server.tls.clientCaSecretName: ""
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.template.spec.volumes
|
||||
count: 4
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content:
|
||||
@@ -130,9 +122,6 @@ tests:
|
||||
certManager.clientCaFromServerTlsSecret: true
|
||||
server.tls.clientCaSecretName: ""
|
||||
asserts:
|
||||
- lengthEqual:
|
||||
path: spec.template.spec.volumes
|
||||
count: 4
|
||||
- notContains:
|
||||
path: spec.template.spec.containers[0].volumeMounts
|
||||
content:
|
||||
|
||||
@@ -41,6 +41,26 @@ tests:
|
||||
resourceNames: ["custom-client-tls", "registry-one"]
|
||||
verbs: ["get"]
|
||||
|
||||
- it: uses schema-v2 workspace mode and image pull Secrets for staged Secret RBAC
|
||||
template: templates/workspace-secret-source-role.yaml
|
||||
set:
|
||||
server.sandboxNamespace: sandboxes
|
||||
server.tls.clientTlsSecretName: custom-client-tls
|
||||
gatewayConfig:
|
||||
openshell.drivers.kubernetes:
|
||||
workspace_mode: managed
|
||||
image_pull_secrets:
|
||||
- schema-registry
|
||||
asserts:
|
||||
- isKind:
|
||||
of: Role
|
||||
- equal:
|
||||
path: metadata.namespace
|
||||
value: sandboxes
|
||||
- equal:
|
||||
path: rules[0].resourceNames
|
||||
value: ["custom-client-tls", "schema-registry"]
|
||||
|
||||
- it: grants the operator-mode TLS Secret read without image-pull Secrets
|
||||
template: templates/workspace-secret-source-role.yaml
|
||||
set:
|
||||
|
||||
@@ -299,6 +299,14 @@ tolerations: []
|
||||
# -- Affinity rules for the gateway pod.
|
||||
affinity: {}
|
||||
|
||||
# -- Non-secret gateway application configuration. Top-level keys name TOML
|
||||
# tables and are rendered into the mounted gateway.toml file. Kubernetes
|
||||
# resource inputs remain outside this map; template expressions derive the
|
||||
# corresponding runtime values from their resource owner.
|
||||
gatewayConfig:
|
||||
openshell:
|
||||
version: 2
|
||||
|
||||
# Server configuration
|
||||
server:
|
||||
# -- Operator-facing gateway name. Defaults to the chart fullname so all
|
||||
@@ -583,6 +591,14 @@ server:
|
||||
# issuer uses a non-public CA (e.g. OpenShift ingress, private PKI).
|
||||
caConfigMapName: ""
|
||||
|
||||
# Kubernetes resource references for external credential drivers. Runtime
|
||||
# driver settings remain in gatewayConfig.
|
||||
credentialDrivers:
|
||||
vault:
|
||||
# -- ConfigMap containing the private Vault/OpenBao CA certificate under
|
||||
# the ca.crt key. Helm mounts it only when the Vault driver is selected.
|
||||
caConfigMapName: ""
|
||||
|
||||
# NetworkPolicy restricting SSH ingress on sandbox pods to the gateway only.
|
||||
networkPolicy:
|
||||
# -- Restrict SSH ingress on sandbox pods to the gateway. In managed mode,
|
||||
|
||||
Executable
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Render the chart exactly as an operator would, then validate gateway.toml
|
||||
# with the gateway binary. Helm unit tests cover template-level assertions;
|
||||
# this test makes the Rust loader the compatibility authority.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
|
||||
chart="${repo_root}/deploy/helm/openshell"
|
||||
fixture="${chart}/tests/fixtures/parser-validation-values.yaml"
|
||||
work_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${work_dir}"' EXIT
|
||||
|
||||
cargo build --quiet --package openshell-gateway
|
||||
gateway_bin="${repo_root}/target/debug/openshell-gateway"
|
||||
|
||||
render() {
|
||||
local output="$1"
|
||||
shift
|
||||
helm template parser-validation "${chart}" \
|
||||
--namespace parser-namespace \
|
||||
--set agentSandbox.preflight.enabled=false \
|
||||
"$@" >"${output}"
|
||||
}
|
||||
|
||||
extract_toml() {
|
||||
local manifest="$1"
|
||||
local toml="$2"
|
||||
yq ea -e -r \
|
||||
'select(.kind == "ConfigMap" and (.data | has("gateway.toml"))) | .data."gateway.toml"' \
|
||||
"${manifest}" >"${toml}"
|
||||
}
|
||||
|
||||
preflight() {
|
||||
local toml="$1"
|
||||
"${gateway_bin}" config preflight --path "${toml}"
|
||||
}
|
||||
|
||||
# The runtime default must render and pass the Rust loader validation.
|
||||
render "${work_dir}/default.yaml"
|
||||
extract_toml "${work_dir}/default.yaml" "${work_dir}/default.toml"
|
||||
preflight "${work_dir}/default.toml"
|
||||
|
||||
# Exercise all serializer shapes through a raw driver table and prove that
|
||||
# literal strings, TOML escaping, and null omission survive parsing.
|
||||
render "${work_dir}/shapes.yaml" --values "${fixture}"
|
||||
extract_toml "${work_dir}/shapes.yaml" "${work_dir}/shapes.toml"
|
||||
preflight "${work_dir}/shapes.toml"
|
||||
grep -F 'rendered_name = "{{ .Release.Namespace }}/{{ .Release.Name }}"' "${work_dir}/shapes.toml" >/dev/null
|
||||
grep -F 'empty_value = ""' "${work_dir}/shapes.toml" >/dev/null
|
||||
if grep -Fq 'omitted_value' "${work_dir}/shapes.toml"; then
|
||||
echo "null gatewayConfig values must be omitted from gateway.toml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The chart gives absent and empty credential-driver selection deliberately
|
||||
# different meanings: absent retains encrypted storage, while an empty list is
|
||||
# invalid before a manifest is rendered.
|
||||
render "${work_dir}/credential-drivers-absent.yaml" \
|
||||
--set-json 'gatewayConfig.openshell\.gateway.credential_drivers=null'
|
||||
extract_toml "${work_dir}/credential-drivers-absent.yaml" "${work_dir}/credential-drivers-absent.toml"
|
||||
preflight "${work_dir}/credential-drivers-absent.toml"
|
||||
if grep -Fq 'credential_drivers' "${work_dir}/credential-drivers-absent.toml"; then
|
||||
echo "null credential_drivers must be absent from gateway.toml" >&2
|
||||
exit 1
|
||||
fi
|
||||
if render "${work_dir}/credential-drivers-empty.yaml" \
|
||||
--set-json 'gatewayConfig.openshell\.gateway.credential_drivers=[]' >"${work_dir}/credential-drivers-empty.err" 2>&1; then
|
||||
echo "the chart accepted empty credential_drivers" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unknown non-secret values are intentionally serializable by Helm, but must
|
||||
# fail at the Rust schema boundary rather than being silently ignored.
|
||||
render "${work_dir}/unknown.yaml" \
|
||||
--set-string 'gatewayConfig.openshell\.gateway.unknown_non_secret=accepted-by-helm'
|
||||
extract_toml "${work_dir}/unknown.yaml" "${work_dir}/unknown.toml"
|
||||
if preflight "${work_dir}/unknown.toml" >"${work_dir}/unknown.err" 2>&1; then
|
||||
echo "the gateway loader accepted an unknown non-secret field" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Secrets remain outside the ConfigMap even when their Secret reference is
|
||||
# rendered into the workload environment.
|
||||
render "${work_dir}/secret-boundary.yaml" --set server.externalDbSecret=parser-database
|
||||
extract_toml "${work_dir}/secret-boundary.yaml" "${work_dir}/secret-boundary.toml"
|
||||
if grep -Eqi 'parser-database|postgresql:|password' "${work_dir}/secret-boundary.toml"; then
|
||||
echo "gateway.toml contains Secret-backed database material" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A ConfigMap-only mutation must change the StatefulSet checksum and trigger a
|
||||
# rollout. Check this against full Helm output, not just a template fragment.
|
||||
default_checksum="$(yq ea -e -r 'select(.kind == "StatefulSet") | .spec.template.metadata.annotations."checksum/gateway-config"' "${work_dir}/default.yaml")"
|
||||
render "${work_dir}/checksum.yaml" \
|
||||
--set-string 'gatewayConfig.openshell\.gateway.log_level=debug'
|
||||
changed_checksum="$(yq ea -e -r 'select(.kind == "StatefulSet") | .spec.template.metadata.annotations."checksum/gateway-config"' "${work_dir}/checksum.yaml")"
|
||||
if [[ -z "${default_checksum}" || "${default_checksum}" == "${changed_checksum}" ]]; then
|
||||
echo "gateway ConfigMap changes must update the StatefulSet checksum" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# All maintained CI/dev overlays must render a loader-valid configuration.
|
||||
for values in "${chart}"/ci/values-*.yaml; do
|
||||
name="$(basename "${values}" .yaml)"
|
||||
render "${work_dir}/${name}.yaml" --values "${values}"
|
||||
extract_toml "${work_dir}/${name}.yaml" "${work_dir}/${name}.toml"
|
||||
preflight "${work_dir}/${name}.toml"
|
||||
done
|
||||
|
||||
echo "rendered gateway TOML passed Rust loader validation"
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
# Assert relationships that span rendered Kubernetes objects and gateway.toml.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd -P)"
|
||||
chart="${repo_root}/deploy/helm/openshell"
|
||||
work_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "${work_dir}"' EXIT
|
||||
|
||||
render() {
|
||||
local name="$1"
|
||||
shift
|
||||
helm template resource-coherence "${chart}" \
|
||||
--namespace resource-namespace \
|
||||
--set agentSandbox.preflight.enabled=false \
|
||||
"$@" >"${work_dir}/${name}.yaml"
|
||||
|
||||
if invalid_documents="$(yq ea -r 'select(. != null and ((has("apiVersion") | not) or (has("kind") | not))) | .kind // "<unknown>"' "${work_dir}/${name}.yaml")" && [[ -n "${invalid_documents}" ]]; then
|
||||
printf 'rendered Kubernetes documents without apiVersion or kind:\n%s\n' "${invalid_documents}" >&2
|
||||
echo "${name}: rendered an invalid Kubernetes document" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
toml() {
|
||||
yq ea -e -r \
|
||||
'select(.kind == "ConfigMap" and (.data | has("gateway.toml"))) | .data."gateway.toml"' \
|
||||
"$1" >"$2"
|
||||
}
|
||||
|
||||
workload_value() {
|
||||
local manifest="$1"
|
||||
local expression="$2"
|
||||
yq ea -e -r "select(.kind == \"StatefulSet\" or .kind == \"Deployment\") | ${expression}" "${manifest}"
|
||||
}
|
||||
|
||||
render default
|
||||
default_manifest="${work_dir}/default.yaml"
|
||||
default_toml="${work_dir}/default.toml"
|
||||
toml "${default_manifest}" "${default_toml}"
|
||||
service_name="$(yq ea -e -r 'select(.kind == "Service" and .metadata.name == "resource-coherence-openshell") | .metadata.name' "${default_manifest}")"
|
||||
service_port="$(yq ea -e -r 'select(.kind == "Service" and .metadata.name == "resource-coherence-openshell") | .spec.ports[] | select(.name == "grpc") | .port' "${default_manifest}")"
|
||||
workload_port="$(workload_value "${default_manifest}" '.spec.template.spec.containers[] | select(.name == "openshell-gateway") | .ports[] | select(.name == "grpc") | .containerPort')"
|
||||
config_map="$(yq ea -e -r 'select(.kind == "ConfigMap" and (.data | has("gateway.toml"))) | .metadata.name' "${default_manifest}")"
|
||||
mounted_config_map="$(workload_value "${default_manifest}" '.spec.template.spec.volumes[] | select(.name == "gateway-config") | .configMap.name')"
|
||||
[[ "${service_port}" == "${workload_port}" && "${config_map}" == "${mounted_config_map}" ]]
|
||||
grep -F "bind_address = \"0.0.0.0:${service_port}\"" "${default_toml}" >/dev/null
|
||||
grep -F "grpc_endpoint = \"https://${service_name}.resource-namespace.svc.cluster.local:${service_port}\"" "${default_toml}" >/dev/null
|
||||
grep -F '[openshell.gateway.tls]' "${default_toml}" >/dev/null
|
||||
[[ "$(workload_value "${default_manifest}" '.spec.template.spec.volumes[] | select(.name == "tls-cert") | .secret.secretName')" == "openshell-server-tls" ]]
|
||||
[[ "$(workload_value "${default_manifest}" '.spec.template.spec.volumes[] | select(.name == "tls-client-ca") | .secret.secretName')" == "openshell-server-tls" ]]
|
||||
|
||||
render tls-disabled --values "${chart}/ci/values-tls-disabled.yaml"
|
||||
tls_disabled_manifest="${work_dir}/tls-disabled.yaml"
|
||||
tls_disabled_toml="${work_dir}/tls-disabled.toml"
|
||||
toml "${tls_disabled_manifest}" "${tls_disabled_toml}"
|
||||
grep -F 'disable_tls = true' "${tls_disabled_toml}" >/dev/null
|
||||
if grep -Fq '[openshell.gateway.tls]' "${tls_disabled_toml}" \
|
||||
|| workload_value "${tls_disabled_manifest}" '.spec.template.spec.volumes[]?.name' | grep -Eq '^(tls-cert|tls-client-ca)$'; then
|
||||
echo "TLS-disabled runtime configuration and workload mounts disagree" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
render openshift-route --values "${chart}/ci/values-openshift-route-cert-manager.yaml"
|
||||
route_manifest="${work_dir}/openshift-route.yaml"
|
||||
route_toml="${work_dir}/openshift-route.toml"
|
||||
toml "${route_manifest}" "${route_toml}"
|
||||
route_service="$(yq ea -e -r 'select(.kind == "Route") | .spec.to.name' "${route_manifest}")"
|
||||
route_port="$(yq ea -e -r 'select(.kind == "Route") | .spec.port.targetPort' "${route_manifest}")"
|
||||
[[ "${route_service}" == "$(yq ea -e -r "select(.kind == \"Service\" and .metadata.name == \"${route_service}\") | .metadata.name" "${route_manifest}")" && "${route_port}" == "grpc" ]]
|
||||
[[ "$(workload_value "${route_manifest}" '.spec.template.spec.volumes[] | select(.name == "tls-external-cert") | .secret.secretName')" == "${route_service}-server-external-tls" ]]
|
||||
[[ "$(yq ea -e -r 'select(.kind == "Certificate") | .spec.secretName' "${route_manifest}" | grep -Fx "${route_service}-server-external-tls")" == "${route_service}-server-external-tls" ]]
|
||||
grep -F 'external_cert_path = "/etc/openshell-tls/server-external/tls.crt"' "${route_toml}" >/dev/null
|
||||
grep -F 'external_key_path = "/etc/openshell-tls/server-external/tls.key"' "${route_toml}" >/dev/null
|
||||
grep -F 'external_server_names = ["openshell.example.com"]' "${route_toml}" >/dev/null
|
||||
|
||||
echo "gateway Service, TLS, PKI, Route, workload, and runtime config are coherent"
|
||||
@@ -100,27 +100,25 @@ The same settings are available through environment variables:
|
||||
| `OPENSHELL_OIDC_USER_ROLE` | Role required for standard user operations. | `openshell-user` |
|
||||
| `OPENSHELL_OIDC_SCOPES_CLAIM` | Dot-separated claim path containing scopes. Empty disables scope enforcement. | Empty |
|
||||
|
||||
For Helm deployments, set the same values under `server.oidc`:
|
||||
For Helm deployments, set the same values in `gatewayConfig` under the
|
||||
`openshell.gateway.oidc` TOML table:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
oidc:
|
||||
gatewayConfig:
|
||||
openshell.gateway.oidc:
|
||||
issuer: https://idp.example.com/realms/openshell
|
||||
audience: openshell-cli
|
||||
# Only needed when discovery returns a deliberately separate JWKS origin.
|
||||
jwksAllowedOrigins:
|
||||
- https://keys.example.com
|
||||
rolesClaim: realm_access.roles
|
||||
adminRole: openshell-admin
|
||||
userRole: openshell-user
|
||||
scopesClaim: ""
|
||||
roles_claim: realm_access.roles
|
||||
admin_role: openshell-admin
|
||||
user_role: openshell-user
|
||||
scopes_claim: ""
|
||||
```
|
||||
|
||||
The gateway requires HTTPS for OIDC discovery and JWKS retrieval, rejects
|
||||
redirects, and trusts the issuer origin for JWKS by default. Add a
|
||||
`jwksAllowedOrigins` entry only when the identity provider intentionally serves
|
||||
`jwks_allowed_origins` entry only when the identity provider intentionally serves
|
||||
keys from another trusted origin. For local tests, numeric-loopback HTTP also
|
||||
requires `dangerouslyAllowInsecureHttp: true`; this setting never permits an
|
||||
requires `dangerously_allow_insecure_http: true`; this setting never permits an
|
||||
HTTP hostname or non-loopback address.
|
||||
|
||||
Register an OIDC gateway with the CLI:
|
||||
@@ -271,7 +269,10 @@ This is transparent to the user. All CLI commands work the same regardless of wh
|
||||
|
||||
### Plaintext
|
||||
|
||||
When a gateway is deployed with `server.disableTls=true`, TLS is disabled entirely. The CLI connects over plain HTTP/2. This mode is intended for local port-forwarding or gateways behind a trusted reverse proxy or tunnel that handles TLS termination externally.
|
||||
When a gateway is deployed with `disable_tls = true` (set through Helm as
|
||||
`server.disableTls=true`), TLS is disabled entirely. The CLI connects over plain
|
||||
HTTP/2. This mode is intended for local port-forwarding or gateways behind a
|
||||
trusted reverse proxy or tunnel that handles TLS termination externally.
|
||||
|
||||
Register a plaintext gateway with an explicit `http://` endpoint:
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ The OpenShell gateway reads its configuration from a TOML file when `--config` o
|
||||
Gateway CLI flag > gateway OPENSHELL_* env var > TOML file > built-in default
|
||||
```
|
||||
|
||||
For Helm deployments, see [Configure Chart Values](/kubernetes/setup#configure-chart-values)
|
||||
for setting `gatewayConfig` and chart-owned Kubernetes resources.
|
||||
|
||||
`database_url` is env-only. The loader rejects it when it appears in the file. When `OPENSHELL_DB_URL` is unset, the gateway stores its SQLite database under `$XDG_STATE_HOME/openshell/gateway/openshell.db`.
|
||||
|
||||
On-disk SQLite databases run in WAL mode with `synchronous=FULL`: the gateway keeps `openshell.db-wal` and `openshell.db-shm` next to the database file, the file must live on a local filesystem, and a backup must use `sqlite3 openshell.db ".backup <copy>"` rather than copying `openshell.db` on its own. Every acknowledged write, including SSH session revocations, survives a power loss. The one exception is SSH session issuance, which skips the per-commit sync; a power loss can drop a just-issued session token, and the client must request a new one.
|
||||
@@ -532,7 +535,7 @@ credential_drivers = ["vault"]
|
||||
|
||||
[openshell.credential_drivers.vault]
|
||||
address = "https://vault.vault.svc.cluster.local:8200"
|
||||
ca_bundle = "/etc/openshell/vault/ca.pem"
|
||||
ca_bundle = "/etc/openshell-tls/vault-ca/ca.crt"
|
||||
mount = "secret"
|
||||
kv_version = "2"
|
||||
auth_method = "kubernetes"
|
||||
@@ -554,7 +557,7 @@ For `kubernetes-secrets`, `namespace` sets where OpenShell-managed provider Secr
|
||||
|
||||
For `vault`, `address` points at the Vault service. Non-loopback endpoints must use HTTPS; plaintext HTTP is accepted only for `localhost` or an IP loopback address during local development. Vault requests do not follow redirects, preventing credentials from being replayed to a downgraded or substituted endpoint. HTTPS uses platform trust roots by default. Set `ca_bundle` to a certificate-only PEM bundle for a private Vault CA; the bundle augments platform roots and normal hostname verification remains enabled. `mount` and `kv_version` describe the KV engine where OpenShell-managed provider secrets are stored, and `auth_method = "kubernetes"` logs in with the gateway Pod's ServiceAccount token. For local or development validation, use `auth_method = "token_file"` with `token_path = "/path/to/token"`. Do not put literal Vault tokens in TOML.
|
||||
|
||||
For Helm deployments, set `server.credentialDrivers.vault.caConfigMapName` to a ConfigMap containing the private CA bundle under the `ca.crt` key. The chart mounts that key and renders `ca_bundle` automatically.
|
||||
For Helm deployments, set `credentialDrivers.vault.caConfigMapName` to a ConfigMap containing the private CA bundle under the `ca.crt` key. The chart mounts that key and derives `ca_bundle` automatically.
|
||||
|
||||
Provider records that already contain inline database credentials remain readable for upgrade compatibility. New provider create/update requests still submit credential values through the normal API, but the gateway stores those values through the active credential storage path and persists only handles. Before OpenShell 0.1.0, OpenShell does not automatically migrate inline refresh material or credential handles between drivers. Reconfigure refresh grants after an upgrade. Before changing credential drivers, remove affected credentials while the original driver is still available, then select the new driver and create them again. Do not run mixed gateway versions against the same refresh records.
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ For how the CLI resolves gateways and stores credentials, refer to [Gateway Auth
|
||||
|
||||
Kubernetes sandbox supervisors authenticate back to the gateway as sandbox workloads. By default, the Kubernetes compute driver validates each projected ServiceAccount token and returns the authenticated sandbox ID plus a stable runtime identity to the gateway. The gateway requires that identity to match the namespace, immutable Sandbox resource UID, and supervisor Pod UID recorded during provisioning before it mints its own sandbox JWT.
|
||||
|
||||
Dynamic provider token grants can use SPIFFE without changing supervisor-to-gateway authentication. Set `server.providerTokenGrants.spiffe.enabled=true` to mount the SPIFFE CSI Workload API socket into gateway and sandbox pods while keeping the projected ServiceAccount token bootstrap and gateway-minted sandbox JWT path.
|
||||
Dynamic provider token grants can use SPIFFE without changing supervisor-to-gateway authentication. Set `gatewayConfig.openshell.drivers.kubernetes.provider_spiffe_workload_api_socket_path` to mount the SPIFFE CSI Workload API socket into gateway and sandbox pods while keeping the projected ServiceAccount token bootstrap and gateway-minted sandbox JWT path.
|
||||
|
||||
Provider token grants require a SPIFFE implementation such as SPIRE and identities for the gateway and sandbox pods. The repository's local SPIRE overlay assigns sandbox IDs from the pod's `openshell.ai/sandbox-id` annotation, but the gateway validation path only requires the supervisor SVID to be valid and in the same SPIFFE trust domain as the gateway SVID. Provider profiles with `token_grant` metadata cause the sandbox supervisor to request JWT-SVIDs and exchange them for upstream OAuth2 access tokens. Token-exchange profiles also require a gateway SPIFFE identity because the gateway brokers the intermediate token exchange with its own JWT-SVID.
|
||||
|
||||
@@ -32,15 +32,15 @@ The gateway verifies supervisor JWT-SVIDs with JWT bundles fetched from the SPIF
|
||||
|
||||
## OIDC User Authentication
|
||||
|
||||
Set `server.oidc.issuer` to enable OIDC. The gateway validates the `Authorization: Bearer <token>` header on every request against the issuer's JWKS endpoint. It accepts JWTs signed with RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, or EdDSA (Ed25519) keys published in the issuer JWKS. Okta tenants that sign access tokens with ES256 work without additional gateway configuration.
|
||||
Set `gatewayConfig.openshell.gateway.oidc.issuer` to enable OIDC. The gateway validates the `Authorization: Bearer <token>` header on every request against the issuer's JWKS endpoint. It accepts JWTs signed with RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384, or EdDSA (Ed25519) keys published in the issuer JWKS. Okta tenants that sign access tokens with ES256 work without additional gateway configuration.
|
||||
|
||||
```shell
|
||||
helm upgrade openshell \
|
||||
oci://ghcr.io/nvidia/openshell/helm-chart \
|
||||
--version <version> \
|
||||
--namespace openshell \
|
||||
--set server.oidc.issuer=https://your-idp.example.com/realms/openshell \
|
||||
--set server.oidc.audience=openshell-cli \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.issuer=https://your-idp.example.com/realms/openshell' \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.audience=openshell-cli' \
|
||||
--set server.tls.clientCaSecretName=""
|
||||
```
|
||||
|
||||
@@ -52,25 +52,22 @@ The `audience` value must match the client ID configured in your identity provid
|
||||
|
||||
| Value | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `server.oidc.issuer` | `""` | OIDC issuer URL. Empty disables OIDC. |
|
||||
| `server.oidc.dangerouslyAllowInsecureHttp` | `false` | Development-only acknowledgement for numeric-loopback HTTP. It does not allow cluster-service or remote HTTP issuers. |
|
||||
| `server.oidc.jwksAllowedOrigins` | `[]` | Additional trusted HTTPS origins allowed to serve JWKS. |
|
||||
| `server.oidc.caConfigMapName` | `""` | ConfigMap containing the private issuer CA in `ca.crt`. |
|
||||
| `server.oidc.audience` | `openshell-cli` | Expected `aud` claim in the JWT. |
|
||||
| `server.oidc.jwksTtl` | `3600` | JWKS key cache TTL in seconds. Must be greater than zero. |
|
||||
| `server.oidc.rolesClaim` | `""` | Dot-separated path to the roles array in JWT claims. |
|
||||
| `server.oidc.adminRole` | `""` | Role name that grants admin access. |
|
||||
| `server.oidc.userRole` | `""` | Role name that grants standard user access. |
|
||||
| `server.oidc.scopesClaim` | `""` | Dot-separated path to the scopes array in JWT claims. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.issuer` | `""` | OIDC issuer URL. Empty disables OIDC. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.audience` | `openshell-cli` | Expected `aud` claim in the JWT. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.jwks_ttl_secs` | `3600` | JWKS key cache TTL in seconds. Must be greater than zero. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.roles_claim` | `""` | Dot-separated path to the roles array in JWT claims. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.admin_role` | `""` | Role name that grants admin access. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.user_role` | `""` | Role name that grants standard user access. |
|
||||
| `gatewayConfig.openshell.gateway.oidc.scopes_claim` | `""` | Dot-separated path to the scopes array in JWT claims. |
|
||||
|
||||
The issuer must use HTTPS. The gateway rejects discovery and JWKS redirects,
|
||||
limits response sizes, requires a JSON media type, and rejects a `jwks_uri` on
|
||||
a different origin unless that origin appears in `jwksAllowedOrigins`. Use
|
||||
a different origin unless that origin appears in `jwks_allowed_origins`. Use
|
||||
`server.oidc.caConfigMapName` for issuers signed by a private CA.
|
||||
|
||||
### Auth-only mode vs. RBAC mode
|
||||
|
||||
Leave both `adminRole` and `userRole` empty to use auth-only mode: any request with a valid JWT from the configured issuer is accepted, but no role distinction is enforced.
|
||||
Leave both `admin_role` and `user_role` empty to use auth-only mode: any request with a valid JWT from the configured issuer is accepted, but no role distinction is enforced.
|
||||
|
||||
Set both values to enable RBAC mode, where the gateway checks the role claim and enforces access based on the assigned role:
|
||||
|
||||
@@ -79,15 +76,15 @@ helm upgrade openshell \
|
||||
oci://ghcr.io/nvidia/openshell/helm-chart \
|
||||
--version <version> \
|
||||
--namespace openshell \
|
||||
--set server.oidc.issuer=https://your-idp.example.com/realms/openshell \
|
||||
--set server.oidc.audience=openshell-cli \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.issuer=https://your-idp.example.com/realms/openshell' \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.audience=openshell-cli' \
|
||||
--set server.tls.clientCaSecretName="" \
|
||||
--set server.oidc.rolesClaim=realm_access.roles \
|
||||
--set server.oidc.adminRole=openshell-admin \
|
||||
--set server.oidc.userRole=openshell-user
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.roles_claim=realm_access.roles' \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.admin_role=openshell-admin' \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.user_role=openshell-user'
|
||||
```
|
||||
|
||||
Both `adminRole` and `userRole` must be set, or both must be empty. Setting only one is not supported.
|
||||
Both `admin_role` and `user_role` must be set, or both must be empty. Setting only one is not supported.
|
||||
|
||||
OIDC RBAC is method-level authorization. It controls which API operations a caller can perform, but provider and sandbox records are not owned by individual OIDC subjects. In shared clusters, treat provider credentials as gateway-wide resources and use separate gateways or external tenancy controls when users must not see or attach each other's providers and sandboxes.
|
||||
|
||||
@@ -101,27 +98,19 @@ OIDC RBAC is method-level authorization. It controls which API operations a call
|
||||
|
||||
## Reverse-Proxy Auth Termination
|
||||
|
||||
When an access proxy, such as Cloudflare Access, ngrok, or a corporate SSO gateway, handles authentication in front of the OpenShell gateway, you can explicitly allow unauthenticated user calls at the gateway:
|
||||
When an access proxy, such as Cloudflare Access, ngrok, or a corporate SSO gateway, terminates TLS and handles authentication in front of the OpenShell gateway, configure the gateway for plaintext traffic from that trusted proxy and explicitly allow unauthenticated user calls:
|
||||
|
||||
```shell
|
||||
helm upgrade openshell \
|
||||
oci://ghcr.io/nvidia/openshell/helm-chart \
|
||||
--version <version> \
|
||||
--namespace openshell \
|
||||
--set server.auth.allowUnauthenticatedUsers=true
|
||||
```
|
||||
|
||||
The gateway still serves TLS and sandbox supervisors still authenticate with gateway-minted sandbox JWTs. User-facing CLI/API calls without OIDC or mTLS credentials are accepted as an unauthenticated local developer principal. The proxy is responsible for authenticating callers and forwarding only authorized traffic.
|
||||
|
||||
To disable client-certificate verification entirely, set `server.tls.clientCaSecretName=""` as described in the OIDC section above.
|
||||
|
||||
To also disable TLS entirely (when the proxy terminates TLS before the request reaches the gateway):
|
||||
|
||||
```shell
|
||||
--set server.disableTls=true \
|
||||
--set server.auth.allowUnauthenticatedUsers=true
|
||||
--set 'gatewayConfig.openshell\.gateway\.auth.allow_unauthenticated_users=true'
|
||||
```
|
||||
|
||||
The gateway listens on plaintext HTTP behind the trusted proxy, while sandbox supervisors still authenticate with gateway-minted sandbox JWTs. User-facing CLI/API calls without OIDC or mTLS credentials are accepted as an unauthenticated local developer principal. The proxy is responsible for authenticating callers and forwarding only authorized traffic.
|
||||
|
||||
<Warning>
|
||||
Only enable unauthenticated users when the gateway is not reachable from outside a trusted local development environment or the proxy path is fully trusted. Never expose a plaintext, auth-disabled gateway to a public network.
|
||||
</Warning>
|
||||
|
||||
@@ -120,8 +120,8 @@ helm upgrade --install openshell \
|
||||
--set grpcRoute.gateway.listener.port=443 \
|
||||
--set 'grpcRoute.gateway.listener.tls.certificateRefs[0].name=openshell-ingress-tls' \
|
||||
--set server.disableTls=true \
|
||||
--set server.oidc.issuer=https://keycloak.example.com/realms/openshell \
|
||||
--set server.oidc.audience=openshell-cli \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.issuer=https://keycloak.example.com/realms/openshell' \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.audience=openshell-cli' \
|
||||
--set 'grpcRoute.hostnames[0]=gateway.example.com'
|
||||
```
|
||||
|
||||
@@ -147,7 +147,7 @@ As an alternative to the plaintext backend path above, the chart can create a `B
|
||||
client → HTTPS → Gateway (terminate TLS) → TLS (re-encrypt) → openshell gateway pod
|
||||
```
|
||||
|
||||
This keeps TLS on the gateway pod rather than disabling it with `server.disableTls=true`. The Gateway proxy validates the backend's certificate against a CA ConfigMap that the certgen hook auto-creates.
|
||||
This keeps TLS on the gateway pod. The Gateway proxy validates the backend's certificate against a CA ConfigMap that the certgen hook auto-creates.
|
||||
|
||||
BackendTLSPolicy is a standard Gateway API resource. It is supported on OpenShift 4.22+ (via the OpenShift gateway controller) and on other platforms where the Gateway API implementation supports it (check your controller's documentation).
|
||||
|
||||
@@ -168,12 +168,12 @@ helm upgrade --install openshell \
|
||||
--set grpcRoute.gateway.listener.port=443 \
|
||||
--set 'grpcRoute.gateway.listener.tls.certificateRefs[0].name=openshell-ingress-tls' \
|
||||
--set grpcRoute.backendTLSPolicy.enabled=true \
|
||||
--set server.oidc.issuer=https://keycloak.example.com/realms/openshell \
|
||||
--set server.oidc.audience=openshell-cli \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.issuer=https://keycloak.example.com/realms/openshell' \
|
||||
--set 'gatewayConfig.openshell\.gateway\.oidc.audience=openshell-cli' \
|
||||
--set 'grpcRoute.hostnames[0]=gateway.example.com'
|
||||
```
|
||||
|
||||
Note that `server.disableTls` is **not** set — the gateway pod continues to serve TLS — but `server.tls.enableMtls=false` disables mTLS client certificate authentication because the Gateway proxy cannot present a client certificate to the backend. The chart will fail the install if you try to enable both `grpcRoute.backendTLSPolicy.enabled=true` and `server.tls.enableMtls=true` simultaneously. The BackendTLSPolicy hostname defaults to the service FQDN, which matches the SAN on the server certificate. Use OIDC for authentication (configured via `server.oidc.issuer`).
|
||||
The gateway pod continues to serve TLS, while `server.tls.enableMtls=false` disables mTLS client certificate authentication because the Gateway proxy cannot present a client certificate to the backend. The chart will fail the install if you try to enable both `grpcRoute.backendTLSPolicy.enabled=true` and `server.tls.enableMtls=true` simultaneously. The BackendTLSPolicy hostname defaults to the service FQDN, which matches the SAN on the server certificate. Use OIDC for authentication (configured in `gatewayConfig.openshell.gateway.oidc`).
|
||||
|
||||
The example above uses the default `pkiInitJob` for TLS, which creates the backend CA ConfigMap immediately. If using cert-manager instead (`--set certManager.enabled=true`), the Certificate resources are regular release objects, and a separate post-install/post-upgrade Job (`<release>-certgen-backend-ca`) polls for up to 120 seconds waiting for cert-manager to issue the server certificate, then creates the backend CA ConfigMap. This means a single `helm install` is sufficient in most cases.
|
||||
|
||||
|
||||
@@ -103,9 +103,9 @@ validates this at install time and fails with an actionable error if
|
||||
`certManager.serverDnsNames` contains internal-only entries while
|
||||
`serverIssuerRef` is set.
|
||||
|
||||
You do **not** need to set `server.grpcEndpoint` to the external hostname.
|
||||
You do **not** need to set `gatewayConfig.openshell.drivers.kubernetes.grpc_endpoint` to the external hostname.
|
||||
Supervisors connect via the internal service name automatically. Setting
|
||||
`server.grpcEndpoint` to an external hostname would cause supervisors to
|
||||
`grpc_endpoint` to an external hostname would cause supervisors to
|
||||
receive the ACME certificate (via SNI) which they cannot verify against the
|
||||
chart CA.
|
||||
</Note>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
# Migrate Helm gateway configuration to `gatewayConfig`
|
||||
|
||||
Chart schema v2 replaces application-specific Helm values with one non-secret
|
||||
`gatewayConfig` map. Its top-level keys are TOML tables; nested maps are TOML
|
||||
inline tables. A top-level YAML list is an ordered TOML array of tables, with
|
||||
one map per `[[table]]` entry. Kubernetes resource references (Secrets, certificates, Services,
|
||||
and mounts) remain chart values.
|
||||
|
||||
## Upgrade compatibility
|
||||
|
||||
The chart accepts the 0.1.x application values as deprecated aliases while
|
||||
moving to `gatewayConfig`. When both forms set the same runtime field,
|
||||
`gatewayConfig` wins. Runtime images and proxy settings may therefore be
|
||||
migrated incrementally. Kubernetes pull policy spellings such as `Always` are
|
||||
accepted aliases and normalize to `always`, `if_not_present`, or `never`.
|
||||
`gatewayConfig` is strictly a non-secret
|
||||
configuration contract: do not put passwords, tokens, private keys,
|
||||
`database_url`, or other secret material in it; use Secret-backed environment,
|
||||
file, or volume configuration instead. Helm serializes unknown fields
|
||||
generically and cannot determine whether an arbitrary string such as
|
||||
`api_token` is confidential. It rejects `database_url`, inline URL credentials,
|
||||
and PEM private keys, but is not a general secret scanner.
|
||||
|
||||
Keep `server.disableTls`, TLS Secret names, `server.hostGatewayIP`, `server.externalDbSecret`,
|
||||
`server.sandboxNamespace`, `server.oidc.caConfigMapName`, certificate settings and
|
||||
Service settings: these own Kubernetes resources. The chart derives their
|
||||
runtime counterparts.
|
||||
|
||||
## Mapping
|
||||
|
||||
| Deprecated alias | Schema-v2 location |
|
||||
| --- | --- |
|
||||
| `server.name`, `server.logLevel`, `server.enableLoopbackServiceHttp`, `server.policyValidationFailureMode` | `openshell.gateway.{name,log_level,enable_loopback_service_http,policy_validation_failure_mode}` |
|
||||
| `server.grpcRateLimit.{requests,windowSeconds}` | `openshell.gateway.{grpc_rate_limit_requests,grpc_rate_limit_window_seconds}` |
|
||||
| `server.otlp.{endpoint,serviceName}` | `openshell.gateway.otlp.{endpoint,service_name}` |
|
||||
| `server.auth.allowUnauthenticatedUsers` | `openshell.gateway.auth.allow_unauthenticated_users` |
|
||||
| `server.oidc.*` | `openshell.gateway.oidc` with snake_case keys |
|
||||
| `server.sandboxImage*`, workspace storage/runtime and user namespace settings | `openshell.drivers.kubernetes`. Kubernetes AppArmor configuration is removed by RFC 0012. |
|
||||
| `server.drivers.kubernetes.*` | `openshell.drivers.kubernetes` |
|
||||
| `server.sandboxJwt.{gatewayId,ttlSecs,k8sSaTokenTtlSecs}` | `openshell.gateway.gateway_jwt` or `openshell.drivers.kubernetes.sa_token_ttl_secs` |
|
||||
| `supervisor.image.*`, `supervisor.sandboxRuntime.*` | `openshell.drivers.kubernetes.{supervisor_image,supervisor_image_pull_policy,sandbox_runtime}`. `supervisor.topology` and `supervisor.sidecar.*` are removed by RFC 0012 with no replacement. |
|
||||
| `upstreamProxy.*` | `openshell.drivers.kubernetes.{https_proxy,no_proxy,proxy_auth_*,proxy_connect_by_hostname}`. |
|
||||
| `server.credentialDrivers.{kubernetesSecrets,vault}.*` | `openshell.gateway.credential_drivers` and `openshell.credential_drivers.*` |
|
||||
| `server.providerTokenGrants.spiffe.{enabled,workloadApiSocketPath}` | `openshell.drivers.kubernetes.provider_spiffe_workload_api_socket_path`; omit it to disable SPIFFE provider grants. |
|
||||
| `server.hostGatewayIP` | Retained as the chart-owned host-alias input; the chart derives `openshell.drivers.kubernetes.host_gateway_ip`. |
|
||||
|
||||
## Examples
|
||||
|
||||
The following values configure OIDC, telemetry, sandbox images, managed
|
||||
workspaces, rate limiting, and the Kubernetes Secrets credential driver before
|
||||
migration:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
oidc:
|
||||
issuer: https://idp.example/realms/openshell
|
||||
audience: openshell-cli
|
||||
jwksTtl: 3600
|
||||
otlp:
|
||||
endpoint: http://otel-collector:4317
|
||||
serviceName: production-gateway
|
||||
drivers:
|
||||
kubernetes:
|
||||
workspaceMode: managed
|
||||
grpcRateLimit:
|
||||
requests: 120
|
||||
windowSeconds: 60
|
||||
credentialDrivers:
|
||||
kubernetesSecrets:
|
||||
enabled: true
|
||||
namespace: provider-secrets
|
||||
sandbox:
|
||||
image:
|
||||
repository: registry.example/sandbox
|
||||
tag: latest
|
||||
pullPolicy: IfNotPresent
|
||||
supervisor:
|
||||
image:
|
||||
registry: registry.example
|
||||
repository: openshell-supervisor
|
||||
tag: latest
|
||||
sandboxRuntime:
|
||||
boundaryPort: 5500
|
||||
```
|
||||
|
||||
After migration, the same settings use `gatewayConfig`:
|
||||
|
||||
```yaml
|
||||
gatewayConfig:
|
||||
openshell.gateway.oidc:
|
||||
issuer: https://idp.example/realms/openshell
|
||||
audience: openshell-cli
|
||||
jwks_ttl_secs: 3600
|
||||
openshell.gateway.otlp:
|
||||
endpoint: http://otel-collector:4317
|
||||
service_name: production-gateway
|
||||
openshell.drivers.kubernetes:
|
||||
default_image: registry.example/sandbox:latest
|
||||
image_pull_policy: if_not_present
|
||||
supervisor_image: registry.example/openshell-supervisor:latest
|
||||
sandbox_runtime:
|
||||
boundary_port: 5500
|
||||
workspace_mode: managed
|
||||
openshell.gateway:
|
||||
grpc_rate_limit_requests: 120
|
||||
grpc_rate_limit_window_seconds: 60
|
||||
credential_drivers: [kubernetes-secrets]
|
||||
openshell.credential_drivers.kubernetes-secrets:
|
||||
namespace: provider-secrets
|
||||
```
|
||||
|
||||
For Vault, set `openshell.gateway.credential_drivers: [vault]` and configure
|
||||
`openshell.credential_drivers.vault`; use `credentialDrivers.vault.caConfigMapName`
|
||||
for a private Vault CA. For a proxy, use the Kubernetes-driver fields in
|
||||
`gatewayConfig`. TLS remains Secret-backed; set `server.disableTls: true` only for
|
||||
trusted external termination. A null map field omits it; null array elements
|
||||
are invalid. Strings are serialized literally; Helm template expressions inside
|
||||
them are not evaluated.
|
||||
@@ -230,6 +230,41 @@ openshell status
|
||||
|
||||
## Configure Chart Values
|
||||
|
||||
The Helm chart renders `gatewayConfig` into the mounted `gateway.toml`
|
||||
ConfigMap. Each top-level key names a TOML table, and its fields use the names
|
||||
in the [gateway configuration reference](/how-it-works/gateways/configuration).
|
||||
For example, set the gateway log level with:
|
||||
|
||||
```shell
|
||||
helm upgrade openshell \
|
||||
oci://ghcr.io/nvidia/openshell/helm-chart \
|
||||
--version <version> \
|
||||
--namespace openshell \
|
||||
--set 'gatewayConfig.openshell\.gateway.log_level=debug'
|
||||
```
|
||||
|
||||
Keep credentials, database URLs, and private keys in Kubernetes Secrets,
|
||||
using the chart's environment-variable, file, or volume wiring. The chart
|
||||
rejects `database_url`, inline URL credentials, and PEM private keys in
|
||||
`gatewayConfig`, but it does not detect every possible secret.
|
||||
|
||||
Helm values for Services, probes, TLS certificates, Secret references, RBAC,
|
||||
NetworkPolicies, and mounts still control those Kubernetes resources. The
|
||||
chart derives their corresponding runtime settings to keep the gateway
|
||||
configuration aligned with the deployed resources.
|
||||
|
||||
YAML scalar values retain their types, scalar lists become TOML arrays, and
|
||||
nested maps become inline tables. Top-level lists become arrays of tables and
|
||||
preserve item order. Null fields are omitted; null array elements are invalid.
|
||||
Helm template expressions in strings are not evaluated.
|
||||
|
||||
Existing application values remain supported as deprecated aliases. Explicit
|
||||
`gatewayConfig` fields take precedence, including resource-admission settings
|
||||
and the Kubernetes Secrets credential namespace. The global image pull policy
|
||||
applies to runtime and supervisor images. Leave `image_pull_policy` and
|
||||
`sandbox.image.pullPolicy` unset to use Kubernetes defaults for the workload
|
||||
image.
|
||||
|
||||
The most commonly changed values are:
|
||||
|
||||
| Value | Purpose |
|
||||
|
||||
@@ -71,7 +71,7 @@ This provides defense-in-depth: even if a container escape vulnerability exists,
|
||||
|
||||
| Aspect | Detail |
|
||||
|---|---|
|
||||
| Default | Disabled. Set `server.enableUserNamespaces: true` in Helm values or `enable_user_namespaces = true` in `[openshell.drivers.kubernetes]` to enable cluster-wide. |
|
||||
| Default | Disabled. Set `gatewayConfig.openshell.drivers.kubernetes.enable_user_namespaces: true` in Helm values to enable cluster-wide. |
|
||||
| What you can change | Enable cluster-wide through Helm or gateway config. Override per-sandbox through the `user_namespaces` field on `SandboxTemplate` in the API. |
|
||||
| Prerequisites | Kubernetes 1.33+ with user namespace support available (beta through 1.35, GA in 1.36+), a container runtime that supports user namespaces (containerd 2.0+, CRI-O 1.25+), and Linux 5.12+ for ID-mapped mounts. |
|
||||
| Risk if enabled with GPU | NVIDIA device plugin compatibility with user namespaces is unverified. OpenShell logs a warning when both GPU and user namespaces are active on the same sandbox. |
|
||||
|
||||
+4
-3
@@ -20,9 +20,10 @@ publish = false
|
||||
e2e = []
|
||||
# Selects tests that rely on `host.openshell.internal` (the sandbox's stable
|
||||
# alias to the host running test fixtures). docker, podman, and vm wire the
|
||||
# alias unconditionally; the kube driver only does so when the chart's
|
||||
# `server.hostGatewayIP` is set, so `e2e-kubernetes` does NOT imply this and
|
||||
# the helm wrapper opts in explicitly when it has resolved an IP.
|
||||
# alias unconditionally; the kube driver only does so when
|
||||
# `gatewayConfig.openshell.drivers.kubernetes.host_gateway_ip` is set, so
|
||||
# `e2e-kubernetes` does NOT imply this and the Helm wrapper opts in explicitly
|
||||
# when it has resolved an IP.
|
||||
e2e-host-gateway = ["e2e"]
|
||||
e2e-local-container-driver = ["e2e"]
|
||||
e2e-docker = ["e2e", "e2e-host-gateway", "e2e-local-container-driver"]
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
#
|
||||
# Features: the default set includes `e2e-host-gateway` so tests that rely on
|
||||
# the sandbox-side `host.openshell.internal` alias compile and run. The
|
||||
# wrapper detects the cluster's host-routable IP and wires it into the chart
|
||||
# via `server.hostGatewayIP`. Targeting a cluster where the test host is
|
||||
# unreachable from pods? Set OPENSHELL_E2E_KUBERNETES_FEATURES=e2e to drop the
|
||||
# alias-dependent tests entirely.
|
||||
# wrapper detects the cluster's host-routable IP and passes it through the
|
||||
# chart-owned server.hostGatewayIP input, which derives the runtime field.
|
||||
# Targeting a
|
||||
# cluster where the test host is unreachable from pods? Set
|
||||
# OPENSHELL_E2E_KUBERNETES_FEATURES=e2e to drop the alias-dependent tests.
|
||||
#
|
||||
# Results: `run_suite` writes a JUnit + HTML report under `results/`. Set
|
||||
# `OPENSHELL_E2E_REPORT_NAME` to name it per run when invoking this script repeatedly.
|
||||
|
||||
@@ -99,7 +99,6 @@ EXTERNAL_PG_FIXTURE_SECRET=""
|
||||
EXTERNAL_PG_FIXTURE_MANIFEST="${ROOT}/e2e/kubernetes/postgres-fixture.yaml"
|
||||
EXTERNAL_PG_FIXTURE_SERVICE="openshell-e2e-postgres"
|
||||
EXTERNAL_PG_FIXTURE_USER="openshell"
|
||||
EXTERNAL_PG_FIXTURE_PASSWORD="openshell-e2e-postgres"
|
||||
EXTERNAL_PG_FIXTURE_DATABASE="openshell"
|
||||
ENVOY_RELEASE_NAME="${OPENSHELL_E2E_ENVOY_RELEASE_NAME:-envoy-gateway}"
|
||||
ENVOY_NAMESPACE="${OPENSHELL_E2E_ENVOY_NAMESPACE:-envoy-gateway-system}"
|
||||
@@ -157,8 +156,53 @@ kube_workload_ref() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Verify the ConfigMap checksum causes a live gateway rollout. This belongs in
|
||||
# the harness, before port-forwards are established, because replacing a pod
|
||||
# necessarily interrupts any existing port-forward to it.
|
||||
verify_gateway_config_rollout() {
|
||||
local workload_ref old_checksum new_checksum old_pod_uid new_pod_uid attempt
|
||||
local pod_selector="app.kubernetes.io/instance=${RELEASE_NAME},app.kubernetes.io/name=openshell"
|
||||
|
||||
workload_ref="$(kube_workload_ref "${RELEASE_NAME}")"
|
||||
old_checksum="$(kctl -n "${NAMESPACE}" get "${workload_ref}" -o jsonpath='{.spec.template.metadata.annotations.checksum/gateway-config}')"
|
||||
old_pod_uid="$(kctl -n "${NAMESPACE}" get pods -l "${pod_selector}" -o jsonpath='{.items[0].metadata.uid}')"
|
||||
if [[ -z "${old_checksum}" || -z "${old_pod_uid}" ]]; then
|
||||
echo "ERROR: gateway workload is missing its ConfigMap checksum or ready pod" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Verifying ConfigMap-only gateway configuration rollout..."
|
||||
helmctl upgrade "${RELEASE_NAME}" "${ROOT}/deploy/helm/openshell" \
|
||||
--namespace "${NAMESPACE}" \
|
||||
--reuse-values \
|
||||
"${helm_values_args[@]}" \
|
||||
--set "fullnameOverride=openshell" \
|
||||
"${helm_extra_args[@]}" \
|
||||
"${helm_post_renderer_args[@]}" \
|
||||
--set-string 'gatewayConfig.openshell\.gateway.log_level=debug' \
|
||||
--wait --timeout 5m
|
||||
|
||||
new_checksum="$(kctl -n "${NAMESPACE}" get "${workload_ref}" -o jsonpath='{.spec.template.metadata.annotations.checksum/gateway-config}')"
|
||||
if [[ -z "${new_checksum}" || "${new_checksum}" == "${old_checksum}" ]]; then
|
||||
echo "ERROR: ConfigMap-only gateway configuration change did not update workload checksum" >&2
|
||||
return 1
|
||||
fi
|
||||
kctl -n "${NAMESPACE}" rollout status "${workload_ref}" --timeout=5m || return 1
|
||||
|
||||
for attempt in $(seq 1 60); do
|
||||
new_pod_uid="$(kctl -n "${NAMESPACE}" get pods -l "${pod_selector}" -o jsonpath='{.items[0].metadata.uid}')"
|
||||
if [[ -n "${new_pod_uid}" && "${new_pod_uid}" != "${old_pod_uid}" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
echo "ERROR: gateway workload rolled out without replacing its pod" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
deploy_postgres_fixture() {
|
||||
local secret_name="$1"
|
||||
local pg_password
|
||||
local pg_uri
|
||||
|
||||
echo "Deploying external PostgreSQL fixture ${EXTERNAL_PG_FIXTURE_SERVICE}..."
|
||||
@@ -180,9 +224,16 @@ deploy_postgres_fixture() {
|
||||
EXTERNAL_PG_FIXTURE_DEPLOYED=1
|
||||
EXTERNAL_PG_FIXTURE_SECRET="${secret_name}"
|
||||
|
||||
pg_password="$(kctl -n "${NAMESPACE}" get secret openshell-e2e-postgres-credentials \
|
||||
-o jsonpath='{.data.password}' | base64 -d)"
|
||||
if [[ -z "${pg_password}" ]]; then
|
||||
echo "ERROR: external PostgreSQL fixture password is empty" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
kctl -n "${NAMESPACE}" rollout status "deployment/${EXTERNAL_PG_FIXTURE_SERVICE}" --timeout=120s
|
||||
|
||||
pg_uri="postgresql://${EXTERNAL_PG_FIXTURE_USER}:${EXTERNAL_PG_FIXTURE_PASSWORD}@${EXTERNAL_PG_FIXTURE_SERVICE}.${NAMESPACE}.svc.cluster.local:5432/${EXTERNAL_PG_FIXTURE_DATABASE}"
|
||||
pg_uri="postgresql://${EXTERNAL_PG_FIXTURE_USER}:${pg_password}@${EXTERNAL_PG_FIXTURE_SERVICE}.${NAMESPACE}.svc.cluster.local:5432/${EXTERNAL_PG_FIXTURE_DATABASE}"
|
||||
kctl -n "${NAMESPACE}" delete secret "${secret_name}" \
|
||||
--ignore-not-found >/dev/null 2>&1 || true
|
||||
kctl -n "${NAMESPACE}" create secret generic "${secret_name}" \
|
||||
@@ -1171,6 +1222,7 @@ fi
|
||||
helm_extra_args=()
|
||||
helm_post_renderer_args=()
|
||||
helm_extra_args+=(--set "server.telemetryEnabled=${OPENSHELL_TELEMETRY_ENABLED}")
|
||||
helm_extra_args+=(--set supervisor.sandboxRuntime.networkPolicyEnforced=true)
|
||||
if [ "${OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER:-0}" = "1" ]; then
|
||||
if [ "${OPENSHELL_E2E_KUBE_BUILD_IMAGES}" != "1" ]; then
|
||||
echo "ERROR: external Kubernetes driver e2e requires OPENSHELL_E2E_KUBE_BUILD_IMAGES=1." >&2
|
||||
@@ -1182,10 +1234,14 @@ if [ "${OPENSHELL_E2E_EXTERNAL_COMPUTE_DRIVER:-0}" = "1" ]; then
|
||||
)
|
||||
fi
|
||||
if [ -n "${HOST_GATEWAY_IP}" ]; then
|
||||
helm_extra_args+=(--set "server.hostGatewayIP=${HOST_GATEWAY_IP}")
|
||||
# server.hostGatewayIP owns both the gateway runtime field and sandbox-pod
|
||||
# hostAliases. Exercise the public chart input rather than bypassing it with
|
||||
# a direct gatewayConfig override.
|
||||
helm_extra_args+=(--set-string "server.hostGatewayIP=${HOST_GATEWAY_IP}")
|
||||
fi
|
||||
|
||||
helm_values_args=(--values "${ROOT}/deploy/helm/openshell/ci/values-skaffold.yaml")
|
||||
helm_values_args+=(--set-string "global.image.registry=$(e2e_image_reference_registry "${GATEWAY_IMAGE}")")
|
||||
if [ "${OPENSHIFT_DETECTED}" = "1" ]; then
|
||||
echo "OpenShift detected — applying SCC-compatible security context overrides."
|
||||
helm_values_args+=(--values "${ROOT}/deploy/helm/openshell/ci/values-openshift-scc.yaml")
|
||||
@@ -1394,6 +1450,10 @@ else
|
||||
--wait --timeout 5m
|
||||
HELM_INSTALLED=1
|
||||
|
||||
if [ "${OPENSHELL_E2E_KUBE_CONFIG_ROLLOUT:-0}" = "1" ]; then
|
||||
verify_gateway_config_rollout || exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${OPENSHELL_E2E_KUBE_IMAGE_PULL_SECRET:-}" ]; then
|
||||
kctl -n "${NAMESPACE}" create secret docker-registry \
|
||||
"${OPENSHELL_E2E_KUBE_IMAGE_PULL_SECRET}" \
|
||||
|
||||
@@ -443,6 +443,13 @@ the release. Look for failed installs, unexpected values, missing namespace, wro
|
||||
image tag, TLS settings that do not match the registered endpoint, and
|
||||
scheduling failures.
|
||||
|
||||
When checking a Helm values migration, compare the rendered `gateway.toml`
|
||||
with the intended `gatewayConfig` tables. Explicit resource-admission settings
|
||||
and the Kubernetes Secrets credential namespace take precedence over deprecated
|
||||
aliases. Confirm the credential driver's namespace matches its Role and
|
||||
RoleBinding. An unset workload `image_pull_policy` uses Kubernetes defaults;
|
||||
the global Helm pull policy applies to runtime and supervisor images.
|
||||
|
||||
The chart mounts the `gateway.toml` ConfigMap key directly at
|
||||
`/etc/openshell/gateway.toml` as a read-only `subPath` file. This avoids the
|
||||
atomic-writer symlink exposed by a ConfigMap directory mount because the gateway
|
||||
@@ -466,8 +473,9 @@ retained Kubernetes Secret for the shared KEK, injects it into gateway pods, and
|
||||
stores encrypted credential envelopes in the OpenShell database. For
|
||||
`workload.kind=deployment` or multi-replica gateways, confirm
|
||||
`server.externalDbSecret` points at a shared database. A render/install error
|
||||
mentioning `server.credentialDrivers` means the values selected multiple
|
||||
external credential backends.
|
||||
mentioning multiple credential drivers means the
|
||||
`gatewayConfig.openshell.gateway.credential_drivers` list selected more than
|
||||
one external credential backend.
|
||||
|
||||
For HA or PostgreSQL-backed installs, also check the external database Secret
|
||||
referenced by `server.externalDbSecret` and the PostgreSQL workload when it is
|
||||
@@ -642,8 +650,9 @@ kubectl -n openshell get statefulset openshell -o jsonpath='{.spec.template.spec
|
||||
# Should show items filter for ca.crt from openshell-server-tls
|
||||
```
|
||||
|
||||
If `server.providerTokenGrants.spiffe.enabled=true`, the gateway should still
|
||||
render `[openshell.gateway.gateway_jwt]` and mount the `sandbox-jwt` Secret.
|
||||
If `gatewayConfig.openshell.drivers.kubernetes.provider_spiffe_workload_api_socket_path`
|
||||
is set, the gateway should still render `[openshell.gateway.gateway_jwt]` and
|
||||
mount the `sandbox-jwt` Secret.
|
||||
SPIRE is used by both the gateway and sandbox supervisors for dynamic provider
|
||||
token grants. The gateway pod must mount the `spiffe-workload-api` CSI volume
|
||||
and set `OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET`; supervisor Pods must
|
||||
@@ -654,7 +663,7 @@ Verify that SPIRE is installed, the CSI driver is available, and the Kubernetes
|
||||
driver config includes `provider_spiffe_workload_api_socket_path`:
|
||||
|
||||
```bash
|
||||
helm -n openshell get values openshell | grep -E 'providerTokenGrants|workloadApiSocketPath'
|
||||
helm -n openshell get values openshell | grep provider_spiffe_workload_api_socket_path
|
||||
kubectl get pods -A | grep -E 'spire|spiffe'
|
||||
kubectl -n openshell get configmap openshell-config -o yaml | grep provider_spiffe_workload_api_socket_path
|
||||
kubectl -n openshell get pod -l app.kubernetes.io/name=helm-chart -o jsonpath="{.items[*].spec.containers[*].env[?(@.name==\"OPENSHELL_GATEWAY_SPIFFE_WORKLOAD_API_SOCKET\")].value}{\"\n\"}"
|
||||
|
||||
@@ -65,6 +65,8 @@ run = """
|
||||
helm unittest deploy/helm/openshell
|
||||
helm unittest deploy/helm/openshell-workspace
|
||||
deploy/helm/test-split-ownership.sh
|
||||
deploy/helm/test-gateway-config-parser.sh
|
||||
deploy/helm/test-gateway-resource-coherence.sh
|
||||
"""
|
||||
run_windows = "echo Skipping helm:test: Helm validation is not part of the native Windows lane."
|
||||
|
||||
|
||||
@@ -111,4 +111,25 @@ assert_helm_image_translation "sandbox" \
|
||||
"registry.example/sandbox@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" \
|
||||
"registry.example" "sandbox" "" "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
# The certgen hook uses the chart's shared registry fallback. The Kubernetes
|
||||
# harness must propagate the gateway registry there too, including an empty
|
||||
# registry for k3d-imported local images.
|
||||
if grep -Fq 'helm_values_args+=(--set-string "global.image.registry=$(e2e_image_reference_registry "${GATEWAY_IMAGE}")")' "${ROOT}/e2e/with-kube-gateway.sh"; then
|
||||
:
|
||||
else
|
||||
echo "FAIL: Kubernetes Helm installs must propagate the gateway registry to certgen" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The Vault credential-driver overlay must use the fixture alias created in
|
||||
# the gateway namespace. The alias is covered by OpenBao's development TLS
|
||||
# certificate; the chart's namespace service is not.
|
||||
if grep -Fq 'address: https://openbao-0:8200' \
|
||||
"${ROOT}/deploy/helm/openshell/ci/values-credential-driver-vault.yaml"; then
|
||||
:
|
||||
else
|
||||
echo "FAIL: Vault credential-driver overlay must use the OpenBao TLS alias" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "E2E image override tests passed."
|
||||
|
||||
Reference in New Issue
Block a user