diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 93dab354b..7843c4671 100644 --- a/deploy/helm/openshell/README.md +++ b/deploy/helm/openshell/README.md @@ -182,6 +182,8 @@ add `ci/values-spire.yaml` to the OpenShell release values files. | openshiftRoute.annotations | object | `{}` | Extra annotations on the Route (e.g. haproxy.router.openshift.io/*). | | openshiftRoute.enabled | bool | `false` | Create an OpenShift Route with TLS passthrough. | | openshiftRoute.host | string | `""` | Hostname for the Route. Must match a SAN on the gateway's server cert. | +| persistence.size | string | `"1Gi"` | Size of the gateway data volume. Note that volumeClaimTemplates is immutable: changing this only affects claims created from now on, and does not resize the volume of an existing StatefulSet. | +| persistence.storageClassName | string | `""` | StorageClass for the gateway data volume. Empty = omit the field, using the cluster's default StorageClass. Set this on clusters that have no default StorageClass, otherwise the claim stays Pending. | | pkiInitJob.enabled | bool | `true` | Run a pre-install/pre-upgrade Job that creates gateway and client mTLS Secrets. When certManager.enabled=true, cert-manager owns TLS and this same hook runs in JWT-only mode even if pkiInitJob.enabled remains true. | | pkiInitJob.serverDnsNames | list | `[]` | Extra DNS SANs to append to the server certificate. | | pkiInitJob.serverIpAddresses | list | `[]` | Extra IP SANs to append to the server certificate. | @@ -189,6 +191,7 @@ add `ci/values-spire.yaml` to the OpenShell release values files. | podLabels | object | `{}` | Extra labels to add to the gateway pod. | | podLifecycle.terminationGracePeriodSeconds | int | `5` | Grace period, in seconds, before Kubernetes terminates the gateway pod. | | podSecurityContext.fsGroup | int | `1000` | fsGroup assigned to the gateway pod. | +| priorityClassName | string | `""` | PriorityClass for the gateway pod. The gateway is a control-plane component; when it shares nodes with the workloads it serves, the default priority gives it no advantage under node pressure. Empty = omit the field. | | probes.liveness.failureThreshold | int | `3` | Liveness probe failure threshold before the container is restarted. | | probes.liveness.initialDelaySeconds | int | `2` | Liveness probe initial delay, in seconds. | | probes.liveness.periodSeconds | int | `5` | Liveness probe period, in seconds. | @@ -281,6 +284,7 @@ add `ci/values-spire.yaml` to the OpenShell release values files. | supervisor.sideloadMethod | string | `""` | How the supervisor binary is delivered into sandbox pods. Empty (default) = auto-detect from cluster version: K8s >= v1.35 -> "image-volume" (ImageVolume enabled by default; GA in v1.36) K8s < v1.35 -> "init-container" (copies via init container + emptyDir) On K8s v1.33-v1.34 with the ImageVolume feature gate manually enabled, set this to "image-volume" explicitly. | | supervisor.topology | string | `"combined"` | Supervisor pod topology for Kubernetes sandboxes. "combined" runs the current single supervisor container in the agent pod. "sidecar" runs network enforcement in a dedicated sidecar and the process supervisor as a low-capability wrapper in the agent container. | | tolerations | list | `[]` | Tolerations for the gateway pod. | +| topologySpreadConstraints | list | `[]` | Topology spread constraints for the gateway pod, used to spread replicas across zones or nodes. Empty = omit the field. | | upstreamProxy | object | `{"authAllowInsecure":false,"authSecret":{"key":"","name":""},"connectByHostname":false,"noProxy":"","url":""}` | Operator-owned corporate forward proxy for policy-approved TLS egress from Kubernetes sandboxes. The workload cannot select or override it. | | upstreamProxy.authAllowInsecure | bool | `false` | Required when authSecret is configured because Basic auth to an HTTP proxy is cleartext. | | upstreamProxy.authSecret.key | string | `""` | Secret key containing the proxy credential. | diff --git a/deploy/helm/openshell/templates/_gateway-workload.tpl b/deploy/helm/openshell/templates/_gateway-workload.tpl index 5ff608ae5..de9c5106f 100644 --- a/deploy/helm/openshell/templates/_gateway-workload.tpl +++ b/deploy/helm/openshell/templates/_gateway-workload.tpl @@ -180,6 +180,9 @@ spec: configMap: name: {{ .Values.server.oidc.caConfigMapName }} {{- end }} + {{- with .Values.priorityClassName }} + priorityClassName: {{ . }} + {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 4 }} @@ -192,4 +195,8 @@ spec: tolerations: {{- toYaml . | nindent 4 }} {{- end }} + {{- with .Values.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml . | nindent 4 }} + {{- end }} {{- end }} diff --git a/deploy/helm/openshell/templates/statefulset.yaml b/deploy/helm/openshell/templates/statefulset.yaml index 30571f80b..6a7b6013d 100644 --- a/deploy/helm/openshell/templates/statefulset.yaml +++ b/deploy/helm/openshell/templates/statefulset.yaml @@ -21,7 +21,10 @@ spec: name: openshell-data spec: accessModes: ["ReadWriteOnce"] + {{- with .Values.persistence.storageClassName }} + storageClassName: {{ . | quote }} + {{- end }} resources: requests: - storage: 1Gi + storage: {{ .Values.persistence.size }} {{- end }} diff --git a/deploy/helm/openshell/tests/gateway_scheduling_persistence_test.yaml b/deploy/helm/openshell/tests/gateway_scheduling_persistence_test.yaml new file mode 100644 index 000000000..1bca060fb --- /dev/null +++ b/deploy/helm/openshell/tests/gateway_scheduling_persistence_test.yaml @@ -0,0 +1,122 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +suite: gateway scheduling fields and data volume +templates: + - templates/gateway-config.yaml + - templates/statefulset.yaml + - templates/deployment.yaml +release: + name: openshell + namespace: my-namespace + +tests: + # priorityClassName — shared pod template, so both workload shapes + - it: omits priorityClassName by default on the StatefulSet + template: templates/statefulset.yaml + asserts: + - notExists: + path: spec.template.spec.priorityClassName + + - it: sets priorityClassName on the StatefulSet when configured + template: templates/statefulset.yaml + set: + priorityClassName: system-cluster-critical + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-cluster-critical + + - it: sets priorityClassName on the Deployment when configured + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + priorityClassName: system-cluster-critical + asserts: + - equal: + path: spec.template.spec.priorityClassName + value: system-cluster-critical + + # topologySpreadConstraints — shared pod template, so both workload shapes + - it: omits topologySpreadConstraints by default on the StatefulSet + template: templates/statefulset.yaml + asserts: + - notExists: + path: spec.template.spec.topologySpreadConstraints + + - it: sets topologySpreadConstraints on the StatefulSet when configured + template: templates/statefulset.yaml + set: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: openshell + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].maxSkew + value: 1 + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: topology.kubernetes.io/zone + - equal: + path: spec.template.spec.topologySpreadConstraints[0].whenUnsatisfiable + value: DoNotSchedule + + - it: sets topologySpreadConstraints on the Deployment when configured + template: templates/deployment.yaml + set: + workload.kind: deployment + server.externalDbSecret: openshell-db + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + asserts: + - equal: + path: spec.template.spec.topologySpreadConstraints[0].topologyKey + value: kubernetes.io/hostname + + # persistence — StatefulSet only + - it: defaults the data volume to 1Gi with no storageClassName + template: templates/statefulset.yaml + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.resources.requests.storage + value: 1Gi + - notExists: + path: spec.volumeClaimTemplates[0].spec.storageClassName + + - it: sets the data volume size when configured + template: templates/statefulset.yaml + set: + persistence.size: 10Gi + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.resources.requests.storage + value: 10Gi + + - it: sets the data volume storageClassName when configured + template: templates/statefulset.yaml + set: + persistence.storageClassName: fast-ssd + asserts: + - equal: + path: spec.volumeClaimTemplates[0].spec.storageClassName + value: fast-ssd + + - it: keeps the claim name and access mode unchanged + template: templates/statefulset.yaml + set: + persistence.size: 5Gi + persistence.storageClassName: fast-ssd + asserts: + - equal: + path: spec.volumeClaimTemplates[0].metadata.name + value: openshell-data + - equal: + path: spec.volumeClaimTemplates[0].spec.accessModes[0] + value: ReadWriteOnce diff --git a/deploy/helm/openshell/values.yaml b/deploy/helm/openshell/values.yaml index 33337c768..73b1ef359 100644 --- a/deploy/helm/openshell/values.yaml +++ b/deploy/helm/openshell/values.yaml @@ -177,6 +177,27 @@ tolerations: [] # -- Affinity rules for the gateway pod. affinity: {} +# -- PriorityClass for the gateway pod. The gateway is a control-plane +# component; when it shares nodes with the workloads it serves, the default +# priority gives it no advantage under node pressure. Empty = omit the field. +priorityClassName: "" + +# -- Topology spread constraints for the gateway pod, used to spread replicas +# across zones or nodes. Empty = omit the field. +topologySpreadConstraints: [] + +# Gateway data volume (workload.kind=statefulset only). The StatefulSet's +# openshell-data claim holds the default SQLite database. +persistence: + # -- Size of the gateway data volume. Note that volumeClaimTemplates is + # immutable: changing this only affects claims created from now on, and does + # not resize the volume of an existing StatefulSet. + size: 1Gi + # -- StorageClass for the gateway data volume. Empty = omit the field, using + # the cluster's default StorageClass. Set this on clusters that have no + # default StorageClass, otherwise the claim stays Pending. + storageClassName: "" + # Server configuration server: # -- Gateway log level.