diff --git a/api/filters/replacement/doc.go b/api/filters/replacement/doc.go new file mode 100644 index 00000000000..9d9357905d4 --- /dev/null +++ b/api/filters/replacement/doc.go @@ -0,0 +1,4 @@ +// Package replacement contains a kio.Filter implementation of the kustomize +// replacement transformer (accepts sources and looks for targets to replace +// their values with values from the sources). +package replacement diff --git a/api/filters/replacement/example_test.go b/api/filters/replacement/example_test.go new file mode 100644 index 00000000000..55c880f6fad --- /dev/null +++ b/api/filters/replacement/example_test.go @@ -0,0 +1,53 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package replacement + +import ( + "bytes" + "log" + "os" + + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func ExampleFilter() { + f := Filter{} + err := yaml.Unmarshal([]byte(` +replacements: +- source: + value: 99 + target: + objref: + kind: Foo + fieldrefs: + - spec.replicas`), &f) + if err != nil { + log.Fatal(err) + } + + err = kio.Pipeline{ + Inputs: []kio.Reader{&kio.ByteReader{Reader: bytes.NewBufferString(` +apiVersion: example.com/v1 +kind: Foo +metadata: + name: instance +spec: + replicas: 3 +`)}}, + Filters: []kio.Filter{f}, + Outputs: []kio.Writer{kio.ByteWriter{Writer: os.Stdout}}, + }.Execute() + if err != nil { + log.Fatal(err) + } + + // Output: + // apiVersion: example.com/v1 + // kind: Foo + // metadata: + // name: instance + // spec: + // replicas: 99 +} diff --git a/api/filters/replacement/replacement.go b/api/filters/replacement/replacement.go new file mode 100644 index 00000000000..db2ea8f9e04 --- /dev/null +++ b/api/filters/replacement/replacement.go @@ -0,0 +1,115 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package replacement + +import ( + "fmt" + "strings" + + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type Filter struct { + Replacements []types.Replacement +} + +const DefaultFieldRef = ".metadata.name" + +// Filter replaces values of targets with values from sources +// TODO (#3492): Connect this to a replacement transformer plugin +func (f Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + for _, r := range f.Replacements { + value, err := getReplacement(nodes, r.Source) + if err != nil { + return nil, err + } + nodes, err = applyReplacement(nodes, value, r.Target) + if err != nil { + return nil, err + } + } + return nodes, nil +} + +func applyReplacement(nodes []*yaml.RNode, value *yaml.RNode, target *types.ReplTarget) ([]*yaml.RNode, error) { + targets, err := getMatches(nodes, target.ObjRef) + if err != nil { + return nil, err + } + for _, n := range targets { + err := applyToNode(n, value, target) + if err != nil { + return nil, err + } + } + return nodes, nil +} + +func applyToNode(node *yaml.RNode, value *yaml.RNode, target *types.ReplTarget) error { + for i := range target.FieldRefs { + fieldref := strings.Split(target.FieldRefs[i], ".") + // TODO (#3492): Support using map keys in the fieldref (e.g. .spec.containers[name=nginx]) + t, err := node.Pipe(yaml.Lookup(fieldref...)) + if err != nil { + return err + } + if t != nil { + t.SetYNode(value.YNode()) + } + } + return nil +} + +func getReplacement(nodes []*yaml.RNode, source *types.ReplSource) (*yaml.RNode, error) { + if source.Value != "" { + if source.ObjRef != nil || source.FieldRef != "" { + return nil, fmt.Errorf("objref and fieldref must be empty if source value is provided") + } + return yaml.Parse(source.Value) + } + + matches, err := getMatches(nodes, source.ObjRef) + if err != nil { + return nil, err + } + if len(matches) > 1 { + return nil, fmt.Errorf("found more than one resources matching from %v", source.ObjRef) + } + if len(matches) == 0 { + return nil, fmt.Errorf("failed to find a resource matching from %v", source.ObjRef) + } + + fieldRef := source.FieldRef + if fieldRef == "" { + fieldRef = DefaultFieldRef + } + fieldRefSlice := strings.Split(fieldRef, ".") + + // TODO (#3492): Support using map keys in the fieldref (e.g. .spec.containers[name=nginx]) + rn, err := matches[0].Pipe(yaml.Lookup(fieldRefSlice...)) + if err != nil { + return nil, err + } + return rn, nil +} + +// getMatch finds the node that matches the objRef +func getMatches(nodes []*yaml.RNode, objRef *types.Target) ([]*yaml.RNode, error) { + var matches []*yaml.RNode + for _, n := range nodes { + ns, err := n.GetNamespace() + if err != nil { + return nil, err + } + apiVersion := yaml.GetValue(n.Field(yaml.APIVersionField).Value) + if (n.GetName() == objRef.Name || objRef.Name == "") && + (n.GetKind() == objRef.Kind || objRef.Kind == "") && + (ns == objRef.Namespace || objRef.Namespace == "") && + (apiVersion == objRef.ApiVersion() || objRef.ApiVersion() == "") { + matches = append(matches, n) + } + } + return matches, nil +} diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go new file mode 100644 index 00000000000..dcfcfd843e0 --- /dev/null +++ b/api/filters/replacement/replacement_test.go @@ -0,0 +1,350 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package replacement + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + filtertest "sigs.k8s.io/kustomize/api/testutils/filtertest" + "sigs.k8s.io/yaml" +) + +func TestFilter(t *testing.T) { + testCases := map[string]struct { + input string + replacements string + expected string + expectedErr bool + }{ + "simple": { + 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: + value: postgres:latest + target: + objref: + kind: Deployment + name: deploy1 + fieldrefs: + - spec.template.spec.containers.1.image +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:latest + 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 +`, + }, + "complex type": { + 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: + objref: + kind: Pod + name: pod + fieldref: spec.containers + target: + objref: + kind: Deployment + fieldrefs: + - 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: deploy2 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: busybox + name: myapp-container +`, + }, + "from ConfigMap": { + input: `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy + labels: + foo: bar +spec: + template: + metadata: + labels: + foo: bar + spec: + containers: + - name: command-demo-container + image: debian + command: ["printenv"] + args: + - HOSTNAME + - PORT + - name: busybox + image: busybox:latest + args: + - echo + - HOSTNAME + - PORT +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOSTNAME: example.com + PORT: 8080 +`, + replacements: `replacements: +- source: + objref: + kind: ConfigMap + name: cm + fieldref: data.HOSTNAME + target: + objref: + kind: Deployment + fieldrefs: + - spec.template.spec.containers.0.args.0 + - spec.template.spec.containers.1.args.1 +- source: + objref: + kind: ConfigMap + name: cm + fieldref: data.PORT + target: + objref: + kind: Deployment + fieldrefs: + - spec.template.spec.containers.0.args.1 + - spec.template.spec.containers.1.args.2 +`, + expected: `apiVersion: apps/v1 +kind: Deployment +metadata: + name: deploy + labels: + foo: bar +spec: + template: + metadata: + labels: + foo: bar + spec: + containers: + - name: command-demo-container + image: debian + command: ["printenv"] + args: + - example.com + - 8080 + - name: busybox + image: busybox:latest + args: + - echo + - example.com + - 8080 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: cm +data: + HOSTNAME: example.com + PORT: 8080 +`, + }, + "multiple matches for source objref": { + 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 +--- +apiVersion: v1 +kind: Deployment +metadata: + name: deploy3 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + replacements: `replacements: +- source: + objref: + kind: Deployment + fieldrefs: + - spec.template.spec.containers.1.image + target: + objref: + kind: Deployment + fieldrefs: + - spec.template.spec.containers.1.image +`, + expectedErr: true, + }, + "source has objref and value": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: postgres:1.8.0 + name: postgresdb +`, + replacements: `replacements: +- source: + value: postgres:latest + objref: + kind: Deployment + name: deploy + target: + objref: + kind: Deployment + name: deploy + fieldrefs: + - spec.template.spec.containers.1.image +`, + expectedErr: true, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + f := Filter{} + err := yaml.Unmarshal([]byte(tc.replacements), &f) + assert.NoError(t, err) + actual, err := filtertest.RunFilterE(t, tc.input, f) + assert.Equal(t, tc.expectedErr, err != nil) + if !tc.expectedErr && + !assert.Equal(t, strings.TrimSpace(tc.expected), strings.TrimSpace(actual)) { + t.FailNow() + } + }) + } +} diff --git a/api/types/replacement.go b/api/types/replacement.go index 57c2507ada7..4ef1f2fc5f8 100644 --- a/api/types/replacement.go +++ b/api/types/replacement.go @@ -22,6 +22,6 @@ type ReplSource struct { // ReplTarget defines where a substitution is to. type ReplTarget struct { - ObjRef *Selector `json:"objref,omitempty" yaml:"objref,omitempty"` - FieldRefs []string `json:"fieldrefs,omitempty" yaml:"fieldrefs,omitempty"` + ObjRef *Target `json:"objref,omitempty" yaml:"objref,omitempty"` + FieldRefs []string `json:"fieldrefs,omitempty" yaml:"fieldrefs,omitempty"` }