Repository navigation
(fix): Reject conflicting DRA kubelet-plugin healthcheck ports when ComputeDomains are enabled - #3029
jramkissoon wants to merge 7 commits into
Conversation
When computeDomains is enabled, the kubelet-plugin DaemonSet runs two containers that each bind a gRPC health service. They share the pod's network namespace, so equal ports make the second bind fail with EADDRINUSE and that container crash-loops; which one loses is a startup race. Reject the GPUCluster in the DRA driver state before rendering when computeDomains is enabled and both resolved ports are equal and positive. The check runs on resolved values so setting only one port to the other container's default is caught too. Disabled health services resolve to -1 and are skipped. Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
Cover the matrix behind validateHealthcheckPorts through
getManifestObjects so the resolved ports and the render abort are
exercised, not just the helper:
- equal explicit ports with computeDomains enabled: error
- gpus port set to the computeDomains default, other side unset:
error, proving the check runs on resolved values
- different ports, shipped defaults, computeDomains disabled,
both health services disabled, one disabled: no error
The error must name both spec paths and the resolved port.
Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
The gpus and compute-domains containers also bind HTTP_ENDPOINT on 8080 and 8081 for metrics. A healthcheck port set to either collides in the same way as two equal healthcheck ports. The gpus endpoint is bound even with computeDomains disabled, so that collision happens inside one container and is deterministic rather than a race. Replace the pairwise healthcheck comparison with a check over every listener the pod will bind: the gpus metrics endpoint and healthcheck port always, the compute-domains pair only when enabled. Disabled health services resolve to -1 and are skipped. The error names both colliding listeners and the resolved port. Add table-driven tests through getManifestObjects covering equal ports, a single port set to the other container's default, distinct ports, shipped defaults, computeDomains disabled, one or both health services disabled, and healthcheck ports on 8080 and 8081. This goes beyond the issue text, which asked only for the healthcheck pair; it is kept as a separate commit so it can be dropped if the maintainers prefer the narrower scope. Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
📝 WalkthroughWalkthroughThe DRA driver defines metrics ports for the GPU and compute-domains containers. It checks resolved healthcheck ports against enabled listener ports and returns a wrapped configuration error when positive ports conflict. Manifest generation performs this check before rendering. Tests cover collisions, disabled services, and non-colliding configurations. Priority: ⚪ Not assessed Merge Risk: 🟡 Moderate · up to Configurations with custom listener environment values can still render a DaemonSet whose listeners conflict. Reject those overrides or validate their effective values before merging, and add the missing collision tests.
Comment |
kubeletPlugin.env is appended after the HTTP_ENDPOINT and HEALTHCHECK_PORT entries the template renders, and the kubelet keeps the last value for a duplicated name. A user entry for either variable therefore moves a listener behind the back of the port validation: a spec with distinct healthcheck ports could still collide at runtime, and a spec that moved the metrics endpoint could be rejected for a collision that no longer exists. Reject HEALTHCHECK_PORT and HTTP_ENDPOINT in kubeletPlugin.env for the gpus container, and for the compute-domains container when it is rendered. Overriding HEALTHCHECK_PORT could never have worked anyway, since the probes take their port from the spec field. Moving the metrics endpoint, if needed, should become a spec field the validation reads. Issue NVIDIA#2979 Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
The collision table only put the gpus healthcheck on 8080 and 8081. Add the compute-domains side for both, plus the disabled case where the same value is accepted because that container is not rendered. Issue NVIDIA#2979 Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
|
@jramkissoon are you still planning to work on this ? |
|
@kvalliyurnatt Yes, still on it. |
Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
Review on NVIDIA#3029 pointed out that the kubelet-plugin skips its metrics listener when METRICS_PATH is empty, so a healthcheck on the freed port must be accepted. Prove that on the cluster: - compute-domains-metrics-disabled empties METRICS_PATH in both containers and moves their healthchecks onto 8080 / 8081. It must be accepted and both containers must stay ready, which also shows the user env entry wins over the template's METRICS_PATH=/metrics. - gpus-metrics-port-collision puts the gpus healthcheck on 8080 with metrics on. It must still be rejected with the DaemonSet untouched. scripts/metrics-path-proof.sh runs both plus recovery (make metrics-path-proof). post-fix-images.sh takes TAG so a before and an after build from a dirty tree get distinct tags and the operator rolls. Issue NVIDIA#2979 Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
Devin review on NVIDIA#3029: rejecting HTTP_ENDPOINT in kubeletPlugin.env breaks GPUClusters that moved the metrics listener, a configuration that works on released operators. compute-domains-http-endpoint-override moves the gpus metrics endpoint to :9090. scripts/http-endpoint-check.sh (make http-endpoint-check) deploys it and reports whether the operator accepts it, with metrics served on 9090 and not on 8080, or rejects it. EXPECT selects the passing outcome. v26.7.1: accepted, GET :9090/metrics 200, :8080 closed issue/NVIDIA#2979 head: rejected, "must not set HTTP_ENDPOINT" Issue NVIDIA#2979 Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
Rejecting HTTP_ENDPOINT in kubeletPlugin.env broke GPUClusters that moved the metrics listener, which works on released operators and has no spec field to migrate to. Resolve each container's metrics port from its env instead: the last HTTP_ENDPOINT entry wins over the template default, and no server binds when HTTP_ENDPOINT or METRICS_PATH is empty or the port is 0. Only the port is compared, since the health service listens on every address. An endpoint the plugin could not listen on is rejected with the field named. HEALTHCHECK_PORT stays reserved: the probes read healthcheck.port, so an env override would move the service away from its probes. Issue NVIDIA#2979 Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
Description
Fixes #2979
When
draDriver.computeDomains.enabledis true, thenvidia-dra-driver-kubelet-pluginDaemonSet runs two containers,
gpusandcompute-domains, each binding a gRPChealth service that the startup and liveness probes dial. The containers share
the pod's network namespace, so if both are configured with the same port the
second
bind()fails withEADDRINUSE. The plugin treats a failed healthlistener as fatal (
startHealthcheckreturns the error,NewDriverfails, theprocess exits), so the losing container crash-loops with
listen tcp :<port>: bind: address already in use. Which container loses is astartup race, so the symptom is nondeterministic and hard to trace back to the
GPUCluster spec.
This change validates the ports in the DRA driver state before rendering and
returns an error instead of producing a broken DaemonSet:
validateKubeletPluginPortsininternal/state/dra_driver.gorejects aGPUCluster whose kubelet-plugin containers would bind the same TCP port. It
runs on the resolved ports, so setting only one field to the other
container's default (51516 / 51515) is caught too. Disabled health services
resolve to -1 and are skipped, so two disabled sides are not a collision.
IsComputeDomainsEnabled(); with it off the second container is notrendered.
the existing
getManifestObjectspath: the GPUCluster gets aReconcileFailedcondition and the DaemonSet is left untouched, the same wayan invalid image reference is handled today.
Scope note: the third commit also covers the containers' hardcoded metrics
endpoints (
HTTP_ENDPOINTon :8080 and :8081), since a healthcheck port set toeither fails the same way, and the gpus case applies even with computeDomains
disabled. That goes beyond the issue text and is kept as a separate commit so
it can be dropped if you prefer the narrower scope.
A CEL rule was considered and rejected: the defaults live in Go, not in the CRD
schema, so CEL cannot see the single-field collision.
AI assistance: the analysis, implementation and tests were produced with help
from an AI coding assistant (Claude). I have reviewed and can defend every line.
Checklist
make lint)make validate-generated-assets)make validate-modules)Testing
Unit tests.
TestDRADriverHealthcheckPortCollisionininternal/state/dra_driver_test.godrivesgetManifestObjectsthrough tencases: equal explicit ports, one port set to the other container's default,
distinct ports, shipped defaults, computeDomains disabled, one and both health
services disabled, and healthcheck ports on 8080 / 8081 with computeDomains on
and off. Against the pre-fix
dra_driver.goexactly the error cases fail andthe happy paths pass.
go test ./internal/state/passes.Reproduction and verification on a cluster. Done on a two-node kind cluster
(Kubernetes 1.34, no GPUs) using NVIDIA's nvml-mock through the run-ai
fake-gpu-operator's mock backend, so both kubelet-plugin containers actually
start and reach their health bind. The overlays, scripts and captured evidence
are on my fork and are not part of this PR:
https://github.com/jramkissoon/nvidia-gpu-operator/tree/gpu-operator-issue-%232979/deploy/issue-test-bed
scripts/issue-replication.shdeploys a GPUCluster withcomputeDomains.kubeletPlugin.healthcheck.port: 51516against the releaseoperator (v26.7.1). The DaemonSet renders both containers on 51516;
gpuswins the bind and
compute-domainscrash-loops with:scripts/resolution-proof.shruns against an operator built from thisbranch:
startup and liveness probes target them, both containers are ready with
zero restarts across two liveness periods, GPUCluster
ready;Error=True ReconcileFailed: ... spec.draDriver.gpus.kubeletPlugin.healthcheck.port and spec.draDriver.computeDomains.kubeletPlugin.healthcheck.port must differ; both resolve to 51516,DaemonSet generation and ports unchanged, both containers still ready;
ready.Getting the compute-domains container to start on a mock node needed
ALT_PROC_DEVICES_PATHpointing at a devices file with the IMEX channel major(the hook the upstream DRA driver's mock e2e uses) and the
IMEXDaemonsWithDNSNames/ComputeDomainCliquesgates off, since mock NVMLreports driver 550. Those settings are in the test-bed overlay, not in this
change.