sigs.k8s.io/cluster-api@v1.6.3/internal/util/ssa/patch.go (about)

     1  /*
     2  Copyright 2023 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 ssa
    18  
    19  import (
    20  	"context"
    21  
    22  	"github.com/pkg/errors"
    23  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	"k8s.io/klog/v2"
    26  	"sigs.k8s.io/controller-runtime/pkg/client"
    27  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    28  
    29  	"sigs.k8s.io/cluster-api/internal/contract"
    30  )
    31  
    32  // Option is the interface for configuration that modifies Options for a patch request.
    33  type Option interface {
    34  	// ApplyToOptions applies this configuration to the given Options.
    35  	ApplyToOptions(*Options)
    36  }
    37  
    38  // WithCachingProxy enables caching for the patch request.
    39  // The original and modified object will be used to generate an
    40  // identifier for the request.
    41  // The cache will be used to cache the result of the request.
    42  type WithCachingProxy struct {
    43  	Cache    Cache
    44  	Original client.Object
    45  }
    46  
    47  // ApplyToOptions applies WithCachingProxy to the given Options.
    48  func (w WithCachingProxy) ApplyToOptions(in *Options) {
    49  	in.WithCachingProxy = true
    50  	in.Cache = w.Cache
    51  	in.Original = w.Original
    52  }
    53  
    54  // Options contains the options for the Patch func.
    55  type Options struct {
    56  	WithCachingProxy bool
    57  	Cache            Cache
    58  	Original         client.Object
    59  }
    60  
    61  // Patch executes an SSA patch.
    62  // If WithCachingProxy is set and the request didn't change the object
    63  // we will cache this result, so subsequent calls don't have to run SSA again.
    64  func Patch(ctx context.Context, c client.Client, fieldManager string, modified client.Object, opts ...Option) error {
    65  	// Calculate the options.
    66  	options := &Options{}
    67  	for _, opt := range opts {
    68  		opt.ApplyToOptions(options)
    69  	}
    70  
    71  	// Convert the object to unstructured and filter out fields we don't
    72  	// want to set (e.g. metadata creationTimestamp).
    73  	// Note: This is necessary to avoid continuous reconciles.
    74  	modifiedUnstructured, err := prepareModified(c.Scheme(), modified)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	var requestIdentifier string
    80  	if options.WithCachingProxy {
    81  		// Check if the request is cached.
    82  		requestIdentifier, err = ComputeRequestIdentifier(c.Scheme(), options.Original, modifiedUnstructured)
    83  		if err != nil {
    84  			return errors.Wrapf(err, "failed to apply object")
    85  		}
    86  		if options.Cache.Has(requestIdentifier) {
    87  			// If the request is cached return the original object.
    88  			if err := c.Scheme().Convert(options.Original, modified, ctx); err != nil {
    89  				return errors.Wrapf(err, "failed to write original into modified object")
    90  			}
    91  			return nil
    92  		}
    93  	}
    94  
    95  	gvk, err := apiutil.GVKForObject(modifiedUnstructured, c.Scheme())
    96  	if err != nil {
    97  		return errors.Wrapf(err, "failed to apply object: failed to get GroupVersionKind of modified object %s", klog.KObj(modifiedUnstructured))
    98  	}
    99  
   100  	patchOptions := []client.PatchOption{
   101  		client.ForceOwnership,
   102  		client.FieldOwner(fieldManager),
   103  	}
   104  	if err := c.Patch(ctx, modifiedUnstructured, client.Apply, patchOptions...); err != nil {
   105  		return errors.Wrapf(err, "failed to apply %s %s", gvk.Kind, klog.KObj(modifiedUnstructured))
   106  	}
   107  
   108  	// Write back the modified object so callers can access the patched object.
   109  	if err := c.Scheme().Convert(modifiedUnstructured, modified, ctx); err != nil {
   110  		return errors.Wrapf(err, "failed to write modified object")
   111  	}
   112  
   113  	if options.WithCachingProxy {
   114  		// If the SSA call did not update the object, add the request to the cache.
   115  		if options.Original.GetResourceVersion() == modifiedUnstructured.GetResourceVersion() {
   116  			options.Cache.Add(requestIdentifier)
   117  		}
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  // prepareModified converts obj into an Unstructured and filters out undesired fields.
   124  func prepareModified(scheme *runtime.Scheme, obj client.Object) (*unstructured.Unstructured, error) {
   125  	u := &unstructured.Unstructured{}
   126  	switch obj.(type) {
   127  	case *unstructured.Unstructured:
   128  		u = obj.DeepCopyObject().(*unstructured.Unstructured)
   129  	default:
   130  		if err := scheme.Convert(obj, u, nil); err != nil {
   131  			return nil, errors.Wrap(err, "failed to convert object to Unstructured")
   132  		}
   133  	}
   134  
   135  	// Only keep the paths that we have opinions on.
   136  	FilterObject(u, &FilterObjectInput{
   137  		AllowedPaths: []contract.Path{
   138  			// apiVersion, kind, name and namespace are required field for a server side apply intent.
   139  			{"apiVersion"},
   140  			{"kind"},
   141  			{"metadata", "name"},
   142  			{"metadata", "namespace"},
   143  			// uid is optional for a server side apply intent but sets the expectation of an object getting created or a specific one updated.
   144  			{"metadata", "uid"},
   145  			// our controllers only have an opinion on labels, annotation, finalizers ownerReferences and spec.
   146  			{"metadata", "labels"},
   147  			{"metadata", "annotations"},
   148  			{"metadata", "finalizers"},
   149  			{"metadata", "ownerReferences"},
   150  			{"spec"},
   151  		},
   152  	})
   153  	return u, nil
   154  }