From 02e7589323128e8560d94642d781d426c03167d4 Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Mon, 16 Nov 2020 16:35:07 -0800 Subject: [PATCH 1/2] Function framework testutil support for functions which refer to local files Some functions may read local files using relative paths. Update the test framework to `cd` into the directory containing the config.yaml before running the function. --- .../frameworktestutil/frameworktestutil.go | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/kyaml/fn/framework/frameworktestutil/frameworktestutil.go b/kyaml/fn/framework/frameworktestutil/frameworktestutil.go index 785df9c4e..7e22e19d0 100644 --- a/kyaml/fn/framework/frameworktestutil/frameworktestutil.go +++ b/kyaml/fn/framework/frameworktestutil/frameworktestutil.go @@ -83,16 +83,26 @@ func (rc ResultsChecker) Assert(t *testing.T) bool { } func (rc ResultsChecker) compare(t *testing.T, path string) { + // cd into the directory so we can test functions that refer + // local files by relative paths + d, err := os.Getwd() + if !assert.NoError(t, err) { + t.FailNow() + } + defer func() { _ = os.Chdir(d) }() + if !assert.NoError(t, os.Chdir(path)) { + t.FailNow() + } + // make sure this directory contains test data - configPath := filepath.Join(path, rc.ConfigInputFilename) - _, err := os.Stat(configPath) + _, err = os.Stat(rc.ConfigInputFilename) if os.IsNotExist(err) { // missing input return } - args := []string{configPath} + args := []string{rc.ConfigInputFilename} - expectedOutput, expectedError := rc.getExpected(t, path) + expectedOutput, expectedError := rc.getExpected(t) if expectedError == "" && expectedOutput == "" { // missing expected return @@ -104,7 +114,7 @@ func (rc ResultsChecker) compare(t *testing.T, path string) { // run the test t.Run(path, func(t *testing.T) { if rc.InputFilenameGlob != "" { - inputs, err := filepath.Glob(filepath.Join(path, rc.InputFilenameGlob)) + inputs, err := filepath.Glob(rc.InputFilenameGlob) if !assert.NoError(t, err) { t.FailNow() } @@ -140,17 +150,17 @@ func (rc ResultsChecker) compare(t *testing.T, path string) { } // getExpected reads the expected results and error files -func (rc ResultsChecker) getExpected(t *testing.T, path string) (string, string) { +func (rc ResultsChecker) getExpected(t *testing.T) (string, string) { // read the expected results var expectedOutput, expectedError string if rc.ExpectedOutputFilename != "" { - _, err := os.Stat(filepath.Join(path, rc.ExpectedOutputFilename)) + _, err := os.Stat(rc.ExpectedOutputFilename) if !os.IsNotExist(err) && err != nil { t.FailNow() } if err == nil { // only read the file if it exists - b, err := ioutil.ReadFile(filepath.Join(path, rc.ExpectedOutputFilename)) + b, err := ioutil.ReadFile(rc.ExpectedOutputFilename) if !assert.NoError(t, err) { t.FailNow() } @@ -158,13 +168,13 @@ func (rc ResultsChecker) getExpected(t *testing.T, path string) (string, string) } } if rc.ExpectedErrorFilename != "" { - _, err := os.Stat(filepath.Join(path, rc.ExpectedErrorFilename)) + _, err := os.Stat(rc.ExpectedErrorFilename) if !os.IsNotExist(err) && err != nil { t.FailNow() } if err == nil { // only read the file if it exists - b, err := ioutil.ReadFile(filepath.Join(path, rc.ExpectedErrorFilename)) + b, err := ioutil.ReadFile(rc.ExpectedErrorFilename) if !assert.NoError(t, err) { t.FailNow() } From 8c4841c28f729f932c70a2b6672ed5deab6470c1 Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Tue, 17 Nov 2020 08:52:34 -0800 Subject: [PATCH 2/2] 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() + } + }) + } +}