diff --git a/deploy/helm/nvca-operator/Makefile b/deploy/helm/nvca-operator/Makefile index aaf56522d..fc56fe52a 100644 --- a/deploy/helm/nvca-operator/Makefile +++ b/deploy/helm/nvca-operator/Makefile @@ -44,7 +44,7 @@ OCI_REGISTRY_NAMESPACE ?= CHART_NAME := $(shell yq -r .name $(helm_dir)/Chart.yaml) CHART_VERSION := $(shell yq -r .version $(helm_dir)/Chart.yaml) -.PHONY: install uninstall status lint template validate clean package push-oci sync-chart check-synced-chart render-values-from-stack install-from-stack test-render-values test-build-release-assets test-release-image-manifest test-release-artifact-permissions test-package-release-assets test-attach-release-assets test-release-sbom-wrapper test-self-managed-nvca-image-reference test-image-pull-secret-defaults +.PHONY: install uninstall status lint template validate clean package push-oci sync-chart check-synced-chart render-values-from-stack install-from-stack test-render-values test-build-release-assets test-release-image-manifest test-release-artifact-permissions test-package-release-assets test-attach-release-assets test-release-sbom-wrapper test-self-managed-nvca-image-reference test-image-pull-secret-defaults test-pod-disruption-budget install: ifndef values @@ -116,6 +116,9 @@ test-self-managed-nvca-image-reference: test-image-pull-secret-defaults: @bash ./tests/image_pull_secret_defaults_test.sh +test-pod-disruption-budget: + @bash ./tests/pod_disruption_budget_test.sh + uninstall: @echo "Deleting $(release) from namespace $(namespace)..." helm uninstall $(release) --namespace $(namespace) diff --git a/deploy/helm/nvca-operator/nvca-operator/README.md b/deploy/helm/nvca-operator/nvca-operator/README.md index 9389a3aec..c8e600621 100644 --- a/deploy/helm/nvca-operator/nvca-operator/README.md +++ b/deploy/helm/nvca-operator/nvca-operator/README.md @@ -58,6 +58,14 @@ used in Kubernetes Clusters to run NVCF Workloads. | `resources.requests.cpu` | CPU request for the nvca-operator container | `50m` | | `resources.requests.memory` | Memory request for the nvca-operator container | `50Mi` | +### PodDisruptionBudget configuration + +| Name | Description | Value | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------- | +| `podDisruptionBudget.enabled` | Enable a PodDisruptionBudget for the NVCA Operator. | `false` | +| `podDisruptionBudget.minAvailable` | Minimum available pods. When enabled, set exactly one of this value or `maxUnavailable`. Supports an integer or 0-100%. | `""` | +| `podDisruptionBudget.maxUnavailable` | Maximum unavailable pods. When enabled, set exactly one of this value or `minAvailable`. Supports an integer or 0-100%. | `""` | + ### Agent Container Resource configuration | Name | Description | Value | @@ -80,7 +88,7 @@ used in Kubernetes Clusters to run NVCF Workloads. | `agent.functionEnvOverrides` | Map of environment variable overrides for function workloads (e.g., {"INIT_CONTAINER": "nvcr.io/custom/init:v1.0", "UTILS_CONTAINER": "nvcr.io/custom/utils:v1.0"}) | `{}` | | `agent.taskEnvOverrides` | Map of environment variable overrides for task workloads (e.g., {"INIT_CONTAINER": "nvcr.io/custom/init:v1.0", "ESS_AGENT_CONTAINER": "nvcr.io/custom/ess:v1.0"}) | `{}` | | `agent.overrideEnvironmentVariables` | Map of environment variables to override on the NVCA agent container. These take precedence over default values. Example: {"LOG_LEVEL": "debug", "CUSTOM_FLAG": "enabled"} | `{}` | -| `agent.llm.requestRouterAddress` | Default LLM request-router worker address rendered as STARGATE_ADDRESS for LLM workers | `""` | +| `agent.llm.requestRouterAddress` | Operator default LLM request-router address. Workers read LLM_REQUEST_ROUTER_ADDRESS from the launch environment; not a runtime fallback | `""` | | `agent.serviceOAuth` | OAuth token and JWKS endpoints used by dependent services | See `values.yaml` | ### Webhook Container Resource configuration diff --git a/deploy/helm/nvca-operator/nvca-operator/values.schema.json b/deploy/helm/nvca-operator/nvca-operator/values.schema.json index 05c451fe4..8dbec0762 100644 --- a/deploy/helm/nvca-operator/nvca-operator/values.schema.json +++ b/deploy/helm/nvca-operator/nvca-operator/values.schema.json @@ -133,6 +133,57 @@ "description": "Replica count for the operator deployment", "default": 1 }, + "podDisruptionBudget": { + "type": "object", + "description": "PodDisruptionBudget configuration for the NVCA Operator.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable a PodDisruptionBudget for the NVCA Operator.", + "default": false + }, + "minAvailable": { + "description": "Minimum available pods. Set exactly one of minAvailable or maxUnavailable when enabled.", + "default": "", + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "enum": [ + "" + ] + }, + { + "type": "string", + "pattern": "^(100|[1-9]?[0-9])%$" + } + ] + }, + "maxUnavailable": { + "description": "Maximum unavailable pods. Set exactly one of minAvailable or maxUnavailable when enabled.", + "default": "", + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "enum": [ + "" + ] + }, + { + "type": "string", + "pattern": "^(100|[1-9]?[0-9])%$" + } + ] + } + } + }, "systemNamespace": { "type": "string", "description": "Namespace in which NVCFBackend objects are created.", diff --git a/deploy/helm/nvca-operator/nvca-operator/values.yaml b/deploy/helm/nvca-operator/nvca-operator/values.yaml index bb6609312..d6b54b24c 100644 --- a/deploy/helm/nvca-operator/nvca-operator/values.yaml +++ b/deploy/helm/nvca-operator/nvca-operator/values.yaml @@ -69,7 +69,7 @@ replicaCount: 1 podDisruptionBudget: enabled: false # minAvailable and maxUnavailable are mutually exclusive; set exactly one. - # Accepts an integer or a percentage string (e.g. 1 or "50%"). + # Accepts an integer or a percentage string from 0% to 100%. minAvailable: "" maxUnavailable: "" ## @param systemNamespace Namespace in which NVCFBackend objects are created. @@ -178,7 +178,7 @@ resources: ## @param agent.taskEnvOverrides Map of environment variable overrides for task workloads (e.g., {"INIT_CONTAINER": "nvcr.io/custom/init:v1.0", "ESS_AGENT_CONTAINER": "nvcr.io/custom/ess:v1.0"}) ## @param agent.overrideEnvironmentVariables Map of environment variables to override on the NVCA agent container. These take precedence over default values. Example: {"LOG_LEVEL": "debug", "CUSTOM_FLAG": "enabled"} ## @param agent.tolerations K8s tolerations for the NVCA agent pod -## @param agent.llm.requestRouterAddress Default LLM request-router worker address rendered as STARGATE_ADDRESS for LLM workers +## @param agent.llm.requestRouterAddress Operator default LLM request-router address. Workers read LLM_REQUEST_ROUTER_ADDRESS from the launch environment; not a runtime fallback ## @param agent.serviceOAuth OAuth token and JWKS endpoints used by dependent services agent: cacheMountOptionsEnabled: true diff --git a/deploy/helm/nvca-operator/tests/pod_disruption_budget_test.sh b/deploy/helm/nvca-operator/tests/pod_disruption_budget_test.sh new file mode 100644 index 000000000..d045bb8fd --- /dev/null +++ b/deploy/helm/nvca-operator/tests/pod_disruption_budget_test.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +source_chart="${repo_root}/../../../src/compute-plane-services/nvca/deployments/nvca-operator" +vendored_chart="${repo_root}/nvca-operator" +tmp_dir="$(mktemp -d)" +test_service_key="test-service-key" + +cleanup() { + rm -rf "${tmp_dir}" +} +trap cleanup EXIT + +assert_render_fails() { + local chart="$1" + shift + + if helm template nvca-operator "${chart}" \ + --set-string ngcConfig.serviceKey="${test_service_key}" \ + "$@" > /dev/null 2>&1; then + echo "expected PDB render to fail for ${chart}: $*" >&2 + exit 1 + fi +} + +for chart in "${source_chart}" "${vendored_chart}"; do + chart_name="$(basename "${chart}")" + default_manifest="${tmp_dir}/${chart_name}-default.yaml" + min_available_manifest="${tmp_dir}/${chart_name}-min-available.yaml" + max_unavailable_manifest="${tmp_dir}/${chart_name}-max-unavailable.yaml" + + helm lint "${chart}" --set-string ngcConfig.serviceKey="${test_service_key}" + + helm template nvca-operator "${chart}" \ + --set-string ngcConfig.serviceKey="${test_service_key}" \ + > "${default_manifest}" + if grep -Fq 'kind: PodDisruptionBudget' "${default_manifest}"; then + echo "expected PDB to be disabled by default for ${chart}" >&2 + exit 1 + fi + + helm template nvca-operator "${chart}" \ + --set-string ngcConfig.serviceKey="${test_service_key}" \ + --set podDisruptionBudget.enabled=true \ + --set podDisruptionBudget.minAvailable=1 \ + > "${min_available_manifest}" + if ! grep -Fq 'minAvailable: 1' "${min_available_manifest}"; then + echo "expected minAvailable PDB render for ${chart}" >&2 + exit 1 + fi + + helm template nvca-operator "${chart}" \ + --set-string ngcConfig.serviceKey="${test_service_key}" \ + --set podDisruptionBudget.enabled=true \ + --set-string podDisruptionBudget.maxUnavailable=50% \ + > "${max_unavailable_manifest}" + if ! grep -Fq 'maxUnavailable: 50%' "${max_unavailable_manifest}"; then + echo "expected percentage maxUnavailable PDB render for ${chart}" >&2 + exit 1 + fi + + assert_render_fails "${chart}" --set podDisruptionBudget.enabled=true + assert_render_fails "${chart}" \ + --set podDisruptionBudget.enabled=true \ + --set podDisruptionBudget.minAvailable=1 \ + --set podDisruptionBudget.maxUnavailable=1 + assert_render_fails "${chart}" \ + --set podDisruptionBudget.enabled=true \ + --set-string podDisruptionBudget.minAvailable=101% +done + +echo "validated PodDisruptionBudget values for source and vendored charts" diff --git a/src/compute-plane-services/nvca/deployments/nvca-operator/README.md b/src/compute-plane-services/nvca/deployments/nvca-operator/README.md index 20acd0894..2e706264b 100644 --- a/src/compute-plane-services/nvca/deployments/nvca-operator/README.md +++ b/src/compute-plane-services/nvca/deployments/nvca-operator/README.md @@ -58,6 +58,14 @@ used in Kubernetes Clusters to run NVCF Workloads. | `resources.requests.cpu` | CPU request for the nvca-operator container | `50m` | | `resources.requests.memory` | Memory request for the nvca-operator container | `50Mi` | +### PodDisruptionBudget configuration + +| Name | Description | Value | +| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------- | +| `podDisruptionBudget.enabled` | Enable a PodDisruptionBudget for the NVCA Operator. | `false` | +| `podDisruptionBudget.minAvailable` | Minimum available pods. When enabled, set exactly one of this value or `maxUnavailable`. Supports an integer or 0-100%. | `""` | +| `podDisruptionBudget.maxUnavailable` | Maximum unavailable pods. When enabled, set exactly one of this value or `minAvailable`. Supports an integer or 0-100%. | `""` | + ### Agent Container Resource configuration | Name | Description | Value | diff --git a/src/compute-plane-services/nvca/deployments/nvca-operator/templates/operator-networkpolicy.yaml b/src/compute-plane-services/nvca/deployments/nvca-operator/templates/operator-networkpolicy.yaml index bdb63091c..00e159c94 100644 --- a/src/compute-plane-services/nvca/deployments/nvca-operator/templates/operator-networkpolicy.yaml +++ b/src/compute-plane-services/nvca/deployments/nvca-operator/templates/operator-networkpolicy.yaml @@ -5,7 +5,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, diff --git a/src/compute-plane-services/nvca/deployments/nvca-operator/templates/poddisruptionbudget.yaml b/src/compute-plane-services/nvca/deployments/nvca-operator/templates/poddisruptionbudget.yaml new file mode 100644 index 000000000..6662875aa --- /dev/null +++ b/src/compute-plane-services/nvca/deployments/nvca-operator/templates/poddisruptionbudget.yaml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{{- if .Values.podDisruptionBudget.enabled }} +{{- $pdbMinAvail := .Values.podDisruptionBudget.minAvailable | toString }} +{{- $pdbMaxUnavail := .Values.podDisruptionBudget.maxUnavailable | toString }} +{{- if and (ne $pdbMinAvail "") (ne $pdbMaxUnavail "") }} +{{- fail "podDisruptionBudget: set exactly one of minAvailable or maxUnavailable, not both" }} +{{- end }} +{{- if and (eq $pdbMinAvail "") (eq $pdbMaxUnavail "") }} +{{- fail "podDisruptionBudget: set exactly one of minAvailable or maxUnavailable" }} +{{- end }} +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "nvcaop.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "nvcaop.labels" . | nindent 4 }} +spec: + {{- if ne $pdbMinAvail "" }} + minAvailable: {{ .Values.podDisruptionBudget.minAvailable }} + {{- else }} + maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }} + {{- end }} + selector: + matchLabels: {{- include "nvcaop.baseSelectorLabels" . | nindent 6 }} +{{- end }} diff --git a/src/compute-plane-services/nvca/deployments/nvca-operator/values.schema.json b/src/compute-plane-services/nvca/deployments/nvca-operator/values.schema.json index 4ac4f2910..500795944 100644 --- a/src/compute-plane-services/nvca/deployments/nvca-operator/values.schema.json +++ b/src/compute-plane-services/nvca/deployments/nvca-operator/values.schema.json @@ -133,6 +133,57 @@ "description": "Replica count for the operator deployment", "default": 1 }, + "podDisruptionBudget": { + "type": "object", + "description": "PodDisruptionBudget configuration for the NVCA Operator.", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable a PodDisruptionBudget for the NVCA Operator.", + "default": false + }, + "minAvailable": { + "description": "Minimum available pods. Set exactly one of minAvailable or maxUnavailable when enabled.", + "default": "", + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "enum": [ + "" + ] + }, + { + "type": "string", + "pattern": "^(100|[1-9]?[0-9])%$" + } + ] + }, + "maxUnavailable": { + "description": "Maximum unavailable pods. Set exactly one of minAvailable or maxUnavailable when enabled.", + "default": "", + "oneOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "string", + "enum": [ + "" + ] + }, + { + "type": "string", + "pattern": "^(100|[1-9]?[0-9])%$" + } + ] + } + } + }, "systemNamespace": { "type": "string", "description": "Namespace in which NVCFBackend objects are created.", diff --git a/src/compute-plane-services/nvca/deployments/nvca-operator/values.yaml b/src/compute-plane-services/nvca/deployments/nvca-operator/values.yaml index 89da88093..0fc29dd29 100644 --- a/src/compute-plane-services/nvca/deployments/nvca-operator/values.yaml +++ b/src/compute-plane-services/nvca/deployments/nvca-operator/values.yaml @@ -71,6 +71,14 @@ serviceAccount: ## @param replicaCount Replica count for the operator deployment replicaCount: 1 +## @param podDisruptionBudget PodDisruptionBudget configuration +podDisruptionBudget: + enabled: false + # minAvailable and maxUnavailable are mutually exclusive; set exactly one. + # Accepts an integer or a percentage string from 0% to 100%. + minAvailable: "" + maxUnavailable: "" + ## @param systemNamespace Namespace in which NVCFBackend objects are created. systemNamespace: nvca-operator