From a40c74e545ec6094a5912a6a3a54c3063af21081 Mon Sep 17 00:00:00 2001 From: Natasha Sarkar Date: Fri, 2 Apr 2021 14:26:00 -0700 Subject: [PATCH] use field options to refine replacements --- api/filters/replacement/replacement.go | 58 +- api/filters/replacement/replacement_test.go | 577 ++++++++++++++++++++ api/types/replacement.go | 7 +- 3 files changed, 634 insertions(+), 8 deletions(-) diff --git a/api/filters/replacement/replacement.go b/api/filters/replacement/replacement.go index 11542df14..9cdb03bbf 100644 --- a/api/filters/replacement/replacement.go +++ b/api/filters/replacement/replacement.go @@ -66,18 +66,50 @@ func rejectId(rejects []*types.Selector, nodeId *types.KrmId) bool { func applyToNode(node *yaml.RNode, value *yaml.RNode, target *types.TargetSelector) error { for _, fp := range target.FieldPaths { - t, err := node.Pipe(yaml.Lookup(strings.Split(fp, ".")...)) + fieldPath := strings.Split(fp, ".") + var t *yaml.RNode + var err error + 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...)) + } if err != nil { return err } if t != nil { - // TODO (#3492): Use the field options to refine interpretation of the field - t.SetYNode(value.YNode()) + if err = setTargetValue(target.Options, t, value); err != nil { + return err + } } } return nil } +func setTargetValue(options *types.FieldOptions, t *yaml.RNode, value *yaml.RNode) error { + if options != nil && options.Delimiter != "" { + + if t.YNode().Kind != yaml.ScalarNode { + return fmt.Errorf("delimiter option can only be used with scalar nodes") + } + + tv := strings.Split(t.YNode().Value, options.Delimiter) + v := yaml.GetValue(value) + // TODO: Add a way to remove an element + switch { + case options.Index < 0: // prefix + tv = append([]string{v}, tv...) + case options.Index >= len(tv): // suffix + tv = append(tv, v) + default: // replace an element + tv[options.Index] = v + } + value.YNode().Value = strings.Join(tv, options.Delimiter) + } + t.SetYNode(value.YNode()) + return nil +} + func getReplacement(nodes []*yaml.RNode, r *types.Replacement) (*yaml.RNode, error) { source, err := selectSourceNode(nodes, r.Source) if err != nil { @@ -93,10 +125,28 @@ func getReplacement(nodes []*yaml.RNode, r *types.Replacement) (*yaml.RNode, err if err != nil { return nil, err } - // TODO (#3492): Use the field options to refine interpretation of the field + if !rn.IsNilOrEmpty() { + return getRefinedValue(r.Source.Options, rn) + } return rn, nil } +func getRefinedValue(options *types.FieldOptions, rn *yaml.RNode) (*yaml.RNode, error) { + if options == nil || options.Delimiter == "" { + return rn, nil + } + if rn.YNode().Kind != yaml.ScalarNode { + return nil, fmt.Errorf("delimiter option can only be used with scalar nodes") + } + value := strings.Split(yaml.GetValue(rn), options.Delimiter) + if options.Index >= len(value) || options.Index < 0 { + return nil, fmt.Errorf("options.index %d is out of bounds for value %s", options.Index, yaml.GetValue(rn)) + } + n := rn.Copy() + n.YNode().Value = value[options.Index] + return n, nil +} + // selectSourceNode finds the node that matches the selector, returning // an error if multiple or none are found func selectSourceNode(nodes []*yaml.RNode, selector *types.SourceSelector) (*yaml.RNode, error) { diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go index dcb004b72..d961a92f9 100644 --- a/api/filters/replacement/replacement_test.go +++ b/api/filters/replacement/replacement_test.go @@ -761,6 +761,583 @@ spec: name: postgresdb `, }, + "partial string replacement - replace": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + replacements: `replacements: +- source: + kind: Deployment + name: deploy2 + fieldPath: spec.template.spec.containers.0.image + options: + delimiter: ':' + targets: + - select: + kind: Deployment + name: deploy1 + fieldPaths: + - spec.template.spec.containers.1.image + options: + delimiter: ':' +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:1.8.0 + name: postgresdb +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + }, + "partial string replacement - prefix": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group/config +`, + replacements: `replacements: +- source: + kind: Pod + name: pod1 + fieldPath: spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 0 + targets: + - select: + kind: Pod + name: pod2 + fieldPaths: + - spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: -1 +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group/config +`, + }, + "partial string replacement - suffix": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group/config +`, + replacements: `replacements: +- source: + kind: Pod + name: pod2 + fieldPath: spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 1 + targets: + - select: + kind: Pod + name: pod1 + fieldPaths: + - spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 2 +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group/config +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group/config +`, + }, + "partial string replacement - last element": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group1 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group2 +`, + replacements: `replacements: +- source: + kind: Pod + name: pod2 + fieldPath: spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 0 + targets: + - select: + kind: Pod + name: pod1 + fieldPaths: + - spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 1 +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group2 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group2 +`, + }, + "partial string replacement - first element": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group1/config +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group2 +`, + replacements: `replacements: +- source: + kind: Pod + name: pod2 + fieldPath: spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 0 + targets: + - select: + kind: Pod + name: pod1 + fieldPaths: + - spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 0 +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group2/config +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group2 +`, + }, + "options.index out of bounds": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: my/group1 +--- +apiVersion: v1 +kind: Pod +metadata: + name: pod2 +spec: + volumes: + - projected: + sources: + - configMap: + name: myconfigmap + items: + - key: config + path: group2 +`, + replacements: `replacements: +- source: + kind: Pod + name: pod2 + fieldPath: spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: -1 + targets: + - select: + kind: Pod + name: pod1 + fieldPaths: + - spec.volumes.0.projected.sources.0.configMap.items.0.path + options: + delimiter: '/' + index: 1 +`, + expectedErr: "options.index -1 is out of bounds for value group2", + }, + "create": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +`, + replacements: `replacements: +- source: + kind: Pod + name: pod + fieldPath: spec.containers + targets: + - select: + name: deploy1 + fieldPaths: + - spec.template.spec.containers + options: + create: true +- source: + kind: Pod + name: pod + fieldPath: spec.containers + targets: + - select: + name: deploy2 + fieldPaths: + - spec.template.spec.containers +`, + expected: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +`, + }, + "complex type with delimiter in source": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: {} +`, + replacements: `replacements: +- source: + kind: Pod + name: pod + fieldPath: spec.containers + options: + delimiter: "/" + targets: + - select: + kind: Deployment + fieldPaths: + - spec.template.spec.containers +`, + expectedErr: "delimiter option can only be used with scalar nodes", + }, + "complex type with delimiter in target": { + input: `apiVersion: v1 +kind: Pod +metadata: + name: pod +spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy2 +spec: + template: + spec: + containers: {} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: {} +`, + replacements: `replacements: +- source: + kind: Pod + name: pod + fieldPath: spec.containers + targets: + - select: + kind: Deployment + fieldPaths: + - spec.template.spec.containers + options: + delimiter: "/" +`, + expectedErr: "delimiter option can only be used with scalar nodes", + }, } for tn, tc := range testCases { diff --git a/api/types/replacement.go b/api/types/replacement.go index ae7c7c31e..41bd848a3 100644 --- a/api/types/replacement.go +++ b/api/types/replacement.go @@ -43,8 +43,6 @@ type TargetSelector struct { } // FieldOptions refine the interpretation of FieldPaths. -// TODO (#3492): Implement use of these options, they are -// currently unused type FieldOptions struct { // Used to split/join the field. Delimiter string `json:"delimiter" yaml:"delimiter"` @@ -52,8 +50,9 @@ type FieldOptions struct { // Which position in the split to consider. Index int `json:"index" yaml:"index"` - // None, Base64, URL, Hex, etc. - Encoding string `json:"encoding" yaml:"index"` + // TODO (#3492): Implement use of this option + // None, Base64, URL, Hex, etc + Encoding string `json:"encoding" yaml:"encoding"` // If field missing, add it. Create bool `json:"create" yaml:"create"`