sigs.k8s.io/cluster-api@v1.7.1/util/patch/patch.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package patch 18 19 import ( 20 "context" 21 "encoding/json" 22 "time" 23 24 "github.com/pkg/errors" 25 apierrors "k8s.io/apimachinery/pkg/api/errors" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 kerrors "k8s.io/apimachinery/pkg/util/errors" 30 "k8s.io/apimachinery/pkg/util/wait" 31 "k8s.io/klog/v2" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 "sigs.k8s.io/controller-runtime/pkg/client/apiutil" 34 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 "sigs.k8s.io/cluster-api/util" 37 "sigs.k8s.io/cluster-api/util/conditions" 38 ) 39 40 // Helper is a utility for ensuring the proper patching of objects. 41 type Helper struct { 42 client client.Client 43 gvk schema.GroupVersionKind 44 beforeObject client.Object 45 before *unstructured.Unstructured 46 after *unstructured.Unstructured 47 changes map[string]bool 48 49 isConditionsSetter bool 50 } 51 52 // NewHelper returns an initialized Helper. Use NewHelper before changing 53 // obj. After changing obj use Helper.Patch to persist your changes. 54 func NewHelper(obj client.Object, crClient client.Client) (*Helper, error) { 55 // Return early if the object is nil. 56 if util.IsNil(obj) { 57 return nil, errors.New("failed to create patch helper: object is nil") 58 } 59 60 // Get the GroupVersionKind of the object, 61 // used to validate against later on. 62 gvk, err := apiutil.GVKForObject(obj, crClient.Scheme()) 63 if err != nil { 64 return nil, errors.Wrapf(err, "failed to create patch helper for object %s", klog.KObj(obj)) 65 } 66 67 // Convert the object to unstructured to compare against our before copy. 68 unstructuredObj, err := toUnstructured(obj, gvk) 69 if err != nil { 70 return nil, errors.Wrapf(err, "failed to create patch helper for %s %s: failed to convert object to Unstructured", gvk.Kind, klog.KObj(obj)) 71 } 72 73 // Check if the object satisfies the Cluster API conditions contract. 74 _, canInterfaceConditions := obj.(conditions.Setter) 75 76 return &Helper{ 77 client: crClient, 78 gvk: gvk, 79 before: unstructuredObj, 80 beforeObject: obj.DeepCopyObject().(client.Object), 81 isConditionsSetter: canInterfaceConditions, 82 }, nil 83 } 84 85 // Patch will attempt to patch the given object, including its status. 86 func (h *Helper) Patch(ctx context.Context, obj client.Object, opts ...Option) error { 87 // Return early if the object is nil. 88 if util.IsNil(obj) { 89 return errors.Errorf("failed to patch %s %s: modified object is nil", h.gvk.Kind, klog.KObj(h.beforeObject)) 90 } 91 92 // Get the GroupVersionKind of the object that we want to patch. 93 gvk, err := apiutil.GVKForObject(obj, h.client.Scheme()) 94 if err != nil { 95 return errors.Wrapf(err, "failed to patch %s %s", h.gvk.Kind, klog.KObj(h.beforeObject)) 96 } 97 if gvk != h.gvk { 98 return errors.Errorf("failed to patch %s %s: unmatched GroupVersionKind, expected %q got %q", h.gvk.Kind, klog.KObj(h.beforeObject), h.gvk, gvk) 99 } 100 101 // Calculate the options. 102 options := &HelperOptions{} 103 for _, opt := range opts { 104 opt.ApplyToHelper(options) 105 } 106 107 // Convert the object to unstructured to compare against our before copy. 108 h.after, err = toUnstructured(obj, gvk) 109 if err != nil { 110 return errors.Wrapf(err, "failed to patch %s %s: failed to convert object to Unstructured", h.gvk.Kind, klog.KObj(h.beforeObject)) 111 } 112 113 // Determine if the object has status. 114 if unstructuredHasStatus(h.after) { 115 if options.IncludeStatusObservedGeneration { 116 // Set status.observedGeneration if we're asked to do so. 117 if err := unstructured.SetNestedField(h.after.Object, h.after.GetGeneration(), "status", "observedGeneration"); err != nil { 118 return errors.Wrapf(err, "failed to patch %s %s: failed to set .status.observedGeneration", h.gvk.Kind, klog.KObj(h.beforeObject)) 119 } 120 121 // Restore the changes back to the original object. 122 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(h.after.Object, obj); err != nil { 123 return errors.Wrapf(err, "failed to patch %s %s: failed to converted object from Unstructured", h.gvk.Kind, klog.KObj(h.beforeObject)) 124 } 125 } 126 } 127 128 // Calculate and store the top-level field changes (e.g. "metadata", "spec", "status") we have before/after. 129 h.changes, err = h.calculateChanges(obj) 130 if err != nil { 131 return errors.Wrapf(err, "failed to patch %s %s", h.gvk.Kind, klog.KObj(h.beforeObject)) 132 } 133 134 // Issue patches and return errors in an aggregate. 135 var errs []error 136 // Patch the conditions first. 137 // 138 // Given that we pass in metadata.resourceVersion to perform a 3-way-merge conflict resolution, 139 // patching conditions first avoids an extra loop if spec or status patch succeeds first 140 // given that causes the resourceVersion to mutate. 141 if err := h.patchStatusConditions(ctx, obj, options.ForceOverwriteConditions, options.OwnedConditions); err != nil { 142 errs = append(errs, err) 143 } 144 // Then proceed to patch the rest of the object. 145 if err := h.patch(ctx, obj); err != nil { 146 errs = append(errs, err) 147 } 148 if err := h.patchStatus(ctx, obj); err != nil { 149 errs = append(errs, err) 150 } 151 152 if len(errs) > 0 { 153 return errors.Wrapf(kerrors.NewAggregate(errs), "failed to patch %s %s", h.gvk.Kind, klog.KObj(h.beforeObject)) 154 } 155 return nil 156 } 157 158 // patch issues a patch for metadata and spec. 159 func (h *Helper) patch(ctx context.Context, obj client.Object) error { 160 if !h.shouldPatch("metadata") && !h.shouldPatch("spec") { 161 return nil 162 } 163 beforeObject, afterObject, err := h.calculatePatch(obj, specPatch) 164 if err != nil { 165 return err 166 } 167 return h.client.Patch(ctx, afterObject, client.MergeFrom(beforeObject)) 168 } 169 170 // patchStatus issues a patch if the status has changed. 171 func (h *Helper) patchStatus(ctx context.Context, obj client.Object) error { 172 if !h.shouldPatch("status") { 173 return nil 174 } 175 beforeObject, afterObject, err := h.calculatePatch(obj, statusPatch) 176 if err != nil { 177 return err 178 } 179 return h.client.Status().Patch(ctx, afterObject, client.MergeFrom(beforeObject)) 180 } 181 182 // patchStatusConditions issues a patch if there are any changes to the conditions slice under 183 // the status subresource. This is a special case and it's handled separately given that 184 // we allow different controllers to act on conditions of the same object. 185 // 186 // This method has an internal backoff loop. When a conflict is detected, the method 187 // asks the Client for the a new version of the object we're trying to patch. 188 // 189 // Condition changes are then applied to the latest version of the object, and if there are 190 // no unresolvable conflicts, the patch is sent again. 191 func (h *Helper) patchStatusConditions(ctx context.Context, obj client.Object, forceOverwrite bool, ownedConditions []clusterv1.ConditionType) error { 192 // Nothing to do if the object isn't a condition patcher. 193 if !h.isConditionsSetter { 194 return nil 195 } 196 197 // Make sure our before/after objects satisfy the proper interface before continuing. 198 // 199 // NOTE: The checks and error below are done so that we don't panic if any of the objects don't satisfy the 200 // interface any longer, although this shouldn't happen because we already check when creating the patcher. 201 before, ok := h.beforeObject.(conditions.Getter) 202 if !ok { 203 return errors.Errorf("object %s doesn't satisfy conditions.Getter, cannot patch", before.GetObjectKind()) 204 } 205 after, ok := obj.(conditions.Getter) 206 if !ok { 207 return errors.Errorf("object %s doesn't satisfy conditions.Getter, cannot patch", after.GetObjectKind()) 208 } 209 210 // Store the diff from the before/after object, and return early if there are no changes. 211 diff, err := conditions.NewPatch( 212 before, 213 after, 214 ) 215 if err != nil { 216 return errors.Wrapf(err, "object can not be patched") 217 } 218 if diff.IsZero() { 219 return nil 220 } 221 222 // Make a copy of the object and store the key used if we have conflicts. 223 key := client.ObjectKeyFromObject(after) 224 225 // Define and start a backoff loop to handle conflicts 226 // between controllers working on the same object. 227 // 228 // This has been copied from https://github.com/kubernetes/kubernetes/blob/release-1.16/pkg/controller/controller_utils.go#L86-L88. 229 backoff := wait.Backoff{ 230 Steps: 5, 231 Duration: 100 * time.Millisecond, 232 Jitter: 1.0, 233 } 234 235 // Start the backoff loop and return errors if any. 236 return wait.ExponentialBackoff(backoff, func() (bool, error) { 237 latest, ok := before.DeepCopyObject().(conditions.Setter) 238 if !ok { 239 return false, errors.Errorf("object %s doesn't satisfy conditions.Setter, cannot patch", latest.GetObjectKind()) 240 } 241 242 // Get a new copy of the object. 243 if err := h.client.Get(ctx, key, latest); err != nil { 244 return false, err 245 } 246 247 // Create the condition patch before merging conditions. 248 conditionsPatch := client.MergeFromWithOptions(latest.DeepCopyObject().(conditions.Setter), client.MergeFromWithOptimisticLock{}) 249 250 // Set the condition patch previously created on the new object. 251 if err := diff.Apply(latest, conditions.WithForceOverwrite(forceOverwrite), conditions.WithOwnedConditions(ownedConditions...)); err != nil { 252 return false, err 253 } 254 255 // Issue the patch. 256 err := h.client.Status().Patch(ctx, latest, conditionsPatch) 257 switch { 258 case apierrors.IsConflict(err): 259 // Requeue. 260 return false, nil 261 case err != nil: 262 return false, err 263 default: 264 return true, nil 265 } 266 }) 267 } 268 269 // calculatePatch returns the before/after objects to be given in a controller-runtime patch, scoped down to the absolute necessary. 270 func (h *Helper) calculatePatch(afterObj client.Object, focus patchType) (client.Object, client.Object, error) { 271 // Get a shallow unsafe copy of the before/after object in unstructured form. 272 before := unsafeUnstructuredCopy(h.before, focus, h.isConditionsSetter) 273 after := unsafeUnstructuredCopy(h.after, focus, h.isConditionsSetter) 274 275 // We've now applied all modifications to local unstructured objects, 276 // make copies of the original objects and convert them back. 277 beforeObj := h.beforeObject.DeepCopyObject().(client.Object) 278 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(before.Object, beforeObj); err != nil { 279 return nil, nil, err 280 } 281 afterObj = afterObj.DeepCopyObject().(client.Object) 282 if err := runtime.DefaultUnstructuredConverter.FromUnstructured(after.Object, afterObj); err != nil { 283 return nil, nil, err 284 } 285 return beforeObj, afterObj, nil 286 } 287 288 func (h *Helper) shouldPatch(in string) bool { 289 return h.changes[in] 290 } 291 292 // calculate changes tries to build a patch from the before/after objects we have 293 // and store in a map which top-level fields (e.g. `metadata`, `spec`, `status`, etc.) have changed. 294 func (h *Helper) calculateChanges(after client.Object) (map[string]bool, error) { 295 // Calculate patch data. 296 patch := client.MergeFrom(h.beforeObject) 297 diff, err := patch.Data(after) 298 if err != nil { 299 return nil, errors.Wrapf(err, "failed to calculate patch data") 300 } 301 302 // Unmarshal patch data into a local map. 303 patchDiff := map[string]interface{}{} 304 if err := json.Unmarshal(diff, &patchDiff); err != nil { 305 return nil, errors.Wrapf(err, "failed to unmarshal patch data into a map") 306 } 307 308 // Return the map. 309 res := make(map[string]bool, len(patchDiff)) 310 for key := range patchDiff { 311 res[key] = true 312 } 313 return res, nil 314 }