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  }