@@ -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