/
patch_util.go
144 lines (132 loc) · 4.98 KB
/
patch_util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT license.
*/
package workv1alpha1
import (
"encoding/json"
"fmt"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/jsonmergepatch"
"k8s.io/apimachinery/pkg/util/mergepatch"
"k8s.io/apimachinery/pkg/util/strategicpatch"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
)
var builtinScheme = runtime.NewScheme()
var metadataAccessor = meta.NewAccessor()
func init() {
// we use this trick to check if a resource is k8s built-in
_ = clientgoscheme.AddToScheme(builtinScheme)
}
// threeWayMergePatch creates a patch by computing a three-way diff based on
// an object's current state, modified state, and last-applied-state recorded in its annotation.
func threeWayMergePatch(currentObj, manifestObj client.Object) (client.Patch, error) {
//TODO: see if we should use something like json.ConfigCompatibleWithStandardLibrary.Marshal to make sure that
// the json we created is compatible with the format that json merge patch requires.
current, err := json.Marshal(currentObj)
if err != nil {
return nil, err
}
original, err := getOriginalConfiguration(currentObj)
if err != nil {
return nil, err
}
manifest, err := json.Marshal(manifestObj)
if err != nil {
return nil, err
}
var patchType types.PatchType
var patchData []byte
var lookupPatchMeta strategicpatch.LookupPatchMeta
versionedObject, err := builtinScheme.New(currentObj.GetObjectKind().GroupVersionKind())
switch {
case runtime.IsNotRegisteredError(err):
// use JSONMergePatch for custom resources
// because StrategicMergePatch doesn't support custom resources
patchType = types.MergePatchType
preconditions := []mergepatch.PreconditionFunc{
mergepatch.RequireKeyUnchanged("apiVersion"),
mergepatch.RequireKeyUnchanged("kind"),
mergepatch.RequireMetadataKeyUnchanged("name")}
patchData, err = jsonmergepatch.CreateThreeWayJSONMergePatch(original, manifest, current, preconditions...)
if err != nil {
return nil, err
}
case err != nil:
return nil, err
default:
// use StrategicMergePatch for K8s built-in resources
patchType = types.StrategicMergePatchType
lookupPatchMeta, err = strategicpatch.NewPatchMetaFromStruct(versionedObject)
if err != nil {
return nil, err
}
patchData, err = strategicpatch.CreateThreeWayMergePatch(original, manifest, current, lookupPatchMeta, true)
if err != nil {
return nil, err
}
}
return client.RawPatch(patchType, patchData), nil
}
// setModifiedConfigurationAnnotation serializes the object into byte stream.
// If `updateAnnotation` is true, it embeds the result as an annotation in the
// modified configuration. If annotations size is greater than 256 kB it sets
// to empty string. It returns true if the annotation contains a value, returns
// false if the annotation is set to an empty string.
func setModifiedConfigurationAnnotation(obj runtime.Object) (bool, error) {
var modified []byte
annotations, err := metadataAccessor.Annotations(obj)
if err != nil {
return false, fmt.Errorf("cannot access metadata.annotations: %w", err)
}
if annotations == nil {
annotations = make(map[string]string)
}
// remove the annotation to avoid recursion
delete(annotations, lastAppliedConfigAnnotation)
// do not include an empty map
if len(annotations) == 0 {
_ = metadataAccessor.SetAnnotations(obj, nil)
} else {
_ = metadataAccessor.SetAnnotations(obj, annotations)
}
//TODO: see if we should use something like json.ConfigCompatibleWithStandardLibrary.Marshal to make sure that
// the produced json format is more three way merge friendly
modified, err = json.Marshal(obj)
if err != nil {
return false, err
}
// set the last applied annotation back
annotations[lastAppliedConfigAnnotation] = string(modified)
if err := validation.ValidateAnnotationsSize(annotations); err != nil {
klog.V(2).InfoS(fmt.Sprintf("setting last applied config annotation to empty, %s", err))
annotations[lastAppliedConfigAnnotation] = ""
return false, metadataAccessor.SetAnnotations(obj, annotations)
}
return true, metadataAccessor.SetAnnotations(obj, annotations)
}
// getOriginalConfiguration gets original configuration of the object
// form the annotation, or return an error if no annotation found.
func getOriginalConfiguration(obj runtime.Object) ([]byte, error) {
annots, err := metadataAccessor.Annotations(obj)
if err != nil {
klog.ErrorS(err, "cannot access metadata.annotations", "gvk", obj.GetObjectKind().GroupVersionKind())
return nil, err
}
// The func threeWayMergePatch can handle the case that the original is empty.
if annots == nil {
klog.Warning("object does not have annotation", "obj", obj)
return nil, nil
}
original, ok := annots[lastAppliedConfigAnnotation]
if !ok {
klog.Warning("object does not have lastAppliedConfigAnnotation", "obj", obj)
return nil, nil
}
return []byte(original), nil
}