-
Notifications
You must be signed in to change notification settings - Fork 66
/
apply_setters.go
289 lines (247 loc) · 8.51 KB
/
apply_setters.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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
package applysetters
import (
"fmt"
"regexp"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const SetterCommentIdentifier = "# kpt-set: "
var _ kio.Filter = ApplySetters{}
// ApplySetters applies the setter values to the resource fields which are tagged
// by the setter reference comments
type ApplySetters struct {
// Setters holds the user provided values for all the setters
Setters []Setter `json:"setters,omitempty" yaml:"setters,omitempty"`
}
type Setter struct {
// Name is the name of the setter
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// Value is the input value for setter
Value string `json:"value,omitempty" yaml:"value,omitempty"`
}
// Filter implements Set as a yaml.Filter
func (as ApplySetters) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
for i := range nodes {
err := accept(&as, nodes[i])
if err != nil {
return nil, errors.Wrap(err)
}
}
return nodes, nil
}
/*
visitMapping takes input mapping node, and performs following steps
checks if the key node of the input mapping node has line comment with SetterCommentIdentifier
checks if the value node is of sequence node type
if yes to both, resolves the setter value for the setter name in the line comment
replaces the existing sequence node with the new values provided by user
e.g. for input of Mapping node
environments: # kpt-set: ${env}
- dev
- stage
For input ApplySetters [name: env, value: "[stage, prod]"], qthe yaml node is transformed to
environments: # kpt-set: ${env}
- stage
- prod
*/
func (as *ApplySetters) visitMapping(object *yaml.RNode) error {
return object.VisitFields(func(node *yaml.MapNode) error {
if node.IsNilOrEmpty() {
return nil
}
// the aim of this method is to apply-setter for sequence nodes
if node.Value.YNode().Kind != yaml.SequenceNode {
// return if it is not a sequence node
return nil
}
setterPattern := extractSetterPattern(node.Key)
if setterPattern == "" {
// the node is not tagged with setter pattern
return nil
}
if !shouldSet(setterPattern, as.Setters) {
// this means there is no intent from user to modify this setter tagged resources
return nil
}
// since this setter pattern is found on sequence node, make sure that it is
// not interpolation of setters, it should be simple setter e.g. ${environments}
if !validArraySetterPattern(setterPattern) {
return errors.Errorf("invalid setter pattern for array node: %q", setterPattern)
}
// get the setter value for the setter name in the comment
sv := setterValue(as.Setters, setterPattern)
// parse the setter value as yaml node
rn, err := yaml.Parse(sv)
if err != nil {
return err
}
// the setter value must parse as sequence node
if rn.YNode().Kind != yaml.SequenceNode {
return errors.Errorf("input to array setter must be an array of values, but found %q", sv)
}
node.Value.YNode().Content = rn.YNode().Content
node.Value.YNode().Style = yaml.FoldedStyle
return nil
})
}
/*
visitScalar accepts the input scalar node and performs following steps,
checks if the line comment of input scalar node has prefix SetterCommentIdentifier
resolves the setter values for the setter name in the comment
replaces the existing value of the scalar node with the new value
e.g.for input of scalar node 'nginx:1.7.1 # kpt-set: ${image}:${tag}' in the yaml node
apiVersion: v1
...
image: nginx:1.7.1 # kpt-set: ${image}:${tag}
and for input ApplySetters [[name: image, value: ubuntu], [name: tag, value: 1.8.0]]
The yaml node is transformed to
apiVersion: v1
...
image: ubuntu:1.8.0 # kpt-set: ${image}:${tag}
*/
func (as *ApplySetters) visitScalar(object *yaml.RNode) error {
if object.IsNilOrEmpty() {
return nil
}
if object.YNode().Kind != yaml.ScalarNode {
// return if it is not a scalar node
return nil
}
// perform a direct set of the field if it matches
setterPattern := extractSetterPattern(object)
if setterPattern == "" {
// the node is not tagged with setter pattern
return nil
}
curPattern := setterPattern
if !shouldSet(setterPattern, as.Setters) {
// this means there is no intent from user to modify this setter tagged resources
return nil
}
// replace the setter names in comment pattern with provided values
for _, setter := range as.Setters {
setterPattern = strings.ReplaceAll(
setterPattern,
fmt.Sprintf("${%s}", setter.Name),
fmt.Sprintf("%v", setter.Value),
)
}
// replace the remaining setter names in comment pattern with values derived from current
// field value, these values are not provided by user
currentSetterValues := currentSetterValues(curPattern, object.YNode().Value)
for setterName, setterValue := range currentSetterValues {
setterPattern = strings.ReplaceAll(
setterPattern,
fmt.Sprintf("${%s}", setterName),
fmt.Sprintf("%v", setterValue),
)
}
// check if there are unresolved setters and throw error
urs := unresolvedSetters(setterPattern)
if len(urs) > 0 {
return errors.Errorf("values for setters %v must be provided", urs)
}
object.YNode().Value = setterPattern
object.YNode().Tag = yaml.NodeTagEmpty
return nil
}
// shouldSet takes the setter pattern comment and setter values map and returns true
// iff at least one of the setter names in the pattern match with the setter names
// in input setterValues map
func shouldSet(pattern string, setters []Setter) bool {
for _, s := range setters {
if strings.Contains(pattern, fmt.Sprintf("${%s}", s.Name)) {
return true
}
}
return false
}
// currentSetterValues takes pattern and value and returns setter names to values
// derived using pattern matching
// e.g. pattern = foo-${image}:${tag}-bar, value = foo-nginx:1.7.1-bar
// returns {"image":"nginx", "tag":"1.7.1"}
func currentSetterValues(pattern, value string) map[string]string {
res := make(map[string]string)
// get all setter names enclosed in ${}
urs := unresolvedSetters(pattern)
// transform pattern replace pattern with named matching groups
// e.g. foo-${image}:${tag}-bar => foo-(?P<image>.*):(?P<tag>.*)-bar
for _, setterName := range urs {
pattern = strings.ReplaceAll(
pattern,
setterName,
fmt.Sprintf(`(?P<%s>.*)`, clean(setterName)))
}
r, err := regexp.Compile(pattern)
if err != nil {
// just return empty map if values can't be derived from pattern
return res
}
setterValues := r.FindStringSubmatch(value)
setterNames := r.SubexpNames()
if len(setterNames) != len(setterValues) {
// just return empty map if values can't be derived
return res
}
for i := range setterNames {
if i == 0 {
// first value is just entire value, so skip it
continue
}
if setterValues[i] == "" {
// if any of the value is unresolved return empty map
// and expect users to provide all values
return make(map[string]string)
}
res[setterNames[i]] = setterValues[i]
}
return res
}
// setterValue returns the value for the setter
func setterValue(setters []Setter, setterName string) string {
for _, setter := range setters {
if setter.Name == clean(setterName) {
return setter.Value
}
}
return ""
}
// extractSetterPattern extracts the setter pattern from the line comment of the
// input yaml RNode. If the the line comment doesn't contain SetterCommentIdentifier
// prefix, then it returns empty string
func extractSetterPattern(node *yaml.RNode) string {
if node == nil {
return ""
}
lineComment := node.YNode().LineComment
if !strings.HasPrefix(lineComment, SetterCommentIdentifier) {
return ""
}
return strings.TrimSpace(strings.TrimPrefix(lineComment, SetterCommentIdentifier))
}
// validArraySetterPattern returns true if the array setter pattern is valid
// pattern must not interpolation of setters, it should be simple setter e.g. ${environments}
func validArraySetterPattern(pattern string) bool {
return len(unresolvedSetters(pattern)) == 1 &&
strings.HasPrefix(pattern, "${") &&
strings.HasSuffix(pattern, "}")
}
// unresolvedSetters returns the list of values enclosed in ${} present within given
// pattern e.g. pattern = foo-${image}:${tag}-bar return ["${image}", "${tag}"]
func unresolvedSetters(pattern string) []string {
re := regexp.MustCompile(`\$\{([^}]*)\}`)
return re.FindAllString(pattern, -1)
}
// clean extracts value enclosed in ${}
func clean(input string) string {
input = strings.TrimSpace(input)
return strings.TrimSuffix(strings.TrimPrefix(input, "${"), "}")
}
// Decode decodes the input yaml node into Set struct
func Decode(rn *yaml.RNode, fcd *ApplySetters) {
for k, v := range rn.GetDataMap() {
fcd.Setters = append(fcd.Setters, Setter{Name: k, Value: v})
}
}