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 }