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 }