1 package patch
2
3 import (
4 "context"
5 "encoding/json"
6 "time"
7
8 "github.com/pkg/errors"
9 apierrors "k8s.io/apimachinery/pkg/api/errors"
10 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
11 "k8s.io/apimachinery/pkg/runtime"
12 "k8s.io/apimachinery/pkg/runtime/schema"
13 kerrors "k8s.io/apimachinery/pkg/util/errors"
14 "k8s.io/apimachinery/pkg/util/wait"
15 "sigs.k8s.io/controller-runtime/pkg/client"
16 "sigs.k8s.io/controller-runtime/pkg/client/apiutil"
17
18 "edge-infra.dev/pkg/k8s/runtime/conditions"
19 edgeUnstructured "edge-infra.dev/pkg/k8s/unstructured"
20 )
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108 type Helper struct {
109 client client.Client
110 gvk schema.GroupVersionKind
111 beforeObject client.Object
112 before *unstructured.Unstructured
113 after *unstructured.Unstructured
114 changes map[string]bool
115
116 isConditionsSetter bool
117 }
118
119
120 func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) {
121
122
123 gvk, err := apiutil.GVKForObject(obj, crClient.Scheme())
124 if err != nil {
125 return nil, err
126 }
127
128
129 unstructuredObj, err := edgeUnstructured.ToUnstructured(obj)
130 if err != nil {
131 return nil, err
132 }
133
134
135 _, canInterfaceConditions := obj.(conditions.Setter)
136
137 return &Helper{
138 client: crClient,
139 gvk: gvk,
140 before: unstructuredObj,
141 beforeObject: obj.DeepCopyObject().(client.Object),
142 isConditionsSetter: canInterfaceConditions,
143 }, nil
144 }
145
146
147 func (h *Helper) Patch(ctx context.Context, obj client.Object, opts ...Option) error {
148
149 gvk, err := apiutil.GVKForObject(obj, h.client.Scheme())
150 if err != nil {
151 return err
152 }
153 if gvk != h.gvk {
154 return errors.Errorf("unmatched GroupVersionKind, expected %q got %q", h.gvk, gvk)
155 }
156
157
158 options := &HelperOptions{}
159 for _, opt := range opts {
160 opt.ApplyToHelper(options)
161 }
162
163
164 h.after, err = edgeUnstructured.ToUnstructured(obj)
165 if err != nil {
166 return err
167 }
168
169
170 if unstructuredHasStatus(h.after) && options.IncludeStatusObservedGeneration {
171
172 if err := unstructured.SetNestedField(h.after.Object, h.after.GetGeneration(), "status", "observedGeneration"); err != nil {
173 return err
174 }
175
176
177 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(h.after.Object, obj); err != nil {
178 return err
179 }
180 }
181
182
183 h.changes, err = h.calculateChanges(obj)
184 if err != nil {
185 return err
186 }
187
188
189 var clientOpts []client.PatchOption
190 if options.FieldOwner != "" {
191 clientOpts = append(clientOpts, client.FieldOwner(options.FieldOwner))
192 }
193
194
195 return kerrors.NewAggregate([]error{
196
197
198
199
200
201 h.patchStatusConditions(ctx, obj, options.ForceOverwriteConditions, options.OwnedConditions, clientOpts...),
202
203
204 h.patch(ctx, obj, clientOpts...),
205 h.patchStatus(ctx, obj, clientOpts...),
206 })
207 }
208
209
210 func (h *Helper) patch(ctx context.Context, obj client.Object, opts ...client.PatchOption) error {
211 if !h.shouldPatch("metadata") && !h.shouldPatch("spec") {
212 return nil
213 }
214 beforeObject, afterObject, err := h.calculatePatch(obj, specPatch)
215 if err != nil {
216 return err
217 }
218 return h.client.Patch(ctx, afterObject, client.MergeFromWithOptions(beforeObject), opts...)
219 }
220
221
222 func (h *Helper) patchStatus(ctx context.Context, obj client.Object, opts ...client.PatchOption) error {
223 if !h.shouldPatch("status") {
224 return nil
225 }
226 beforeObject, afterObject, err := h.calculatePatch(obj, statusPatch)
227 if err != nil {
228 return err
229 }
230 patchOpts := &client.PatchOptions{}
231 patchOpts = patchOpts.ApplyOptions(opts)
232 return h.client.Status().Patch(ctx, afterObject, client.MergeFrom(beforeObject), &client.SubResourcePatchOptions{PatchOptions: *patchOpts})
233 }
234
235
236
237
238
239
240
241
242
243
244 func (h *Helper) patchStatusConditions(ctx context.Context, obj client.Object, forceOverwrite bool, ownedConditions []string, opts ...client.PatchOption) error {
245
246 if !h.isConditionsSetter {
247 return nil
248 }
249
250
251
252
253
254 before, ok := h.beforeObject.(conditions.Getter)
255 if !ok {
256 return errors.Errorf("object %s doesn't satisfy conditions.Getter, cannot patch", before.GetObjectKind())
257 }
258 after, ok := obj.(conditions.Getter)
259 if !ok {
260 return errors.Errorf("object %s doesn't satisfy conditions.Getter, cannot patch", after.GetObjectKind())
261 }
262
263
264 diff := conditions.NewPatch(
265 before,
266 after,
267 )
268 if diff.IsZero() {
269 return nil
270 }
271
272
273 key := client.ObjectKeyFromObject(after)
274
275
276
277
278
279 backoff := wait.Backoff{
280 Steps: 5,
281 Duration: 100 * time.Millisecond,
282 Jitter: 1.0,
283 }
284
285
286 return wait.ExponentialBackoff(backoff, func() (bool, error) {
287 latest, ok := before.DeepCopyObject().(conditions.Setter)
288 if !ok {
289 return false, errors.Errorf("object %s doesn't satisfy conditions.Setter, cannot patch", latest.GetObjectKind())
290 }
291
292
293 if err := h.client.Get(ctx, key, latest); err != nil {
294 return false, err
295 }
296
297 conditionsPatch := client.MergeFromWithOptions(latest.DeepCopyObject().(conditions.Setter), client.MergeFromWithOptimisticLock{})
298
299
300 if err := diff.Apply(latest, conditions.WithForceOverwrite(forceOverwrite), conditions.WithOwnedConditions(ownedConditions...)); err != nil {
301 return false, err
302 }
303
304
305 patchOpts := &client.PatchOptions{}
306 patchOpts = patchOpts.ApplyOptions(opts)
307 err := h.client.Status().Patch(ctx, latest, conditionsPatch, &client.SubResourcePatchOptions{PatchOptions: *patchOpts})
308 switch {
309 case apierrors.IsConflict(err):
310
311 return false, nil
312 case err != nil:
313 return false, err
314 default:
315 return true, nil
316 }
317 })
318 }
319
320
321
322 func (h *Helper) calculatePatch(afterObj client.Object, focus patchType) (client.Object, client.Object, error) {
323
324 before := unsafeUnstructuredCopy(h.before, focus, h.isConditionsSetter)
325 after := unsafeUnstructuredCopy(h.after, focus, h.isConditionsSetter)
326
327
328
329 beforeObj := h.beforeObject.DeepCopyObject().(client.Object)
330 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(before.Object, beforeObj); err != nil {
331 return nil, nil, err
332 }
333 afterObj = afterObj.DeepCopyObject().(client.Object)
334 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(after.Object, afterObj); err != nil {
335 return nil, nil, err
336 }
337 return beforeObj, afterObj, nil
338 }
339
340 func (h *Helper) shouldPatch(in string) bool {
341 return h.changes[in]
342 }
343
344
345
346 func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error) {
347
348 patch := client.MergeFrom(h.beforeObject)
349 diff, err := patch.Data(after)
350 if err != nil {
351 return nil, errors.Wrapf(err, "failed to calculate patch data")
352 }
353
354
355 patchDiff := map[string]interface{}{}
356 if err := json.Unmarshal(diff, &patchDiff); err != nil {
357 return nil, errors.Wrapf(err, "failed to unmarshal patch data into a map")
358 }
359
360
361 res := make(map[string]bool, len(patchDiff))
362 for key := range patchDiff {
363 res[key] = true
364 }
365 return res, nil
366 }
367
View as plain text