Skip to content

Commit ccbbdec

Browse files
authored
Merge branch 'main' into xrfxlp/1596-labeler-patch
2 parents b4a7c26 + 7d85fad commit ccbbdec

4 files changed

Lines changed: 396 additions & 5 deletions

File tree

labeler/pkg/devicecounts/device_counts.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,53 @@ func (m *Manager) NodeLabelsAffectDeviceCounts(oldLabels, newLabels map[string]s
222222
return !maps.Equal(oldInputLabels, newInputLabels)
223223
}
224224

225+
// NodeResourcesAffectDeviceCounts reports whether an allocatable or capacity
226+
// change on a node could affect a device-count class whose CEL expression reads
227+
// from node.status.allocatable or node.status.capacity.
228+
func (m *Manager) NodeResourcesAffectDeviceCounts(oldNode, newNode *corev1.Node) bool {
229+
if !m.Enabled() || oldNode == nil || newNode == nil {
230+
return false
231+
}
232+
233+
resourcesReferenced := false
234+
235+
for _, class := range m.classes {
236+
if class.referencesNodeResources() {
237+
resourcesReferenced = true
238+
break
239+
}
240+
}
241+
242+
if !resourcesReferenced {
243+
return false
244+
}
245+
246+
oldAllocatable := resourceListToStringMap(oldNode.Status.Allocatable)
247+
newAllocatable := resourceListToStringMap(newNode.Status.Allocatable)
248+
249+
if !maps.Equal(oldAllocatable, newAllocatable) {
250+
return true
251+
}
252+
253+
oldCapacity := resourceListToStringMap(oldNode.Status.Capacity)
254+
newCapacity := resourceListToStringMap(newNode.Status.Capacity)
255+
256+
return !maps.Equal(oldCapacity, newCapacity)
257+
}
258+
259+
func resourceListToStringMap(rl corev1.ResourceList) map[string]string {
260+
if len(rl) == 0 {
261+
return nil
262+
}
263+
264+
result := make(map[string]string, len(rl))
265+
for k, v := range rl {
266+
result[string(k)] = v.String()
267+
}
268+
269+
return result
270+
}
271+
225272
func newDeviceCountCELEnv() (*cel.Env, error) {
226273
// Keep the CEL surface intentionally small: expressions can only inspect
227274
// the reconciled node, that node's associated ResourceSlices, and sum lists.
@@ -577,6 +624,16 @@ func (class compiledClass) referencesResourceSlices() bool {
577624
return strings.Contains(class.CurrentExpression, "resourceSlices")
578625
}
579626

627+
// referencesNodeResources is a cheap heuristic mirroring referencesResourceSlices.
628+
// It checks specifically for node.status.allocatable or node.status.capacity
629+
// rather than the broader node.status, so expressions that only reference
630+
// node.status.conditions (noisy with heartbeats) do not trigger unnecessary
631+
// allocatable/capacity comparisons on every node update.
632+
func (class compiledClass) referencesNodeResources() bool {
633+
return strings.Contains(class.CurrentExpression, "node.status.allocatable") ||
634+
strings.Contains(class.CurrentExpression, "node.status.capacity")
635+
}
636+
580637
func matchLabels(actual, expected map[string]string) bool {
581638
for key, expectedValue := range expected {
582639
if actual[key] != expectedValue {

labeler/pkg/devicecounts/device_counts_test.go

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,244 @@ func TestManagerRequiresResourceSlices(t *testing.T) {
223223
})
224224
}
225225

226+
func TestNodeResourcesAffectDeviceCounts(t *testing.T) {
227+
nicConfig := Config{
228+
Enabled: true,
229+
Classes: []ClassConfig{
230+
{
231+
Name: "nic",
232+
Enabled: true,
233+
Labels: Labels{
234+
Current: testNICCountCurrentLabel,
235+
Expected: testNICCountExpectedLabel,
236+
},
237+
CurrentExpression: "int(node.status.allocatable['nvidia.com/mlnxnics'])",
238+
},
239+
},
240+
}
241+
242+
gpuOnlyConfig := Config{
243+
Enabled: true,
244+
Classes: []ClassConfig{
245+
{
246+
Name: "gpu",
247+
Enabled: true,
248+
Labels: Labels{
249+
Current: testGPUCountCurrentLabel,
250+
Expected: testGPUCountExpectedLabel,
251+
},
252+
CurrentExpression: "int(node.metadata.labels['nvidia.com/gpu.count'])",
253+
},
254+
},
255+
}
256+
257+
t.Run("triggers on allocatable NIC count change", func(t *testing.T) {
258+
manager := newTestManager(t, nicConfig)
259+
oldNode := testNode("node-a", map[string]string{})
260+
oldNode.Status.Allocatable = corev1.ResourceList{
261+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("0"),
262+
}
263+
264+
newNode := oldNode.DeepCopy()
265+
newNode.Status.Allocatable = corev1.ResourceList{
266+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
267+
}
268+
269+
require.True(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
270+
})
271+
272+
t.Run("triggers on allocatable key added", func(t *testing.T) {
273+
manager := newTestManager(t, nicConfig)
274+
oldNode := testNode("node-a", map[string]string{})
275+
276+
newNode := oldNode.DeepCopy()
277+
newNode.Status.Allocatable = corev1.ResourceList{
278+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
279+
}
280+
281+
require.True(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
282+
})
283+
284+
t.Run("triggers on allocatable key removed", func(t *testing.T) {
285+
manager := newTestManager(t, nicConfig)
286+
oldNode := testNode("node-a", map[string]string{})
287+
oldNode.Status.Allocatable = corev1.ResourceList{
288+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
289+
}
290+
291+
newNode := oldNode.DeepCopy()
292+
newNode.Status.Allocatable = nil
293+
294+
require.True(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
295+
})
296+
297+
t.Run("triggers on capacity change", func(t *testing.T) {
298+
capacityConfig := Config{
299+
Enabled: true,
300+
Classes: []ClassConfig{
301+
{
302+
Name: "nic",
303+
Enabled: true,
304+
Labels: Labels{
305+
Current: testNICCountCurrentLabel,
306+
Expected: testNICCountExpectedLabel,
307+
},
308+
CurrentExpression: "int(node.status.capacity['nvidia.com/mlnxnics'])",
309+
},
310+
},
311+
}
312+
313+
manager := newTestManager(t, capacityConfig)
314+
oldNode := testNode("node-a", map[string]string{})
315+
oldNode.Status.Capacity = corev1.ResourceList{
316+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
317+
}
318+
319+
newNode := oldNode.DeepCopy()
320+
newNode.Status.Capacity = corev1.ResourceList{
321+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("8"),
322+
}
323+
324+
require.True(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
325+
})
326+
327+
t.Run("does not trigger when allocatable is unchanged", func(t *testing.T) {
328+
manager := newTestManager(t, nicConfig)
329+
oldNode := testNode("node-a", map[string]string{})
330+
oldNode.Status.Allocatable = corev1.ResourceList{
331+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
332+
}
333+
334+
newNode := oldNode.DeepCopy()
335+
336+
require.False(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
337+
})
338+
339+
t.Run("does not trigger for nil vs empty allocatable", func(t *testing.T) {
340+
manager := newTestManager(t, nicConfig)
341+
oldNode := testNode("node-a", map[string]string{})
342+
oldNode.Status.Allocatable = nil
343+
344+
newNode := oldNode.DeepCopy()
345+
newNode.Status.Allocatable = corev1.ResourceList{}
346+
347+
require.False(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
348+
})
349+
350+
t.Run("does not trigger when no class references node status", func(t *testing.T) {
351+
manager := newTestManager(t, gpuOnlyConfig)
352+
oldNode := testNode("node-a", map[string]string{
353+
"nvidia.com/gpu.count": "8",
354+
})
355+
oldNode.Status.Allocatable = corev1.ResourceList{
356+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("0"),
357+
}
358+
359+
newNode := oldNode.DeepCopy()
360+
newNode.Status.Allocatable = corev1.ResourceList{
361+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
362+
}
363+
364+
require.False(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
365+
})
366+
367+
t.Run("detects change when one of multiple classes references status", func(t *testing.T) {
368+
mixedConfig := Config{
369+
Enabled: true,
370+
Classes: []ClassConfig{
371+
{
372+
Name: "gpu",
373+
Enabled: true,
374+
Labels: Labels{
375+
Current: testGPUCountCurrentLabel,
376+
Expected: testGPUCountExpectedLabel,
377+
},
378+
CurrentExpression: "int(node.metadata.labels['nvidia.com/gpu.count'])",
379+
},
380+
{
381+
Name: "nic",
382+
Enabled: true,
383+
Labels: Labels{
384+
Current: testNICCountCurrentLabel,
385+
Expected: testNICCountExpectedLabel,
386+
},
387+
CurrentExpression: "int(node.status.allocatable['nvidia.com/mlnxnics'])",
388+
},
389+
},
390+
}
391+
392+
manager := newTestManager(t, mixedConfig)
393+
oldNode := testNode("node-a", map[string]string{
394+
"nvidia.com/gpu.count": "8",
395+
})
396+
oldNode.Status.Allocatable = corev1.ResourceList{
397+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("0"),
398+
}
399+
400+
newNode := oldNode.DeepCopy()
401+
newNode.Status.Allocatable = corev1.ResourceList{
402+
corev1.ResourceName("nvidia.com/mlnxnics"): resource.MustParse("4"),
403+
}
404+
405+
require.True(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
406+
})
407+
408+
t.Run("returns false for disabled manager", func(t *testing.T) {
409+
var manager *Manager
410+
oldNode := testNode("node-a", map[string]string{})
411+
newNode := oldNode.DeepCopy()
412+
413+
require.False(t, manager.NodeResourcesAffectDeviceCounts(oldNode, newNode))
414+
})
415+
}
416+
417+
func TestReferencesNodeResources(t *testing.T) {
418+
t.Run("returns true for allocatable expression", func(t *testing.T) {
419+
class := compiledClass{
420+
ClassConfig: ClassConfig{
421+
CurrentExpression: "int(node.status.allocatable['nvidia.com/mlnxnics'])",
422+
},
423+
}
424+
require.True(t, class.referencesNodeResources())
425+
})
426+
427+
t.Run("returns true for capacity expression", func(t *testing.T) {
428+
class := compiledClass{
429+
ClassConfig: ClassConfig{
430+
CurrentExpression: "int(node.status.capacity['nvidia.com/mlnxnics'])",
431+
},
432+
}
433+
require.True(t, class.referencesNodeResources())
434+
})
435+
436+
t.Run("returns false for label expression", func(t *testing.T) {
437+
class := compiledClass{
438+
ClassConfig: ClassConfig{
439+
CurrentExpression: "int(node.metadata.labels['nvidia.com/gpu.count'])",
440+
},
441+
}
442+
require.False(t, class.referencesNodeResources())
443+
})
444+
445+
t.Run("returns false for resourceSlices expression", func(t *testing.T) {
446+
class := compiledClass{
447+
ClassConfig: ClassConfig{
448+
CurrentExpression: "resourceSlices.size()",
449+
},
450+
}
451+
require.False(t, class.referencesNodeResources())
452+
})
453+
454+
t.Run("returns false for conditions expression", func(t *testing.T) {
455+
class := compiledClass{
456+
ClassConfig: ClassConfig{
457+
CurrentExpression: "node.status.conditions.exists(c, c.type == 'Ready')",
458+
},
459+
}
460+
require.False(t, class.referencesNodeResources())
461+
})
462+
}
463+
226464
func newTestManager(t *testing.T, config Config) *Manager {
227465
t.Helper()
228466

labeler/pkg/labeler/labeler.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -631,10 +631,12 @@ func hasReadyDriverPod(objs []any, excludePod *v1.Pod) bool {
631631
return false
632632
}
633633

634-
// nodeRequiresReconciliation returns true only when a node update changed an
635-
// input label the labeler reads from nodes. DCGM and driver labels are driven
636-
// by pod events, so the node UpdateFunc only needs to react to changes in kata
637-
// detection labels and the gpu-present label (for assumeDriverInstalled mode).
634+
// nodeRequiresReconciliation returns true when a node update changed an input
635+
// that the labeler reads from nodes: labels, allocatable, or capacity. DCGM
636+
// and driver labels are driven by pod events, so the node UpdateFunc only needs
637+
// to react to changes in kata detection labels, the gpu-present label (for
638+
// assumeDriverInstalled mode), and resource changes for status-backed device
639+
// count classes.
638640
func (l *Labeler) nodeRequiresReconciliation(oldObj, newObj any) bool {
639641
oldNode, ok1 := oldObj.(*v1.Node)
640642
newNode, ok2 := newObj.(*v1.Node)
@@ -657,7 +659,8 @@ func (l *Labeler) nodeRequiresReconciliation(oldObj, newObj any) bool {
657659
}
658660
}
659661

660-
return l.deviceCounts.NodeLabelsAffectDeviceCounts(oldNode.Labels, newNode.Labels)
662+
return l.deviceCounts.NodeResourcesAffectDeviceCounts(oldNode, newNode) ||
663+
l.deviceCounts.NodeLabelsAffectDeviceCounts(oldNode.Labels, newNode.Labels)
661664
}
662665

663666
const gpuPresentLabel = "nvidia.com/gpu.present"

0 commit comments

Comments
 (0)