• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NVIDIA / gpu-operator / 29513807383

16 Jul 2026 03:58PM UTC coverage: 32.688% (+0.9%) from 31.812%
29513807383

Pull #2571

github

karthikvetrivel
Add golden render tests for GPUCluster operand manifests

Signed-off-by: Karthik Vetrivel <kvetrivel@nvidia.com>
Pull Request #2571: Add GPUCluster CRD and controller for DRA-based stack

513 of 1343 new or added lines in 31 files covered. (38.2%)

5 existing lines in 4 files now uncovered.

4663 of 14265 relevant lines covered (32.69%)

0.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

61.26
/controllers/nodelabeling_controller.go
1
/**
2
# Copyright (c) NVIDIA CORPORATION.  All rights reserved.
3
#
4
# Licensed under the Apache License, Version 2.0 (the "License");
5
# you may not use this file except in compliance with the License.
6
# You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
**/
16

17
package controllers
18

19
import (
20
        "context"
21
        "fmt"
22
        "os"
23

24
        "github.com/NVIDIA/k8s-operator-libs/pkg/upgrade"
25
        "github.com/go-logr/logr"
26
        corev1 "k8s.io/api/core/v1"
27
        "k8s.io/apimachinery/pkg/runtime"
28
        "k8s.io/apimachinery/pkg/types"
29
        "k8s.io/client-go/util/workqueue"
30
        ctrl "sigs.k8s.io/controller-runtime"
31
        "sigs.k8s.io/controller-runtime/pkg/client"
32
        "sigs.k8s.io/controller-runtime/pkg/controller"
33
        "sigs.k8s.io/controller-runtime/pkg/event"
34
        "sigs.k8s.io/controller-runtime/pkg/handler"
35
        "sigs.k8s.io/controller-runtime/pkg/predicate"
36
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
37
        "sigs.k8s.io/controller-runtime/pkg/source"
38

39
        gpuv1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1"
40
        nvidiav1alpha1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1alpha1"
41
        "github.com/NVIDIA/gpu-operator/internal/consts"
42
        nvidiadriverutil "github.com/NVIDIA/gpu-operator/internal/nvidiadriver"
43
)
44

45
const nodeLabelingControllerSingletonName = "cluster"
46

47
// NodeLabelingReconciler applies GPU-Operator related labels and annotations to Kubernetes nodes.
48
// All node label write operations for the GPU Operator are centralized here.
49
type NodeLabelingReconciler struct {
50
        client.Client
51
        Scheme    *runtime.Scheme
52
        Namespace string
53
        Log       logr.Logger
54
}
55

56
// nodeLabelingController holds per-reconcile state so that helper methods don't need to
57
// re-receive that state as arguments. clusterPolicy drives the device-plugin stack and
58
// gpuCluster the DRA stack; the two may coexist, with each node served by exactly
59
// one stack according to its nvidia.com/gpu-operator.resource-allocation.mode label. defaultMode is the mode
60
// applied to GPU nodes that do not have the label yet.
61
type nodeLabelingController struct {
62
        client        client.Client
63
        namespace     string
64
        clusterPolicy *gpuv1.ClusterPolicy
65
        gpuCluster    *nvidiav1alpha1.GPUCluster
66
        defaultMode   consts.GPUAllocationMode
67
        logger        logr.Logger
68
}
69

70
// gpuNodeLabelsUpdateResult reports total node patches and the subset where GPU
71
// discovery state changed. The discovery state is stored in nvidia.com/gpu.present,
72
// which AssignOwners uses to find GPU nodes, so dependent operations are deferred
73
// until the informer cache observes those node updates.
74
type gpuNodeLabelsUpdateResult struct {
75
        totalPatchedNodeCount             int
76
        gpuDiscoveryStateChangedNodeCount int
77
}
78

79
// nodeLabelUpdateReasons captures why a node update event should trigger node-label reconciliation.
80
type nodeLabelUpdateReasons struct {
81
        gpuCommonLabelMissing        bool
82
        gpuCommonLabelOutdated       bool
83
        gpuCommonLabelChanged        bool
84
        commonOperandsLabelChanged   bool
85
        modeLabelMissing             bool
86
        modeLabelChanged             bool
87
        gpuWorkloadConfigChanged     bool
88
        migCapableLabelChanged       bool
89
        osTreeLabelChanged           bool
90
        nvidiaDriverOwnerLabelChange bool
91
}
92

93
// needsUpdate reports whether any tracked node-label change requires reconciliation.
94
func (r nodeLabelUpdateReasons) needsUpdate() bool {
1✔
95
        return r.gpuCommonLabelMissing ||
1✔
96
                r.gpuCommonLabelOutdated ||
1✔
97
                r.gpuCommonLabelChanged ||
1✔
98
                r.commonOperandsLabelChanged ||
1✔
99
                r.modeLabelMissing ||
1✔
100
                r.modeLabelChanged ||
1✔
101
                r.gpuWorkloadConfigChanged ||
1✔
102
                r.migCapableLabelChanged ||
1✔
103
                r.osTreeLabelChanged ||
1✔
104
                r.nvidiaDriverOwnerLabelChange
1✔
105
}
1✔
106

