From fd70213ca2556a640857ac0a51c10ae98874bb03 Mon Sep 17 00:00:00 2001 From: Phillip Wittrock Date: Sun, 3 May 2020 15:43:50 -0700 Subject: [PATCH] Create shared functions filter library --- kyaml/fn/runtime/runtimeutil/doc.go | 5 + kyaml/fn/runtime/runtimeutil/example_test.go | 1 + kyaml/fn/runtime/runtimeutil/runtimeutil.go | 198 ++++ .../runtime/runtimeutil/runtimeutil_test.go | 998 ++++++++++++++++++ kyaml/fn/runtime/runtimeutil/types.go | 5 + 5 files changed, 1207 insertions(+) create mode 100644 kyaml/fn/runtime/runtimeutil/doc.go create mode 100644 kyaml/fn/runtime/runtimeutil/example_test.go create mode 100644 kyaml/fn/runtime/runtimeutil/runtimeutil.go create mode 100644 kyaml/fn/runtime/runtimeutil/runtimeutil_test.go create mode 100644 kyaml/fn/runtime/runtimeutil/types.go diff --git a/kyaml/fn/runtime/runtimeutil/doc.go b/kyaml/fn/runtime/runtimeutil/doc.go new file mode 100644 index 000000000..89f9036a4 --- /dev/null +++ b/kyaml/fn/runtime/runtimeutil/doc.go @@ -0,0 +1,5 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package runtimeutil contains libraries for implementing function runtimes. +package runtimeutil diff --git a/kyaml/fn/runtime/runtimeutil/example_test.go b/kyaml/fn/runtime/runtimeutil/example_test.go new file mode 100644 index 000000000..7b7fa960b --- /dev/null +++ b/kyaml/fn/runtime/runtimeutil/example_test.go @@ -0,0 +1 @@ +package runtimeutil diff --git a/kyaml/fn/runtime/runtimeutil/runtimeutil.go b/kyaml/fn/runtime/runtimeutil/runtimeutil.go new file mode 100644 index 000000000..d2321bf01 --- /dev/null +++ b/kyaml/fn/runtime/runtimeutil/runtimeutil.go @@ -0,0 +1,198 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package runtimeutil + +import ( + "bytes" + "io" + "io/ioutil" + "path" + "strings" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/kio/kioutil" + + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// FunctionFilter wraps another filter to be invoked in the context of a function. +// FunctionFilter manages scoping the function, deferring failures, and saving results +// to files. +type FunctionFilter struct { + // Run implements the function. + Run func(reader io.Reader, writer io.Writer) error + + // FunctionConfig is passed to the function through ResourceList.functionConfig. + FunctionConfig *yaml.RNode `yaml:"functionConfig,omitempty"` + + // GlobalScope explicitly scopes the function to all input resources rather than only those + // resources scoped to it by path. + GlobalScope bool + + // ResultsFile is the file to write function ResourceList.results to. + // If unset, results will not be written. + ResultsFile string + + // DeferFailure will cause the Filter to return a nil error even if Run returns an error. + // The Run error will be available through GetExit(). + DeferFailure bool + + // results saves the results emitted from Run + results *yaml.RNode + + // exit saves the error returned from Run + exit error +} + +// GetExit returns the error from Run +func (c FunctionFilter) GetExit() error { + return c.exit +} + +// functionsDirectoryName is keyword directory name for functions scoped 1 directory higher +const functionsDirectoryName = "functions" + +// getFunctionScope returns the path of the directory containing the function config, +// or its parent directory if the base directory is named "functions" +func (c *FunctionFilter) getFunctionScope() (string, error) { + m, err := c.FunctionConfig.GetMeta() + if err != nil { + return "", errors.Wrap(err) + } + p, found := m.Annotations[kioutil.PathAnnotation] + if !found { + return "", nil + } + + functionDir := path.Clean(path.Dir(p)) + + if path.Base(functionDir) == functionsDirectoryName { + // the scope of functions in a directory called "functions" is 1 level higher + // this is similar to how the golang "internal" directory scoping works + functionDir = path.Dir(functionDir) + } + return functionDir, nil +} + +// scope partitions the input nodes into 2 slices. The first slice contains only Resources +// which are scoped under dir, and the second slice contains the Resources which are not. +func (c *FunctionFilter) scope(dir string, nodes []*yaml.RNode) ([]*yaml.RNode, []*yaml.RNode, error) { + // scope container filtered Resources to Resources under that directory + var input, saved []*yaml.RNode + if c.GlobalScope { + return nodes, nil, nil + } + + // global function + if dir == "" || dir == "." { + return nodes, nil, nil + } + + // identify Resources read from directories under the function configuration + for i := range nodes { + m, err := nodes[i].GetMeta() + if err != nil { + return nil, nil, err + } + p, found := m.Annotations[kioutil.PathAnnotation] + if !found { + // this Resource isn't scoped under the function -- don't know where it came from + // consider it out of scope + saved = append(saved, nodes[i]) + continue + } + + resourceDir := path.Clean(path.Dir(p)) + if path.Base(resourceDir) == functionsDirectoryName { + // Functions in the `functions` directory are scoped to + // themselves, and should see themselves as input + resourceDir = path.Dir(resourceDir) + } + if !strings.HasPrefix(resourceDir, dir) { + // this Resource doesn't fall under the function scope if it + // isn't in a subdirectory of where the function lives + saved = append(saved, nodes[i]) + continue + } + + // this input is scoped under the function + input = append(input, nodes[i]) + } + + return input, saved, nil +} + +func (c *FunctionFilter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + in := &bytes.Buffer{} + out := &bytes.Buffer{} + + // only process Resources scoped to this function, save the others + functionDir, err := c.getFunctionScope() + if err != nil { + return nil, err + } + input, saved, err := c.scope(functionDir, nodes) + if err != nil { + return nil, err + } + + // write the input + err = kio.ByteWriter{ + WrappingAPIVersion: kio.ResourceListAPIVersion, + WrappingKind: kio.ResourceListKind, + Writer: in, + KeepReaderAnnotations: true, + FunctionConfig: c.FunctionConfig}.Write(input) + if err != nil { + return nil, err + } + + // capture the command stdout for the return value + r := &kio.ByteReader{Reader: out} + + // don't exit immediately if the function fails -- write out the validation + c.exit = c.Run(in, out) + + output, err := r.Read() + if err != nil { + return nil, err + } + + if err := c.doResults(r); err != nil { + return nil, err + } + + if c.exit != nil && !c.DeferFailure { + return append(output, saved...), c.exit + } + + // annotate any generated Resources with a path and index if they don't already have one + if err := kioutil.DefaultPathAnnotation(functionDir, output); err != nil { + return nil, err + } + + // emit both the Resources output from the function, and the out-of-scope Resources + // which were not provided to the function + return append(output, saved...), nil +} + +func (c *FunctionFilter) doResults(r *kio.ByteReader) error { + // Write the results to a file if configured to do so + if c.ResultsFile != "" && r.Results != nil { + results, err := r.Results.String() + if err != nil { + return err + } + err = ioutil.WriteFile(c.ResultsFile, []byte(results), 0600) + if err != nil { + return err + } + } + + if r.Results != nil { + c.results = r.Results + } + return nil +} diff --git a/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go b/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go new file mode 100644 index 000000000..dc12fa762 --- /dev/null +++ b/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go @@ -0,0 +1,998 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package runtimeutil + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type testRun struct { + err error + expectedInput string + output string + t *testing.T +} + +func (r testRun) run(reader io.Reader, writer io.Writer) error { + if r.expectedInput != "" { + input, err := ioutil.ReadAll(reader) + if !assert.NoError(r.t, err) { + r.t.FailNow() + } + + // verify input matches expected + if !assert.Equal(r.t, r.expectedInput, string(input)) { + r.t.FailNow() + } + } + + _, err := writer.Write([]byte(r.output)) + if !assert.NoError(r.t, err) { + r.t.FailNow() + } + + return r.err +} + +func TestFunctionFilter_Filter(t *testing.T) { + var tests = []struct { + run testRun + name string + input []string + functionConfig string + expectedOutput []string + expectedError string + expectedSavedError string + expectedResults string + noMakeResultsFile bool + instance FunctionFilter + }{ + // verify that resources emitted from the function have a file path defaulted + // if none already exists + { + name: "default_file_path_annotation", + run: testRun{ + output: ` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo +- apiVersion: v1 + kind: Service + metadata: + name: service-foo +`, + }, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'deployment_deployment-foo.yaml' +`, + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'service_service-foo.yaml' +`, + }, + }, + + // verify that resources emitted from the function do not have a file path defaulted + // if one already exists + { + name: "no_default_file_path_annotation", + run: testRun{ + output: ` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo.yaml' +`, + }, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'deployment_deployment-foo.yaml' +`, + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo.yaml' +`, + }, + }, + + // verify the FunctionFilter correctly writes the inputs and reads the outputs + // of Run + { + name: "write_read", + run: testRun{ + output: ` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo.yaml' +- apiVersion: v1 + kind: ConfigMap + metadata: + name: configmap-foo + annotations: + config.kubernetes.io/path: 'foo.yaml' +`, + }, + input: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo +`, + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo +`, + }, + expectedOutput: []string{` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo.yaml' +`, + ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: configmap-foo + annotations: + config.kubernetes.io/path: 'foo.yaml' +`, + }, + }, + + // verify that the results file is written + // + { + name: "write_results_file", + run: testRun{ + output: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo +- apiVersion: v1 + kind: Service + metadata: + name: service-foo +results: +- apiVersion: config.k8s.io/v1alpha1 + kind: ObjectError + name: "some-validator" + items: + - type: error + message: "some message" + resourceRef: + apiVersion: apps/v1 + kind: Deployment + name: foo + namespace: bar + file: + path: deploy.yaml + index: 0 + field: + path: "spec.template.spec.containers[3].resources.limits.cpu" + currentValue: "200" + suggestedValue: "2" +`, + }, + expectedOutput: []string{` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'deployment_deployment-foo.yaml' +`, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'service_service-foo.yaml' +`, + }, + expectedResults: ` +- apiVersion: config.k8s.io/v1alpha1 + kind: ObjectError + name: "some-validator" + items: + - type: error + message: "some message" + resourceRef: + apiVersion: apps/v1 + kind: Deployment + name: foo + namespace: bar + file: + path: deploy.yaml + index: 0 + field: + path: "spec.template.spec.containers[3].resources.limits.cpu" + currentValue: "200" + suggestedValue: "2" +`, + }, + + // verify that the results file is written for functions that exist non-0 + // and the FunctionFilter returns the error + { + name: "write_results_file_function_exit_non_0", + expectedError: "failed", + run: testRun{ + err: fmt.Errorf("failed"), + output: ` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo +- apiVersion: v1 + kind: Service + metadata: + name: service-foo +results: +- apiVersion: config.k8s.io/v1alpha1 + kind: ObjectError + name: "some-validator" + items: + - type: error + message: "some message" + resourceRef: + apiVersion: apps/v1 + kind: Deployment + name: foo + namespace: bar + file: + path: deploy.yaml + index: 0 + field: + path: "spec.template.spec.containers[3].resources.limits.cpu" + currentValue: "200" + suggestedValue: "2" +`, + }, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'deployment_deployment-foo.yaml' + `, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'service_service-foo.yaml' + `, + }, + expectedResults: ` +- apiVersion: config.k8s.io/v1alpha1 + kind: ObjectError + name: "some-validator" + items: + - type: error + message: "some message" + resourceRef: + apiVersion: apps/v1 + kind: Deployment + name: foo + namespace: bar + file: + path: deploy.yaml + index: 0 + field: + path: "spec.template.spec.containers[3].resources.limits.cpu" + currentValue: "200" + suggestedValue: "2" + `, + }, + + // verify that if deferFailure is set, the results file is written and the + // exit error is saved, but the FunctionFilter does not return an error. + { + name: "write_results_defer_failure", + instance: FunctionFilter{DeferFailure: true}, + expectedSavedError: "failed", + run: testRun{ + err: fmt.Errorf("failed"), + output: ` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo +- apiVersion: v1 + kind: Service + metadata: + name: service-foo +results: +- apiVersion: config.k8s.io/v1alpha1 + kind: ObjectError + name: "some-validator" + items: + - type: error + message: "some message" + resourceRef: + apiVersion: apps/v1 + kind: Deployment + name: foo + namespace: bar + file: + path: deploy.yaml + index: 0 + field: + path: "spec.template.spec.containers[3].resources.limits.cpu" + currentValue: "200" + suggestedValue: "2"`, + }, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'deployment_deployment-foo.yaml' + `, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'service_service-foo.yaml' + `, + }, + expectedResults: ` +- apiVersion: config.k8s.io/v1alpha1 + kind: ObjectError + name: "some-validator" + items: + - type: error + message: "some message" + resourceRef: + apiVersion: apps/v1 + kind: Deployment + name: foo + namespace: bar + file: + path: deploy.yaml + index: 0 + field: + path: "spec.template.spec.containers[3].resources.limits.cpu" + currentValue: "200" + suggestedValue: "2"`, + }, + + { + name: "write_results_bad_results_file", + expectedError: "open /not/real/file: no such file or directory", + noMakeResultsFile: true, + run: testRun{ + output: ` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo +- apiVersion: v1 + kind: Service + metadata: + name: service-foo +results: +- apiVersion: config.k8s.io/v1alpha1 + name: "some-validator" +`, + }, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'deployment_deployment-foo.yaml' + `, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'service_service-foo.yaml' + `, + }, + // these aren't written, expect an error + expectedResults: ` +- apiVersion: config.k8s.io/v1alpha1 + name: "some-validator" +`, + }, + + // verify the function only sees resources scoped to it based on the directory + // containing the functionConfig and the directory containing each resource. + // resources not provided to the function should still appear in the FunctionFilter + // output + { + name: "scope_resources_by_directory", + run: testRun{ + expectedInput: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + output: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + }, + functionConfig: ` +apiVersion: example.com/v1 +kind: Example +metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + input: []string{ + // this should not be in scope + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, + // this should be in scope + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +`}, + expectedOutput: []string{ + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +`, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, + }, + }, + + // verify functions without file path annotation are not scoped to functions + { + name: "scope_resources_by_directory_resources_missing_path", + run: testRun{ + expectedInput: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + output: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + }, + functionConfig: ` +apiVersion: example.com/v1 +kind: Example +metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + input: []string{ + // this should not be in scope + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo +`, + // this should be in scope + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +`}, + expectedOutput: []string{ + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +`, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo +`, + }, + }, + + // verify the functions can see all resources if global scope is set + { + name: "scope_resources_global", + instance: FunctionFilter{GlobalScope: true}, + run: testRun{ + expectedInput: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + output: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + }, + functionConfig: ` +apiVersion: example.com/v1 +kind: Example +metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + input: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +`}, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +`, + }, + }, + + { + name: "scope_no_resources", + run: testRun{ + expectedInput: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: [] +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + output: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: [] +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + }, + functionConfig: ` +apiVersion: example.com/v1 +kind: Example +metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/bar.yaml' +`, + input: []string{ + // these should not be in scope + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'biz/bar/s.yaml' +`}, + expectedOutput: []string{ + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'biz/bar/s.yaml' +`, + }, + }, + + { + name: "scope_functions_dir", + run: testRun{ + expectedInput: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/functions/bar.yaml' +`, + output: `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service + metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +functionConfig: + apiVersion: example.com/v1 + kind: Example + metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/functions/bar.yaml' +`, + }, + functionConfig: ` +apiVersion: example.com/v1 +kind: Example +metadata: + name: foo + annotations: + config.kubernetes.io/path: 'foo/functions/bar.yaml' +`, + input: []string{ + // this should not be in scope + ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, + // this should be in scope + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' +`}, + expectedOutput: []string{ + ` +apiVersion: v1 +kind: Service +metadata: + name: service-foo + annotations: + config.kubernetes.io/path: 'foo/bar/s.yaml' + new: annotation +`, ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-foo + annotations: + config.kubernetes.io/path: 'baz/bar/d.yaml' +`, + }, + }, + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + // results file setup + if len(tt.expectedResults) > 0 && !tt.noMakeResultsFile { + // expect result files to be written -- create a directory for them + f, err := ioutil.TempFile("", "test-kyaml-*.yaml") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.RemoveAll(f.Name()) + tt.instance.ResultsFile = f.Name() + } else if len(tt.expectedResults) > 0 { + // failure case for writing to bad results location + tt.instance.ResultsFile = "/not/real/file" + } + + // initialize the inputs for the FunctionFilter + var inputs []*yaml.RNode + for i := range tt.input { + node, err := yaml.Parse(tt.input[i]) + if !assert.NoError(t, err) { + t.FailNow() + } + inputs = append(inputs, node) + } + + // run the FunctionFilter + tt.run.t = t + tt.instance.Run = tt.run.run + if tt.functionConfig != "" { + fc, err := yaml.Parse(tt.functionConfig) + if !assert.NoError(t, err) { + t.FailNow() + } + tt.instance.FunctionConfig = fc + } + output, err := tt.instance.Filter(inputs) + if tt.expectedError != "" { + if !assert.EqualError(t, err, tt.expectedError) { + t.FailNow() + } + return + } + + // check for saved error + if tt.expectedSavedError != "" { + if !assert.EqualError(t, tt.instance.exit, tt.expectedSavedError) { + t.FailNow() + } + } + + if !assert.NoError(t, err) { + t.FailNow() + } + + // verify function output + var actual []string + for i := range output { + s, err := output[i].String() + if !assert.NoError(t, err) { + t.FailNow() + } + actual = append(actual, strings.TrimSpace(s)) + } + var expected []string + for i := range tt.expectedOutput { + expected = append(expected, strings.TrimSpace(tt.expectedOutput[i])) + } + + if !assert.Equal(t, expected, actual) { + t.FailNow() + } + + // verify results files + if len(tt.instance.ResultsFile) > 0 { + tt.expectedResults = strings.TrimSpace(tt.expectedResults) + + results, err := tt.instance.results.String() + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, tt.expectedResults, strings.TrimSpace(results)) { + t.FailNow() + } + + b, err := ioutil.ReadFile(tt.instance.ResultsFile) + writtenResults := strings.TrimSpace(string(b)) + if !assert.NoError(t, err) { + t.FailNow() + } + if !assert.Equal(t, tt.expectedResults, writtenResults) { + t.FailNow() + } + } + }) + } +} diff --git a/kyaml/fn/runtime/runtimeutil/types.go b/kyaml/fn/runtime/runtimeutil/types.go new file mode 100644 index 000000000..fbbb7ad59 --- /dev/null +++ b/kyaml/fn/runtime/runtimeutil/types.go @@ -0,0 +1,5 @@ +package runtimeutil + +type DeferFailureFunction interface { + GetExit() error +}