/
patch.go
211 lines (175 loc) · 6.16 KB
/
patch.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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package patch implements a simple patching mechanism for k8s resources.
Paths are specified in the form a.b.c.[key:value].d.[list_entry_value], where:
- [key:value] selects a list entry in list c which contains an entry with key:value
- [list_entry_value] selects a list entry in list d which is a regex match of list_entry_value.
Some examples are given below. Given a resource:
kind: Deployment
metadata:
name: istio-citadel
namespace: istio-system
a:
b:
- name: n1
value: v1
- name: n2
list:
- "vv1"
- vv2=foo
values and list entries can be added, modified or deleted.
# MODIFY
1. set v1 to v1new
path: a.b.[name:n1].value
value: v1new
2. set vv1 to vv3
// Note the lack of quotes around vv1 (see NOTES below).
path: a.b.[name:n2].list.[vv1]
value: vv3
3. set vv2=foo to vv2=bar (using regex match)
path: a.b.[name:n2].list.[vv2]
value: vv2=bar
4. replace a port whose port was 15010
- path: spec.ports.[port:15010]
value:
port: 15020
name: grpc-xds
protocol: TCP
# DELETE
1. Delete container with name: n1
path: a.b.[name:n1]
2. Delete list value vv1
path: a.b.[name:n2].list.[vv1]
# ADD
1. Add vv3 to list
path: a.b.[name:n2].list.[1000]
value: vv3
Note: the value 1000 is an example. That value used in the patch should
be a value greater than number of the items in the list. Choose 1000 is
just an example which normally is greater than the most of the lists used.
2. Add new key:value to container name: n1
path: a.b.[name:n1]
value:
new_attr: v3
*NOTES*
- Due to loss of string quoting during unmarshaling, keys and values should not be string quoted, even if they appear
that way in the object being patched.
- [key:value] treats ':' as a special separator character. Any ':' in the key or value string must be escaped as \:.
*/
package patch
import (
"fmt"
"strings"
yaml2 "gopkg.in/yaml.v2"
"istio.io/api/operator/v1alpha1"
"istio.io/istio/operator/pkg/helm"
"istio.io/istio/operator/pkg/metrics"
"istio.io/istio/operator/pkg/object"
"istio.io/istio/operator/pkg/tpath"
"istio.io/istio/operator/pkg/util"
"istio.io/istio/pkg/log"
)
var scope = log.RegisterScope("patch", "patch")
// overlayMatches reports whether obj matches the overlay for either the default namespace or no namespace (cluster scope).
func overlayMatches(overlay *v1alpha1.K8SObjectOverlay, obj *object.K8sObject, defaultNamespace string) bool {
oh := obj.Hash()
if oh == object.Hash(overlay.Kind, defaultNamespace, overlay.Name) ||
oh == object.Hash(overlay.Kind, "", overlay.Name) {
return true
}
return false
}
// YAMLManifestPatch patches a base YAML in the given namespace with a list of overlays.
// Each overlay has the format described in the K8SObjectOverlay definition.
// It returns the patched manifest YAML.
func YAMLManifestPatch(baseYAML string, defaultNamespace string, overlays []*v1alpha1.K8SObjectOverlay) (string, error) {
var ret strings.Builder
var errs util.Errors
objs, err := object.ParseK8sObjectsFromYAMLManifest(baseYAML)
if err != nil {
return "", err
}
matches := make(map[*v1alpha1.K8SObjectOverlay]object.K8sObjects)
// Try to apply the defined overlays.
for _, obj := range objs {
oy, err := obj.YAML()
if err != nil {
errs = util.AppendErr(errs, fmt.Errorf("object to YAML error (%s) for base object: \n%s", err, obj.YAMLDebugString()))
continue
}
oys := string(oy)
for _, overlay := range overlays {
if overlayMatches(overlay, obj, defaultNamespace) {
matches[overlay] = append(matches[overlay], obj)
var errs2 util.Errors
oys, errs2 = applyPatches(obj, overlay.Patches)
errs = util.AppendErrs(errs, errs2)
}
}
if _, err := ret.WriteString(oys + helm.YAMLSeparator); err != nil {
errs = util.AppendErr(errs, fmt.Errorf("writeString: %s", err))
}
}
for _, overlay := range overlays {
// Each overlay should have exactly one match in the output manifest.
switch {
case len(matches[overlay]) == 0:
errs = util.AppendErr(errs, fmt.Errorf("overlay for %s:%s does not match any object in output manifest. Available objects are:\n%s",
overlay.Kind, overlay.Name, strings.Join(objs.Keys(), "\n")))
case len(matches[overlay]) > 1:
errs = util.AppendErr(errs, fmt.Errorf("overlay for %s:%s matches multiple objects in output manifest:\n%s",
overlay.Kind, overlay.Name, strings.Join(objs.Keys(), "\n")))
}
}
return ret.String(), errs.ToError()
}
// applyPatches applies the given patches against the given object. It returns the resulting patched YAML if successful,
// or a list of errors otherwise.
func applyPatches(base *object.K8sObject, patches []*v1alpha1.K8SObjectOverlay_PathValue) (outYAML string, errs util.Errors) {
bo := make(map[any]any)
by, err := base.YAML()
if err != nil {
return "", util.NewErrs(err)
}
// Use yaml2 specifically to allow interface{} as key which WritePathContext treats specially
err = yaml2.Unmarshal(by, &bo)
if err != nil {
return "", util.NewErrs(err)
}
for _, p := range patches {
v := p.Value.AsInterface()
if strings.TrimSpace(p.Path) == "" {
scope.Warnf("value=%s has empty path, skip\n", v)
continue
}
scope.Debugf("applying path=%s, value=%s\n", p.Path, v)
inc, _, err := tpath.GetPathContext(bo, util.PathFromString(p.Path), true)
if err != nil {
errs = util.AppendErr(errs, err)
metrics.ManifestPatchErrorTotal.Increment()
continue
}
err = tpath.WritePathContext(inc, v, false)
if err != nil {
errs = util.AppendErr(errs, err)
metrics.ManifestPatchErrorTotal.Increment()
}
}
oy, err := yaml2.Marshal(bo)
if err != nil {
return "", util.AppendErr(errs, err)
}
return string(oy), errs
}