sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/topology/cluster/structuredmerge/twowayspatchhelper.go (about) 1 /* 2 Copyright 2021 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 structuredmerge 18 19 import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 24 jsonpatch "github.com/evanphx/json-patch/v5" 25 "github.com/pkg/errors" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "k8s.io/apimachinery/pkg/types" 28 ctrl "sigs.k8s.io/controller-runtime" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 31 "sigs.k8s.io/cluster-api/internal/contract" 32 "sigs.k8s.io/cluster-api/internal/util/ssa" 33 "sigs.k8s.io/cluster-api/util" 34 ) 35 36 // TwoWaysPatchHelper helps with a patch that yields the modified document when applied to the original document. 37 type TwoWaysPatchHelper struct { 38 client client.Client 39 40 // original holds the object to which the patch should apply to, to be used in the Patch method. 41 original client.Object 42 43 // patch holds the merge patch in json format. 44 patch []byte 45 46 // hasSpecChanges documents if the patch impacts the object spec 47 hasSpecChanges bool 48 } 49 50 // NewTwoWaysPatchHelper will return a patch that yields the modified document when applied to the original document 51 // using the two-ways merge algorithm. 52 // NOTE: In the case of ClusterTopologyReconciler, original is the current object, modified is the desired object, and 53 // the patch returns all the changes required to align current to what is defined in desired; fields not managed 54 // by the topology controller are going to be preserved without changes. 55 // NOTE: TwoWaysPatch is considered a minimal viable replacement for server side apply during topology dry run, with 56 // the following limitations: 57 // - TwoWaysPatch doesn't consider OpenAPI schema extension like +ListMap this can lead to false positive when topology 58 // dry run is simulating a change to an existing slice 59 // (TwoWaysPatch always revert external changes, like server side apply when +ListMap=atomic). 60 // - TwoWaysPatch doesn't consider existing metadata.managedFields, and this can lead to false negative when topology dry run 61 // is simulating a change to an existing object where the topology controller is dropping an opinion for a field 62 // (TwoWaysPatch always preserve dropped fields, like server side apply when the field has more than one manager). 63 // - TwoWaysPatch doesn't generate metadata.managedFields as server side apply does. 64 // 65 // NOTE: NewTwoWaysPatchHelper consider changes only in metadata.labels, metadata.annotation and spec; it also respects 66 // the ignorePath option (same as the server side apply helper). 67 func NewTwoWaysPatchHelper(original, modified client.Object, c client.Client, opts ...HelperOption) (*TwoWaysPatchHelper, error) { 68 helperOptions := &HelperOptions{} 69 helperOptions = helperOptions.ApplyOptions(opts) 70 helperOptions.AllowedPaths = []contract.Path{ 71 {"metadata", "labels"}, 72 {"metadata", "annotations"}, 73 {"spec"}, // NOTE: The handling of managed path requires/assumes spec to be within allowed path. 74 } 75 // In case we are creating an object, we extend the set of allowed fields adding apiVersion, Kind 76 // metadata.name, metadata.namespace (who are required by the API server) and metadata.ownerReferences 77 // that gets set to avoid orphaned objects. 78 if util.IsNil(original) { 79 helperOptions.AllowedPaths = append(helperOptions.AllowedPaths, 80 contract.Path{"apiVersion"}, 81 contract.Path{"kind"}, 82 contract.Path{"metadata", "name"}, 83 contract.Path{"metadata", "namespace"}, 84 contract.Path{"metadata", "ownerReferences"}, 85 ) 86 } 87 88 // Convert the input objects to json; if original is nil, use empty object so the 89 // following logic works without panicking. 90 originalJSON, err := json.Marshal(original) 91 if err != nil { 92 return nil, errors.Wrap(err, "failed to marshal original object to json") 93 } 94 if util.IsNil(original) { 95 originalJSON = []byte("{}") 96 } 97 98 modifiedJSON, err := json.Marshal(modified) 99 if err != nil { 100 return nil, errors.Wrap(err, "failed to marshal modified object to json") 101 } 102 103 // Apply patch options including: 104 // - exclude paths (fields to not consider, e.g. status); 105 // - ignore paths (well known fields owned by something else, e.g. spec.controlPlaneEndpoint in the 106 // InfrastructureCluster object); 107 // NOTE: All the above options trigger changes in the modified object so the resulting two ways patch 108 // includes or not the specific change. 109 modifiedJSON, err = applyOptions(&applyOptionsInput{ 110 original: originalJSON, 111 modified: modifiedJSON, 112 options: helperOptions, 113 }) 114 if err != nil { 115 return nil, errors.Wrap(err, "failed to apply options to modified") 116 } 117 118 // Apply the modified object to the original one, merging the values of both; 119 // in case of conflicts, values from the modified object are preserved. 120 originalWithModifiedJSON, err := jsonpatch.MergePatch(originalJSON, modifiedJSON) 121 if err != nil { 122 return nil, errors.Wrap(err, "failed to apply modified json to original json") 123 } 124 125 // Compute the merge patch that will align the original object to the target 126 // state defined above. 127 twoWayPatch, err := jsonpatch.CreateMergePatch(originalJSON, originalWithModifiedJSON) 128 if err != nil { 129 return nil, errors.Wrap(err, "failed to create merge patch") 130 } 131 132 twoWayPatchMap := make(map[string]interface{}) 133 if err := json.Unmarshal(twoWayPatch, &twoWayPatchMap); err != nil { 134 return nil, errors.Wrap(err, "failed to unmarshal two way merge patch") 135 } 136 137 // check if the changes impact the spec field. 138 hasSpecChanges := twoWayPatchMap["spec"] != nil 139 140 return &TwoWaysPatchHelper{ 141 client: c, 142 patch: twoWayPatch, 143 hasSpecChanges: hasSpecChanges, 144 original: original, 145 }, nil 146 } 147 148 type applyOptionsInput struct { 149 original []byte 150 modified []byte 151 options *HelperOptions 152 } 153 154 // Apply patch options changing the modified object so the resulting two ways patch 155 // includes or not the specific change. 156 func applyOptions(in *applyOptionsInput) ([]byte, error) { 157 originalMap := make(map[string]interface{}) 158 if err := json.Unmarshal(in.original, &originalMap); err != nil { 159 return nil, errors.Wrap(err, "failed to unmarshal original") 160 } 161 162 modifiedMap := make(map[string]interface{}) 163 if err := json.Unmarshal(in.modified, &modifiedMap); err != nil { 164 return nil, errors.Wrap(err, "failed to unmarshal modified") 165 } 166 167 // drop changes for exclude paths (fields to not consider, e.g. status); 168 // Note: for everything not allowed it sets modified equal to original, so the generated patch doesn't include this change 169 if len(in.options.AllowedPaths) > 0 { 170 dropDiff(&dropDiffInput{ 171 path: contract.Path{}, 172 original: originalMap, 173 modified: modifiedMap, 174 shouldDropDiffFunc: ssa.IsPathNotAllowed(in.options.AllowedPaths), 175 }) 176 } 177 178 // drop changes for ignore paths (well known fields owned by something else, e.g. 179 // spec.controlPlaneEndpoint in the InfrastructureCluster object); 180 // Note: for everything ignored it sets modified equal to original, so the generated patch doesn't include this change 181 if len(in.options.IgnorePaths) > 0 { 182 dropDiff(&dropDiffInput{ 183 path: contract.Path{}, 184 original: originalMap, 185 modified: modifiedMap, 186 shouldDropDiffFunc: ssa.IsPathIgnored(in.options.IgnorePaths), 187 }) 188 } 189 190 modified, err := json.Marshal(&modifiedMap) 191 if err != nil { 192 return nil, errors.Wrap(err, "failed to marshal modified") 193 } 194 195 return modified, nil 196 } 197 198 // HasSpecChanges return true if the patch has changes to the spec field. 199 func (h *TwoWaysPatchHelper) HasSpecChanges() bool { 200 return h.hasSpecChanges 201 } 202 203 // HasChanges return true if the patch has changes. 204 func (h *TwoWaysPatchHelper) HasChanges() bool { 205 return !bytes.Equal(h.patch, []byte("{}")) 206 } 207 208 // Patch will attempt to apply the twoWaysPatch to the original object. 209 func (h *TwoWaysPatchHelper) Patch(ctx context.Context) error { 210 if !h.HasChanges() { 211 return nil 212 } 213 log := ctrl.LoggerFrom(ctx) 214 215 if util.IsNil(h.original) { 216 modifiedMap := make(map[string]interface{}) 217 if err := json.Unmarshal(h.patch, &modifiedMap); err != nil { 218 return errors.Wrap(err, "failed to unmarshal two way merge patch") 219 } 220 221 obj := &unstructured.Unstructured{ 222 Object: modifiedMap, 223 } 224 return h.client.Create(ctx, obj) 225 } 226 227 // Note: deepcopy before patching in order to avoid modifications to the original object. 228 log.V(5).Info("Patching object", "Patch", string(h.patch)) 229 return h.client.Patch(ctx, h.original.DeepCopyObject().(client.Object), client.RawPatch(types.MergePatchType, h.patch)) 230 }