107
// getNodeLabelUpdateReasons compares old and new node labels for changes that affect GPU Operator labels.
108
func getNodeLabelUpdateReasons(oldLabels, newLabels map[string]string) nodeLabelUpdateReasons {
1✔
109
        oldGPUWorkloadConfig, _ := getWorkloadConfig(oldLabels, true)
1✔
110
        newGPUWorkloadConfig, _ := getWorkloadConfig(newLabels, true)
1✔
111

1✔
112
        return nodeLabelUpdateReasons{
1✔
113
                gpuCommonLabelMissing:        hasGPULabels(newLabels) && !hasCommonGPULabel(newLabels),
1✔
114
                gpuCommonLabelOutdated:       !hasGPULabels(newLabels) && hasCommonGPULabel(newLabels),
1✔
115
                gpuCommonLabelChanged:        oldLabels[commonGPULabelKey] != newLabels[commonGPULabelKey],
1✔
116
                commonOperandsLabelChanged:   hasOperandsDisabled(oldLabels) != hasOperandsDisabled(newLabels),
1✔
117
                modeLabelMissing:             hasCommonGPULabel(newLabels) && newLabels[consts.GPUAllocationModeLabelKey] == "",
1✔
118
                modeLabelChanged:             oldLabels[consts.GPUAllocationModeLabelKey] != newLabels[consts.GPUAllocationModeLabelKey],
1✔
119
                gpuWorkloadConfigChanged:     oldGPUWorkloadConfig != newGPUWorkloadConfig,
1✔
120
                migCapableLabelChanged:       hasMIGCapableGPU(oldLabels) != hasMIGCapableGPU(newLabels),
1✔
121
                osTreeLabelChanged:           oldLabels[nfdOSTreeVersionLabelKey] != newLabels[nfdOSTreeVersionLabelKey],
1✔
122
                nvidiaDriverOwnerLabelChange: oldLabels[consts.NVIDIADriverOwnerLabel] != newLabels[consts.NVIDIADriverOwnerLabel],
1✔
123
        }
1✔
124
}
1✔
125

126
// +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch;update;patch
127

128
// Reconcile applies GPU-Operator related labels and annotations to all cluster nodes.
129
func (r *NodeLabelingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
1✔
130
        r.Log.Info("Reconciling node labels")
1✔
131

1✔
132
        // The ClusterPolicy (device-plugin stack) and GPUCluster (DRA stack) CRs may
1✔
133
        // coexist; neither existing means there is nothing to label.
1✔
134
        clusterPolicy, gpuCluster, err := resolveActiveConfig(ctx, r.Client)
1✔
135
        if err != nil {
1✔
NEW
136
                return reconcile.Result{}, err
×
UNCOV
137
        }
×
138
        if clusterPolicy == nil && gpuCluster == nil {
2✔
139
                r.Log.Info("No ClusterPolicy or GPUCluster CR exists, skipping node labeling")
1✔
140
                return reconcile.Result{}, nil
1✔
141
        }
1✔
142

143
        envDefaultMode, err := defaultModeFromEnv()
1✔
144
        if err != nil {
2✔
145
                return reconcile.Result{}, err
1✔
146
        }
1✔
147
        if clusterPolicy != nil && gpuCluster != nil && envDefaultMode == "" {
2✔
148
                r.Log.Info("WARNING: both ClusterPolicy and GPUCluster exist but DEFAULT_GPU_ALLOCATION_MODE is unset; " +
1✔
149
                        "defaulting new GPU nodes to the device-plugin stack")
1✔
150
        }
1✔
151

152
        nlc := &nodeLabelingController{
1✔
153
                client:        r.Client,
1✔
154
                namespace:     r.Namespace,
1✔
155
                clusterPolicy: clusterPolicy,
1✔
156
                gpuCluster:    gpuCluster,
1✔
157
                defaultMode:   resolveDefaultMode(clusterPolicy != nil, gpuCluster != nil, envDefaultMode),
1✔
158
                logger:        r.Log,
1✔
159
        }
1✔
160

1✔
161
        gpuLabelUpdateResult, err := nlc.labelGPUNodes(ctx)
1✔
162
        if err != nil {
1✔
163
                if gpuLabelUpdateResult.totalPatchedNodeCount > 0 {
×
164
                        r.Log.Error(err, "GPU node label update failed after partially updating nodes",
×
165
                                "totalPatchedNodeCount", gpuLabelUpdateResult.totalPatchedNodeCount,
×
166
                                "gpuDiscoveryStateChangedNodeCount", gpuLabelUpdateResult.gpuDiscoveryStateChangedNodeCount,
×
167
                        )
×
168
                }
×
169
                return reconcile.Result{}, err
×
170
        }
171
        if gpuLabelUpdateResult.gpuDiscoveryStateChangedNodeCount > 0 {
2✔
172
                r.Log.V(consts.LogLevelDebug).Info("GPU discovery state used by owner assignment updated; dependent node label operations will run after the node update event",
1✔
173
                        "totalPatchedNodeCount", gpuLabelUpdateResult.totalPatchedNodeCount,
1✔
174
                        "gpuDiscoveryStateChangedNodeCount", gpuLabelUpdateResult.gpuDiscoveryStateChangedNodeCount,
1✔
175
                )
1✔
176
                return reconcile.Result{}, nil
1✔
177
        }
1✔
178

179
        // Route each GPU node to its NVIDIADriver CR. Skipping this leaves the NVIDIADriver controller owning no nodes, and it
180
        // then removes the driver DaemonSet.
181
        usesNvidiaDriverCRD := nlc.gpuCluster != nil ||
1✔
182
                (nlc.clusterPolicy != nil && nlc.clusterPolicy.Spec.Driver.UseNvidiaDriverCRDType())
1✔
183
        if usesNvidiaDriverCRD {
2✔
184
                if _, err := nvidiadriverutil.AssignOwners(ctx, r.Client); err != nil {
1✔
185
                        return reconcile.Result{}, fmt.Errorf("failed to assign NVIDIADriver owners to nodes: %w", err)
×
186
                }
×
187
                if err := nlc.labelNodesWithOrphanedDriverPods(ctx); err != nil {
1✔
188
                        return reconcile.Result{}, fmt.Errorf("failed to label nodes with orphaned NVIDIA driver pods: %w", err)
×
189
                }
×
190
        }
191

192
        // The k8s-driver-manager init container consumes this annotation on either stack.
193
        if err := nlc.applyDriverAutoUpgradeAnnotation(ctx); err != nil {
1✔
194
                return reconcile.Result{}, err
×
195
        }
×
196

197
        return reconcile.Result{}, nil
1✔
198
}
199

