@@ -79,8 +79,8 @@ func TestNodePatcher_CachedNode_UsesPatchAndSkipsNoOp(t *testing.T) {
7979 assert .Empty (t , clientset .Actions ())
8080}
8181
82- func TestNodePatcher_PreviousWriteNotInCache_ReadsLiveNode (t * testing.T ) {
83- current := node (map [string ]string {"a " : "1" }, nil )
82+ func TestNodePatcher_NoOpBetweenWrites_KeepsReadingLiveNode (t * testing.T ) {
83+ current := node (nil , map [string ]string {"events " : "base" } )
8484 clientset := fake .NewSimpleClientset (current .DeepCopy ())
8585 var patcher NodePatcher
8686
@@ -90,7 +90,7 @@ func TestNodePatcher_PreviousWriteNotInCache_ReadsLiveNode(t *testing.T) {
9090 current .Name ,
9191 current ,
9292 func (node * v1.Node ) error {
93- node .Labels [ "b " ] = "2 "
93+ node .Annotations [ "events " ] + = "|first "
9494 return nil
9595 },
9696 )
@@ -111,12 +111,29 @@ func TestNodePatcher_PreviousWriteNotInCache_ReadsLiveNode(t *testing.T) {
111111 require .Len (t , clientset .Actions (), 1 )
112112 assert .Equal (t , "get" , clientset .Actions ()[0 ].GetVerb ())
113113
114+ clientset .ClearActions ()
115+ changed , err = patcher .Patch (
116+ context .Background (),
117+ clientset .CoreV1 ().Nodes (),
118+ current .Name ,
119+ stale ,
120+ func (node * v1.Node ) error {
121+ node .Annotations ["events" ] += "|second"
122+ return nil
123+ },
124+ )
125+ require .NoError (t , err )
126+ assert .True (t , changed )
127+ require .Len (t , clientset .Actions (), 2 )
128+ assert .Equal (t , "get" , clientset .Actions ()[0 ].GetVerb ())
129+ assert .Equal (t , "patch" , clientset .Actions ()[1 ].GetVerb ())
130+
114131 updated , err := clientset .CoreV1 ().Nodes ().Get (t .Context (), current .Name , metav1.GetOptions {})
115132 require .NoError (t , err )
116- assert .Equal (t , "2 " , updated .Labels [ "b " ])
133+ assert .Equal (t , "base|first|second " , updated .Annotations [ "events " ])
117134}
118135
119- func TestNodePatcher_LiveReadFailure_PreservesPendingVersion (t * testing.T ) {
136+ func TestNodePatcher_LiveReadRetriesTransientFailure (t * testing.T ) {
120137 current := node (map [string ]string {"a" : "1" }, nil )
121138 clientset := fake .NewSimpleClientset (current .DeepCopy ())
122139 var patcher NodePatcher
@@ -128,22 +145,12 @@ func TestNodePatcher_LiveReadFailure_PreservesPendingVersion(t *testing.T) {
128145 clientset .PrependReactor ("get" , "nodes" , func (k8stesting.Action ) (bool , runtime.Object , error ) {
129146 getAttempts ++
130147 if getAttempts == 1 {
131- return true , nil , assert . AnError
148+ return true , nil , apierrors . NewTooManyRequests ( "try again" , 0 )
132149 }
133150
134151 return false , nil , nil
135152 })
136153
137- _ , err := patcher .Patch (
138- context .Background (),
139- clientset .CoreV1 ().Nodes (),
140- current .Name ,
141- stale ,
142- func (* v1.Node ) error { return nil },
143- )
144- require .ErrorIs (t , err , assert .AnError )
145- assert .ErrorContains (t , err , `refresh node "node-1" while pending write is not in cache` )
146-
147154 changed , err := patcher .Patch (
148155 context .Background (),
149156 clientset .CoreV1 ().Nodes (),
@@ -198,12 +205,34 @@ func TestNodePatcher_Conflict_RefreshesLiveNodeBeforeRetry(t *testing.T) {
198205 assert .Equal (t , "true" , updated .Labels ["desired" ])
199206}
200207
201- func TestNodeMergePatch_MetadataChanges_ReturnsExpectedPatch (t * testing.T ) {
208+ func TestNodeMergePatch_ReturnsExpectedPatch (t * testing.T ) {
209+ resourceVersionOriginal := node (nil , nil )
210+ resourceVersionOriginal .ResourceVersion = "42"
211+ resourceVersionModified := resourceVersionOriginal .DeepCopy ()
212+ resourceVersionModified .Spec .Unschedulable = true
213+
214+ projected := & v1.Node {
215+ ObjectMeta : metav1.ObjectMeta {
216+ Name : "node-1" ,
217+ ResourceVersion : "1" ,
218+ Labels : map [string ]string {"gpu" : "true" },
219+ Annotations : map [string ]string {"kept" : "yes" },
220+ },
221+ }
222+ projectedModified := projected .DeepCopy ()
223+ projectedModified .Labels ["driver.installed" ] = "true"
224+
225+ specOriginal := node (nil , nil )
226+ specModified := specOriginal .DeepCopy ()
227+ specModified .Spec .Unschedulable = true
228+ specModified .Spec .Taints = []v1.Taint {{Key : "held" , Effect : v1 .TaintEffectNoSchedule }}
229+
202230 tests := []struct {
203231 name string
204232 original * v1.Node
205233 modified * v1.Node
206234 expected string
235+ excluded []string
207236 }{
208237 {
209238 name : "no change produces no patch" ,
@@ -253,6 +282,31 @@ func TestNodeMergePatch_MetadataChanges_ReturnsExpectedPatch(t *testing.T) {
253282 modified : node (map [string ]string {"a" : "1" }, nil ),
254283 expected : `{"metadata":{"labels":{"a":"1"}}}` ,
255284 },
285+ {
286+ name : "spec change includes original resource version" ,
287+ original : resourceVersionOriginal ,
288+ modified : resourceVersionModified ,
289+ expected : `{"metadata":{"resourceVersion":"42"},"spec":{"unschedulable":true}}` ,
290+ },
291+ {
292+ name : "projected fields remain absent" ,
293+ original : projected ,
294+ modified : projectedModified ,
295+ expected : `{"metadata":{"labels":{"driver.installed":"true"}}}` ,
296+ excluded : []string {"annotations" , "spec" },
297+ },
298+ {
299+ name : "sets spec fields" ,
300+ original : specOriginal ,
301+ modified : specModified ,
302+ expected : `{"metadata":{"resourceVersion":"1"},"spec":{"taints":[{"key":"held","effect":"NoSchedule"}],"unschedulable":true}}` ,
303+ },
304+ {
305+ name : "clears spec fields" ,
306+ original : specModified ,
307+ modified : specOriginal ,
308+ expected : `{"metadata":{"resourceVersion":"1"},"spec":{"taints":null,"unschedulable":null}}` ,
309+ },
256310 }
257311
258312 for _ , tt := range tests {
@@ -266,49 +320,9 @@ func TestNodeMergePatch_MetadataChanges_ReturnsExpectedPatch(t *testing.T) {
266320 }
267321
268322 assert .JSONEq (t , tt .expected , string (patch ))
323+ for _ , field := range tt .excluded {
324+ assert .NotContains (t , string (patch ), field )
325+ }
269326 })
270327 }
271328}
272-
273- // TestNodeMergePatchLeavesProjectedFieldsAlone pins the reason the patch is built key
274- // by key. Informer caches often hold a projected Node — the labeler's transform keeps
275- // only one annotation and clears Spec entirely — and a patch derived from that
276- // projection must not describe the fields the projection dropped, or it would erase
277- // them on the real object.
278- func TestNodeMergePatch_ProjectedFields_LeavesThemAlone (t * testing.T ) {
279- projected := & v1.Node {
280- ObjectMeta : metav1.ObjectMeta {
281- Name : "node-1" ,
282- ResourceVersion : "1" ,
283- Labels : map [string ]string {"gpu" : "true" },
284- Annotations : map [string ]string {"kept" : "yes" },
285- },
286- }
287-
288- modified := projected .DeepCopy ()
289- modified .Labels ["driver.installed" ] = "true"
290-
291- patch , err := NodeMergePatch (projected , modified )
292- require .NoError (t , err )
293-
294- assert .JSONEq (t ,
295- `{"metadata":{"labels":{"driver.installed":"true"}}}` ,
296- string (patch ),
297- )
298- assert .NotContains (t , string (patch ), "annotations" ,
299- "an untouched annotation must not appear in the patch" )
300- assert .NotContains (t , string (patch ), "spec" ,
301- "a cleared Spec must never reach the patch, or real taints would be dropped" )
302- }
303-
304- func TestNodeMergePatch_SpecChanges_ReturnsNoPatch (t * testing.T ) {
305- original := node (nil , nil )
306- modified := original .DeepCopy ()
307- modified .Spec .Unschedulable = true
308- modified .Spec .Taints = []v1.Taint {{Key : "held" , Effect : v1 .TaintEffectNoSchedule }}
309-
310- patch , err := NodeMergePatch (original , modified )
311- require .NoError (t , err )
312-
313- assert .Nil (t , patch , "spec is out of scope until a caller needs it" )
314- }
0 commit comments