@@ -19,6 +19,7 @@ import (
1919 "context"
2020 "encoding/json"
2121 "fmt"
22+ "reflect"
2223 "sync"
2324 "time"
2425
@@ -50,7 +51,15 @@ func (p *NodePatcher) Patch(
5051 cached * v1.Node ,
5152 mutate func (* v1.Node ) error ,
5253) (bool , error ) {
53- current , err := p .currentNode (ctx , nodes , nodeName , cached )
54+ var current * v1.Node
55+
56+ err := retry .OnError (nodePatchBackoff (), isRetryableNodePatchError , func () error {
57+ var err error
58+
59+ current , err = p .currentNode (ctx , nodes , nodeName , cached )
60+
61+ return err
62+ })
5463 if err != nil {
5564 return false , err
5665 }
@@ -85,11 +94,13 @@ func (p *NodePatcher) Patch(
8594 if errors .IsConflict (err ) {
8695 patchErr := err
8796
88- current , err = nodes .Get (ctx , nodeName , metav1.GetOptions {})
97+ refreshed , err : = nodes .Get (ctx , nodeName , metav1.GetOptions {})
8998 if err != nil {
9099 return fmt .Errorf ("refresh node %q after patch conflict: %w" , nodeName , err )
91100 }
92101
102+ current = refreshed
103+
93104 return fmt .Errorf ("patch node %q: %w" , nodeName , patchErr )
94105 }
95106
@@ -134,8 +145,6 @@ func (p *NodePatcher) currentNode(
134145 return nil , fmt .Errorf ("refresh node %q while pending write is not in cache: %w" , nodeName , err )
135146 }
136147
137- p .pendingVersions .CompareAndDelete (nodeName , writtenVersionValue )
138-
139148 return current , nil
140149}
141150
@@ -156,40 +165,36 @@ func isRetryableNodePatchError(err error) bool {
156165 errors .IsServiceUnavailable (err )
157166}
158167
159- // NodeMergePatch builds an RFC 7386 JSON merge patch carrying the label and
160- // annotation differences between original and modified. It returns a nil patch when
161- // the two already agree, so callers can skip the write instead of spending an API
162- // call on a no-op.
168+ // NodeMergePatch builds an RFC 7386 JSON merge patch carrying differences in labels,
169+ // annotations, taints, and unschedulable state. It returns a nil patch when the two
170+ // nodes already agree, so callers can skip a no-op write.
163171//
164- // CreateTwoWayMergePatch compares metadata-only projections of the two Nodes.
165- // Excluding every other field from both inputs ensures an informer projection cannot
166- // patch its gaps back over the live object.
172+ // CreateTwoWayMergePatch compares projections containing only the fields this helper
173+ // supports. Excluding every other field from both inputs ensures an informer
174+ // projection cannot patch its gaps back over the live object.
167175//
168- // Spec fields such as taints and unschedulable are deliberately out of scope: a merge
169- // patch replaces a list wholesale, so patching taints from a projected Node whose Spec
170- // had been cleared would silently drop every taint on the real object.
176+ // Taints are emitted only when the caller changed them. A projected Node whose Spec
177+ // is empty on both sides therefore cannot erase taints from the real object.
171178func NodeMergePatch (original , modified * v1.Node ) ([]byte , error ) {
172- originalMetadata := & v1.Node {
173- ObjectMeta : metav1.ObjectMeta {
174- Labels : original .Labels ,
175- Annotations : original .Annotations ,
176- },
177- }
178- modifiedMetadata := & v1.Node {
179- ObjectMeta : metav1.ObjectMeta {
180- Labels : modified .Labels ,
181- Annotations : modified .Annotations ,
182- },
179+ originalProjection := projectNodePatchableFields (original )
180+ modifiedProjection := projectNodePatchableFields (modified )
181+
182+ specChanged := ! reflect .DeepEqual (original .Spec .Taints , modified .Spec .Taints ) ||
183+ original .Spec .Unschedulable != modified .Spec .Unschedulable
184+ if specChanged {
185+ // Lists in spec, such as taints, are replaced wholesale. ResourceVersion
186+ // prevents a stale list from overwriting a concurrent update.
187+ modifiedProjection .ResourceVersion = original .ResourceVersion
183188 }
184189
185- originalJSON , err := json .Marshal (originalMetadata )
190+ originalJSON , err := json .Marshal (originalProjection )
186191 if err != nil {
187- return nil , fmt .Errorf ("marshal original metadata for node %q: %w" , original .Name , err )
192+ return nil , fmt .Errorf ("marshal original patch projection for node %q: %w" , original .Name , err )
188193 }
189194
190- modifiedJSON , err := json .Marshal (modifiedMetadata )
195+ modifiedJSON , err := json .Marshal (modifiedProjection )
191196 if err != nil {
192- return nil , fmt .Errorf ("marshal modified metadata for node %q: %w" , original .Name , err )
197+ return nil , fmt .Errorf ("marshal modified patch projection for node %q: %w" , original .Name , err )
193198 }
194199
195200 patch , err := strategicpatch .CreateTwoWayMergePatch (originalJSON , modifiedJSON , v1.Node {})
@@ -203,3 +208,18 @@ func NodeMergePatch(original, modified *v1.Node) ([]byte, error) {
203208
204209 return patch , nil
205210}
211+
212+ // projectNodePatchableFields restricts patch generation to the Node fields this
213+ // helper intentionally supports, preventing callbacks from patching unrelated fields.
214+ func projectNodePatchableFields (node * v1.Node ) * v1.Node {
215+ return & v1.Node {
216+ ObjectMeta : metav1.ObjectMeta {
217+ Labels : node .Labels ,
218+ Annotations : node .Annotations ,
219+ },
220+ Spec : v1.NodeSpec {
221+ Taints : node .Spec .Taints ,
222+ Unschedulable : node .Spec .Unschedulable ,
223+ },
224+ }
225+ }
0 commit comments