Skip to content

(fix): Reject conflicting DRA kubelet-plugin healthcheck ports when ComputeDomains are enabled - #3029

Open
jramkissoon wants to merge 7 commits into
NVIDIA:mainfrom
jramkissoon:issue/#2979
Open

jramkissoon wants to merge 7 commits into
NVIDIA:mainfrom
jramkissoon:issue/#2979

Conversation

@jramkissoon

@jramkissoon jramkissoon commented Oct 8, 2026 •

Copy link
Copy Markdown

Description

Fixes #2979

When draDriver.computeDomains.enabled is true, the nvidia-dra-driver-kubelet-plugin
DaemonSet runs two containers, gpus and compute-domains, each binding a gRPC
health 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 with EADDRINUSE. The plugin treats a failed health
listener as fatal (startHealthcheck returns the error, NewDriver fails, the
process exits), so the losing container crash-loops with
listen tcp :<port>: bind: address already in use. Which container loses is a
startup 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:

  • validateKubeletPluginPorts in internal/state/dra_driver.go rejects a
    GPUCluster 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.
  • The compute-domains listeners are only considered when
    IsComputeDomainsEnabled(); with it off the second container is not
    rendered.
  • The error names both colliding listeners and the port, and surfaces through
    the existing getManifestObjects path: the GPUCluster gets a
    ReconcileFailed condition and the DaemonSet is left untouched, the same way
    an invalid image reference is handled today.

Scope note: the third commit also covers the containers' hardcoded metrics
endpoints (HTTP_ENDPOINT on :8080 and :8081), since a healthcheck port set to
either 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

  • No secrets, sensitive information, or unrelated changes
  • Lint checks passing (make lint)
  • Generated assets in-sync (make validate-generated-assets)
  • Go mod artifacts in-sync (make validate-modules)
  • Test cases are added for new code paths

Testing

Unit tests. TestDRADriverHealthcheckPortCollision in
internal/state/dra_driver_test.go drives getManifestObjects through ten
cases: 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.go exactly the error cases fail and
the 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.sh deploys a GPUCluster with
    computeDomains.kubeletPlugin.healthcheck.port: 51516 against the release
    operator (v26.7.1). The DaemonSet renders both containers on 51516; gpus
    wins the bind and compute-domains crash-loops with:

    Error: error creating driver: error setting up healtcheck primitives: failed to listen on :51516: listen tcp :51516: bind: address already in use
    
  • scripts/resolution-proof.sh runs against an operator built from this
    branch:

    • good config, ports 52000 / 52001: the DaemonSet renders both ports, the
      startup and liveness probes target them, both containers are ready with
      zero restarts across two liveness periods, GPUCluster ready;
    • bad config, computeDomains port 51516: GPUCluster condition
      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;
    • restoring distinct ports returns the GPUCluster to ready.

Getting the compute-domains container to start on a mock node needed
ALT_PROC_DEVICES_PATH pointing at a devices file with the IMEX channel major
(the hook the upstream DRA driver's mock e2e uses) and the
IMEXDaemonsWithDNSNames / ComputeDomainCliques gates off, since mock NVML
reports driver 550. Those settings are in the test-bed overlay, not in this
change.


Devin Review

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>
@copy-pr-bot

copy-pr-bot Bot commented Oct 8, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@jramkissoon
jramkissoon requested a review from a team as a code owner October 8, 2026 12:31
@jramkissoon jramkissoon changed the title Reject conflicting DRA kubelet-plugin healthcheck ports when ComputeDomains are enabled (fix): Reject conflicting DRA kubelet-plugin healthcheck ports when ComputeDomains are enabled Oct 8, 2026
@coderabbitai

coderabbitai Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

📝 Walkthrough

Walkthrough

The 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 b4275

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.

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]

This comment was marked as resolved.

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>
@kvalliyurnatt kvalliyurnatt self-assigned this Oct 13, 2026
Comment thread internal/state/dra_driver.go Outdated
@kvalliyurnatt

Copy link
Copy Markdown
Contributor

@jramkissoon are you still planning to work on this ?

@jramkissoon

Copy link
Copy Markdown
Author

@kvalliyurnatt Yes, still on it.

Signed-off-by: jramkissoon <julianramkissoon@icloud.com>
jramkissoon added a commit to jramkissoon/nvidia-gpu-operator that referenced this pull request Oct 22, 2026
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-ai-integration devin-ai-integration 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.

Devin Review found 2 potential issues.

Devin Review

Comment thread internal/state/dra_driver.go
Comment thread internal/state/dra_driver.go Outdated
jramkissoon added a commit to jramkissoon/nvidia-gpu-operator that referenced this pull request Oct 22, 2026
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>

This branch has not been deployed

No deployments
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.

Reject conflicting healthcheck ports in the DRA kubelet-plugin daemonset

2 participants