sigs.k8s.io/cluster-api@v1.6.3/internal/controllers/topology/cluster/structuredmerge/dryrun.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 structuredmerge 18 19 import ( 20 "context" 21 "encoding/json" 22 23 jsonpatch "github.com/evanphx/json-patch/v5" 24 "github.com/pkg/errors" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 "sigs.k8s.io/controller-runtime/pkg/client" 28 29 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 30 "sigs.k8s.io/cluster-api/internal/contract" 31 "sigs.k8s.io/cluster-api/internal/util/ssa" 32 "sigs.k8s.io/cluster-api/util/conversion" 33 ) 34 35 type dryRunSSAPatchInput struct { 36 client client.Client 37 // ssaCache caches SSA request to allow us to only run SSA when we actually have to. 38 ssaCache ssa.Cache 39 // originalUnstructured contains the current state of the object. 40 // Note: We will run SSA dry-run on originalUnstructured and modifiedUnstructured and then compare them. 41 originalUnstructured *unstructured.Unstructured 42 // modifiedUnstructured contains the intended changes to the object. 43 // Note: We will run SSA dry-run on originalUnstructured and modifiedUnstructured and then compare them. 44 modifiedUnstructured *unstructured.Unstructured 45 // helperOptions contains the helper options for filtering the intent. 46 helperOptions *HelperOptions 47 } 48 49 // dryRunSSAPatch uses server side apply dry run to determine if the operation is going to change the actual object. 50 func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool, bool, error) { 51 // Compute a request identifier. 52 // The identifier is unique for a specific request to ensure we don't have to re-run the request 53 // once we found out that it would not produce a diff. 54 // The identifier consists of: gvk, namespace, name and resourceVersion of originalUnstructured 55 // and a hash of modifiedUnstructured. 56 // This ensures that we re-run the request as soon as either original or modified changes. 57 requestIdentifier, err := ssa.ComputeRequestIdentifier(dryRunCtx.client.Scheme(), dryRunCtx.originalUnstructured, dryRunCtx.modifiedUnstructured) 58 if err != nil { 59 return false, false, err 60 } 61 62 // Check if we already ran this request before by checking if the cache already contains this identifier. 63 // Note: We only add an identifier to the cache if the result of the dry run was no diff. 64 if exists := dryRunCtx.ssaCache.Has(requestIdentifier); exists { 65 return false, false, nil 66 } 67 68 // For dry run we use the same options as for the intent but with adding metadata.managedFields 69 // to ensure that changes to ownership are detected. 70 filterObjectInput := &ssa.FilterObjectInput{ 71 AllowedPaths: append(dryRunCtx.helperOptions.AllowedPaths, []string{"metadata", "managedFields"}), 72 IgnorePaths: dryRunCtx.helperOptions.IgnorePaths, 73 } 74 75 // Add TopologyDryRunAnnotation to notify validation webhooks to skip immutability checks. 76 if err := unstructured.SetNestedField(dryRunCtx.originalUnstructured.Object, "", "metadata", "annotations", clusterv1.TopologyDryRunAnnotation); err != nil { 77 return false, false, errors.Wrap(err, "failed to add topology dry-run annotation to original object") 78 } 79 if err := unstructured.SetNestedField(dryRunCtx.modifiedUnstructured.Object, "", "metadata", "annotations", clusterv1.TopologyDryRunAnnotation); err != nil { 80 return false, false, errors.Wrap(err, "failed to add topology dry-run annotation to modified object") 81 } 82 83 // Do a server-side apply dry-run with modifiedUnstructured to get the updated object. 84 err = dryRunCtx.client.Patch(ctx, dryRunCtx.modifiedUnstructured, client.Apply, client.DryRunAll, client.FieldOwner(TopologyManagerName), client.ForceOwnership) 85 if err != nil { 86 // This catches errors like metadata.uid changes. 87 return false, false, errors.Wrap(err, "server side apply dry-run failed for modified object") 88 } 89 90 // Do a server-side apply dry-run with originalUnstructured to ensure the latest defaulting is applied. 91 // Note: After a Cluster API upgrade there is no guarantee that defaulting has been run on existing objects. 92 // We have to ensure defaulting has been applied before comparing original with modified, because otherwise 93 // differences in defaulting would trigger rollouts. 94 // Note: We cannot use the managed fields of originalUnstructured after SSA dryrun, because applying 95 // the whole originalUnstructured will give capi-topology ownership of all fields. Thus, we back up the 96 // managed fields and restore them after the dry run. 97 // It's fine to compare the managed fields of modifiedUnstructured after dry-run with originalUnstructured 98 // before dry-run as we want to know if applying modifiedUnstructured would change managed fields on original. 99 100 // Filter object to drop fields which are not part of our intent. 101 // Note: It's especially important to also drop metadata.resourceVersion, otherwise we could get the following 102 // error: "the object has been modified; please apply your changes to the latest version and try again" 103 ssa.FilterObject(dryRunCtx.originalUnstructured, filterObjectInput) 104 // Backup managed fields. 105 originalUnstructuredManagedFieldsBeforeSSA := dryRunCtx.originalUnstructured.GetManagedFields() 106 // Set managed fields to nil. 107 // Note: Otherwise we would get the following error: 108 // "failed to request dry-run server side apply: metadata.managedFields must be nil" 109 dryRunCtx.originalUnstructured.SetManagedFields(nil) 110 err = dryRunCtx.client.Patch(ctx, dryRunCtx.originalUnstructured, client.Apply, client.DryRunAll, client.FieldOwner(TopologyManagerName), client.ForceOwnership) 111 if err != nil { 112 return false, false, errors.Wrap(err, "server side apply dry-run failed for original object") 113 } 114 // Restore managed fields. 115 dryRunCtx.originalUnstructured.SetManagedFields(originalUnstructuredManagedFieldsBeforeSSA) 116 117 // Cleanup the dryRunUnstructured object to remove the added TopologyDryRunAnnotation 118 // and remove the affected managedFields for `manager=capi-topology` which would 119 // otherwise show the additional field ownership for the annotation we added and 120 // the changed managedField timestamp. 121 // We also drop managedFields of other managers as we don't care about if other managers 122 // made changes to the object. We only want to trigger a ServerSideApply if we would make 123 // changes to the object. 124 // Please note that if other managers made changes to fields that we care about and thus ownership changed, 125 // this would affect our managed fields as well and we would still detect it by diffing our managed fields. 126 if err := cleanupManagedFieldsAndAnnotation(dryRunCtx.modifiedUnstructured); err != nil { 127 return false, false, errors.Wrap(err, "failed to filter topology dry-run annotation on modified object") 128 } 129 130 // Also run the function for the originalUnstructured to remove the managedField 131 // timestamp for `manager=capi-topology`. 132 // We also drop managedFields of other managers as we don't care about if other managers 133 // made changes to the object. We only want to trigger a ServerSideApply if we would make 134 // changes to the object. 135 // Please note that if other managers made changes to fields that we care about and thus ownership changed, 136 // this would affect our managed fields as well and we would still detect it by diffing our managed fields. 137 if err := cleanupManagedFieldsAndAnnotation(dryRunCtx.originalUnstructured); err != nil { 138 return false, false, errors.Wrap(err, "failed to filter topology dry-run annotation on original object") 139 } 140 141 // Drop the other fields which are not part of our intent. 142 ssa.FilterObject(dryRunCtx.modifiedUnstructured, filterObjectInput) 143 ssa.FilterObject(dryRunCtx.originalUnstructured, filterObjectInput) 144 145 // Compare the output of dry run to the original object. 146 originalJSON, err := json.Marshal(dryRunCtx.originalUnstructured) 147 if err != nil { 148 return false, false, err 149 } 150 modifiedJSON, err := json.Marshal(dryRunCtx.modifiedUnstructured) 151 if err != nil { 152 return false, false, err 153 } 154 155 rawDiff, err := jsonpatch.CreateMergePatch(originalJSON, modifiedJSON) 156 if err != nil { 157 return false, false, err 158 } 159 160 // Determine if there are changes to the spec and object. 161 diff := &unstructured.Unstructured{} 162 if err := json.Unmarshal(rawDiff, &diff.Object); err != nil { 163 return false, false, err 164 } 165 166 hasChanges := len(diff.Object) > 0 167 _, hasSpecChanges := diff.Object["spec"] 168 169 // If there is no diff add the request identifier to the cache. 170 if !hasChanges { 171 dryRunCtx.ssaCache.Add(requestIdentifier) 172 } 173 174 return hasChanges, hasSpecChanges, nil 175 } 176 177 // cleanupManagedFieldsAndAnnotation adjusts the obj to remove the topology.cluster.x-k8s.io/dry-run 178 // and cluster.x-k8s.io/conversion-data annotations as well as the field ownership reference in managedFields. It does 179 // also remove the timestamp of the managedField for `manager=capi-topology` because 180 // it is expected to change due to the additional annotation. 181 func cleanupManagedFieldsAndAnnotation(obj *unstructured.Unstructured) error { 182 // Filter the topology.cluster.x-k8s.io/dry-run annotation as well as leftover empty maps. 183 ssa.FilterIntent(&ssa.FilterIntentInput{ 184 Path: contract.Path{}, 185 Value: obj.Object, 186 ShouldFilter: ssa.IsPathIgnored([]contract.Path{ 187 {"metadata", "annotations", clusterv1.TopologyDryRunAnnotation}, 188 // In case the ClusterClass we are reconciling is using not the latest apiVersion the conversion 189 // annotation might be added to objects. As we don't care about differences in conversion as we 190 // are working on the old apiVersion we want to ignore the annotation when diffing. 191 {"metadata", "annotations", conversion.DataAnnotation}, 192 }), 193 }) 194 195 // Adjust the managed field for Manager=TopologyManagerName, Subresource="", Operation="Apply" and 196 // drop managed fields of other controllers. 197 oldManagedFields := obj.GetManagedFields() 198 newManagedFields := []metav1.ManagedFieldsEntry{} 199 for _, managedField := range oldManagedFields { 200 if managedField.Manager != TopologyManagerName { 201 continue 202 } 203 if managedField.Subresource != "" { 204 continue 205 } 206 if managedField.Operation != metav1.ManagedFieldsOperationApply { 207 continue 208 } 209 210 // Unset the managedField timestamp because managedFields are treated as atomic map. 211 managedField.Time = nil 212 213 // Unmarshal the managed fields into a map[string]interface{} 214 fieldsV1 := map[string]interface{}{} 215 if err := json.Unmarshal(managedField.FieldsV1.Raw, &fieldsV1); err != nil { 216 return errors.Wrap(err, "failed to unmarshal managed fields") 217 } 218 219 // Filter out the annotation ownership as well as leftover empty maps. 220 ssa.FilterIntent(&ssa.FilterIntentInput{ 221 Path: contract.Path{}, 222 Value: fieldsV1, 223 ShouldFilter: ssa.IsPathIgnored([]contract.Path{ 224 {"f:metadata", "f:annotations", "f:" + clusterv1.TopologyDryRunAnnotation}, 225 {"f:metadata", "f:annotations", "f:" + conversion.DataAnnotation}, 226 }), 227 }) 228 229 fieldsV1Raw, err := json.Marshal(fieldsV1) 230 if err != nil { 231 return errors.Wrap(err, "failed to marshal managed fields") 232 } 233 managedField.FieldsV1.Raw = fieldsV1Raw 234 235 newManagedFields = append(newManagedFields, managedField) 236 } 237 238 obj.SetManagedFields(newManagedFields) 239 240 return nil 241 }