@@ -62,8 +62,12 @@ const (
6262 // host-managed drivers (e.g. GKE A4X Max requiring R580.95.05+) cannot be
6363 // gated by GPU Operator version alone when driver.enabled is false; this
6464 // constraint is evaluated against the nvidia-smi banner on each verified
65- // node. Absent from the recipe, the check keeps its original banner-presence
66- // behavior and does not invent a floor.
65+ // node. The value must carry a comparison operator (typically ">=") to
66+ // behave as a floor; a bare version is exact string match. Absent from the
67+ // recipe, the check keeps its original banner-presence behavior and does
68+ // not invent a floor. When the constraint is set but no node can be
69+ // measured (no GPU nodes, all cordoned, or busy), the check fails closed
70+ // instead of Skip — a declared gate must not PASS unenforced.
6771 gpuDriverVersionConstraint = "Deployment.gpu-driver.version"
6872)
6973
@@ -184,6 +188,10 @@ func checkNvidiaSMI(ctx *validators.Context) error {
184188 "recipe declares gpu-operator but the cluster has no GPU nodes to verify — " +
185189 "check node provisioning, the GPU Operator rollout, or validator RBAC" )
186190 }
191+ if err := failClosedIfUnmeasurableGPUDriverFloor (ctx ,
192+ "no GPU nodes found in the cluster" ); err != nil {
193+ return err
194+ }
187195 emitExtraOrWarn (nvidiaSMISkipExtra (skipReasonNoGPUNodes ))
188196 return validators .Skip ("no GPU nodes found in the cluster" )
189197 }
@@ -198,9 +206,13 @@ func checkNvidiaSMI(ctx *validators.Context) error {
198206 // probe error. There is nothing schedulable to verify, so Skip with a
199207 // distinct, accurate code — never a false "no-gpu-nodes" in the signed
200208 // evidence.
209+ reason := fmt .Sprintf (
210+ "all %d GPU node(s) are cordoned; nothing to verify" , len (coverage .cordoned ))
211+ if err := failClosedIfUnmeasurableGPUDriverFloor (ctx , reason ); err != nil {
212+ return err
213+ }
201214 emitExtraOrWarn (nvidiaSMISkipExtra (skipReasonNoSchedulableGPUNodes ))
202- return validators .Skip (fmt .Sprintf (
203- "all %d GPU node(s) are cordoned; nothing to verify" , len (coverage .cordoned )))
215+ return validators .Skip (reason )
204216 }
205217
206218 // Check if any nodes are busy.
@@ -231,11 +243,16 @@ func checkNvidiaSMI(ctx *validators.Context) error {
231243 if len (busyNodes ) > 0 {
232244 printLines (coverage .coverageLine (0 ))
233245 if confirmedBusy {
234- // At least one node was CONFIRMED occupied: a legitimate
235- // scope-narrowing Skip. Sign nodes-busy so the signed evidence records
236- // the occupancy. Probe errors on other nodes are already logged above.
246+ // At least one node was CONFIRMED occupied. Without a host-driver
247+ // floor this is a legitimate scope-narrowing Skip. With a declared
248+ // floor, occupancy is not a reason to leave the gate unevaluated
249+ // (#1995): Skip is non-blocking, so a below-floor cluster would PASS.
250+ reason := fmt .Sprintf ("GPU nodes busy with existing workloads: %v" , busyNodes )
251+ if err := failClosedIfUnmeasurableGPUDriverFloor (ctx , reason ); err != nil {
252+ return err
253+ }
237254 emitExtraOrWarn (nvidiaSMISkipExtra (skipReasonNodesBusy ))
238- return validators .Skip (fmt . Sprintf ( "GPU nodes busy with existing workloads: %v" , busyNodes ) )
255+ return validators .Skip (reason )
239256 }
240257 // #2122 fail-closed: every "busy" node was actually a busy-probe ERROR —
241258 // the probe proved occupancy on no node. An infra error (RBAC denial,
@@ -306,6 +323,21 @@ func nvidiaSMISkipExtra(reason string) map[string]string {
306323 return map [string ]string {"skipReason" : reason }
307324}
308325
326+ // failClosedIfUnmeasurableGPUDriverFloor returns a blocking error when the
327+ // recipe declares Deployment.gpu-driver.version but check-nvidia-smi cannot
328+ // run per-node verification (no GPU nodes, all cordoned, or busy). Skip on
329+ // those paths is non-blocking; a declared floor that cannot be measured must
330+ // not PASS (#1995). No constraint keeps the existing Skip.
331+ func failClosedIfUnmeasurableGPUDriverFloor (ctx * validators.Context , reason string ) error {
332+ expr , found := findDeploymentConstraint (ctx , gpuDriverVersionConstraint )
333+ if ! found {
334+ return nil
335+ }
336+ return errors .New (errors .ErrCodeNotFound ,
337+ fmt .Sprintf ("%s %q is set but the host driver version could not be measured (%s)" ,
338+ gpuDriverVersionConstraint , expr , reason ))
339+ }
340+
309341// emitExtraOrWarn emits structured extra evidence, logging (never failing) on
310342// error — a failed stdout write must not flip the check's verdict.
311343func emitExtraOrWarn (extra map [string ]string ) {
@@ -446,12 +478,9 @@ func parseNvidiaSMIDriverVersion(podLogs string) (string, error) {
446478 return "" , errors .New (errors .ErrCodeNotFound ,
447479 "nvidia-smi driver version is not a three-component numeric field" )
448480 }
449- version := podLogs [loc [2 ]:loc [3 ]]
450- if version == "" {
451- return "" , errors .New (errors .ErrCodeNotFound ,
452- "nvidia-smi output has no parseable Driver Version / KMD Version" )
453- }
454- return version , nil
481+ // The capture is `[0-9]+(?:\.[0-9]+){0,2}` so loc[2]:loc[3] is never empty
482+ // once FindStringSubmatchIndex returned a match.
483+ return podLogs [loc [2 ]:loc [3 ]], nil
455484}
456485
457486// driverVersionFieldTerminated reports whether the character after a captured
@@ -476,7 +505,9 @@ func driverVersionFieldTerminated(podLogs string, end int) bool {
476505// the driver version parsed from nvidia-smi logs (issue #1995). No constraint
477506// in the recipe is a no-op — the check must not invent a floor. A constraint
478507// with an unreadable banner fails closed: a host-driver floor that cannot be
479- // measured must not PASS.
508+ // measured must not PASS. Enumeration paths that would Skip (no GPU nodes,
509+ // all cordoned, busy) use failClosedIfUnmeasurableGPUDriverFloor for the
510+ // same contract before per-node verification runs.
480511func enforceGPUDriverVersionFloor (ctx * validators.Context , podLogs , nodeName string ) error {
481512 constraintExpr , found := findDeploymentConstraint (ctx , gpuDriverVersionConstraint )
482513 if ! found {
@@ -502,9 +533,9 @@ func enforceGPUDriverVersionFloor(ctx *validators.Context, podLogs, nodeName str
502533
503534 passed , err := parsed .Evaluate (version )
504535 if err != nil {
505- return errors .Wrap ( errors .ErrCodeInternal ,
536+ return errors .PropagateOrWrap ( err , errors .ErrCodeInternal ,
506537 fmt .Sprintf ("%s constraint evaluation failed on node %s" ,
507- gpuDriverVersionConstraint , nodeName ), err )
538+ gpuDriverVersionConstraint , nodeName ))
508539 }
509540
510541 fmt .Printf (" %s: host driver %s, constraint %s → %v\n " ,
0 commit comments