diff --git a/api/filters/replacement/replacement.go b/api/filters/replacement/replacement.go index bff8bba411..0cf269419f 100644 --- a/api/filters/replacement/replacement.go +++ b/api/filters/replacement/replacement.go @@ -119,13 +119,13 @@ func applyToNode(node *yaml.RNode, value *yaml.RNode, target *types.TargetSelect if target.Options != nil && target.Options.Create { t, err = node.Pipe(yaml.LookupCreate(value.YNode().Kind, fieldPath...)) } else { - t, err = node.Pipe(yaml.Lookup(fieldPath...)) + t, err = node.Pipe(&yaml.PathMatcher{Path: fieldPath}) } if err != nil { return err } if t != nil { - if err = setTargetValue(target.Options, t, value); err != nil { + if err = applyToOneNode(target.Options, t, value); err != nil { return err } } @@ -133,6 +133,27 @@ func applyToNode(node *yaml.RNode, value *yaml.RNode, target *types.TargetSelect return nil } +func applyToOneNode(options *types.FieldOptions, t *yaml.RNode, value *yaml.RNode) error { + if len(t.YNode().Content) == 0 { + if err := setTargetValue(options, t, value); err != nil { + return err + } + return nil + } + + for _, scalarNode := range t.YNode().Content { + if options != nil && options.Create { + return fmt.Errorf("cannot use create option in a multi-value target") + } + rn := yaml.NewRNode(scalarNode) + if err := setTargetValue(options, rn, value); err != nil { + return err + } + } + + return nil +} + func setTargetValue(options *types.FieldOptions, t *yaml.RNode, value *yaml.RNode) error { value = value.Copy() if options != nil && options.Delimiter != "" { @@ -152,7 +173,9 @@ func setTargetValue(options *types.FieldOptions, t *yaml.RNode, value *yaml.RNod } value.YNode().Value = strings.Join(tv, options.Delimiter) } + t.SetYNode(value.YNode()) + return nil } diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go index b06b074f4a..e5077b93f6 100644 --- a/api/filters/replacement/replacement_test.go +++ b/api/filters/replacement/replacement_test.go @@ -42,7 +42,7 @@ spec: - select: kind: Deployment name: deploy - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -95,7 +95,7 @@ spec: targets: - select: kind: Deployment - fieldPaths: + fieldPaths: - spec.template.spec.containers `, expected: `apiVersion: v1 @@ -328,7 +328,7 @@ spec: - select: kind: Deployment name: deploy1 - fieldPaths: + fieldPaths: - spec.template.spec.containers.[name=postgresdb].image `, expected: `apiVersion: v1 @@ -405,7 +405,7 @@ spec: targets: - select: version: v3 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: my-group-1/v1 @@ -492,7 +492,7 @@ spec: targets: - select: name: my-name-2 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `spec: @@ -582,7 +582,7 @@ spec: reject: - name: deploy2 - name: deploy3 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -662,7 +662,7 @@ spec: reject: - kind: Deployment name: my-name - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -731,7 +731,7 @@ spec: reject: - kind: Deployment - name: my-name - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -799,7 +799,7 @@ spec: - select: kind: Deployment name: deploy1 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image options: delimiter: ':' @@ -872,7 +872,7 @@ spec: - select: kind: Pod name: pod2 - fieldPaths: + fieldPaths: - spec.volumes.0.projected.sources.0.configMap.items.0.path options: delimiter: '/' @@ -948,7 +948,7 @@ spec: - select: kind: Pod name: pod1 - fieldPaths: + fieldPaths: - spec.volumes.0.projected.sources.0.configMap.items.0.path options: delimiter: '/' @@ -1024,7 +1024,7 @@ spec: - select: kind: Pod name: pod1 - fieldPaths: + fieldPaths: - spec.volumes.0.projected.sources.0.configMap.items.0.path options: delimiter: '/' @@ -1100,7 +1100,7 @@ spec: - select: kind: Pod name: pod1 - fieldPaths: + fieldPaths: - spec.volumes.0.projected.sources.0.configMap.items.0.path options: delimiter: '/' @@ -1176,7 +1176,7 @@ spec: - select: kind: Pod name: pod1 - fieldPaths: + fieldPaths: - spec.volumes.0.projected.sources.0.configMap.items.0.path options: delimiter: '/' @@ -1212,7 +1212,7 @@ metadata: targets: - select: name: deploy1 - fieldPaths: + fieldPaths: - spec.template.spec.containers options: create: true @@ -1223,7 +1223,7 @@ metadata: targets: - select: name: deploy2 - fieldPaths: + fieldPaths: - spec.template.spec.containers `, expected: `apiVersion: v1 @@ -1285,12 +1285,12 @@ spec: kind: Pod name: pod fieldPath: spec.containers - options: + options: delimiter: "/" targets: - select: kind: Deployment - fieldPaths: + fieldPaths: - spec.template.spec.containers `, expectedErr: "delimiter option can only be used with scalar nodes", @@ -1331,9 +1331,9 @@ spec: targets: - select: kind: Deployment - fieldPaths: + fieldPaths: - spec.template.spec.containers - options: + options: delimiter: "/" `, expectedErr: "delimiter option can only be used with scalar nodes", @@ -1354,7 +1354,7 @@ metadata: targets: - select: name: custom - fieldPaths: + fieldPaths: - metadata.annotations.[f.g.h/i-j] `, expected: `apiVersion: v1 @@ -1431,6 +1431,208 @@ spec: name: second version: latest property: second`, + }, + "one replacements target has multiple value": { + input: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sample-deploy + name: sample-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: sample-deploy + template: + metadata: + labels: + app: sample-deploy + spec: + containers: + - image: nginx + name: main + env: + - name: deployment-name + value: XXXXX + - name: foo + value: bar + - image: nginx + name: sidecar + env: + - name: deployment-name + value: YYYYY +`, + replacements: `replacements: +- source: + kind: Deployment + name: sample-deploy + fieldPath: metadata.name + targets: + - select: + kind: Deployment + fieldPaths: + - spec.template.spec.containers.[image=nginx].env.[name=deployment-name].value +`, + expected: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sample-deploy + name: sample-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: sample-deploy + template: + metadata: + labels: + app: sample-deploy + spec: + containers: + - image: nginx + name: main + env: + - name: deployment-name + value: sample-deploy + - name: foo + value: bar + - image: nginx + name: sidecar + env: + - name: deployment-name + value: sample-deploy`, + }, + "index contains '*' character": { + input: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sample-deploy + name: sample-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: sample-deploy + template: + metadata: + labels: + app: sample-deploy + spec: + containers: + - image: nginx + name: main + env: + - name: deployment-name + value: XXXXX +`, + replacements: `replacements: +- source: + kind: Deployment + name: sample-deploy + fieldPath: metadata.name + targets: + - select: + kind: Deployment + fieldPaths: + - spec.template.spec.containers.*.env.[name=deployment-name].value +`, + expected: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sample-deploy + name: sample-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: sample-deploy + template: + metadata: + labels: + app: sample-deploy + spec: + containers: + - image: nginx + name: main + env: + - name: deployment-name + value: sample-deploy`, + }, + "list index contains '*' character": { + input: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sample-deploy + name: sample-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: sample-deploy + template: + metadata: + labels: + app: sample-deploy + spec: + containers: + - image: nginx + name: main + env: + - name: deployment-name + value: XXXXX + - name: foo + value: bar + - image: nginx + name: sidecar + env: + - name: deployment-name + value: YYYYY +`, + replacements: `replacements: +- source: + kind: Deployment + name: sample-deploy + fieldPath: metadata.name + targets: + - select: + kind: Deployment + fieldPaths: + - spec.template.spec.containers.*.env.[name=deployment-name].value +`, + expected: `apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: sample-deploy + name: sample-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: sample-deploy + template: + metadata: + labels: + app: sample-deploy + spec: + containers: + - image: nginx + name: main + env: + - name: deployment-name + value: sample-deploy + - name: foo + value: bar + - image: nginx + name: sidecar + env: + - name: deployment-name + value: sample-deploy`, }, "multiple field paths in target": { input: `apiVersion: v1 @@ -1513,7 +1715,7 @@ spec: kind: Deployment metadata: name: pre-deploy - annotations: + annotations: internal.config.kubernetes.io/previousNames: deploy,deploy internal.config.kubernetes.io/previousKinds: CronJob,Deployment internal.config.kubernetes.io/previousNamespaces: default,default @@ -1535,7 +1737,7 @@ spec: - select: kind: Deployment name: deploy - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -1556,7 +1758,6 @@ spec: name: postgresdb `, }, - "replacement source.fieldPath does not exist": { input: `apiVersion: v1 kind: ConfigMap @@ -1628,7 +1829,7 @@ spec: targets: - select: annotationSelector: foo=bar-1 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -1702,7 +1903,7 @@ spec: targets: - select: labelSelector: foo=bar-1 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 @@ -1778,7 +1979,7 @@ spec: kind: Deployment reject: - labelSelector: foo=bar-2 - fieldPaths: + fieldPaths: - spec.template.spec.containers.1.image `, expected: `apiVersion: v1 diff --git a/kyaml/yaml/fns.go b/kyaml/yaml/fns.go index 43e54ce36c..22a9c14d9d 100644 --- a/kyaml/yaml/fns.go +++ b/kyaml/yaml/fns.go @@ -782,6 +782,19 @@ func IsListIndex(p string) bool { return strings.HasPrefix(p, "[") && strings.HasSuffix(p, "]") } +// IsIdxNumber returns true if p is an index number. +// e.g. 1 +func IsIdxNumber(p string) bool { + idx, err := strconv.Atoi(p) + return err == nil && idx >= 0 +} + +// IsWildcard returns true if p is matching every elements. +// e.g. "*" +func IsWildcard(p string) bool { + return p == "*" +} + // SplitIndexNameValue splits a lookup part Val index into the field name // and field value to match. // e.g. splits [name=nginx] into (name, nginx) diff --git a/kyaml/yaml/match.go b/kyaml/yaml/match.go index 149716063c..a7cdf83d82 100644 --- a/kyaml/yaml/match.go +++ b/kyaml/yaml/match.go @@ -5,6 +5,7 @@ package yaml import ( "regexp" + "strconv" "strings" ) @@ -42,9 +43,10 @@ type PathMatcher struct { // This is useful for if the nodes are to be printed in FlowStyle. StripComments bool - val *RNode - field string - matchRegex string + val *RNode + field string + matchRegex string + indexNumber int } func (p *PathMatcher) stripComments(n *Node) { @@ -79,14 +81,49 @@ func (p *PathMatcher) filter(rn *RNode) (*RNode, error) { return p.val, nil } + if IsIdxNumber(p.Path[0]) { + return p.doIndexSeq(rn) + } + if IsListIndex(p.Path[0]) { // match seq elements return p.doSeq(rn) } + + if IsWildcard(p.Path[0]) { + // match every elements (*) + return p.doMatchEvery(rn) + } // match a field return p.doField(rn) } +func (p *PathMatcher) doMatchEvery(rn *RNode) (*RNode, error) { + + if err := rn.VisitElements(p.visitEveryElem); err != nil { + return nil, err + } + + return p.val, nil +} + +func (p *PathMatcher) visitEveryElem(elem *RNode) error { + + fieldName := p.Path[0] + // recurse on the matching element + pm := &PathMatcher{Path: p.Path[1:]} + add, err := pm.filter(elem) + for k, v := range pm.Matches { + p.Matches[k] = v + } + if err != nil || add == nil { + return err + } + p.append(fieldName, add.Content()...) + + return nil +} + func (p *PathMatcher) doField(rn *RNode) (*RNode, error) { // lookup the field field, err := rn.Pipe(Get(p.Path[0])) @@ -102,6 +139,36 @@ func (p *PathMatcher) doField(rn *RNode) (*RNode, error) { return p.val, err } +// doIndexSeq iterates over a sequence and appends elements matching the index p.Val +func (p *PathMatcher) doIndexSeq(rn *RNode) (*RNode, error) { + // parse to index number + idx, err := strconv.Atoi(p.Path[0]) + if err != nil { + return nil, err + } + p.indexNumber = idx + + elements, err := rn.Elements() + if err != nil { + return nil, err + } + + // get target element + element := elements[idx] + + // recurse on the matching element + pm := &PathMatcher{Path: p.Path[1:]} + add, err := pm.filter(element) + for k, v := range pm.Matches { + p.Matches[k] = v + } + if err != nil || add == nil { + return nil, err + } + p.append("", add.Content()...) + return p.val, nil +} + // doSeq iterates over a sequence and appends elements matching the path regex to p.Val func (p *PathMatcher) doSeq(rn *RNode) (*RNode, error) { // parse the field + match pair diff --git a/kyaml/yaml/match_test.go b/kyaml/yaml/match_test.go index 70852398b3..abe51c6019 100644 --- a/kyaml/yaml/match_test.go +++ b/kyaml/yaml/match_test.go @@ -77,6 +77,12 @@ spec: {[]string{ "spec", "template", "spec", "containers", "[name=s.*]", "ports", "[containerPort=.*2]"}, ""}, + {[]string{ + "spec", "template", "spec", "containers", "*", "image"}, + "- nginx:1.7.9\n- sidecar:1.0.0\n"}, + {[]string{ + "spec", "template", "spec", "containers", "*", "ports", "*"}, + "- containerPort: 80\n- containerPort: 8081\n- containerPort: 9090\n"}, } for i, u := range updates { result, err := node.Pipe(&PathMatcher{Path: u.path})