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 }