200
// defaultModeFromEnv reads and validates the DEFAULT_GPU_ALLOCATION_MODE operator
201
// environment variable. Unset yields the empty mode (resolveDefaultMode then falls back
202
// to device-plugin); a set-but-invalid value is an error.
203
func defaultModeFromEnv() (consts.GPUAllocationMode, error) {
1✔
204
        raw := os.Getenv(consts.DefaultGPUAllocationModeEnvName)
1✔
205
        switch mode := consts.GPUAllocationMode(raw); mode {
1✔
206
        case "", consts.GPUAllocationModeDevicePlugin, consts.GPUAllocationModeDRA:
1✔
207
                return mode, nil
1✔
208
        default:
1✔
209
                return "", fmt.Errorf("invalid %s environment variable: %q is not one of %q or %q",
1✔
210
                        consts.DefaultGPUAllocationModeEnvName, raw,
1✔
211
                        consts.GPUAllocationModeDevicePlugin, consts.GPUAllocationModeDRA)
1✔
212
        }
213
}
214

215
// labelGPUNodes reconciles GPU-related labels and reports which node labels were patched.
216
func (nlc *nodeLabelingController) labelGPUNodes(ctx context.Context) (gpuNodeLabelsUpdateResult, error) {
1✔
217
        result := gpuNodeLabelsUpdateResult{}
1✔
218
        nodeList := &corev1.NodeList{}
1✔
219
        if err := nlc.client.List(ctx, nodeList); err != nil {
1✔
220
                return result, fmt.Errorf("unable to list nodes: %w", err)
×
221
        }
×
222

223
        for _, node := range nodeList.Items {
2✔
224
                original := node.DeepCopy()
1✔
225
                labels := node.GetLabels()
1✔
226
                gpuDiscoveryStateChanged := false
1✔
227
                modeLabelModified := false
1✔
228
                stateLabelsModified := false
1✔
229

1✔
230
                if nlc.reconcileCommonGPULabel(labels, node.Name) {
2✔
231
                        node.SetLabels(labels)
1✔
232
                        gpuDiscoveryStateChanged = true
1✔
233
                }
1✔
234

235
                if nlc.reconcileModeLabel(labels, node.Name) {
2✔
236
                        node.SetLabels(labels)
1✔
237
                        modeLabelModified = true
1✔
238
                }
1✔
239

240
                if nlc.updateGPUStateLabels(labels, node.Name) {
2✔
241
                        node.SetLabels(labels)
1✔
242
                        stateLabelsModified = true
1✔
243
                }
1✔
244

245
                modified := gpuDiscoveryStateChanged || modeLabelModified || stateLabelsModified
1✔
246
                if modified {
2✔
247
                        if err := nlc.client.Patch(ctx, &node, client.MergeFrom(original)); err != nil {
2✔
248
                                return result, fmt.Errorf("unable to label node %s: %w", node.Name, err)
1✔
249
                        }
1✔
250
                        result.totalPatchedNodeCount++
1✔
251
                        if gpuDiscoveryStateChanged {
2✔
252
                                result.gpuDiscoveryStateChangedNodeCount++
1✔
253
                        }
1✔
254
                }
255
        }
256
        return result, nil
1✔
257
}
258

