Skip to content

fix(chart): Helm-managed GPU device plugin (client#564) - #712

Merged
shujaatTracebloc merged 10 commits into
developfrom
feature/564
Aug 14, 2026
Merged

fix(chart): Helm-managed GPU device plugin (client#564)#712
shujaatTracebloc merged 10 commits into
developfrom
feature/564

Conversation

@shujaatTracebloc

@shujaatTracebloc shujaatTracebloc commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Problem

The GPU device plugin was applied with an imperative kubectl apply of an upstream manifest fetched at install time, outside any Helm release. It wasn't release-tracked, so it lingered through helm uninstall and was re-created on every installer re-run, and it added a raw.githubusercontent.com network dependency to the install path. (scripts/install-k8s.ps1:2490 in the issue is stale; the live Linux apply was scripts/lib/gpu-plugins.shdeploy_gpu_device_plugin.)

Fix

Render the plugin as a Helm-managed DaemonSetclient/templates/gpu-device-plugin.yaml, gated on gpu.devicePlugin.enabled + vendor, in kube-system, with the manifest baked into the chart (no install-time download). It's reconciled on upgrade and removed on helm uninstall. No helm.sh/resource-policy: keep — sharing the release lifecycle is the point.

  • lib/install-client-helm.sh sets gpu.devicePlugin.{enabled,vendor} from GPU_VENDOR, in lockstep with the GPU request it already writes for training jobs.
  • install-k8s.sh no longer applies the plugin imperatively before Helm; verify_gpu moves to after the Helm install (the plugin now rolls out with the release).
  • A bounded, best-effort adoption shim (_adopt_orphaned_gpu_device_plugin) labels/annotates a pre-existing, Helm-unowned DaemonSet so a re-run after helm uninstall adopts it in place instead of failing with "exists and cannot be imported" — exactly the issue's re-run scenario.
  • Chart.yaml version + appVersion bumped 1.9.39 → 1.9.40 (chart-version-guard).

Scoping: Linux vs Windows

Linux requests a GPU purely on GPU_VENDOR detection, so a chart-managed plugin works cleanly. Windows can't use it here: the non-WSL2 fallback must verify the node advertises a GPU before it writes the GPU request, and a Helm-managed resource only rolls out during Helm install — so that path keeps its imperative apply for now. The WSL2/CDI path deliberately never runs the NVML plugin. Default gpu.devicePlugin.enabled: false means Windows renders no chart DaemonSet (no double-deploy).

Tests

  • New helm-unittest suite client/tests/gpu_device_plugin_test.yaml (default renders nothing; nvidia/amd render; namespace+image override; Helm-ownership label; fail-on-missing-vendor).
  • scripts/tests/gpu-nvidia.bats retargeted from the removed imperative-apply assertions to the chart-managed invariants; install-client-helm.bats +3 for the adoption shim.
  • Local: helm lint --strict (all 4 platforms), helm template renders, bats suites all green. (4 unrelated helm-unittest suites fail only under local helm v4 / unittest 1.1.0 vs CI-pinned v3.15.4 / 0.5.2 — same on develop.)

Follow-ups (not in this PR)

  1. Migrate the Windows non-WSL2 fallback once Confirm-GpuNode can be reordered (verify-before-request). The chart template already supports it.
  2. Pin the AMD image (rocm/k8s-device-plugin:latest) to a digest for air-gapped/reproducible installs.

Closes #564

🤖 Generated with Claude Code


Note

Medium Risk
Changes cluster-level DaemonSets in kube-system and install ordering for GPU paths; adoption/delete shims and bounded rollout waits reduce but do not eliminate upgrade friction on clusters with pre-existing plugins.

Overview
Moves the GPU device plugin from imperative kubectl apply + GitHub manifest fetch into the Helm chart, so it is reconciled on upgrade and removed on helm uninstall (client#564). Chart 1.9.41 adds gpu.devicePlugin (NVIDIA/AMD DaemonSets in kube-system by default), nil-safe gating, global.imageRegistry / digest pinning, and optional imagePullSecrets with the pull secret mirrored into the plugin namespace.

The Linux installer stops deploying the plugin before cluster/Helm setup; install-client-helm.sh turns the plugin on from GPU_VENDOR and runs _adopt_orphaned_gpu_device_plugin so leftover imperative DaemonSets are adopted (or deleted) instead of blocking helm upgrade --install. verify_gpu runs after Helm and waits on DaemonSet rollout before node capacity polling. Imperative deploy logic is removed from gpu-plugins.sh; helm-unittest and bats cover rendering, adoption, and ordering.

Reviewed by Cursor Bugbot for commit dd78549. Bugbot is set up for automated code reviews on this repo. Configure here.

…ubectl (client#564)

The GPU device plugin was applied with an imperative `kubectl apply` of an
upstream manifest fetched at install time, outside any Helm release. It was not
release-tracked, so it lingered through `helm uninstall` and was re-created on
every installer re-run, and it added a raw.githubusercontent.com network
dependency to the install path.

Render it as a Helm-managed DaemonSet (templates/gpu-device-plugin.yaml) gated
on gpu.devicePlugin.enabled + vendor, in kube-system, with the manifest baked
into the chart (no install-time download). The Linux installer sets the value
from GPU_VENDOR in lockstep with the GPU request it writes for training jobs,
and no longer applies the plugin imperatively; node verification moves to after
the Helm install. A bounded, best-effort adoption shim labels/annotates a
pre-existing unowned DaemonSet so a re-run after `helm uninstall` adopts it
instead of failing with "exists and cannot be imported".

Scope: the Windows non-WSL2 fallback keeps its imperative apply. That path must
verify the node advertises a GPU BEFORE it writes the GPU request, which a
Helm-managed resource (rolled out during Helm install) can't satisfy in a single
install; the WSL2/CDI path deliberately doesn't use the NVML plugin at all.

Chart.yaml version + appVersion bumped 1.9.39 -> 1.9.40 (chart-version-guard).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc shujaatTracebloc self-assigned this Aug 13, 2026
shujaatTracebloc and others added 2 commits August 13, 2026 16:30
… edits (client#564)

The Static-analysis job's supply-chain check (gen-manifest.sh --check) fails
when a hashed sub-script changes without a manifest refresh. Re-hash the three
edited scripts (install-k8s.sh, lib/gpu-plugins.sh, lib/install-client-helm.sh).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… --reuse-values (client#564)

The Fleet auto-upgrade E2E upgrades from the last published release (no gpu
block) with --reuse-values, which does NOT merge values.yaml defaults, so
.Values.gpu was nil and .Values.gpu.devicePlugin.enabled panicked with a
nil pointer. Access via parenthesised indexing ((.Values.gpu).devicePlugin) and
default the per-vendor image/namespace sub-keys. Adds a helm-unittest case with
gpu: null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTracebloc marked this pull request as ready for review August 13, 2026 14:34
Comment thread client/templates/gpu-device-plugin.yaml Outdated
Comment thread client/templates/gpu-device-plugin.yaml Outdated
shujaatTracebloc and others added 2 commits August 13, 2026 16:53
…ull-secrets (client#564)

Address Cursor Bugbot findings on the Helm-managed GPU device plugin:
- HIGH: guard gpu values with the chart's `default dict` pattern so a
  `helm upgrade --reuse-values` from a pre-bump release (no gpu key) can
  never evaluate .enabled on nil. Go-template `and` is not short-circuit,
  so `and $dp $dp.enabled` still panicked; coalescing to dict removes it.
- MEDIUM: route both vendor images through tracebloc.image with
  (dig "imageRegistry" "" $g) | default <vendor> so global.imageRegistry
  (#585 mirror/air-gap) re-homes them and an empty imageRegistry falls back
  to the vendor registry (was silently becoming docker.io); add
  imagePullSecrets + copy the pull secret into the plugin namespace
  (resource-monitor pattern) so mirrored/air-gapped hosts can pull.
- Split images into registry/repository/tag/digest in values + schema; add
  unittests for digest pinning, mirror re-home, and pull-secret presence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread scripts/install-k8s.sh
Comment thread client/templates/gpu-device-plugin.yaml Outdated
Comment thread client/templates/gpu-device-plugin.yaml
…ate (client#564)

Address Cursor Bugbot findings on the Helm-managed GPU device plugin:
- MEDIUM (chart bumps restart the plugin): the nvidia/amd pod templates
  embedded tracebloc.labels (helm.sh/chart + app.kubernetes.io/version), so
  every client chart bump changed the pod template and rolling-restarted the
  device plugin. Use version-free tracebloc.selectorLabels on the pod template
  (resource-monitor pattern); tracebloc.labels stays on DaemonSet metadata.
- MEDIUM (verify races plugin rollout): verify_gpu ran after Helm but the
  release does not --wait and the old rollout gate was removed, so the node
  poll could expire while the plugin was still pulling and warn 'may still be
  initializing' on a healthy install. Restore a bounded, best-effort
  'kubectl rollout status daemonset' (120s) before the node poll.
- Update the gpu-nvidia.bats gating assertion to match the nil-safe
  'default dict' chain (the pre-refactor literal was removed), and regenerate
  scripts/manifest.sha256 for the gpu-plugins.sh edit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread scripts/lib/install-client-helm.sh Outdated
Comment thread scripts/lib/gpu-plugins.sh Outdated
…ut timeout (client#564)

Address the follow-up Cursor Bugbot findings on 012d001:
- HIGH (adoption probe fails open): _adopt_orphaned_gpu_device_plugin read
  any failed 'kubectl get' as 'DaemonSet absent' and swallowed label/annotate
  failures with '|| true', so a leftover-but-unadopted DS made
  'helm upgrade --install' die 'exists and cannot be imported' and abort the
  whole (GPU-optional) install. Now: distinguish absent/NotFound from a live
  API error (warn, don't fake-adopt); and if adoption can't be completed,
  delete the orphan so the chart recreates a clean Helm-owned copy instead of
  bricking Step e. Added bats coverage for both paths.
- MEDIUM (rollout wait uses request-timeout): dropped --request-timeout=10s
  from the verify_gpu 'kubectl rollout status' — on a watch it capped the wait
  to ~10s and defeated --timeout=120s. --timeout is the correct explicit bound.
- Regenerated scripts/manifest.sha256.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread scripts/lib/install-client-helm.sh
shujaatTracebloc and others added 2 commits August 14, 2026 09:17
…elop (client#564)

develop's Chart.yaml reached 1.9.40 — the version this branch had bumped to —
so chart-version-guard's version-check failed (chart content changed without a
version strictly greater than develop). Bump version+appVersion to 1.9.41 in
lockstep and merge origin/develop to stay current.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2438ef0. Configure here.

Comment thread scripts/lib/install-client-helm.sh Outdated
… can't abort a fresh install (client#564)

Bugbot (High): the adoption probe used a bare 'probe=$(kubectl get ...); rc=$?'.
Under set -e a NotFound on a fresh GPU host makes the assignment exit non-zero
and aborts installer step e before the absent-vs-error branch runs. Use the
house '|| rc=$?' idiom so a non-zero lookup is classified, not fatal. Add a
subshell set -e regression test (the mocked 'run' tests can't catch it since
run disables errexit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@shujaatTracebloc
shujaatTracebloc requested review from LukasWodka and removed request for saadqbal August 14, 2026 07:32

@LukasWodka LukasWodka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the full diff and the Bugbot iteration end-to-end. This correctly moves the GPU device plugin off the imperative kubectl apply of a fetched raw.githubusercontent.com manifest and into a Helm-tracked DaemonSet (client/templates/gpu-device-plugin.yaml), gated on gpu.devicePlugin.enabled (default false, so CPU-only and the Windows/WSL2 path render nothing), with the manifest baked into the chart and deliberately no helm.sh/resource-policy: keep — so it now reconciles on upgrade and is removed on helm uninstall, which is exactly what #564 asked for.

I confirmed the old Linux imperative path is fully gone, not just superseded: deploy_gpu_device_plugin / _apply_remote_manifest / _deploy_{nvidia,amd}_plugin are deleted from scripts/lib/gpu-plugins.sh, install-k8s.sh no longer calls them (verify_gpu now runs after the Helm install), and a bats guard asserts no kubectl apply/curl/old-helpers survive — so there's no orphaned DaemonSet double-management. The NVIDIA/AMD DaemonSets faithfully mirror upstream (kube-system, nvidia.com/gpu toleration + system-node-critical, FAIL_ON_INIT_ERROR=false so a non-GPU node no-ops instead of crash-looping, arch nodeSelector + CriticalAddonsOnly for AMD, hostPath device-plugins socket), the installer flips the flag in lockstep with GPU_VENDOR, and _adopt_orphaned_gpu_device_plugin cleanly handles the re-run-after-uninstall 'exists and cannot be imported' collision. The reuse-values nil-guard (default dict chain) and global.imageRegistry + pull-secret mirroring close the air-gap/reuse-values gaps Bugbot raised.

Chart bumped 1.9.40 -> 1.9.41 (version guard green), manifest.sha256 regenerated for the 3 touched scripts (Static-analysis/R8 green), all 9 Bugbot findings resolved, and every required check is green. Solid, well-tested change (helm-unittest + bats both assert real invariants rather than restating the template).

Minor, non-blocking: the adoption-fallback kubectl delete in install-client-helm.sh still carries only --timeout=30s while its sibling get/label/annotate calls all gained --request-timeout — a fully wedged apiserver could leave that one HTTP call unbounded. Bugbot flagged and it was resolved, so just noting for awareness. Nice work.

@shujaatTracebloc
shujaatTracebloc merged commit c693a95 into develop Aug 14, 2026
47 checks passed
@shujaatTracebloc
shujaatTracebloc deleted the feature/564 branch August 14, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GPU device-plugin applied via imperative kubectl (outside Helm)

2 participants