From f6728c1254bff08fb87cbb3556834bf00322fd72 Mon Sep 17 00:00:00 2001 From: Bryce Wilkinson <22760097+bjw123@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:21:33 +0000 Subject: [PATCH] feat(helm): expose gateway scheduling fields and data volume config The gateway pod template exposes nodeSelector, affinity and tolerations but not priorityClassName or topologySpreadConstraints, and the StatefulSet hardcodes the data volume at 1Gi with no StorageClass control. Operators who need any of these have to fork the chart or mutate the rendered output out-of-band. Add four optional passthroughs: - priorityClassName: 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, so a local capacity crunch can evict it and widen into a fleet-wide outage. - topologySpreadConstraints: spread replicas across zones or nodes. Only affinity was available, which is a blunter tool for even spreading. - persistence.size / persistence.storageClassName: volumeClaimTemplates is immutable, so a claim created at 1Gi cannot be grown through the chart later. Omitting storageClassName also leaves the claim Pending on clusters with no default StorageClass. Both scheduling fields go in the shared gatewayPodTemplate, so they apply identically to the statefulset and deployment workload shapes. persistence applies to the statefulset only, which is where the volume exists. All four render only when set and default to current behaviour: 1Gi, no storageClassName, and neither scheduling field emitted. A default render is byte-identical to before this change. Signed-off-by: Bryce Wilkinson <22760097+bjw123@users.noreply.github.com> --- deploy/helm/openshell/README.md | 4 + .../openshell/templates/_gateway-workload.tpl | 7 + .../helm/openshell/templates/statefulset.yaml | 5 +- .../gateway_scheduling_persistence_test.yaml | 122 ++++++++++++++++++ deploy/helm/openshell/values.yaml | 21 +++ 5 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 deploy/helm/openshell/tests/gateway_scheduling_persistence_test.yaml diff --git a/deploy/helm/openshell/README.md b/deploy/helm/openshell/README.md index 93dab354b6..7843c46719 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 5ff608ae59..de9c5106f6 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 30571f80ba..6a7b6013d4 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 0000000000..1bca060fb9 --- /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 33337c768e..73b1ef3592 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.