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  }