sigs.k8s.io/cluster-api@v1.7.1/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  	gvk, err := apiutil.GVKForObject(modifiedUnstructured, c.Scheme())
    80  	if err != nil {
    81  		return errors.Wrapf(err, "failed to apply object: failed to get GroupVersionKind of modified object %s", klog.KObj(modifiedUnstructured))
    82  	}
    83  
    84  	var requestIdentifier string
    85  	if options.WithCachingProxy {
    86  		// Check if the request is cached.
    87  		requestIdentifier, err = ComputeRequestIdentifier(c.Scheme(), options.Original, modifiedUnstructured)
    88  		if err != nil {
    89  			return errors.Wrapf(err, "failed to apply object")
    90  		}
    91  		if options.Cache.Has(requestIdentifier) {
    92  			// If the request is cached return the original object.
    93  			if err := c.Scheme().Convert(options.Original, modified, ctx); err != nil {
    94  				return errors.Wrapf(err, "failed to write original into modified object")
    95  			}
    96  			// Recover gvk e.g. for logging.
    97  			modified.GetObjectKind().SetGroupVersionKind(gvk)
    98  			return nil
    99  		}
   100  	}
   101  
   102  	patchOptions := []client.PatchOption{
   103  		client.ForceOwnership,
   104  		client.FieldOwner(fieldManager),
   105  	}
   106  	if err := c.Patch(ctx, modifiedUnstructured, client.Apply, patchOptions...); err != nil {
   107  		return errors.Wrapf(err, "failed to apply %s %s", gvk.Kind, klog.KObj(modifiedUnstructured))
   108  	}
   109  
   110  	// Write back the modified object so callers can access the patched object.
   111  	if err := c.Scheme().Convert(modifiedUnstructured, modified, ctx); err != nil {
   112  		return errors.Wrapf(err, "failed to write modified object")
   113  	}
   114  
   115  	// Recover gvk e.g. for logging.
   116  	modified.GetObjectKind().SetGroupVersionKind(gvk)
   117  
   118  	if options.WithCachingProxy {
   119  		// If the SSA call did not update the object, add the request to the cache.
   120  		if options.Original.GetResourceVersion() == modifiedUnstructured.GetResourceVersion() {
   121  			options.Cache.Add(requestIdentifier)
   122  		}
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  // prepareModified converts obj into an Unstructured and filters out undesired fields.
   129  func prepareModified(scheme *runtime.Scheme, obj client.Object) (*unstructured.Unstructured, error) {
   130  	u := &unstructured.Unstructured{}
   131  	switch obj.(type) {
   132  	case *unstructured.Unstructured:
   133  		u = obj.DeepCopyObject().(*unstructured.Unstructured)
   134  	default:
   135  		if err := scheme.Convert(obj, u, nil); err != nil {
   136  			return nil, errors.Wrap(err, "failed to convert object to Unstructured")
   137  		}
   138  	}
   139  
   140  	// Only keep the paths that we have opinions on.
   141  	FilterObject(u, &FilterObjectInput{
   142  		AllowedPaths: []contract.Path{
   143  			// apiVersion, kind, name and namespace are required field for a server side apply intent.
   144  			{"apiVersion"},
   145  			{"kind"},
   146  			{"metadata", "name"},
   147  			{"metadata", "namespace"},
   148  			// uid is optional for a server side apply intent but sets the expectation of an object getting created or a specific one updated.
   149  			{"metadata", "uid"},
   150  			// our controllers only have an opinion on labels, annotation, finalizers ownerReferences and spec.
   151  			{"metadata", "labels"},
   152  			{"metadata", "annotations"},
   153  			{"metadata", "finalizers"},
   154  			{"metadata", "ownerReferences"},
   155  			{"spec"},
   156  		},
   157  	})
   158  	return u, nil
   159  }