diff --git a/api/filters/replacement/doc.go b/api/filters/replacement/doc.go new file mode 100644 index 00000000000..73211494f16 --- /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 form 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..15ddec0ca8d --- /dev/null +++ b/api/filters/replacement/example_test.go @@ -0,0 +1,56 @@ +// Copyright 2020 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package replacement + +import ( + "bytes" + "log" + "os" + + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func ExampleFilter() { + f := Filter{ + Replacements: []types.Replacement{}, + } + 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..1cc47c8b42a --- /dev/null +++ b/api/filters/replacement/replacement.go @@ -0,0 +1,111 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package replacement + +import ( + "fmt" + "strings" + + "sigs.k8s.io/kustomize/api/provider" + "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type Filter struct { + Replacements []types.Replacement +} + +var _ kio.Filter = Filter{} + +// 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) { + var result []*yaml.RNode + // TODO (#3492): Move getReplacement to the replacement transformer plugin + // and avoid creating a new resmap here + rf := provider.NewDefaultDepProvider().GetResourceFactory() + m, err := resmap.NewFactory(rf).NewResMapFromRNodeSlice(nodes) + if err != nil { + return nil, err + } + for i := range nodes { + r, err := applyReplacements(f.Replacements, nodes[i], m) + if err != nil { + return nil, err + } + if r != nil { + result = append(result, r) + } + } + return result, nil +} + +func applyReplacements(replacements []types.Replacement, node *yaml.RNode, + m resmap.ResMap) (*yaml.RNode, error) { + for _, r := range replacements { + value, err := getReplacement(m, r.Source) + if err != nil { + return node, err + } + node, err := applyReplacement(r, node, value) + if err != nil { + return node, err + } + } + return node, nil +} + +func applyReplacement(r types.Replacement, node *yaml.RNode, value *yaml.RNode) (*yaml.RNode, error) { + for i := range r.Target.FieldRefs { + fieldref := strings.Split(r.Target.FieldRefs[i], ".") + // TODO (#3492): Support using map keys in the fieldref (e.g. .spec.containers[name=nginx]) + target, err := node.Pipe(yaml.Lookup(fieldref...)) + if err != nil { + return node, err + } + if target == nil { + continue + } + target.SetYNode(value.YNode()) + } + return node, nil +} + +// TODO (#3492): Move this to the replacement transformer plugin, and have it pass the +// source value to the filter. The plugin will already have a ResMap so it will not +// need to convert the nodes to a ResMap. +func getReplacement(m resmap.ResMap, source *types.ReplSource) (*yaml.RNode, error) { + if source.Value != "" { + return yaml.Parse(source.Value) + } + objRef := source.ObjRef + fieldRef := source.FieldRef + s := types.Selector{ + Gvk: objRef.Gvk, + Name: objRef.Name, + Namespace: objRef.Namespace, + } + resources, err := m.Select(s) + if err != nil { + return nil, err + } + if len(resources) > 1 { + return nil, fmt.Errorf("found more than one resources matching from %v", resources) + } + if len(resources) == 0 { + return nil, fmt.Errorf("failed to find one resource matching from %v", objRef) + } + if fieldRef == "" { + fieldRef = ".metadata.name" + } + fieldRefSlice := strings.Split(fieldRef, ".") + // TODO (#3492): Support using map keys in the fieldref (e.g. .spec.containers[name=nginx]) + rn, err := resources[0].AsRNode().Pipe(yaml.Lookup(fieldRefSlice...)) + if err != nil { + return nil, err + } + return rn, nil +} diff --git a/api/filters/replacement/replacement_test.go b/api/filters/replacement/replacement_test.go new file mode 100644 index 00000000000..6f86f6d7606 --- /dev/null +++ b/api/filters/replacement/replacement_test.go @@ -0,0 +1,265 @@ +// 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/kustomize/api/types" + "sigs.k8s.io/yaml" +) + +func TestFilter(t *testing.T) { + testCases := map[string]struct { + input string + replacements string + expected string + }{ + "simple": { + input: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:latest + name: nginx-latest + - image: foobar:1 + name: replaced-with-digest + - image: postgres:1.8.0 + name: postgresdb + initContainers: + - image: nginx + name: nginx-notag + - image: nginx@sha256:111111111111111111 + name: nginx-sha256 + - image: alpine:1.8.0 + name: init-alpine +`, + replacements: `replacements: +- source: + value: postgres:latest + target: + objref: + kind: Deployment + fieldrefs: + - spec.template.spec.containers.3.image +`, + expected: `apiVersion: v1 +kind: Deployment +metadata: + name: deploy1 +spec: + template: + spec: + containers: + - image: nginx:1.7.9 + name: nginx-tagged + - image: nginx:latest + name: nginx-latest + - image: foobar:1 + name: replaced-with-digest + - image: postgres:latest + name: postgresdb + initContainers: + - image: nginx + name: nginx-notag + - image: nginx@sha256:111111111111111111 + name: nginx-sha256 + - image: alpine:1.8.0 + name: init-alpine +`, + }, + + "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 +`, + }, + } + + for tn, tc := range testCases { + t.Run(tn, func(t *testing.T) { + f := Filter{ + Replacements: []types.Replacement{}, + } + err := yaml.Unmarshal([]byte(tc.replacements), &f) + assert.NoError(t, err) + if !assert.Equal(t, + strings.TrimSpace(tc.expected), + strings.TrimSpace( + filtertest.RunFilter(t, tc.input, f))) { + t.FailNow() + } + }) + } +}