sigs.k8s.io/cluster-api@v1.7.1/exp/runtime/topologymutation/walker.go (about) 1 /* 2 Copyright 2022 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 topologymutation 18 19 import ( 20 "context" 21 "encoding/json" 22 23 mergepatch "github.com/evanphx/json-patch/v5" 24 "github.com/pkg/errors" 25 "gomodules.xyz/jsonpatch/v2" 26 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/runtime/schema" 29 ctrl "sigs.k8s.io/controller-runtime" 30 31 runtimehooksv1 "sigs.k8s.io/cluster-api/exp/runtime/hooks/api/v1alpha1" 32 ) 33 34 // WalkTemplatesOption is some configuration that modifies WalkTemplates behavior. 35 type WalkTemplatesOption interface { 36 // ApplyToWalkTemplates applies this configuration to the given WalkTemplatesOptions. 37 ApplyToWalkTemplates(*WalkTemplatesOptions) 38 } 39 40 // WalkTemplatesOptions contains options for WalkTemplates behavior. 41 type WalkTemplatesOptions struct { 42 failForUnknownTypes bool 43 patchFormat runtimehooksv1.PatchType 44 // TODO: add the possibility to set patchFormat for single patches only, eg. via a func(requestItem) format. 45 } 46 47 // newWalkTemplatesOptions returns a WalkTemplatesOptions with default values. 48 func newWalkTemplatesOptions() *WalkTemplatesOptions { 49 return &WalkTemplatesOptions{ 50 patchFormat: runtimehooksv1.JSONPatchType, 51 } 52 } 53 54 // FailForUnknownTypes defines if WalkTemplates should fail when processing unknown types. 55 // If not set unknown types will be silently ignored, which allows to the WalkTemplates decoder to 56 // be configured only the API types it cares about. 57 type FailForUnknownTypes struct{} 58 59 // ApplyToWalkTemplates applies this configuration to the given WalkTemplatesOptions. 60 func (f FailForUnknownTypes) ApplyToWalkTemplates(in *WalkTemplatesOptions) { 61 in.failForUnknownTypes = true 62 } 63 64 // PatchFormat defines the patch format that WalkTemplates should generate. 65 // If not set, JSONPatchType will be used. 66 type PatchFormat struct { 67 Format runtimehooksv1.PatchType 68 } 69 70 // ApplyToWalkTemplates applies this configuration to the given WalkTemplatesOptions. 71 func (d PatchFormat) ApplyToWalkTemplates(in *WalkTemplatesOptions) { 72 in.patchFormat = d.Format 73 } 74 75 // WalkTemplates walks through all templates of a GeneratePatchesRequest and calls the mutateFunc. 76 // By using walk templates it is possible to implement patches using typed API objects, which makes code 77 // easier to read and less error prone than using unstructured or working with raw json/yaml. 78 // Also, by using this func it is possible to ignore most of the details of the GeneratePatchesRequest 79 // and GeneratePatchesResponse messages format and focus on writing patches/modifying the templates. 80 func WalkTemplates(ctx context.Context, decoder runtime.Decoder, req *runtimehooksv1.GeneratePatchesRequest, 81 resp *runtimehooksv1.GeneratePatchesResponse, mutateFunc func(ctx context.Context, obj runtime.Object, 82 variables map[string]apiextensionsv1.JSON, holderRef runtimehooksv1.HolderReference) error, opts ...WalkTemplatesOption) { 83 log := ctrl.LoggerFrom(ctx) 84 globalVariables := ToMap(req.Variables) 85 86 options := newWalkTemplatesOptions() 87 for _, o := range opts { 88 o.ApplyToWalkTemplates(options) 89 } 90 91 // For all the templates in a request. 92 // TODO: add a notion of ordering the patch implementers can rely on. Ideally ordering could be pluggable via options. 93 for _, requestItem := range req.Items { 94 // Computes the variables that apply to the template, by merging global and template variables. 95 templateVariables, err := MergeVariableMaps(globalVariables, ToMap(requestItem.Variables)) 96 if err != nil { 97 resp.Status = runtimehooksv1.ResponseStatusFailure 98 resp.Message = err.Error() 99 return 100 } 101 102 // Convert the template object into a typed object. 103 original, _, err := decoder.Decode(requestItem.Object.Raw, nil, requestItem.Object.Object) 104 if err != nil { 105 if options.failForUnknownTypes { 106 resp.Status = runtimehooksv1.ResponseStatusFailure 107 resp.Message = err.Error() 108 return 109 } 110 // Continue, object has a type which hasn't been registered with the scheme. 111 continue 112 } 113 114 // Setup contextual logging for the requestItem. 115 holderRefGV, err := schema.ParseGroupVersion(requestItem.HolderReference.APIVersion) 116 if err != nil { 117 resp.Status = runtimehooksv1.ResponseStatusFailure 118 resp.Message = errors.Wrapf(err, "error generating patches - HolderReference apiVersion %q is not in valid format", requestItem.HolderReference.APIVersion).Error() 119 return 120 } 121 requestItemLog := log.WithValues( 122 "template", logRef{ 123 Group: original.GetObjectKind().GroupVersionKind().Group, 124 Version: original.GetObjectKind().GroupVersionKind().Version, 125 Kind: original.GetObjectKind().GroupVersionKind().Kind, 126 }, 127 "holder", logRef{ 128 Group: holderRefGV.Group, 129 Version: holderRefGV.Version, 130 Kind: requestItem.HolderReference.Kind, 131 Namespace: requestItem.HolderReference.Namespace, 132 Name: requestItem.HolderReference.Name, 133 }, 134 ) 135 requestItemCtx := ctrl.LoggerInto(ctx, requestItemLog) 136 137 // Calls the mutateFunc. 138 requestItemLog.V(4).Info("Generating patch for template") 139 modified := original.DeepCopyObject() 140 if err := mutateFunc(requestItemCtx, modified, templateVariables, requestItem.HolderReference); err != nil { 141 resp.Status = runtimehooksv1.ResponseStatusFailure 142 resp.Message = err.Error() 143 return 144 } 145 146 // Generate the Patch by comparing original and modified object. 147 var patch []byte 148 switch options.patchFormat { 149 case runtimehooksv1.JSONPatchType: 150 patch, err = createJSONPatch(original, modified) 151 if err != nil { 152 resp.Status = runtimehooksv1.ResponseStatusFailure 153 resp.Message = err.Error() 154 return 155 } 156 case runtimehooksv1.JSONMergePatchType: 157 patch, err = createJSONMergePatch(original, modified) 158 if err != nil { 159 resp.Status = runtimehooksv1.ResponseStatusFailure 160 resp.Message = err.Error() 161 return 162 } 163 } 164 165 resp.Items = append(resp.Items, runtimehooksv1.GeneratePatchesResponseItem{ 166 UID: requestItem.UID, 167 PatchType: options.patchFormat, 168 Patch: patch, 169 }) 170 requestItemLog.V(5).Info("Generated patch", "uid", requestItem.UID, "patch", string(patch)) 171 } 172 173 resp.Status = runtimehooksv1.ResponseStatusSuccess 174 } 175 176 // createJSONPatch creates a RFC 6902 JSON patch from the original and the modified object. 177 func createJSONPatch(original, modified runtime.Object) ([]byte, error) { 178 marshalledOriginal, err := json.Marshal(original) 179 if err != nil { 180 return nil, errors.Errorf("failed to marshal original object: %v", err) 181 } 182 183 marshalledModified, err := json.Marshal(modified) 184 if err != nil { 185 return nil, errors.Errorf("failed to marshal modified object: %v", err) 186 } 187 188 patch, err := jsonpatch.CreatePatch(marshalledOriginal, marshalledModified) 189 if err != nil { 190 return nil, errors.Errorf("failed to create patch: %v", err) 191 } 192 193 patchBytes, err := json.Marshal(patch) 194 if err != nil { 195 return nil, errors.Errorf("failed to marshal patch: %v", err) 196 } 197 198 return patchBytes, nil 199 } 200 201 // createJSONMergePatch creates a RFC 7396 JSON merge patch from the original and the modified object. 202 func createJSONMergePatch(original, modified runtime.Object) ([]byte, error) { 203 marshalledOriginal, err := json.Marshal(original) 204 if err != nil { 205 return nil, errors.Errorf("failed to marshal original object: %v", err) 206 } 207 208 marshalledModified, err := json.Marshal(modified) 209 if err != nil { 210 return nil, errors.Errorf("failed to marshal modified object: %v", err) 211 } 212 213 patch, err := mergepatch.CreateMergePatch(marshalledOriginal, marshalledModified) 214 if err != nil { 215 return nil, errors.Errorf("failed to create patch: %v", err) 216 } 217 218 return patch, nil 219 }