Skip to content

Commit ba82b70

Browse files
Merge pull request #2532 from PiotrProkop/reconcile-on-spec-change
perf(nfd-master): reconcile only on NodeFeature/Rule/Group spec changes
2 parents e9d1a00 + 7832978 commit ba82b70

2 files changed

Lines changed: 105 additions & 0 deletions

File tree

pkg/nfd-master/nfd-api-controller.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"time"
2222

23+
apiequality "k8s.io/apimachinery/pkg/api/equality"
2324
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2526
k8sclient "k8s.io/client-go/kubernetes"
@@ -71,6 +72,22 @@ func init() {
7172
utilruntime.Must(nfdv1alpha1.AddToScheme(nfdscheme.Scheme))
7273
}
7374

75+
// specChanged reports whether the reconcile-relevant part of an object — its
76+
// .Spec — differs between the old and new versions delivered to an informer
77+
// UpdateFunc. Metadata-only updates (annotations, labels, ownerReferences,
78+
// resourceVersion), status-only updates, and resync no-ops (oldObj == newObj)
79+
// therefore return false, letting the handler skip an unnecessary reconcile.
80+
// It fails open (returns true) when the objects cannot be type-asserted, so a
81+
// needed reconcile is never wrongly skipped.
82+
func specChanged[T any](oldObj, newObj interface{}, specOf func(*T) any) bool {
83+
oldT, ok1 := oldObj.(*T)
84+
newT, ok2 := newObj.(*T)
85+
if !ok1 || !ok2 {
86+
return true
87+
}
88+
return !apiequality.Semantic.DeepEqual(specOf(oldT), specOf(newT))
89+
}
90+
7491
func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiControllerOptions) (*nfdController, error) {
7592
c := &nfdController{
7693
stopChan: make(chan struct{}),
@@ -133,6 +150,9 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
133150
}
134151
},
135152
UpdateFunc: func(oldObj, newObj interface{}) {
153+
if !specChanged(oldObj, newObj, func(o *nfdv1alpha1.NodeFeature) any { return o.Spec }) {
154+
return
155+
}
136156
nfr := newObj.(*nfdv1alpha1.NodeFeature)
137157
klog.V(2).InfoS("NodeFeature updated", "nodefeature", klog.KObj(nfr))
138158
c.updateOneNode("NodeFeature", nfr)
@@ -188,6 +208,9 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
188208
c.updateAllNodes()
189209
},
190210
UpdateFunc: func(oldObject, newObject interface{}) {
211+
if !specChanged(oldObject, newObject, func(o *nfdv1alpha1.NodeFeatureRule) any { return o.Spec }) {
212+
return
213+
}
191214
klog.V(2).InfoS("NodeFeatureRule updated", "nodefeaturerule", klog.KObj(newObject.(metav1.Object)))
192215
c.updateAllNodes()
193216
},
@@ -210,6 +233,9 @@ func newNfdController(config *restclient.Config, nfdApiControllerOptions nfdApiC
210233
c.updateNodeFeatureGroup(nfg.Name)
211234
},
212235
UpdateFunc: func(oldObj, newObj interface{}) {
236+
if !specChanged(oldObj, newObj, func(o *nfdv1alpha1.NodeFeatureGroup) any { return o.Spec }) {
237+
return
238+
}
213239
nfg := newObj.(*nfdv1alpha1.NodeFeatureGroup)
214240
klog.V(2).InfoS("NodeFeatureGroup updated", "nodeFeatureGroup", klog.KObj(nfg))
215241
c.updateNodeFeatureGroup(nfg.Name)

pkg/nfd-master/nfd-api-controller_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,82 @@ func TestIsNamespaceSelected(t *testing.T) {
111111
assert.Equal(t, res, tc.expectedResult)
112112
}
113113
}
114+
115+
func TestSpecChanged(t *testing.T) {
116+
nfSpec := func(o *nfdv1alpha1.NodeFeature) any { return o.Spec }
117+
nfrSpec := func(o *nfdv1alpha1.NodeFeatureRule) any { return o.Spec }
118+
nfgSpec := func(o *nfdv1alpha1.NodeFeatureGroup) any { return o.Spec }
119+
120+
// NodeFeature: Spec{Features, Labels}, no status subresource.
121+
nf := &nfdv1alpha1.NodeFeature{
122+
ObjectMeta: metav1.ObjectMeta{Name: "n", ResourceVersion: "1"},
123+
Spec: nfdv1alpha1.NodeFeatureSpec{
124+
Features: nfdv1alpha1.Features{
125+
Attributes: map[string]nfdv1alpha1.AttributeFeatureSet{
126+
"cpu.model": {Elements: map[string]string{"family": "6"}},
127+
},
128+
},
129+
Labels: map[string]string{"feature.node.kubernetes.io/foo": "bar"},
130+
},
131+
}
132+
nfMetaOnly := nf.DeepCopy()
133+
nfMetaOnly.ResourceVersion = "2"
134+
nfMetaOnly.Annotations = map[string]string{"nfd.node.kubernetes.io/worker.version": "v0.18"}
135+
nfMetaOnly.Labels = map[string]string{nfdv1alpha1.NodeFeatureObjNodeNameLabel: "node-1"}
136+
137+
nfFeatureChanged := nf.DeepCopy()
138+
nfFeatureChanged.Spec.Features.Attributes["cpu.model"] = nfdv1alpha1.AttributeFeatureSet{Elements: map[string]string{"family": "7"}}
139+
140+
nfLabelsChanged := nf.DeepCopy()
141+
nfLabelsChanged.Spec.Labels["feature.node.kubernetes.io/foo"] = "baz"
142+
143+
// NodeFeatureRule: Spec{Rules}, no status. UpdateFunc triggers a full reconcile.
144+
nfr := &nfdv1alpha1.NodeFeatureRule{
145+
ObjectMeta: metav1.ObjectMeta{Name: "r", ResourceVersion: "1"},
146+
Spec: nfdv1alpha1.NodeFeatureRuleSpec{Rules: []nfdv1alpha1.Rule{{Name: "rule-1"}}},
147+
}
148+
nfrMetaOnly := nfr.DeepCopy()
149+
nfrMetaOnly.ResourceVersion = "2"
150+
nfrMetaOnly.Annotations = map[string]string{"x": "y"}
151+
152+
nfrSpecChanged := nfr.DeepCopy()
153+
nfrSpecChanged.Spec.Rules[0].Name = "rule-2"
154+
155+
// NodeFeatureGroup: Spec{Rules} AND Status; master writes its status, so a
156+
// status-only update must NOT reconcile (else it feeds back on itself).
157+
nfg := &nfdv1alpha1.NodeFeatureGroup{
158+
ObjectMeta: metav1.ObjectMeta{Name: "g", ResourceVersion: "1"},
159+
Spec: nfdv1alpha1.NodeFeatureGroupSpec{Rules: []nfdv1alpha1.GroupRule{{Name: "grule-1"}}},
160+
}
161+
nfgStatusOnly := nfg.DeepCopy()
162+
nfgStatusOnly.ResourceVersion = "2"
163+
nfgStatusOnly.Status = nfdv1alpha1.NodeFeatureGroupStatus{Nodes: []nfdv1alpha1.FeatureGroupNode{{Name: "node-1"}}}
164+
165+
nfgSpecChanged := nfg.DeepCopy()
166+
nfgSpecChanged.Spec.Rules[0].Name = "grule-2"
167+
168+
testcases := []struct {
169+
name string
170+
got bool
171+
want bool
172+
}{
173+
{"NodeFeature resync no-op", specChanged(nf, nf.DeepCopy(), nfSpec), false},
174+
{"NodeFeature metadata-only change", specChanged(nf, nfMetaOnly, nfSpec), false},
175+
{"NodeFeature feature change", specChanged(nf, nfFeatureChanged, nfSpec), true},
176+
{"NodeFeature spec.Labels change", specChanged(nf, nfLabelsChanged, nfSpec), true},
177+
178+
{"NodeFeatureRule metadata-only change", specChanged(nfr, nfrMetaOnly, nfrSpec), false},
179+
{"NodeFeatureRule spec change", specChanged(nfr, nfrSpecChanged, nfrSpec), true},
180+
181+
{"NodeFeatureGroup status-only change", specChanged(nfg, nfgStatusOnly, nfgSpec), false},
182+
{"NodeFeatureGroup spec change", specChanged(nfg, nfgSpecChanged, nfgSpec), true},
183+
184+
// Fail open: never skip a reconcile when the objects can't be compared.
185+
{"nil old object", specChanged(nil, nf, nfSpec), true},
186+
{"mismatched types", specChanged(&nfdv1alpha1.NodeFeatureRule{}, nf, nfSpec), true},
187+
}
188+
189+
for _, tc := range testcases {
190+
assert.Equalf(t, tc.want, tc.got, tc.name)
191+
}
192+
}

0 commit comments

Comments
 (0)