259
// reconcileCommonGPULabel keeps nvidia.com/gpu.present in sync with NFD GPU PCI labels.
260
// Returns true if labels were modified.
261
func (nlc *nodeLabelingController) reconcileCommonGPULabel(labels map[string]string, nodeName string) bool {
1✔
262
        if !hasCommonGPULabel(labels) && hasGPULabels(labels) {
2✔
263
                nlc.logger.Info("Node has GPU(s), setting common GPU label", "NodeName", nodeName)
1✔
264
                labels[commonGPULabelKey] = commonGPULabelValue
1✔
265
                return true
1✔
266
        }
1✔
267
        if hasCommonGPULabel(labels) && !hasGPULabels(labels) {
2✔
268
                nlc.logger.Info("Node no longer has GPUs, clearing GPU labels", "NodeName", nodeName)
1✔
269
                labels[commonGPULabelKey] = "false"
1✔
270
                return true
1✔
271
        }
1✔
272
        return false
1✔
273
}
274

275
// reconcileModeLabel writes nvidia.com/gpu-operator.resource-allocation.mode on GPU nodes that do not have it
276
// yet. An existing value is never overwritten (or removed), whether set by a previous
277
// reconcile or manually by a user: changing the cluster configuration or DEFAULT_GPU_ALLOCATION_MODE
278
// must not migrate nodes that are already serving GPUs through one stack. Returns true if
279
// labels were modified.
280
func (nlc *nodeLabelingController) reconcileModeLabel(labels map[string]string, nodeName string) bool {
1✔
281
        if !hasCommonGPULabel(labels) {
2✔
282
                return false
1✔
283
        }
1✔
284
        if _, ok := labels[consts.GPUAllocationModeLabelKey]; ok {
2✔
285
                return false
1✔
286
        }
1✔
287
        nlc.logger.Info("Setting GPU Operator mode label", "NodeName", nodeName,
1✔
288
                "Label", consts.GPUAllocationModeLabelKey, "Value", nlc.defaultMode)
1✔
289
        labels[consts.GPUAllocationModeLabelKey] = string(nlc.defaultMode)
1✔
290
        return true
1✔
291
}
292

293
// updateGPUStateLabels syncs nvidia.com/gpu.deploy.* labels and sets the MIG config label when
294
// appropriate. Which label set is applied follows the node's nvidia.com/gpu-operator.resource-allocation.mode
295
// label; deploy labels exclusive to the other stack are swept away, while shared and
296
// unrecognized deploy labels are left alone. If the node does not have the common GPU
297
// label, all state labels are removed. Returns true if labels were modified.
298
func (nlc *nodeLabelingController) updateGPUStateLabels(labels map[string]string, nodeName string) bool {
1✔
299
        if !hasCommonGPULabel(labels) {
2✔
300
                return removeAllGPUStateLabels(labels)
1✔
301
        }
1✔
302

303
        switch consts.GPUAllocationMode(labels[consts.GPUAllocationModeLabelKey]) {
1✔
304
        case consts.GPUAllocationModeDRA:
1✔
305
                if nlc.gpuCluster == nil {
2✔
306
                        return false
1✔
307
                }
1✔
308
                sweptPreviousStack := nlc.clearPreviousStackStateLabels(labels, devicePluginOnlyStateLabelKeys(), nodeName)
1✔
309
                appliedStackLabels := updateGPUClusterStateLabels(labels)
1✔
310
                return sweptPreviousStack || appliedStackLabels
1✔
311
        case consts.GPUAllocationModeDevicePlugin:
1✔
312
                if nlc.clusterPolicy == nil {
2✔
313
                        return false
1✔
314
                }
1✔
315
        default:
1✔
316
                // Unlabeled (or unrecognized mode): apply no deploy labels. The mode nodeSelector
1✔
317
                // gate on every operand keeps the node empty until a mode is set.
1✔
318
                return false
1✔
319
        }
320

321
        cp := nlc.clusterPolicy
1✔
322
        sandboxEnabled := cp != nil && cp.Spec.SandboxWorkloads.IsEnabled()
1✔
323
        sandboxMode := ""
1✔
324
        if cp != nil {
2✔
325
                sandboxMode = cp.Spec.SandboxWorkloads.Mode
1✔
326
        }
1✔
327

328
        config, err := getWorkloadConfig(labels, sandboxEnabled)
1✔
329
        if err != nil {
1✔
330
                nlc.logger.Info("WARNING: failed to get GPU workload config for node; using default",
×
331
                        "NodeName", nodeName, "SandboxEnabled", sandboxEnabled,
×
332
                        "Error", err, "defaultGPUWorkloadConfig", defaultGPUWorkloadConfig)
×
333
        }
×
334
        gpuWorkloadConfig := &gpuWorkloadConfiguration{
1✔
335
                config:      config,
1✔
336
                sandboxMode: sandboxMode,
1✔
337
                node:        nodeName,
1✔
338
                log:         nlc.logger,
1✔
339
        }
1✔
340
        modified := gpuWorkloadConfig.updateGPUStateLabels(labels)
1✔
341

1✔
342
        if cp != nil && cp.Spec.MIGManager.IsEnabled() && hasMIGCapableGPU(labels) && !hasMIGConfigLabel(labels) {
2✔
343
                migConfigDefault := ""
1✔
344
                if cp.Spec.MIGManager.Config != nil {
2✔
345
                        migConfigDefault = cp.Spec.MIGManager.Config.Default
1✔
346
                }
1✔
347
                if migConfigDefault == migConfigDisabledValue {
2✔
348
                        nlc.logger.Info("Setting MIG config label", "NodeName", nodeName,
1✔
349
                                "Label", migConfigLabelKey, "Value", migConfigDisabledValue)
1✔
350
                        labels[migConfigLabelKey] = migConfigDisabledValue
1✔
351
                        modified = true
1✔
352
                }
1✔
353
        }
354
        return modified
1✔
355
}
356

