From 8c4841c28f729f932c70a2b6672ed5deab6470c1 Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Tue, 17 Nov 2020 08:52:34 -0800 Subject: [PATCH] Support for framework.Selector substitutions --- kyaml/fn/framework/example_test.go | 104 ++++++++++++++++ kyaml/fn/framework/patch.go | 64 +++++++++- kyaml/fn/framework/patch_test.go | 191 +++++++++++++++++++++++++++++ 3 files changed, 358 insertions(+), 1 deletion(-) diff --git a/kyaml/fn/framework/example_test.go b/kyaml/fn/framework/example_test.go index cd6c3af15..28548dfdb 100644 --- a/kyaml/fn/framework/example_test.go +++ b/kyaml/fn/framework/example_test.go @@ -6,6 +6,7 @@ package framework_test import ( "bytes" "fmt" + "os" "path/filepath" "text/template" @@ -786,3 +787,106 @@ metadata: // annotations: // a: b } + +func ExampleSelector_templatizeKinds() { + type api struct { + KindName string `yaml:"kindName"` + } + rl := &framework.ResourceList{ + FunctionConfig: &api{KindName: "Deployment"}, + Reader: bytes.NewBufferString(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: foo + namespace: default +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: bar + namespace: default +`), + Writer: os.Stdout, + } + if err := rl.Read(); err != nil { + panic(err) + } + + var err error + s := &framework.Selector{ + TemplatizeValues: true, + Kinds: []string{"{{ .KindName }}"}, + } + rl.Items, err = s.GetMatches(rl) + if err != nil { + panic(err) + } + + if err := rl.Write(); err != nil { + panic(err) + } + + // Output: + // apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // namespace: default + // annotations: + // config.kubernetes.io/index: '0' +} + +func ExampleSelector_templatizeAnnotations() { + type api struct { + Value string `yaml:"vaue"` + } + rl := &framework.ResourceList{ + FunctionConfig: &api{Value: "bar"}, + Reader: bytes.NewBufferString(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: foo + namespace: default + annotations: + key: foo +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar + namespace: default + annotations: + key: bar +`), + Writer: os.Stdout, + } + if err := rl.Read(); err != nil { + panic(err) + } + + var err error + s := &framework.Selector{ + TemplatizeValues: true, + Annotations: map[string]string{"key": "{{ .Value }}"}, + } + rl.Items, err = s.GetMatches(rl) + if err != nil { + panic(err) + } + + if err := rl.Write(); err != nil { + panic(err) + } + + // Output: + // apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: bar + // namespace: default + // annotations: + // key: bar + // config.kubernetes.io/index: '1' +} diff --git a/kyaml/fn/framework/patch.go b/kyaml/fn/framework/patch.go index c8654bf31..2af0a7774 100644 --- a/kyaml/fn/framework/patch.go +++ b/kyaml/fn/framework/patch.go @@ -131,6 +131,10 @@ type Selector struct { // matches contains a list of matching reosurces. matches []*yaml.RNode + + // TemplatizeValues if set to true will parse the selector values as templates + // and execute them with the functionConfig + TemplatizeValues bool } // GetMatches returns them matching resources from rl @@ -141,7 +145,65 @@ func (s *Selector) GetMatches(rl *ResourceList) ([]*yaml.RNode, error) { return s.matches, nil } +// templatize templatizes the value +func (s *Selector) templatize(value string, api interface{}) (string, error) { + t, err := template.New("kinds").Parse(value) + if err != nil { + return "", errors.Wrap(err) + } + var b bytes.Buffer + err = t.Execute(&b, api) + if err != nil { + return "", errors.Wrap(err) + } + return b.String(), nil +} + +func (s *Selector) templatizeSlice(values []string, api interface{}) error { + var err error + for i := range values { + values[i], err = s.templatize(values[i], api) + if err != nil { + return err + } + } + return nil +} + +func (s *Selector) templatizeMap(values map[string]string, api interface{}) error { + var err error + for k := range values { + values[k], err = s.templatize(values[k], api) + if err != nil { + return err + } + } + return nil +} + func (s *Selector) init(rl *ResourceList) error { + if s.TemplatizeValues { + // templatize the selector values from the input configuration + if err := s.templatizeSlice(s.Kinds, rl.FunctionConfig); err != nil { + return err + } + if err := s.templatizeSlice(s.APIVersions, rl.FunctionConfig); err != nil { + return err + } + if err := s.templatizeSlice(s.Names, rl.FunctionConfig); err != nil { + return err + } + if err := s.templatizeSlice(s.Namespaces, rl.FunctionConfig); err != nil { + return err + } + if err := s.templatizeMap(s.Labels, rl.FunctionConfig); err != nil { + return err + } + if err := s.templatizeMap(s.Annotations, rl.FunctionConfig); err != nil { + return err + } + } + // index the selectors s.matches = nil s.kindsSet = sets.String{} @@ -153,7 +215,7 @@ func (s *Selector) init(rl *ResourceList) error { s.namespaceSet = sets.String{} s.namespaceSet.Insert(s.Namespaces...) - //check each resource that matches the patch selector + // check each resource that matches the patch selector for i := range rl.Items { if match, err := s.isMatch(rl.Items[i]); err != nil { return err diff --git a/kyaml/fn/framework/patch_test.go b/kyaml/fn/framework/patch_test.go index 467ee52b6..6860edfc5 100644 --- a/kyaml/fn/framework/patch_test.go +++ b/kyaml/fn/framework/patch_test.go @@ -4,10 +4,13 @@ package framework_test import ( + "bytes" + "strings" "testing" "text/template" "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/kyaml/fn/framework" "sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil" "sigs.k8s.io/kustomize/kyaml/testutil" @@ -79,3 +82,191 @@ metadata: frameworktestutil.ResultsChecker{Command: cmdFn, TestDataDirectory: "patchtestdata"}.Assert(t) } + +func TestSelector(t *testing.T) { + type Test struct { + // Name is the name of the test + Name string + + // Fn configures the selector + Fn func(*framework.Selector) + + // ValueFoo is the value to substitute to select the foo resource + ValueFoo string + + // ValueBar is the value to substitute to select the bar resource + ValueBar string + + // Value is set by the test to either ValueFoo or ValueBar + // and substituted into the selector + Value string + } + tests := []Test{ + // Test the name template + { + Name: "names", + Fn: func(s *framework.Selector) { + s.Names = []string{"{{ .Value }}"} + }, + ValueFoo: "foo", + ValueBar: "bar", + }, + + // Test the kind template + { + Name: "kinds", + Fn: func(s *framework.Selector) { + s.Kinds = []string{"{{ .Value }}"} + }, + ValueFoo: "StatefulSet", + ValueBar: "Deployment", + }, + + // Test the apiVersion template + { + Fn: func(s *framework.Selector) { + s.APIVersions = []string{"{{ .Value }}"} + }, + ValueFoo: "apps/v1beta1", + ValueBar: "apps/v1", + }, + + // Test the namespace template + { + Name: "namespaces", + Fn: func(s *framework.Selector) { + s.Namespaces = []string{"{{ .Value }}"} + }, + ValueFoo: "foo-default", + ValueBar: "bar-default", + }, + + // Test the annotations template + { + Name: "annotations", + Fn: func(s *framework.Selector) { + s.Annotations = map[string]string{"key": "{{ .Value }}"} + }, + ValueFoo: "foo-a", + ValueBar: "bar-a", + }, + + // Test the labels template + { + Name: "labels", + Fn: func(s *framework.Selector) { + s.Labels = map[string]string{"key": "{{ .Value }}"} + }, + ValueFoo: "foo-l", + ValueBar: "bar-l", + }, + } + + // input is the input resources that are selected + input := ` +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: foo + namespace: foo-default + annotations: + key: foo-a + labels: + key: foo-l +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar + namespace: bar-default + annotations: + key: bar-a + labels: + key: bar-l +` + // expectedFoo is the expected output when the FooValue is substituted + expectedFoo := ` +apiVersion: apps/v1beta1 +kind: StatefulSet +metadata: + name: foo + namespace: foo-default + annotations: + key: foo-a + config.kubernetes.io/index: '0' + labels: + key: foo-l +` + // expectedFoo is the expected output when the BarValue is substituted + expectedBar := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar + namespace: bar-default + annotations: + key: bar-a + config.kubernetes.io/index: '1' + labels: + key: bar-l +` + + // Run the tests by substituting the FooValues + var err error + for i := range tests { + test := tests[i] + t.Run(tests[i].Name+"-foo", func(t *testing.T) { + test.Value = test.ValueFoo + var out bytes.Buffer + rl := &framework.ResourceList{ + FunctionConfig: test, + Reader: bytes.NewBufferString(input), + Writer: &out, + } + if !assert.NoError(t, rl.Read()) { + t.FailNow() + } + s := &framework.Selector{TemplatizeValues: true} + test.Fn(s) + rl.Items, err = s.GetMatches(rl) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.NoError(t, rl.Write()) { + t.FailNow() + } + if !assert.Equal(t, strings.TrimSpace(expectedFoo), strings.TrimSpace(out.String())) { + t.FailNow() + } + }) + } + + // Run the tests by substituting the BarValues + for i := range tests { + test := tests[i] + t.Run(tests[i].Name+"-bar", func(t *testing.T) { + test.Value = test.ValueBar + var out bytes.Buffer + rl := &framework.ResourceList{ + FunctionConfig: test, + Reader: bytes.NewBufferString(input), + Writer: &out, + } + if !assert.NoError(t, rl.Read()) { + t.FailNow() + } + s := &framework.Selector{TemplatizeValues: true} + test.Fn(s) + rl.Items, err = s.GetMatches(rl) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.NoError(t, rl.Write()) { + t.FailNow() + } + if !assert.Equal(t, strings.TrimSpace(expectedBar), strings.TrimSpace(out.String())) { + t.FailNow() + } + }) + } +}