Skip to content

Commit 900d420

Browse files
authored
Merge branch 'main' into lfriedman/add-priorityclassname-support
2 parents 8d5ecd4 + 497b499 commit 900d420

4 files changed

Lines changed: 753 additions & 41 deletions

File tree

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package kubeclient
16+
17+
import (
18+
"bytes"
19+
"context"
20+
"encoding/json"
21+
"fmt"
22+
"sync"
23+
"time"
24+
25+
v1 "k8s.io/api/core/v1"
26+
"k8s.io/apimachinery/pkg/api/errors"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/types"
29+
"k8s.io/apimachinery/pkg/util/strategicpatch"
30+
"k8s.io/apimachinery/pkg/util/wait"
31+
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
32+
"k8s.io/client-go/util/retry"
33+
)
34+
35+
// NodePatcher applies Node mutations with cache-aware retries.
36+
//
37+
// The zero value is ready to use.
38+
type NodePatcher struct {
39+
pendingVersions sync.Map // map[string]string
40+
}
41+
42+
// Patch applies mutate to a copy of cached and writes only the resulting diff.
43+
// cached may be nil when an informer is unavailable; in that case Patch reads the
44+
// current Node from the API server. A write not yet observed at cached's
45+
// ResourceVersion is also refreshed before the next mutation.
46+
func (p *NodePatcher) Patch(
47+
ctx context.Context,
48+
nodes typedcorev1.NodeInterface,
49+
nodeName string,
50+
cached *v1.Node,
51+
mutate func(*v1.Node) error,
52+
) (bool, error) {
53+
current, err := p.currentNode(ctx, nodes, nodeName, cached)
54+
if err != nil {
55+
return false, err
56+
}
57+
58+
changed := false
59+
60+
err = retry.OnError(nodePatchBackoff(), isRetryableNodePatchError, func() error {
61+
desired := current.DeepCopy()
62+
63+
if err := mutate(desired); err != nil {
64+
return fmt.Errorf("mutate node %q: %w", nodeName, err)
65+
}
66+
67+
patch, err := NodeMergePatch(current, desired)
68+
if err != nil {
69+
return fmt.Errorf("build merge patch for node %q: %w", nodeName, err)
70+
}
71+
72+
if patch == nil {
73+
return nil
74+
}
75+
76+
updated, err := nodes.Patch(ctx, nodeName, types.MergePatchType, patch, metav1.PatchOptions{})
77+
if err == nil {
78+
p.pendingVersions.Store(nodeName, updated.ResourceVersion)
79+
80+
changed = true
81+
82+
return nil
83+
}
84+
85+
if errors.IsConflict(err) {
86+
patchErr := err
87+
88+
current, err = nodes.Get(ctx, nodeName, metav1.GetOptions{})
89+
if err != nil {
90+
return fmt.Errorf("refresh node %q after patch conflict: %w", nodeName, err)
91+
}
92+
93+
return fmt.Errorf("patch node %q: %w", nodeName, patchErr)
94+
}
95+
96+
return fmt.Errorf("patch node %q: %w", nodeName, err)
97+
})
98+
if err != nil {
99+
return false, err
100+
}
101+
102+
return changed, nil
103+
}
104+
105+
func (p *NodePatcher) currentNode(
106+
ctx context.Context,
107+
nodes typedcorev1.NodeInterface,
108+
nodeName string,
109+
cached *v1.Node,
110+
) (*v1.Node, error) {
111+
writtenVersionValue, hasPendingWrite := p.pendingVersions.Load(nodeName)
112+
if !hasPendingWrite {
113+
if cached != nil {
114+
return cached, nil
115+
}
116+
117+
current, err := nodes.Get(ctx, nodeName, metav1.GetOptions{})
118+
if err != nil {
119+
return nil, fmt.Errorf("get node %q from API server: %w", nodeName, err)
120+
}
121+
122+
return current, nil
123+
}
124+
125+
writtenVersion, _ := writtenVersionValue.(string)
126+
if cached != nil && writtenVersion != "" && cached.ResourceVersion == writtenVersion {
127+
p.pendingVersions.CompareAndDelete(nodeName, writtenVersionValue)
128+
129+
return cached, nil
130+
}
131+
132+
current, err := nodes.Get(ctx, nodeName, metav1.GetOptions{})
133+
if err != nil {
134+
return nil, fmt.Errorf("refresh node %q while pending write is not in cache: %w", nodeName, err)
135+
}
136+
137+
p.pendingVersions.CompareAndDelete(nodeName, writtenVersionValue)
138+
139+
return current, nil
140+
}
141+
142+
func nodePatchBackoff() wait.Backoff {
143+
return wait.Backoff{
144+
Steps: 10,
145+
Duration: 20 * time.Millisecond,
146+
Factor: 2,
147+
Jitter: 0.1,
148+
}
149+
}
150+
151+
func isRetryableNodePatchError(err error) bool {
152+
return errors.IsConflict(err) ||
153+
errors.IsServerTimeout(err) ||
154+
errors.IsTooManyRequests(err) ||
155+
errors.IsTimeout(err) ||
156+
errors.IsServiceUnavailable(err)
157+
}
158+
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.
163+
//
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.
167+
//
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.
171+
func 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+
},
183+
}
184+
185+
originalJSON, err := json.Marshal(originalMetadata)
186+
if err != nil {
187+
return nil, fmt.Errorf("marshal original metadata for node %q: %w", original.Name, err)
188+
}
189+
190+
modifiedJSON, err := json.Marshal(modifiedMetadata)
191+
if err != nil {
192+
return nil, fmt.Errorf("marshal modified metadata for node %q: %w", original.Name, err)
193+
}
194+
195+
patch, err := strategicpatch.CreateTwoWayMergePatch(originalJSON, modifiedJSON, v1.Node{})
196+
if err != nil {
197+
return nil, fmt.Errorf("build strategic merge patch for node %q: %w", original.Name, err)
198+
}
199+
200+
if bytes.Equal(patch, []byte("{}")) {
201+
return nil, nil
202+
}
203+
204+
return patch, nil
205+
}

0 commit comments

Comments
 (0)