Skip to content

Commit cefd89d

Browse files
MDhamanimchmarny
andauthored
feat(bundler): wire DRA eviction for GPU driver upgrades (#2401)
Signed-off-by: Manav Kamleshbhai Dhamani <35000278+MDhamani@users.noreply.github.com> Co-authored-by: Mark Chmarny <mchmarny@users.noreply.github.com>
1 parent 702e58e commit cefd89d

27 files changed

Lines changed: 1418 additions & 94 deletions

.github/workflows/gpu-h100-kind-runtime-test.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ jobs:
8585
control_plane_resource_patches: 'true'
8686
control_plane_leader_election_tuning: 'true'
8787

88+
- name: Label nodes for the DRA kubelet plugin
89+
shell: bash
90+
env:
91+
DRA_EVICTION_NODE_LABEL: nvidia.com/dra-kubelet-plugin=true
92+
run: >-
93+
kubectl label nodes
94+
--selector='!node-role.kubernetes.io/control-plane'
95+
"${DRA_EVICTION_NODE_LABEL}"
96+
--overwrite
97+
8898
- name: Build AICR CLI
8999
timeout-minutes: 10
90100
uses: ./.github/actions/aicr-build

api/aicr/v1/server.yaml

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1285,6 +1285,7 @@ paths:
12851285
- $ref: "#/components/parameters/SystemNodeToleration"
12861286
- $ref: "#/components/parameters/AcceleratedNodeSelector"
12871287
- $ref: "#/components/parameters/AcceleratedNodeToleration"
1288+
- $ref: "#/components/parameters/DRAEvictionNodeLabel"
12881289
- $ref: "#/components/parameters/BundleDeployer"
12891290
- $ref: "#/components/parameters/BundleRepo"
12901291
- $ref: "#/components/parameters/WorkloadGate"
@@ -1498,9 +1499,11 @@ paths:
14981499
14991500
A declaration whose component is absent from the generated
15001501
bundle is rejected with HTTP 400 (INVALID_REQUEST, "cannot
1501-
take effect"); no path is exempt. Certain gate-verified paths
1502-
on present components are also rejected — driver-ownership
1503-
paths, GPU allocation-policy keys, and, where the
1502+
take effect"); no path is exempt. Certain gate- or contract-owned
1503+
paths on present components are also rejected — driver-ownership
1504+
paths, GPU allocation-policy keys, the DRA eviction paths
1505+
kubeletPlugin.nodeSelector and driver.manager.env when both
1506+
contract components are enabled, and, where the
15041507
corresponding NVSentinel gate applies on the recipe's
15051508
platform and configuration, the NVSentinel
15061509
remedy/consumer/runtime-class paths — because an install-time
@@ -1570,6 +1573,7 @@ paths:
15701573
style: form
15711574
explode: true
15721575
example: ["nvidia.com/gpu=present:NoSchedule"]
1576+
- $ref: "#/components/parameters/DRAEvictionNodeLabel"
15731577
- name: deployer
15741578
in: query
15751579
required: false
@@ -2094,11 +2098,13 @@ components:
20942098
description: >-
20952099
Repeatable install-time component:path declaration. Rejected with
20962100
HTTP 400 when the component is absent from the generated bundle;
2097-
no path is exempt. Certain gate-verified paths on present
2101+
no path is exempt. Certain gate- or contract-owned paths on present
20982102
components are also rejected (driver-ownership paths, GPU
2099-
allocation-policy keys, and — where the corresponding NVSentinel
2100-
gate applies on the recipe's platform and configuration — the
2101-
NVSentinel remedy/consumer/runtime-class paths).
2103+
allocation-policy keys, the DRA eviction paths
2104+
kubeletPlugin.nodeSelector and driver.manager.env when both contract
2105+
components are enabled, and — where the corresponding NVSentinel gate
2106+
applies on the recipe's platform and configuration — the NVSentinel
2107+
remedy/consumer/runtime-class paths).
21022108
schema:
21032109
type: array
21042110
items:
@@ -2149,6 +2155,18 @@ components:
21492155
type: string
21502156
style: form
21512157
explode: true
2158+
DRAEvictionNodeLabel:
2159+
name: dra-eviction-node-label
2160+
in: query
2161+
required: false
2162+
description: >
2163+
Single key=value node label coordinating NVIDIA DRA kubelet-plugin
2164+
eviction with GPU Operator driver upgrades. Applied only when both
2165+
components are enabled.
2166+
schema:
2167+
type: string
2168+
default: nvidia.com/dra-kubelet-plugin=true
2169+
example: "example.com/dra-ready=enabled"
21522170
BundleDeployer:
21532171
name: deployer
21542172
in: query

docs/contributor/component.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ Merge order is base → `valuesFile` → overlay `overrides` →
233233
recipe default. The node-class flags (`--system-node-selector` et al.)
234234
write into their routed paths at bundle time, alongside `--set`.
235235

236+
The narrow exception is a **bundler-owned derived integration value**:
237+
when AICR renders both sides of a cross-chart contract and allowing either
238+
side to drift would produce an invalid bundle, the bundler may enforce that
239+
contract after ordinary overrides. The DRA/GPU Operator integration is the
240+
worked example: AICR merges the configured DRA eviction label into
241+
`kubeletPlugin.nodeSelector` and writes the same label key to GPU Operator's
242+
`NODE_LABEL_FOR_GPU_POD_EVICTION`. This exception must remain gated on both
243+
components being enabled, must reject dynamic declarations that could move
244+
either managed path to install-time configuration, and must be covered across
245+
every deployer. Ordinary workload placement still uses registry paths and must
246+
not grow a special flag.
247+
236248
**Deciding where a knob belongs:**
237249

238250
| Situation | Where it goes | Why |

docs/user/api-reference.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,11 +549,12 @@ Generate deployment bundles from a recipe.
549549
|-----------|------|---------|-------------|
550550
| `bundlers` | string | (all) | Comma-delimited list of recipe component names to bundle (e.g. `gpu-operator,network-operator`). Whitespace around names is trimmed. Components not listed are skipped as if disabled (their dependency edges are treated as satisfied externally). A name the recipe does not declare, or one that is disabled (by the recipe or a `set` `enabled=false` override), is rejected with HTTP 400. |
551551
| `set` | string[] | | Value overrides (format: `bundler:path.to.field=value`). Repeat for multiple. The reserved prefix `deployer:` carries Argo CD Application options for `deployer=argocd` and `deployer=argocd-helm` (`namePrefix`, `destinationServer`, `project`, `cascadeDelete`), e.g. `set=deployer:namePrefix=tenant-a-`. Unknown `deployer:` keys — or the prefix with any other deployer — are rejected with HTTP 400. An override whose component is absent from the generated bundle is rejected with HTTP 400 (`INVALID_REQUEST`, "cannot take effect") rather than silently discarded; the scalar `enabled=false` spelling is exempt on a declared component. See [Overrides that cannot take effect are rejected](bundling.md#overrides-that-cannot-take-effect-are-rejected) and the CLI reference's [Argo CD Deployer Options](cli-reference.md#argo-cd-deployer-options) for full semantics. |
552-
| `dynamic` | string[] | | Declare value paths as install-time parameters (format: `component:path.to.field`). Repeat for multiple. Supported with `deployer=helm`, `deployer=argocd-helm`, `deployer=flux`, and `deployer=helmfile`. A declaration whose component is absent from the generated bundle is rejected with HTTP 400 (`INVALID_REQUEST`, "cannot take effect"); no path is exempt. Certain gate-verified paths on **present** components are also rejected — driver-ownership paths, GPU allocation-policy keys, and, where the corresponding NVSentinel gate applies on the recipe's platform and configuration, the NVSentinel remedy/consumer/runtime-class paths; see [NVSentinel on provider-installed-driver platforms](component-catalog.md#nvsentinel-on-provider-installed-driver-platforms). See [Overrides that cannot take effect are rejected](bundling.md#overrides-that-cannot-take-effect-are-rejected). |
552+
| `dynamic` | string[] | | Declare value paths as install-time parameters (format: `component:path.to.field`). Repeat for multiple. Supported with `deployer=helm`, `deployer=argocd-helm`, `deployer=flux`, and `deployer=helmfile`. A declaration whose component is absent from the generated bundle is rejected with HTTP 400 (`INVALID_REQUEST`, "cannot take effect"); no path is exempt. Certain gate- or contract-owned paths on **present** components are also rejected — driver-ownership paths, GPU allocation-policy keys, the DRA eviction paths `kubeletPlugin.nodeSelector` and `driver.manager.env` when both contract components are enabled, and, where the corresponding NVSentinel gate applies on the recipe's platform and configuration, the NVSentinel remedy/consumer/runtime-class paths; see [NVSentinel on provider-installed-driver platforms](component-catalog.md#nvsentinel-on-provider-installed-driver-platforms). See [Overrides that cannot take effect are rejected](bundling.md#overrides-that-cannot-take-effect-are-rejected). |
553553
| `system-node-selector` | string[] | | Node selectors for system components (format: `key=value`). Repeat for multiple. |
554554
| `system-node-toleration` | string[] | | Tolerations for system components (format: `key=value:effect`). Repeat for multiple. |
555555
| `accelerated-node-selector` | string[] | | Node selectors for GPU nodes (format: `key=value`). Repeat for multiple. |
556556
| `accelerated-node-toleration` | string[] | | Tolerations for GPU nodes (format: `key=value:effect`). Repeat for multiple. |
557+
| `dra-eviction-node-label` | string | `nvidia.com/dra-kubelet-plugin=true` | Single node label coordinating DRA kubelet-plugin eviction with GPU Operator driver upgrades (format: `key=value`). Applied only when both components are enabled. Nodes used for DRA GPU allocation must carry the same label. |
557558
| `nodes` | int | 0 | Estimated number of GPU nodes (0 = unset). Written to Helm value paths declared in the registry under `nodeScheduling.nodeCountPaths`. |
558559
| `vendor-charts` | bool | false | Pull upstream Helm chart bytes into the bundle at bundle time so the artifact is fully self-contained and air-gap deployable. Each vendored chart is recorded in `provenance.yaml` with name, version, source URL, and SHA256. Trades the upstream CVE-yank fail-loud signal for offline deployability — see the CLI reference's "Vendoring Charts for Air-Gap" section for the full tradeoff. Requires the `helm` binary on the API server's `$PATH`. **The server-side vendor path is opt-in and off by default** — the operator must set `AICR_ALLOW_VENDOR_CHARTS=true`, otherwise `vendor-charts=true` returns `400 vendor-charts is not enabled on this server`. Even when enabled, repository hosts that resolve to loopback, link-local, private, or cloud-metadata IPs are rejected with `400 INVALID_REQUEST`, and vendored artifacts are capped at 64 MiB. **Private HTTP(S) repository credentials:** the aicrd pre-check sends `HELM_REPOSITORY_USERNAME`/`HELM_REPOSITORY_PASSWORD` (as HTTP Basic auth) ONLY when `AICR_HELM_REPOSITORY_HOST` is set to that repository's exact host, the request scheme is `https`, and the request host matches (case-insensitive). All three conditions must hold — an operator setting only the username/password env vars will get no credentials attached, preventing a caller-supplied `Repository` URL from harvesting the operator's helm credentials. (Note: the upstream `helm pull --repo` subprocess does not itself read these env vars — private HTTP repos require a prior `helm repo add --username --password` in the aicrd image or an SDK-based puller.) OCI credentials flow through the standard docker config (`~/.docker/config.json` or `$DOCKER_CONFIG`), exactly like `helm pull oci://...`. If prerequisites are missing the request fails with a structured error code (`SERVICE_UNAVAILABLE` / HTTP 503 for missing helm). The index pre-check surfaces upstream HTTP status by class: `404` → `NOT_FOUND` / HTTP 404, `401`/`403` → `UNAUTHORIZED` / HTTP 401, `408`/`429` → `SERVICE_UNAVAILABLE` / HTTP 503 (retryable), other `4xx` → `INVALID_REQUEST` / HTTP 400, `5xx` → `SERVICE_UNAVAILABLE` / HTTP 503. |
559560
| `serial` | bool | false | Sequence components strictly one at a time in deployment order, disabling the parallel rollout of independent components. Affects `deployer=argocd`, `argocd-helm`, `flux`, and `helmfile` (`helm` is already serial): argocd falls back to a linear sync-wave per folder, flux chains each `HelmRelease` `dependsOn` to the previous component, and helmfile chains every release via `needs:` into one linear apply order. An escape hatch for reproducing the pre-parallelism ordering or bisecting a rollout. |
@@ -713,6 +714,12 @@ curl -X POST "http://localhost:8080/v1/bundle?system-node-selector=nodeGroup=sys
713714
-d @recipe.json \
714715
-o bundles.zip
715716
717+
# Override the shared DRA eviction label when bundling DRA with GPU Operator
718+
curl -X POST "http://localhost:8080/v1/bundle?dra-eviction-node-label=example.com%2Fdra-ready%3Denabled" \
719+
-H "Content-Type: application/json" \
720+
-d @recipe.json \
721+
-o bundles.zip
722+
716723
# Generate bundles from a saved (fully-hydrated) recipe
717724
curl -X POST "http://localhost:8080/v1/bundle" \
718725
-H "Content-Type: application/json" \

docs/user/bundling.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,49 @@ aicr bundle --recipe recipe.yaml \
179179
--output ./bundles
180180
```
181181

182+
## Prepare DRA nodes before applying upgraded bundles
183+
184+
> **Upgrade requirement:** A bundle containing both `gpu-operator` and
185+
> `nvidia-dra-driver-gpu` now selects DRA kubelet-plugin nodes with
186+
> `nvidia.com/dra-kubelet-plugin=true` by default. The same applies to the
187+
> corresponding `-ocp` components. Before applying the first newly generated
188+
> bundle to an existing deployment, label every GPU node that must run the DRA
189+
> kubelet plugin. Applying the bundle first can reduce the DaemonSet to zero
190+
> eligible nodes, interrupting ComputeDomain/IMEX and any whole-GPU resources
191+
> advertised through DRA.
192+
193+
Label the intended nodes, confirm the selector matches at least one node, and
194+
then apply the bundle:
195+
196+
```bash
197+
kubectl label node <node-name> nvidia.com/dra-kubelet-plugin=true
198+
kubectl get nodes -l nvidia.com/dra-kubelet-plugin=true
199+
```
200+
201+
If the cluster uses a different convention, generate the bundle with
202+
`--dra-eviction-node-label key=value` and apply that exact pair to the nodes.
203+
AICR gives the full pair to the DRA node selector, but GPU Operator's Driver
204+
Manager receives only the label key because its eviction contract matches and
205+
temporarily removes the label by key.
206+
207+
After installation and after every GPU driver upgrade, monitor the kubelet
208+
plugin DaemonSet until all desired pods are ready. This also catches a Driver
209+
Manager rollout that did not restore the eviction label:
210+
211+
```bash
212+
kubectl -n nvidia-dra-driver get daemonset \
213+
nvidia-dra-driver-gpu-kubelet-plugin
214+
```
215+
216+
The integration is not rendered when either component is absent. A dynamic
217+
declaration intersecting `kubeletPlugin.nodeSelector` or `driver.manager.env`
218+
is rejected because moving either path to install-time configuration would let
219+
the two halves drift independently. See
220+
[DRA Driver Upgrade Eviction](cli-reference.md#dra-driver-upgrade-eviction) for
221+
configuration details and NVIDIA's
222+
[GPU Operator DRA installation guide](https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/26.3/dra-intro-install.html)
223+
for the upstream contract.
224+
182225
## Produce an offline (vendored) bundle
183226

184227
`--vendor-charts` pulls upstream Helm chart bytes into the bundle at bundle

docs/user/cli-config.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ spec:
156156
nodeGroup: gpu-worker
157157
acceleratedNodeTolerations:
158158
- nvidia.com/gpu=present:NoSchedule
159+
draEvictionNodeLabel: nvidia.com/dra-kubelet-plugin=true
159160
workloadGate: ""
160161
workloadSelector: {}
161162
nodes: 2
@@ -265,7 +266,7 @@ Inputs to `aicr bundle`.
265266
| `deployment.set` / `.dynamic` | []string | Value overrides, `key:path=value` |
266267
| `deployment.vendorCharts` | bool | Vendor charts into the bundle |
267268
| `deployment.appName` | string | Argo CD parent `Application` name override (multi-bundle installs sharing a namespace) |
268-
| `scheduling.*` | object | `systemNodeSelector`/`Tolerations`, `acceleratedNodeSelector`/`Tolerations`, `workloadGate`, `workloadSelector`, `nodes`, `storageClass`, `sharedStorageClass`. Selectors are YAML maps; tolerations use the CLI's `key=value:effect` strings |
269+
| `scheduling.*` | object | `systemNodeSelector`/`Tolerations`, `acceleratedNodeSelector`/`Tolerations`, `draEvictionNodeLabel`, `workloadGate`, `workloadSelector`, `nodes`, `storageClass`, `sharedStorageClass`. Selectors are YAML maps; tolerations use the CLI's `key=value:effect` strings. `draEvictionNodeLabel` accepts one `key=value` label and defaults to `nvidia.com/dra-kubelet-plugin=true`; AICR applies it only when DRA and GPU Operator are both enabled. |
269270
| `attestation.enabled` | bool | Enable bundle attestation (signing); keyless OIDC by default, KMS-backed when `signingKey` is set |
270271
| `attestation.certificateIdentityRegexp` | string | Expected signer identity |
271272
| `attestation.oidcDeviceFlow` | bool | Device-code flow for headless signing |

0 commit comments

Comments
 (0)