357
// updateGPUClusterStateLabels is the GPUCluster analogue of the ClusterPolicy
358
// gpuWorkloadConfiguration state-label logic: it sets the DRA operand deploy labels on a GPU
359
// node (removal once the GPUs are gone is handled by removeAllGPUStateLabels). Like the
360
// ClusterPolicy path it honors an existing value (set only when absent) so the
361
// k8s-driver-manager can pause an operand by flipping its label to drain it off a node
362
// during a driver reload. Returns true if modified.
363
func updateGPUClusterStateLabels(labels map[string]string) bool {
1✔
364
        modified := false
1✔
365
        for key, value := range gpuClusterStateLabels {
2✔
366
                if _, ok := labels[key]; !ok {
2✔
367
                        labels[key] = value
1✔
368
                        modified = true
1✔
369
                }
1✔
370
        }
371
        return modified
1✔
372
}
373

374
// clearPreviousStackStateLabels deletes the given deploy-label keys exclusive to the
375
// other (previously serving) stack, value-blind.
376
// Keys outside deleteKeys are never touched, so k8s-driver-manager pause state on the
377
// owning stack's keys survives. Returns true if labels were modified.
378
func (nlc *nodeLabelingController) clearPreviousStackStateLabels(labels map[string]string, deleteKeys map[string]bool, nodeName string) bool {
1✔
379
        modified := false
1✔
380
        for key := range deleteKeys {
2✔
381
                if _, ok := labels[key]; !ok {
2✔
382
                        continue
1✔
383
                }
384
                nlc.logger.Info("Deleting node label", "NodeName", nodeName, "Label", key)
1✔
385
                delete(labels, key)
1✔
386
                modified = true
1✔
387
        }
388
        return modified
1✔
389
}
390

391
func (nlc *nodeLabelingController) setDriverAutoUpgradeAnnotation(ctx context.Context, node *corev1.Node, autoUpgradeEnabled bool) error {
1✔
392
        annotationValue, annotationExists := node.Annotations[driverAutoUpgradeAnnotationKey]
1✔
393
        updateRequired := false
1✔
394
        if autoUpgradeEnabled {
2✔
395
                updateRequired = !annotationExists || annotationValue != "true"
1✔
396
        } else {
2✔
397
                updateRequired = annotationExists
1✔
398
        }
1✔
399
        if !updateRequired {
2✔
400
                return nil
1✔
401
        }
1✔
402

403
        original := node.DeepCopy()
1✔
404
        if node.Annotations == nil {
2✔
405
                node.Annotations = map[string]string{}
1✔
406
        }
1✔
407
        if autoUpgradeEnabled {
2✔
408
                node.Annotations[driverAutoUpgradeAnnotationKey] = "true"
1✔
409
        } else {
2✔
410
                delete(node.Annotations, driverAutoUpgradeAnnotationKey)
1✔
411
        }
1✔
412
        if err := nlc.client.Patch(ctx, node, client.MergeFrom(original)); err != nil {
1✔
413
                nlc.logger.Error(err, "Failed to patch driver auto-upgrade annotation",
×
414
                        "node", node.Name, "enabled", autoUpgradeEnabled)
×
415
                return err
×
416
        }
×
417

418
        return nil
1✔
419
}
420

421
// applyDriverAutoUpgradeAnnotation sets or clears the driver auto-upgrade annotation on GPU nodes.
422
func (nlc *nodeLabelingController) applyDriverAutoUpgradeAnnotation(ctx context.Context) error {
1✔
423
        cp := nlc.clusterPolicy
1✔
424

1✔
425
        // Without a ClusterPolicy, operator-managed drivers must be NVIDIADriver-managed.
1✔
426
        upgradePolicyFromNVIDIADriverCRs := cp == nil ||
1✔
427
                (cp.Spec.Driver.UseNvidiaDriverCRDType() && !cp.Spec.SandboxWorkloads.IsEnabled())
1✔
428
        if upgradePolicyFromNVIDIADriverCRs {
2✔
429
                return nlc.applyDriverAutoUpgradeAnnotationForNVD(ctx)
1✔
430
        }
1✔
431

432
        autoUpgradeEnabled := cp.Spec.Driver.IsEnabled() &&
×
433
                cp.Spec.Driver.IsAutoUpgradeEnabled() &&
×
434
                !cp.Spec.SandboxWorkloads.IsEnabled()
×
435

×
436
        nodeList := &corev1.NodeList{}
×
437
        if err := nlc.client.List(ctx, nodeList, client.MatchingLabels{consts.GPUPresentLabel: "true"}); err != nil {
×
438
                return fmt.Errorf("unable to list nodes: %w", err)
×
439
        }
×
440

441
        for _, node := range nodeList.Items {
×
442
                err := nlc.setDriverAutoUpgradeAnnotation(ctx, &node, autoUpgradeEnabled)
×
443
                if err != nil {
×
444
                        return fmt.Errorf("failed to set driver auto-upgrade annotation on node %q: %w", node.Name, err)
×
445
                }
×
446
        }
447
        return nil
×
448
}
449

