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  }