github.com/oam-dev/kubevela@v1.9.11/pkg/utils/apply/patch.go (about)

     1  /*
     2  Copyright 2021 The KubeVela 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 apply
    18  
    19  import (
    20  	"encoding/json"
    21  	"time"
    22  
    23  	corev1 "k8s.io/api/core/v1"
    24  
    25  	"github.com/pkg/errors"
    26  	"k8s.io/apimachinery/pkg/api/meta"
    27  	"k8s.io/apimachinery/pkg/runtime"
    28  	"k8s.io/apimachinery/pkg/types"
    29  	"k8s.io/apimachinery/pkg/util/jsonmergepatch"
    30  	"k8s.io/apimachinery/pkg/util/mergepatch"
    31  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    32  	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
    33  	"sigs.k8s.io/controller-runtime/pkg/client"
    34  
    35  	"github.com/oam-dev/kubevela/pkg/oam"
    36  )
    37  
    38  var k8sScheme = runtime.NewScheme()
    39  var metadataAccessor = meta.NewAccessor()
    40  
    41  func init() {
    42  	_ = clientgoscheme.AddToScheme(k8sScheme)
    43  }
    44  
    45  // threeWayMergePatch creates a patch by computing a three way diff based on
    46  // its current state, modified state, and last-applied-state recorded in the
    47  // annotation.
    48  func threeWayMergePatch(currentObj, modifiedObj client.Object, a *applyAction) (client.Patch, error) {
    49  	current, err := json.Marshal(currentObj)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	original, err := getOriginalConfiguration(currentObj)
    54  	if err != nil {
    55  		return nil, err
    56  	}
    57  	modified, err := getModifiedConfiguration(modifiedObj, a.updateAnnotation)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	var patchType types.PatchType
    63  	var patchData []byte
    64  	var lookupPatchMeta strategicpatch.LookupPatchMeta
    65  
    66  	versionedObject, err := k8sScheme.New(currentObj.GetObjectKind().GroupVersionKind())
    67  	switch {
    68  	case runtime.IsNotRegisteredError(err):
    69  		// use JSONMergePatch for custom resources
    70  		// because StrategicMergePatch doesn't support custom resources
    71  		patchType = types.MergePatchType
    72  		preconditions := []mergepatch.PreconditionFunc{
    73  			mergepatch.RequireKeyUnchanged("apiVersion"),
    74  			mergepatch.RequireKeyUnchanged("kind"),
    75  			mergepatch.RequireMetadataKeyUnchanged("name")}
    76  		patchData, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, modified, current, preconditions...)
    77  		if err != nil {
    78  			return nil, err
    79  		}
    80  	case err != nil:
    81  		return nil, err
    82  	default:
    83  		// use StrategicMergePatch for K8s built-in resources
    84  		patchType = types.StrategicMergePatchType
    85  		lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
    86  		if err != nil {
    87  			return nil, err
    88  		}
    89  		patchData, err = strategicpatch.CreateThreeWayMergePatch(original, modified, current, lookupPatchMeta, true)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  	}
    94  	return client.RawPatch(patchType, patchData), nil
    95  }
    96  
    97  // addLastAppliedConfigAnnotation creates annotation recording current configuration as
    98  // original configuration for latter use in computing a three way diff
    99  func addLastAppliedConfigAnnotation(obj runtime.Object) error {
   100  	config, err := getModifiedConfiguration(obj, false)
   101  	if err != nil {
   102  		return err
   103  	}
   104  	annots, _ := metadataAccessor.Annotations(obj)
   105  	if annots == nil {
   106  		annots = make(map[string]string)
   107  	}
   108  	annots[oam.AnnotationLastAppliedConfig] = string(config)
   109  	return metadataAccessor.SetAnnotations(obj, annots)
   110  }
   111  
   112  // getModifiedConfiguration serializes the object into byte stream.
   113  // If `updateAnnotation` is true, it embeds the result as an annotation in the
   114  // modified configuration.
   115  func getModifiedConfiguration(obj runtime.Object, updateAnnotation bool) ([]byte, error) {
   116  	annots, err := metadataAccessor.Annotations(obj)
   117  	if err != nil {
   118  		return nil, errors.Wrap(err, "cannot access metadata.annotations")
   119  	}
   120  	if annots == nil {
   121  		annots = make(map[string]string)
   122  	}
   123  
   124  	original := annots[oam.AnnotationLastAppliedConfig]
   125  	// remove the annotation to avoid recursion
   126  	delete(annots, oam.AnnotationLastAppliedConfig)
   127  	_ = metadataAccessor.SetAnnotations(obj, annots)
   128  	// do not include an empty map
   129  	if len(annots) == 0 {
   130  		_ = metadataAccessor.SetAnnotations(obj, nil)
   131  	}
   132  
   133  	var modified []byte
   134  	modified, err = json.Marshal(obj)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	if updateAnnotation {
   140  		annots[oam.AnnotationLastAppliedConfig] = string(modified)
   141  		err = metadataAccessor.SetAnnotations(obj, annots)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  		modified, err = json.Marshal(obj)
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  	}
   150  
   151  	// restore original annotations back to the object
   152  	annots[oam.AnnotationLastAppliedConfig] = original
   153  	annots[oam.AnnotationLastAppliedTime] = time.Now().Format(time.RFC3339)
   154  	_ = metadataAccessor.SetAnnotations(obj, annots)
   155  	return modified, nil
   156  }
   157  
   158  // getOriginalConfiguration gets original configuration of the object
   159  // form the annotation, or nil if no annotation found.
   160  func getOriginalConfiguration(obj runtime.Object) ([]byte, error) {
   161  	annots, err := metadataAccessor.Annotations(obj)
   162  	if err != nil {
   163  		return nil, errors.Wrap(err, "cannot access metadata.annotations")
   164  	}
   165  	if annots == nil {
   166  		return nil, nil
   167  	}
   168  
   169  	oamOriginal, oamOk := annots[oam.AnnotationLastAppliedConfig]
   170  	if oamOk {
   171  		if oamOriginal == "-" || oamOriginal == "skip" {
   172  			return nil, nil
   173  		}
   174  		return []byte(oamOriginal), nil
   175  	}
   176  
   177  	kubectlOriginal, kubectlOK := annots[corev1.LastAppliedConfigAnnotation]
   178  	if kubectlOK {
   179  		return []byte(kubectlOriginal), nil
   180  	}
   181  	return nil, nil
   182  }
   183  
   184  func isEmptyPatch(patch client.Patch) bool {
   185  	if patch == nil {
   186  		return true
   187  	}
   188  	data, _ := patch.Data(nil)
   189  	return data != nil && string(data) == "{}"
   190  }