450
func (nlc *nodeLabelingController) applyDriverAutoUpgradeAnnotationForNVD(ctx context.Context) error {
1✔
451
        nvidiaDriverList := &nvidiav1alpha1.NVIDIADriverList{}
1✔
452
        if err := nlc.client.List(ctx, nvidiaDriverList); err != nil {
1✔
453
                return fmt.Errorf("failed to list NVIDIADriver instances: %w", err)
×
454
        }
×
455

456
        for _, nvd := range nvidiaDriverList.Items {
2✔
457
                nodeList := &corev1.NodeList{}
1✔
458
                if err := nlc.client.List(ctx, nodeList, client.MatchingLabels{consts.NVIDIADriverOwnerLabel: nvd.Name}); err != nil {
1✔
459
                        nlc.logger.Error(err, "Failed to list nodes for NVIDIADriver", "name", nvd.Name)
×
460
                        return err
×
461
                }
×
462
                autoUpgradeEnabled := nvd.Spec.GetUpgradePolicyWithDefaults().AutoUpgrade
1✔
463
                for _, node := range nodeList.Items {
2✔
464
                        err := nlc.setDriverAutoUpgradeAnnotation(ctx, &node, autoUpgradeEnabled)
1✔
465
                        if err != nil {
1✔
466
                                return fmt.Errorf("failed to set driver auto-upgrade annotation on node %q: %w", node.Name, err)
×
467
                        }
×
468
                }
469
        }
470

471
        return nil
1✔
472
}
473

474
// labelNodesWithOrphanedDriverPods marks nodes that still have unowned (orphaned) ClusterPolicy
475
// driver pods so the upgrade controller can replace them in the normal upgrade flow.
476
func (nlc *nodeLabelingController) labelNodesWithOrphanedDriverPods(ctx context.Context) error {
1✔
477
        nvidiaDrivers := &nvidiav1alpha1.NVIDIADriverList{}
1✔
478
        if err := nlc.client.List(ctx, nvidiaDrivers); err != nil {
1✔
479
                return fmt.Errorf("failed to list NVIDIADriver CRs: %w", err)
×
480
        }
×
481
        if len(nvidiaDrivers.Items) == 0 {
2✔
482
                return nil
1✔
483
        }
1✔
484

485
        pods := &corev1.PodList{}
1✔
486
        if err := nlc.client.List(ctx, pods,
1✔
487
                client.InNamespace(nlc.namespace),
1✔
488
                client.MatchingLabels{AppComponentLabelKey: DriverAppComponentLabelValue},
1✔
489
        ); err != nil {
1✔
490
                return fmt.Errorf("failed to list NVIDIA driver pods: %w", err)
×
491
        }
×
492

493
        for _, pod := range pods.Items {
2✔
494
                if len(pod.OwnerReferences) > 0 || pod.Status.Phase != corev1.PodRunning || pod.Spec.NodeName == "" {
2✔
495
                        continue
1✔
496
                }
497

498
                node := &corev1.Node{}
1✔
499
                if err := nlc.client.Get(ctx, types.NamespacedName{Name: pod.Spec.NodeName}, node); err != nil {
1✔
500
                        nlc.logger.Error(err, "failed to get node for orphaned driver pod", "pod", pod.Name, "node", pod.Spec.NodeName)
×
501
                        continue
×
502
                }
503
                if !nodeOwnedByNVIDIADriver(node, nvidiaDrivers.Items) {
2✔
504
                        continue
1✔
505
                }
506

507
                upgradeStateLabel := upgrade.GetUpgradeStateLabelKey()
1✔
508
                upgradeState := upgrade.UpgradeStateUnknown
1✔
509
                if node.Labels != nil {
2✔
510
                        upgradeState = node.Labels[upgradeStateLabel]
1✔
511
                }
1✔
512
                if !isDriverUpgradeRequestAllowed(upgradeState) {
2✔
513
                        continue
1✔
514
                }
515

516
                original := node.DeepCopy()
1✔
517
                if node.Labels == nil {
1✔
518
                        node.Labels = map[string]string{}
×
519
                }
×
520
                node.Labels[upgradeStateLabel] = upgrade.UpgradeStateUpgradeRequired
1✔
521
                if err := nlc.client.Patch(ctx, node, client.MergeFrom(original)); err != nil {
2✔
522
                        return fmt.Errorf("failed to label node %q for orphaned driver pod %q: %w", node.Name, pod.Name, err)
1✔
523
                }
1✔
524
        }
525
        return nil
1✔
526
}
527

528
// nodeOwnedByNVIDIADriver returns true when the node has an owner label matching a live NVIDIADriver.
529
func nodeOwnedByNVIDIADriver(node *corev1.Node, nvidiaDrivers []nvidiav1alpha1.NVIDIADriver) bool {
1✔
530
        if node.Labels == nil || node.Labels[consts.NVIDIADriverOwnerLabel] == "" {
2✔
531
                return false
1✔
532
        }
1✔
533
        for _, nvidiaDriver := range nvidiaDrivers {
2✔
534
                if nvidiaDriver.HasDeletionTimestamp() {
2✔
535
                        continue
1✔
536
                }
537
                if node.Labels[consts.NVIDIADriverOwnerLabel] == nvidiaDriver.Name {
2✔
538
                        return true
1✔
539
                }
1✔
540
        }
541
        return false
1✔
542
}
543

544
// isDriverUpgradeRequestAllowed returns true when migration can request a driver upgrade
545
// without overwriting an active or failed upgrade state.
546
func isDriverUpgradeRequestAllowed(upgradeState string) bool {
1✔
547
        return upgradeState == upgrade.UpgradeStateUnknown || upgradeState == upgrade.UpgradeStateDone
1✔
548
}
1✔
549

550
// SetupWithManager registers the NodeLabelingReconciler with the controller-runtime manager.
551
func (r *NodeLabelingReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error {
×
552
        mapToSingleton := func(_ context.Context, _ client.Object) []reconcile.Request {
×
553
                return []reconcile.Request{{NamespacedName: types.NamespacedName{Name: nodeLabelingControllerSingletonName}}}
×
554
        }
×
555

556
        c, err := controller.New("node-labeling-controller", mgr, controller.Options{
×
557
                Reconciler:              r,
×
558
                MaxConcurrentReconciles: 1,
×
559
                RateLimiter:             workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](minDelayCR, maxDelayCR),
×
560
        })
×
561
        if err != nil {
×
562
                return fmt.Errorf("error creating node-labeling controller: %w", err)
×
563
        }
×
564

565
        clusterPolicyMapFn := func(ctx context.Context, cp *gpuv1.ClusterPolicy) []reconcile.Request {
×
566
                return mapToSingleton(ctx, cp)
×
567
        }
×
568
        if err := c.Watch(source.Kind(
×
569
                mgr.GetCache(),
×
570
                &gpuv1.ClusterPolicy{},
×
571
                handler.TypedEnqueueRequestsFromMapFunc(clusterPolicyMapFn),
×
572
                predicate.TypedGenerationChangedPredicate[*gpuv1.ClusterPolicy]{},
×
573
        )); err != nil {
×
574
                return fmt.Errorf("error watching ClusterPolicy: %w", err)
×
575
        }
×
576

577
        // Watch GPUCluster so GPU nodes are (re)labeled for the DRA stack as the CR is
578
        // created or removed, mirroring the ClusterPolicy watch above.
NEW
579
        gpuClusterMapFn := func(ctx context.Context, gc *nvidiav1alpha1.GPUCluster) []reconcile.Request {
×
NEW
580
                return mapToSingleton(ctx, gc)
×
NEW
581
        }
×
NEW
582
        if err := c.Watch(source.Kind(
×
NEW
583
                mgr.GetCache(),
×
NEW
584
                &nvidiav1alpha1.GPUCluster{},
×
NEW
585
                handler.TypedEnqueueRequestsFromMapFunc(gpuClusterMapFn),
×
NEW
586
                predicate.TypedGenerationChangedPredicate[*nvidiav1alpha1.GPUCluster]{},
×
NEW
587
        )); err != nil {
×
NEW
588
                return fmt.Errorf("error watching GPUCluster: %w", err)
×
NEW
589
        }
×
590

591
        // Watch NVIDIADriver including delete events so owner labels are cleaned up promptly.
592
        nvidiaDriverMapFn := func(ctx context.Context, nd *nvidiav1alpha1.NVIDIADriver) []reconcile.Request {
×
593
                return mapToSingleton(ctx, nd)
×
594
        }
×
595
        if err := c.Watch(source.Kind(
×
596
                mgr.GetCache(),
×
597
                &nvidiav1alpha1.NVIDIADriver{},
×
598
                handler.TypedEnqueueRequestsFromMapFunc(nvidiaDriverMapFn),
×
599
                predicate.TypedGenerationChangedPredicate[*nvidiav1alpha1.NVIDIADriver]{},
×
600
        )); err != nil {
×
601
                return fmt.Errorf("error watching NVIDIADriver: %w", err)
×
602
        }
×
603

604
        nodePredicate := predicate.TypedFuncs[*corev1.Node]{
×
605
                CreateFunc: func(e event.TypedCreateEvent[*corev1.Node]) bool {
×
606
                        labels := e.Object.GetLabels()
×
607
                        return hasGPULabels(labels)
×
608
                },
×
609
                UpdateFunc: func(e event.TypedUpdateEvent[*corev1.Node]) bool {
×
610
                        newLabels := e.ObjectNew.GetLabels()
×
611
                        oldLabels := e.ObjectOld.GetLabels()
×
612
                        nodeName := e.ObjectNew.GetName()
×
613

×
614
                        reasons := getNodeLabelUpdateReasons(oldLabels, newLabels)
×
615
                        needsUpdate := reasons.needsUpdate()
×
616

×
617
                        // When an NVIDIADriver daemonset pod is running on the node, check if any
×
618
                        // label which is configured in the NVIDIADriver's node selector has changed.
×
619
                        nvidiaDriverNodeSelectorLabelChanged := false
×
620
                        if !needsUpdate && newLabels[consts.NVIDIADriverOwnerLabel] != "" {
×
621
                                name := newLabels[consts.NVIDIADriverOwnerLabel]
×
622
                                nvidiaDriver := &nvidiav1alpha1.NVIDIADriver{}
×
623
                                err := r.Get(ctx, types.NamespacedName{Name: name}, nvidiaDriver)
×
624
                                if err != nil {
×
625
                                        r.Log.Error(err, "failed to get NVIDIADriver object that owns this node", "name", name, "node", nodeName)
×
626
                                        return false
×
627
                                }
×
628
                                for key := range nvidiaDriver.Spec.NodeSelector {
×
629
                                        if oldLabels[key] != newLabels[key] {
×
630
                                                nvidiaDriverNodeSelectorLabelChanged = true
×
631
                                                needsUpdate = true
×
632
                                                break
×
633
                                        }
634
                                }
635
                        }
636

637
                        if needsUpdate {
×
638
                                r.Log.Info("Node needs an update",
×
639
                                        "name", nodeName,
×
640
                                        "gpuCommonLabelMissing", reasons.gpuCommonLabelMissing,
×
641
                                        "gpuCommonLabelOutdated", reasons.gpuCommonLabelOutdated,
×
642
                                        "gpuCommonLabelChanged", reasons.gpuCommonLabelChanged,
×
643
                                        "commonOperandsLabelChanged", reasons.commonOperandsLabelChanged,
×
NEW
644
                                        "modeLabelMissing", reasons.modeLabelMissing,
×
NEW
645
                                        "modeLabelChanged", reasons.modeLabelChanged,
×
646
                                        "gpuWorkloadConfigLabelChanged", reasons.gpuWorkloadConfigChanged,
×
647
                                        "migCapableLabelChanged", reasons.migCapableLabelChanged,
×
648
                                        "osTreeLabelChanged", reasons.osTreeLabelChanged,
×
649
                                        "nvidiaDriverOwnerLabelChanged", reasons.nvidiaDriverOwnerLabelChange,
×
650
                                        "nvidiaDriverNodeSelectorLabelChanged", nvidiaDriverNodeSelectorLabelChanged,
×
651
                                )
×
652
                        }
×
653
                        return needsUpdate
×
654
                },
655
                DeleteFunc: func(e event.TypedDeleteEvent[*corev1.Node]) bool {
×
656
                        return false
×
657
                },
×
658
        }
659
        nodeMapFn := func(ctx context.Context, n *corev1.Node) []reconcile.Request {
×
660
                return mapToSingleton(ctx, n)
×
661
        }
×
662
        if err := c.Watch(source.Kind(
×
663
                mgr.GetCache(),
×
664
                &corev1.Node{},
×
665
                handler.TypedEnqueueRequestsFromMapFunc(nodeMapFn),
×
666
                nodePredicate,
×
667
        )); err != nil {
×
668
                return fmt.Errorf("error watching Nodes: %w", err)
×
669
        }
×
670

671
        // Trigger on driver pods becoming Running so orphaned pods are detected promptly.
672
        podPredicate := predicate.TypedFuncs[*corev1.Pod]{
×
673
                CreateFunc: func(e event.TypedCreateEvent[*corev1.Pod]) bool {
×
674
                        return e.Object.GetLabels()[AppComponentLabelKey] == DriverAppComponentLabelValue
×
675
                },
×
676
                UpdateFunc: func(e event.TypedUpdateEvent[*corev1.Pod]) bool {
×
677
                        if e.ObjectNew.GetLabels()[AppComponentLabelKey] != DriverAppComponentLabelValue {
×
678
                                return false
×
679
                        }
×
680
                        return e.ObjectOld.Status.Phase != corev1.PodRunning &&
×
681
                                e.ObjectNew.Status.Phase == corev1.PodRunning
×
682
                },
683
                DeleteFunc: func(e event.TypedDeleteEvent[*corev1.Pod]) bool {
×
684
                        return false
×
685
                },
×
686
        }
687
        podMapFn := func(ctx context.Context, p *corev1.Pod) []reconcile.Request {
×
688
                return mapToSingleton(ctx, p)
×
689
        }
×
690
        if err := c.Watch(source.Kind(
×
691
                mgr.GetCache(),
×
692
                &corev1.Pod{},
×
693
                handler.TypedEnqueueRequestsFromMapFunc(podMapFn),
×
694
                podPredicate,
×
695
        )); err != nil {
×
696
                return fmt.Errorf("error watching Pods: %w", err)
×
697
        }
×
698

699
        return nil
×
700
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc