diff --git a/cmd/pluginator/go.mod b/cmd/pluginator/go.mod index 7b5b5152a..a0c42ae79 100644 --- a/cmd/pluginator/go.mod +++ b/cmd/pluginator/go.mod @@ -8,7 +8,6 @@ require ( github.com/stretchr/testify v1.4.0 sigs.k8s.io/kustomize/api v0.8.4 sigs.k8s.io/kustomize/kyaml v0.10.13 - sigs.k8s.io/yaml v1.2.0 ) replace sigs.k8s.io/kustomize/api => ../../api diff --git a/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go b/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go index 3f4cb7f73..ffacc9ab2 100644 --- a/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go +++ b/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go @@ -9,7 +9,6 @@ import ( "sigs.k8s.io/kustomize/api/provider" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/kyaml/fn/framework" - "sigs.k8s.io/yaml" ) //nolint @@ -21,24 +20,17 @@ func main() { pluginHelpers := resmap.NewPluginHelpers( nil, p.GetFieldValidator(), resmapFactory) - resourceList := &framework.ResourceList{} - resourceList.FunctionConfig = map[string]interface{}{} - - cmd := framework.Command(resourceList, func() error { + processor := framework.ResourceListProcessorFunc(func(resourceList *framework.ResourceList) error { resMap, err := resmapFactory.NewResMapFromRNodeSlice(resourceList.Items) if err != nil { return err } - dataField, err := getDataFromFunctionConfig(resourceList.FunctionConfig) - if err != nil { - return err - } - dataValue, err := yaml.Marshal(dataField) + dataValue, err := resourceList.FunctionConfig.Field("data").Value.String() if err != nil { return err } - err = plugin.Config(pluginHelpers, dataValue) + err = plugin.Config(pluginHelpers, []byte(dataValue)) if err != nil { return err } @@ -60,17 +52,8 @@ func main() { } return nil }) - if err := cmd.Execute(); err != nil { + if err := framework.Execute(&processor, nil); err != nil { fmt.Println(err) os.Exit(1) } } - -//nolint -func getDataFromFunctionConfig(fc interface{}) (interface{}, error) { - f, ok := fc.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("function config %#v is not valid", fc) - } - return f["data"], nil -} diff --git a/kyaml/fn/framework/command/command.go b/kyaml/fn/framework/command/command.go new file mode 100644 index 000000000..8f4d98464 --- /dev/null +++ b/kyaml/fn/framework/command/command.go @@ -0,0 +1,166 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package command + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +type CLIMode byte + +const ( + StandaloneEnabled CLIMode = iota + StandaloneDisabled +) + +// Build returns a cobra.Command to run a function. +// +// The cobra.Command reads the input from STDIN, invokes the provided processor, +// and then writes the output to STDOUT. +// +// The cobra.Command has a boolean `--stack` flag to print stack traces on failure. +// +// By default, invoking the returned cobra.Command with arguments triggers "standalone" mode. +// In this mode: +// - The first argument must be the name of a file containing the FunctionConfig. +// - The remaining arguments must be filenames containing input resources for ResourceList.Items. +// - The argument "-", if present, will cause resources to be read from STDIN as well. +// The output will be a raw stream of resources (not wrapped in a List type). +// Example usage: `cat input1.yaml | go run main.go config.yaml input2.yaml input3.yaml -` +// +// If mode is `StandaloneDisabled`, all arguments are ignored, and STDIN must contain +// a Kubernetes List type. To pass a function config in this mode, use a ResourceList as the input. +// The output will be of the same type as the input (e.g. ResourceList). +// Example usage: `cat resource_list.yaml | go run main.go` +// +// By default, any error returned by the ResourceListProcessor will be printed to STDERR. +// Set noPrintError to true to suppress this. +func Build(p framework.ResourceListProcessor, mode CLIMode, noPrintError bool) *cobra.Command { + cmd := cobra.Command{} + + var printStack bool + cmd.Flags().BoolVar(&printStack, "stack", false, "print the stack trace on failure") + cmd.Args = cobra.MinimumNArgs(0) + cmd.SilenceErrors = true + cmd.SilenceUsage = true + + cmd.RunE = func(cmd *cobra.Command, args []string) error { + var readers []io.Reader + rw := &kio.ByteReadWriter{ + Writer: cmd.OutOrStdout(), + KeepReaderAnnotations: true, + } + + if len(args) > 0 && mode == StandaloneEnabled { + // Don't keep the reader annotations if we are in standalone mode + rw.KeepReaderAnnotations = false + // Don't wrap the resources in a resourceList -- we are in + // standalone mode and writing to stdout to be applied + rw.NoWrap = true + + for i := range args { + // the first argument is the resourceList.FunctionConfig + if i == 0 { + var err error + if rw.FunctionConfig, err = functionConfigFromFile(args[0]); err != nil { + return errors.Wrap(err) + } + continue + } + if args[i] == "-" { + readers = append([]io.Reader{cmd.InOrStdin()}, readers...) + } else { + readers = append(readers, &deferredFileReader{path: args[i]}) + } + } + } else { + // legacy kustomize plugin input style + legacyPlugin := os.Getenv("KUSTOMIZE_PLUGIN_CONFIG_STRING") + if legacyPlugin != "" && rw.FunctionConfig != nil { + if err := yaml.Unmarshal([]byte(legacyPlugin), rw.FunctionConfig); err != nil { + return err + } + } + readers = append(readers, cmd.InOrStdin()) + } + rw.Reader = io.MultiReader(readers...) + + err := framework.Execute(p, rw) + if err != nil && !noPrintError { + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v", err) + } + // print the stack if requested + if s := errors.GetStack(err); printStack && s != "" { + _, _ = fmt.Fprintln(cmd.ErrOrStderr(), s) + } + return err + } + + return &cmd +} + +// AddGenerateDockerfile adds a "gen" subcommand to create a Dockerfile for building +// the function into a container image. +// The gen command takes one argument: the directory where the Dockerfile will be created. +// +// go run main.go gen DIR/ +func AddGenerateDockerfile(cmd *cobra.Command) { + gen := &cobra.Command{ + Use: "gen [DIR]", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return ioutil.WriteFile(filepath.Join(args[0], "Dockerfile"), []byte(`FROM golang:1.15-alpine as builder +ENV CGO_ENABLED=0 +WORKDIR /go/src/ +COPY . . +RUN go build -tags netgo -ldflags '-w' -v -o /usr/local/bin/function ./ + +FROM alpine:latest +COPY --from=builder /usr/local/bin/function /usr/local/bin/function +CMD ["function"] +`), 0600) + }, + } + cmd.AddCommand(gen) +} + +func functionConfigFromFile(file string) (*yaml.RNode, error) { + b, err := ioutil.ReadFile(file) + if err != nil { + return nil, errors.WrapPrefixf(err, "unable to read configuration file %q", file) + } + fc, err := yaml.Parse(string(b)) + if err != nil { + return nil, errors.WrapPrefixf(err, "unable to parse configuration file %q", file) + } + return fc, nil +} + +type deferredFileReader struct { + path string + srcReader io.Reader +} + +func (fr *deferredFileReader) Read(dest []byte) (int, error) { + if fr.srcReader == nil { + src, err := ioutil.ReadFile(fr.path) + if err != nil { + return 0, errors.WrapPrefixf(err, "unable to read input file %s", fr.path) + } + fr.srcReader = bytes.NewReader(src) + } + return fr.srcReader.Read(dest) +} diff --git a/kyaml/fn/framework/command/command_test.go b/kyaml/fn/framework/command/command_test.go new file mode 100644 index 000000000..896420f89 --- /dev/null +++ b/kyaml/fn/framework/command/command_test.go @@ -0,0 +1,164 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package command_test + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/fn/framework/command" + "sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +func TestCommand_dockerfile(t *testing.T) { + d, err := ioutil.TempDir("", "kustomize") + if !assert.NoError(t, err) { + t.FailNow() + } + defer os.RemoveAll(d) + + // create a function + cmd := command.Build(&framework.SimpleProcessor{}, command.StandaloneEnabled, false) + // add the Dockerfile generator + command.AddGenerateDockerfile(cmd) + + // generate the Dockerfile + cmd.SetArgs([]string{"gen", d}) + if !assert.NoError(t, cmd.Execute()) { + t.FailNow() + } + + b, err := ioutil.ReadFile(filepath.Join(d, "Dockerfile")) + if !assert.NoError(t, err) { + t.FailNow() + } + + expected := `FROM golang:1.15-alpine as builder +ENV CGO_ENABLED=0 +WORKDIR /go/src/ +COPY . . +RUN go build -tags netgo -ldflags '-w' -v -o /usr/local/bin/function ./ + +FROM alpine:latest +COPY --from=builder /usr/local/bin/function /usr/local/bin/function +CMD ["function"] +` + if !assert.Equal(t, expected, string(b)) { + t.FailNow() + } +} + +// TestCommand_standalone tests the framework works in standalone mode +func TestCommand_standalone(t *testing.T) { + var config struct { + A string `json:"a" yaml:"a"` + } + + fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { + items = append(items, yaml.MustParse(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar1 + namespace: default + annotations: + foo: bar1 +`)) + for i := range items { + err := items[i].PipeE(yaml.SetAnnotation("a", config.A)) + if err != nil { + return nil, err + } + } + + return items, nil + } + + cmdFn := func() *cobra.Command { + return command.Build(&framework.SimpleProcessor{Filter: kio.FilterFunc(fn), Config: &config}, command.StandaloneEnabled, false) + } + + tc := frameworktestutil.CommandResultsChecker{Command: cmdFn} + tc.Assert(t) +} + +func TestCommand_standalone_stdin(t *testing.T) { + var config struct { + A string `json:"a" yaml:"a"` + } + + p := &framework.SimpleProcessor{ + Config: &config, + + Filter: kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + items = append(items, yaml.MustParse(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar2 + namespace: default + annotations: + foo: bar2 +`)) + for i := range items { + err := items[i].PipeE(yaml.SetAnnotation("a", config.A)) + if err != nil { + return nil, err + } + } + + return items, nil + }), + } + cmd := command.Build(p, command.StandaloneEnabled, false) + cmd.SetIn(bytes.NewBufferString(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar1 + namespace: default + annotations: + foo: bar1 +spec: + replicas: 1 +`)) + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetArgs([]string{filepath.Join("testdata", "standalone", "config.yaml"), "-"}) + + require.NoError(t, cmd.Execute()) + + require.Equal(t, strings.TrimSpace(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar1 + namespace: default + annotations: + foo: bar1 + a: 'b' +spec: + replicas: 1 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bar2 + namespace: default + annotations: + foo: bar2 + a: 'b' +`), strings.TrimSpace(out.String())) +} diff --git a/kyaml/fn/framework/command/doc.go b/kyaml/fn/framework/command/doc.go new file mode 100644 index 000000000..1f1a7fe25 --- /dev/null +++ b/kyaml/fn/framework/command/doc.go @@ -0,0 +1,90 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +// Package command contains a builder for creating cobra.Commands based on configuration functions +// written using the kyaml function framework. The commands this package generates can be used as +// standalone executables or as part of a configuration management pipeline that complies with the +// Configuration Functions Specification (e.g. Kustomize generators or transformers): +// https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +// +// Example standalone usage +// +// Function template input: +// +// # config.yaml -- this is the input to the template +// apiVersion: example.com/v1alpha1 +// kind: Example +// Key: a +// Value: b +// +// Additional function inputs: +// +// # patch.yaml -- this will be applied as a patch +// apiVersion: apps/v1 +// kind: Deployment +// metadata: +// name: foo +// namespace: default +// annotations: +// patch-key: patch-value +// +// Manually run the function: +// +// # build the function +// $ go build example-fn/ +// +// # run the function +// $ ./example-fn config.yaml patch.yaml +// +// Go implementation +// +// // example-fn/main.go +// func main() { +// // Define the template used to generate resources +// p := framework.TemplateProcessor{ +// MergeResources: true, // apply inputs as patches to the template output +// TemplateData: new(struct { +// Key string `json:"key" yaml:"key"` +// Value string `json:"value" yaml:"value"` +// }), +// ResourceTemplates: []framework.ResourceTemplate{{ +// Templates: framework.StringTemplates(` +// apiVersion: apps/v1 +// kind: Deployment +// metadata: +// name: foo +// namespace: default +// annotations: +// {{ .Key }}: {{ .Value }} +// `)}}, +// } +// +// // Run the command +// if err := command.Build(p, command.StandaloneEnabled, true).Execute(); err != nil { +// fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) +// os.Exit(1) +// } +// } +// +// Example function implementation using command.Build with flag input +// +// func main() { +// var value string +// fn := func(rl *framework.ResourceList) error { +// for i := range rl.Items { +// // set the annotation on each resource item +// if err := rl.Items[i].PipeE(yaml.SetAnnotation("value", value)); err != nil { +// return err +// } +// } +// return nil +// } +// cmd := command.Build(framework.ResourceListProcessorFunc(fn), command.StandaloneEnabled, false) +// cmd.Flags().StringVar(&value, "value", "", "annotation value") +// +// if err := cmd.Execute(); err != nil { +// fmt.Println(err) +// os.Exit(1) +// } +// } +package command diff --git a/kyaml/fn/framework/command/example_test.go b/kyaml/fn/framework/command/example_test.go new file mode 100644 index 000000000..ad26db4bf --- /dev/null +++ b/kyaml/fn/framework/command/example_test.go @@ -0,0 +1,382 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package command_test + +import ( + "bytes" + "fmt" + + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/fn/framework/command" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +const service = "Service" + +// ExampleBuild_modify implements a function that sets an annotation on each resource. +// The annotation value is configured via ResourceList.FunctionConfig. +func ExampleBuild_modify() { + // create a struct matching the structure of ResourceList.FunctionConfig to hold its data + var config struct { + Data map[string]string `yaml:"data"` + } + fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { + for i := range items { + // set the annotation on each resource item + err := items[i].PipeE(yaml.SetAnnotation("value", config.Data["value"])) + if err != nil { + return nil, err + } + } + return items, nil + } + p := framework.SimpleProcessor{Filter: kio.FilterFunc(fn), Config: &config} + cmd := command.Build(p, command.StandaloneDisabled, false) + + // for testing purposes only -- normally read from stdin when Executing + cmd.SetIn(bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +# items are provided as nodes +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo +- apiVersion: v1 + kind: Service + metadata: + name: foo +functionConfig: + apiVersion: v1 + kind: ConfigMap + data: + value: baz +`)) + // run the command + if err := cmd.Execute(); err != nil { + panic(err) + } + + // Output: + // apiVersion: config.kubernetes.io/v1alpha1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // annotations: + // value: 'baz' + // - apiVersion: v1 + // kind: Service + // metadata: + // name: foo + // annotations: + // value: 'baz' + // functionConfig: + // apiVersion: v1 + // kind: ConfigMap + // data: + // value: baz +} + +// ExampleBuild_generateReplace generates a resource from a FunctionConfig. +// If the resource already exists, it replaces the resource with a new copy. +func ExampleBuild_generateReplace() { + // function API definition which will be parsed from the ResourceList.FunctionConfig + // read from stdin + type Spec struct { + Name string `yaml:"name,omitempty"` + } + type ExampleServiceGenerator struct { + Spec Spec `yaml:"spec,omitempty"` + } + functionConfig := &ExampleServiceGenerator{} + + // function implementation -- generate a Service resource + p := &framework.SimpleProcessor{ + Config: functionConfig, + Filter: kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + var newNodes []*yaml.RNode + for i := range items { + meta, err := items[i].GetMeta() + if err != nil { + return nil, err + } + + // something we already generated, remove it from the list so we regenerate it + if meta.Name == functionConfig.Spec.Name && + meta.Kind == service && + meta.APIVersion == "v1" { + continue + } + newNodes = append(newNodes, items[i]) + } + + // generate the resource + n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1 +kind: Service +metadata: + name: %s +`, functionConfig.Spec.Name)) + if err != nil { + return nil, err + } + newNodes = append(newNodes, n) + return newNodes, nil + }), + } + cmd := command.Build(p, command.StandaloneDisabled, false) + + // for testing purposes only -- normally read from stdin when Executing + cmd.SetIn(bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +# items are provided as nodes +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo +functionConfig: + apiVersion: example.com/v1alpha1 + kind: ExampleServiceGenerator + spec: + name: bar +`)) + + // run the command + if err := cmd.Execute(); err != nil { + panic(err) + } + + // Output: + // apiVersion: config.kubernetes.io/v1alpha1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // - apiVersion: v1 + // kind: Service + // metadata: + // name: bar + // functionConfig: + // apiVersion: example.com/v1alpha1 + // kind: ExampleServiceGenerator + // spec: + // name: bar +} + +// ExampleBuild_generateUpdate generates a resource, updating the previously generated +// copy rather than replacing it. +// +// Note: This will keep manual edits to the previously generated copy. +func ExampleBuild_generateUpdate() { + // function API definition which will be parsed from the ResourceList.FunctionConfig + // read from stdin + type Spec struct { + Name string `yaml:"name,omitempty"` + Annotations map[string]string `yaml:"annotations,omitempty"` + } + type ExampleServiceGenerator struct { + Spec Spec `yaml:"spec,omitempty"` + } + functionConfig := &ExampleServiceGenerator{} + + // function implementation -- generate or update a Service resource + fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { + var found bool + for i := range items { + meta, err := items[i].GetMeta() + if err != nil { + return nil, err + } + + // something we already generated, reconcile it to make sure it matches what + // is specified by the FunctionConfig + if meta.Name == functionConfig.Spec.Name && + meta.Kind == service && + meta.APIVersion == "v1" { + // set some values + for k, v := range functionConfig.Spec.Annotations { + err := items[i].PipeE(yaml.SetAnnotation(k, v)) + if err != nil { + return nil, err + } + } + found = true + break + } + } + if found { + return items, nil + } + + // generate the resource if not found + n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1 +kind: Service +metadata: + name: %s +`, functionConfig.Spec.Name)) + if err != nil { + return nil, err + } + for k, v := range functionConfig.Spec.Annotations { + err := n.PipeE(yaml.SetAnnotation(k, v)) + if err != nil { + return nil, err + } + } + items = append(items, n) + return items, nil + } + + p := &framework.SimpleProcessor{Config: functionConfig, Filter: kio.FilterFunc(fn)} + cmd := command.Build(p, command.StandaloneDisabled, false) + + // for testing purposes only -- normally read from stdin when Executing + cmd.SetIn(bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +# items are provided as nodes +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo +- apiVersion: v1 + kind: Service + metadata: + name: bar +functionConfig: + apiVersion: example.com/v1alpha1 + kind: ExampleServiceGenerator + spec: + name: bar + annotations: + a: b +`)) + + // run the command + if err := cmd.Execute(); err != nil { + panic(err) + } + + // Output: + // apiVersion: config.kubernetes.io/v1alpha1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // - apiVersion: v1 + // kind: Service + // metadata: + // name: bar + // annotations: + // a: 'b' + // functionConfig: + // apiVersion: example.com/v1alpha1 + // kind: ExampleServiceGenerator + // spec: + // name: bar + // annotations: + // a: b +} + +// ExampleBuild_validate validates that all Deployment resources have the replicas field set. +// If any Deployments do not contain spec.replicas, then the function will return results +// which will be set on ResourceList.results +func ExampleBuild_validate() { + fn := func(rl *framework.ResourceList) error { + // validation results + var validationResults []framework.ResultItem + + // validate that each Deployment resource has spec.replicas set + for i := range rl.Items { + // only check Deployment resources + meta, err := rl.Items[i].GetMeta() + if err != nil { + return err + } + if meta.Kind != "Deployment" { + continue + } + + // lookup replicas field + r, err := rl.Items[i].Pipe(yaml.Lookup("spec", "replicas")) + if err != nil { + return err + } + + // check replicas not specified + if r != nil { + continue + } + validationResults = append(validationResults, framework.ResultItem{ + Severity: framework.Error, + Message: "field is required", + ResourceRef: meta, + Field: framework.Field{ + Path: "spec.replicas", + SuggestedValue: "1", + }, + }) + } + + if len(validationResults) > 0 { + rl.Result = &framework.Result{ + Name: "replicas-validator", + Items: validationResults, + } + } + + return rl.Result + } + + cmd := command.Build(framework.ResourceListProcessorFunc(fn), command.StandaloneDisabled, true) + // for testing purposes only -- normally read from stdin when Executing + cmd.SetIn(bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +# items are provided as nodes +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo +`)) + + // run the command + if err := cmd.Execute(); err != nil { + // normally exit 1 here + } + + // Output: + // apiVersion: config.kubernetes.io/v1alpha1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // results: + // name: replicas-validator + // items: + // - message: field is required + // severity: error + // resourceRef: + // apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // field: + // path: spec.replicas + // suggestedValue: "1" +} diff --git a/kyaml/fn/framework/testdata/command/config.yaml b/kyaml/fn/framework/command/testdata/standalone/config.yaml similarity index 100% rename from kyaml/fn/framework/testdata/command/config.yaml rename to kyaml/fn/framework/command/testdata/standalone/config.yaml diff --git a/kyaml/fn/framework/testdata/command/expected.yaml b/kyaml/fn/framework/command/testdata/standalone/expected.yaml similarity index 100% rename from kyaml/fn/framework/testdata/command/expected.yaml rename to kyaml/fn/framework/command/testdata/standalone/expected.yaml diff --git a/kyaml/fn/framework/testdata/command/input.yaml b/kyaml/fn/framework/command/testdata/standalone/input.yaml similarity index 100% rename from kyaml/fn/framework/testdata/command/input.yaml rename to kyaml/fn/framework/command/testdata/standalone/input.yaml diff --git a/kyaml/fn/framework/containers.go b/kyaml/fn/framework/containers.go deleted file mode 100644 index 4cc81b086..000000000 --- a/kyaml/fn/framework/containers.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package framework - -import ( - "bytes" - "text/template" - - "sigs.k8s.io/kustomize/kyaml/errors" - "sigs.k8s.io/kustomize/kyaml/sets" - "sigs.k8s.io/kustomize/kyaml/yaml" - "sigs.k8s.io/kustomize/kyaml/yaml/merge2" -) - -// PatchContainersWithString executes t as a template and patches each container in each resource -// with the result. -func PatchContainersWithString(resources []*yaml.RNode, t string, input interface{}, containers ...string) error { - resourcePatch := template.Must(template.New("containers").Parse(t)) - return PatchContainersWithTemplate(resources, resourcePatch, input, containers...) -} - -// PatchContainersWithTemplate executes t and patches each container in each resource -// with the result. -func PatchContainersWithTemplate(resources []*yaml.RNode, t *template.Template, input interface{}, containers ...string) error { - var b bytes.Buffer - if err := t.Execute(&b, input); err != nil { - return errors.Wrap(err) - } - patch, err := yaml.Parse(b.String()) - if err != nil { - return errors.WrapPrefixf(err, b.String()) - } - return PatchContainers(resources, patch, containers...) -} - -// PatchContainers applies patch to each container in each resource. -func PatchContainers(resources []*yaml.RNode, patch *yaml.RNode, containers ...string) error { - names := sets.String{} - names.Insert(containers...) - - for i := range resources { - containers, err := resources[i].Pipe(yaml.Lookup("spec", "template", "spec", "containers")) - if err != nil { - return errors.Wrap(err) - } - if containers == nil { - continue - } - err = containers.VisitElements(func(node *yaml.RNode) error { - f := node.Field("name") - if f == nil { - return nil - } - if names.Len() > 0 && !names.Has(yaml.GetValue(f.Value)) { - return nil - } - _, err := merge2.Merge(patch, node, yaml.MergeOptions{}) - return errors.Wrap(err) - }) - if err != nil { - return errors.Wrap(err) - } - } - return nil -} diff --git a/kyaml/fn/framework/doc.go b/kyaml/fn/framework/doc.go index 8a1b62d3d..1c6d59108 100644 --- a/kyaml/fn/framework/doc.go +++ b/kyaml/fn/framework/doc.go @@ -1,106 +1,39 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -// Package framework contains a framework for writing functions in go. The function spec +// Package framework contains a framework for writing functions in Go. The function specification // is defined at: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md // -// Functions are executables which generate, modify, delete or validate Kubernetes resources. +// Functions are executables that generate, modify, delete or validate Kubernetes resources. // They are often used used to implement abstractions ("kind: JavaSpringBoot") and // cross-cutting logic ("kind: SidecarInjector"). // // Functions may be run as standalone executables or invoked as part of an orchestrated // pipeline (e.g. kustomize). // -// Example standalone usage +// Example function implementation using framework.SimpleProcessor with a struct input // -// Function template input: +// type Spec struct { +// Value string `yaml:"value,omitempty"` +// } +// type Example struct { +// Spec Spec `yaml:"spec,omitempty"` +// } // -// # config.yaml -- this is the input to the template -// apiVersion: example.com/v1alpha1 -// kind: Example -// Key: a -// Value: b +// func runFunction(rlSource *kio.ByteReadWriter) error { +// functionConfig := &Example{} // -// Additional function inputs: +// fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { +// for i := range rl.Items { +// // modify the items... +// } +// return items, nil +// } // -// # patch.yaml -- this will be applied as a patch -// apiVersion: apps/v1 -// kind: Deployment -// metadata: -// name: foo -// namespace: default -// annotations: -// patch-key: patch-value -// -// Manually run the function: -// -// # build the function -// $ go build example-fn/ -// -// # run the function using the -// $ ./example-fn config.yaml patch.yaml -// -// Go implementation -// -// // example-fn/main.go -// func main() { -// -// // Define the template used to generate resources -// tc := framework.TemplateCommand{ -// Merge: true, // apply inputs as patches to the template output -// API: &struct { -// Key string `json:"key" yaml:"key"` -// Value string `json:"value" yaml:"value"` -// }{}, -// Template: template.Must(template.New("example").Parse(` -// apiVersion: apps/v1 -// kind: Deployment -// metadata: -// name: foo -// namespace: default -// annotations: -// {{ .Key }}: {{ .Value }} -// `))} -// -// // Run the command -// if err := tc.GetCommand().Execute(); err != nil { -// fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) -// os.Exit(1) -// } -// } -// -// More Examples -// -// Example function implementation using framework.Command with flag input -// -// var value string -// resourceList := &framework.ResourceList{} -// cmd := framework.Command(resourceList, func() error { -// for i := range resourceList.Items { -// // modify the items... -// } -// return nil -// }) -// cmd.Flags().StringVar(&value, "value", "", "annotation value") -// if err := cmd.Execute(); err != nil { return err } -// -// Example function implementation using framework.ResourceList with a struct input -// -// type Spec struct { -// Value string `yaml:"value,omitempty"` -// } -// type Example struct { -// Spec Spec `yaml:"spec,omitempty"` -// } -// functionConfig := &Example{} -// -// rl := framework.ResourceList{FunctionConfig: functionConfig} -// if err := rl.Read(); err != nil { return err } -// -// for i := range rl.Items { -// // modify the items... -// } -// if err := rl.Write(); err != nil { return err } +// p := framework.SimpleProcessor{Config: functionConfig, Filter: kio.FilterFunc(fn)} +// err := framework.Execute(p, rlSource) +// return errors.Wrap(err) +// } // // Architecture // @@ -108,7 +41,7 @@ // as output. The function itself may be configured through a functionConfig // (ResourceList.FunctionConfig). // -// Example Function Input: +// Example function input: // // kind: ResourceList // items: @@ -140,10 +73,9 @@ // The framework takes care of serializing and deserializing the ResourceList. // // Generated ResourceList.functionConfig -- ConfigMaps -// // Functions may also be specified imperatively and run using: // -// config run DIR/ --image image/containing/function:impl -- value=foo +// kpt fn run DIR/ --image image/containing/function:impl -- value=foo // // When run imperatively, a ConfigMap is generated for the functionConfig, and the command // arguments are set as ConfigMap data entries. @@ -165,15 +97,9 @@ // // Configuring Functions // -// Functions may be configured through a functionConfig (i.e. a client side custom resource), +// Functions may be configured through a functionConfig (i.e. a client-side custom resource), // or through flags (which the framework parses from a ConfigMap provided as input). // -// When using framework.Command, any flags registered on the cobra.Command will be parsed -// from the functionConfig input if they are defined as functionConfig.data entries. -// -// When using framework.ResourceList, any flags set on the ResourceList.Flags will be -// parsed from the functionConfig input if they are defined as functionConfig.data entries. -// // Functions may also access environment variables set by the caller. // // Building a container image for the function diff --git a/kyaml/fn/framework/example/doc.go b/kyaml/fn/framework/example/doc.go index 18cd40ee6..5f5500182 100644 --- a/kyaml/fn/framework/example/doc.go +++ b/kyaml/fn/framework/example/doc.go @@ -2,8 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 // Package main contains an example using the the framework. +// The example annotates all resources in the input with the value provided as a flag. +// +// To execute the function, run: +// +// $ cat input/cm.yaml | go run ./main.go --value=foo // // To generate the Dockerfile for the function image run: // -// $ go run ./main.go . +// $ go run ./main.go gen ./ package main diff --git a/kyaml/fn/framework/example/input/cm.yaml b/kyaml/fn/framework/example/input/cm.yaml new file mode 100644 index 000000000..3c66a10b5 --- /dev/null +++ b/kyaml/fn/framework/example/input/cm.yaml @@ -0,0 +1,9 @@ +# Copyright 2021 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 + +kind: ConfigMap +apiVersion: v1 +metadata: + name: tester +data: + some: data diff --git a/kyaml/fn/framework/example/main.go b/kyaml/fn/framework/example/main.go index 239b6b7c7..24cb430d7 100644 --- a/kyaml/fn/framework/example/main.go +++ b/kyaml/fn/framework/example/main.go @@ -4,28 +4,30 @@ package main import ( + "fmt" "os" "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/fn/framework/command" "sigs.k8s.io/kustomize/kyaml/yaml" ) func main() { var value string - resourceList := framework.ResourceList{} - cmd := framework.Command(&resourceList, func() error { - for i := range resourceList.Items { + fn := func(rl *framework.ResourceList) error { + for i := range rl.Items { // set the annotation on each resource item - err := resourceList.Items[i].PipeE(yaml.SetAnnotation("value", value)) - if err != nil { + if err := rl.Items[i].PipeE(yaml.SetAnnotation("value", value)); err != nil { return err } } return nil - }) + } + cmd := command.Build(framework.ResourceListProcessorFunc(fn), command.StandaloneEnabled, false) cmd.Flags().StringVar(&value, "value", "", "annotation value") if err := cmd.Execute(); err != nil { + fmt.Println(err) os.Exit(1) } } diff --git a/kyaml/fn/framework/example2/doc.go b/kyaml/fn/framework/example2/doc.go deleted file mode 100644 index b4fb32979..000000000 --- a/kyaml/fn/framework/example2/doc.go +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package example2 diff --git a/kyaml/fn/framework/example2/example_test.go b/kyaml/fn/framework/example2/example_test.go deleted file mode 100644 index 44db08ca2..000000000 --- a/kyaml/fn/framework/example2/example_test.go +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -//go:generate go run github.com/markbates/pkger/cmd/pkger -o fn/framework/example2 -package example2 - -import ( - "bytes" - "strings" - "testing" - - "github.com/markbates/pkger" - "github.com/stretchr/testify/require" - "sigs.k8s.io/kustomize/kyaml/fn/framework" -) - -func TestTemplate(t *testing.T) { - type API struct { - Image string `json:"image" yaml:"image"` - } - - tpl, err := framework.TemplatesFromDir(pkger.Dir("/fn/framework/example2/data/templates"))(nil) - require.NoError(t, err) - cmd := framework.TemplateCommand{ - API: &API{}, - Templates: tpl, - }.GetCommand() - - var in, out bytes.Buffer - cmd.SetIn(&in) - cmd.SetOut(&out) - - in.WriteString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: v1 - kind: Service -functionConfig: - image: baz -`) - - require.NoError(t, cmd.Execute()) - require.Equal(t, strings.TrimSpace(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: v1 - kind: Service -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo - namespace: bar - annotations: - config.kubernetes.io/index: '0' - spec: - template: - spec: - containers: - - name: foo - image: baz -functionConfig: - image: baz -`), strings.TrimSpace(out.String())) -} - -func TestPatchTemplate(t *testing.T) { - type API struct { - Replicas int `json:"replicas" yaml:"replicas"` - } - - cmd := framework.TemplateCommand{ - API: &API{}, - PatchTemplatesFn: framework.PatchTemplatesFromDir( - framework.PT{ - Dir: pkger.Dir("/fn/framework/example2/data/patches"), - Selector: func() *framework.Selector { - return &framework.Selector{Names: []string{"foo"}} - }, - }, - ), - }.GetCommand() - - var in, out bytes.Buffer - cmd.SetIn(&in) - cmd.SetOut(&out) - - in.WriteString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo - spec: - template: - spec: - containers: - - name: foo - image: baz -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar - spec: - template: - spec: - containers: - - name: foo - image: baz -functionConfig: - replicas: 5 -`) - - require.NoError(t, cmd.Execute()) - require.Equal(t, strings.TrimSpace(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo - annotations: - config.kubernetes.io/index: '0' - spec: - template: - spec: - containers: - - name: foo - image: baz - replicas: 5 -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar - spec: - template: - spec: - containers: - - name: foo - image: baz -functionConfig: - replicas: 5 -`), strings.TrimSpace(out.String())) -} - -func TestContainerPatchTemplate(t *testing.T) { - type API struct { - Key string `json:"key" yaml:"key"` - Value string `json:"value" yaml:"value"` - } - - cmd := framework.TemplateCommand{ - API: &API{}, - PatchContainerTemplatesFn: framework.ContainerPatchTemplatesFromDir( - framework.CPT{ - Dir: pkger.Dir("/fn/framework/example2/data/container-patches"), - Selector: func() *framework.Selector { - return &framework.Selector{Names: []string{"foo"}} - }, - }, - ), - }.GetCommand() - - var in, out bytes.Buffer - cmd.SetIn(&in) - cmd.SetOut(&out) - - in.WriteString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo - spec: - template: - spec: - containers: - - name: a - - name: b - - name: c -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar - spec: - template: - spec: - containers: - - name: foo - image: baz -functionConfig: - key: Hello - value: World -`) - - require.NoError(t, cmd.Execute()) - require.Equal(t, strings.TrimSpace(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo - spec: - template: - spec: - containers: - - name: a - env: - key: Hello - value: World - - name: b - env: - key: Hello - value: World - - name: c - env: - key: Hello - value: World -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar - spec: - template: - spec: - containers: - - name: foo - image: baz -functionConfig: - key: Hello - value: World -`), strings.TrimSpace(out.String())) -} diff --git a/kyaml/fn/framework/example2/pkged.go b/kyaml/fn/framework/example2/pkged.go deleted file mode 100644 index cbf6d5975..000000000 --- a/kyaml/fn/framework/example2/pkged.go +++ /dev/null @@ -1,12 +0,0 @@ -// Code generated by pkger; DO NOT EDIT. - -// +build !skippkger - -package example2 - -import ( - "github.com/markbates/pkger" - "github.com/markbates/pkger/pkging/mem" -) - -var _ = pkger.Apply(mem.UnmarshalEmbed([]byte(`1f8b08000000000000ffec985b739a4e1bc0bf0af35c13590e9e98e94530959854d368226aa7935960c5d585756089d18cdffd1d30689be66d931ed27f5bae641ff6d9f3fefc0df740a3294fc0bc8784064965d1482a942b8b34113ca41ba22cd63864d9eb131a8309ca8c874499c724c09112f025163325893de56bc93274c2258fc57b2c66607eb51f197a38246042513ce11e98f01e7b0b1c10298f4a1e8f04a6512231eac638a62491a63c9662827d1a05128e7c691553913d9fa72e8923224822f549c2d3d82359fa9406698c05e5918413296bb402325ce13820024c0019fa9c8bef9e6e170b6f06e607a8c0471906023302a68853f250e8139cf0084c88b890689408cc18f125371512bec5946197118946929b52e64b1ef6660464b0799b329264edfadcab043c6b7cb7b2099851ca980c2764b97fbe2289d8e71c428f32badc4fb3e1ddc3b3b6a78b6954cce5474e84cdbbdcffbe6c25e09590fb7923431227345f4ab5a25661bbddca30ddcdf8ab07da54a691328d7148563c5e28e40e874b4634c5c7022b0fe78bc447cb6c23778d65d724fbf589c094e5a1687754bfac2e434237044c03356b3284dc27606aaa51371a865aade7911b41f3640d69ea11d28e54ed4a354ca36e1aa8a222b551d71aa87a841a264220034d6efc6cb177eb9eacf3ee4fc82d98b59a8e1a3274220ea656d38dbaa6ab32f4188d16606af9f61230d55aa3a9cb704d7d305584900cf6e1717473b3c43e0213c9d0f7b346910c834f2660b145f259917b8b04cc860cc78286d95006c4cb3a518daa960d5d865e92459a486d20bd86d05686eea3aaba5a47a8868aaafb396f65683dbfeae8e6268dd284f8607e403292d1c7fc08cc485c22ab44d69f812c1996f934eee1fd2278ee617b19bfb632646f8a055ce29844e2d0e3a1d97c383f8d9b8748459070c9b0209542279ec1d34749055555cd28a06ae8b51fa0e914b3e4db3835f638550b9ceaba868cefc0693ef297d054475ad328b8a7a33ad29bf56afd4fa3a932a5778f897a3872bb973b683e4db4039e76c7f7814e0fdbf7399e3e85ceae76899bd7c6cdffbdf67b0c41676d9db8ba55f54216e1d3cba03b3fbebb1858d7d8be0e06fa904e4667e964d467dedab2fc519fbbfad9e65dc0e79d76efb2df0ec495cde613a7ba990cd4b3893d4c7d9b85d819ae2fa86579767b8eed6bd1a5c6aa152c991b8d6b2d7a1c606d58bda0569dac8fd38136ac764ed56616f7c376e23bd7b5cea9a8775a5567ecdca99381d59c5ebe7903bf0c96cf52cbff84501a5a2994a5509642f9ef12fe7535b2f817c97f5f228e4f25eca551d55f511a8dea4f94c66ce4a53496d2f83722e5c94bfe5b3471e3d9c3f945c083cee919f3ecbbe5586b6f0a5dbc0e872bd766f3f1a81b4c47e8176a61b112df10c343b5dfab86f5520d4b352cd5f0dfe5f80144af2387fbfe149f2c195fbfc40f9fcc28f8a9198d5714c4aafef304311f792988a520fe9d6079faa2ff16479c79367326a3b30d769ae905b566dea9b579a78fef5aa158bae165adf3b69fb9e32d7154e64697e7ae334463a73ff3edb7f92748376c8bc9150f2661f3765f1ef55663a7c72ea845c7a3ceb9a75b6cbce1e79d9685268eba72ed369a5c65e5e3a073da5b4d9c6ede565eb67bb76ed49f61a7cabce893780b7dd1d72e7e1c60479d4db4fde7ce33d76947bbcf9dfc9b5ebbbbb6a4f49dd277fe0c2c6dff070000ffff010000ffff2eef01c0e0240000`))) diff --git a/kyaml/fn/framework/example_test.go b/kyaml/fn/framework/example_test.go index f739b0af3..1fa536a69 100644 --- a/kyaml/fn/framework/example_test.go +++ b/kyaml/fn/framework/example_test.go @@ -7,22 +7,19 @@ import ( "bytes" "fmt" "log" - "os" "path/filepath" - "text/template" - "github.com/spf13/pflag" + "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/fn/framework/command" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) const service = "Service" -// ExampleResourceList_modify implements a function that sets an annotation on each resource. -// The annotation value is configured via a flag value parsed from ResourceList.functionConfig.data -func ExampleResourceList_modify() { - // for testing purposes only -- normally read from stdin when Executing +// ExampleSimpleProcessor_modify implements a function that sets an annotation on each resource. +func ExampleSimpleProcessor_modify() { input := bytes.NewBufferString(` apiVersion: config.kubernetes.io/v1alpha1 kind: ResourceList @@ -36,32 +33,27 @@ items: kind: Service metadata: name: foo -# functionConfig is parsed into flags by framework.Command functionConfig: apiVersion: v1 kind: ConfigMap data: value: baz `) - - // configure the annotation value using a flag parsed from - // ResourceList.functionConfig.data.value - fs := pflag.NewFlagSet("tests", pflag.ContinueOnError) - value := fs.String("value", "", "annotation value") - rl := framework.ResourceList{ - Flags: fs, - Reader: input, // for testing only - } - if err := rl.Read(); err != nil { - panic(err) - } - for i := range rl.Items { - // set the annotation on each resource item - if err := rl.Items[i].PipeE(yaml.SetAnnotation("value", *value)); err != nil { - panic(err) + config := new(struct { + Data map[string]string `yaml:"data" json:"data"` + }) + fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { + for i := range items { + // set the annotation on each resource item + if err := items[i].PipeE(yaml.SetAnnotation("value", config.Data["value"])); err != nil { + return nil, err + } } + return items, nil } - if err := rl.Write(); err != nil { + + err := framework.Execute(framework.SimpleProcessor{Config: config, Filter: kio.FilterFunc(fn)}, &kio.ByteReadWriter{Reader: input}) + if err != nil { panic(err) } @@ -88,28 +80,10 @@ functionConfig: // value: baz } -// ExampleCommand_modify implements a function that sets an annotation on each resource. -// The annotation value is configured via a flag value parsed from -// ResourceList.functionConfig.data -func ExampleCommand_modify() { - // configure the annotation value using a flag parsed from - // ResourceList.functionConfig.data.value - resourceList := framework.ResourceList{} - var value string - cmd := framework.Command(&resourceList, func() error { - for i := range resourceList.Items { - // set the annotation on each resource item - err := resourceList.Items[i].PipeE(yaml.SetAnnotation("value", value)) - if err != nil { - return err - } - } - return nil - }) - cmd.Flags().StringVar(&value, "value", "", "annotation value") - - // for testing purposes only -- normally read from stdin when Executing - cmd.SetIn(bytes.NewBufferString(` +// ExampleSimpleProcessor_generateReplace generates a resource from a FunctionConfig. +// If the resource already exists, it replaces the resource with a new copy. +func ExampleSimpleProcessor_generateReplace() { + input := bytes.NewBufferString(` apiVersion: config.kubernetes.io/v1alpha1 kind: ResourceList # items are provided as nodes @@ -118,49 +92,14 @@ items: kind: Deployment metadata: name: foo -- apiVersion: v1 - kind: Service - metadata: - name: foo -# functionConfig is parsed into flags by framework.Command functionConfig: - apiVersion: v1 - kind: ConfigMap - data: - value: baz -`)) - // run the command - if err := cmd.Execute(); err != nil { - panic(err) - } + apiVersion: example.com/v1alpha1 + kind: ExampleServiceGenerator + spec: + name: bar +`) - // Output: - // apiVersion: config.kubernetes.io/v1alpha1 - // kind: ResourceList - // items: - // - apiVersion: apps/v1 - // kind: Deployment - // metadata: - // name: foo - // annotations: - // value: 'baz' - // - apiVersion: v1 - // kind: Service - // metadata: - // name: foo - // annotations: - // value: 'baz' - // functionConfig: - // apiVersion: v1 - // kind: ConfigMap - // data: - // value: baz -} - -// ExampleCommand_generateReplace generates a resource from a functionConfig. -// If the resource already exist s, it replaces the resource with a new copy. -func ExampleCommand_generateReplace() { - // function API definition which will be parsed from the ResourceList.functionConfig + // function API definition which will be parsed from the ResourceList.FunctionConfig // read from stdin type Spec struct { Name string `yaml:"name,omitempty"` @@ -168,152 +107,45 @@ func ExampleCommand_generateReplace() { type ExampleServiceGenerator struct { Spec Spec `yaml:"spec,omitempty"` } + functionConfig := &ExampleServiceGenerator{} - // function implementation -- generate a Service resource - resourceList := &framework.ResourceList{FunctionConfig: functionConfig} - cmd := framework.Command(resourceList, func() error { + fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) { + // remove the last generated resource var newNodes []*yaml.RNode - for i := range resourceList.Items { - meta, err := resourceList.Items[i].GetMeta() + for i := range items { + meta, err := items[i].GetMeta() if err != nil { - return err + return nil, err } - // something we already generated, remove it from the list so we regenerate it if meta.Name == functionConfig.Spec.Name && meta.Kind == service && meta.APIVersion == "v1" { continue } - newNodes = append(newNodes, resourceList.Items[i]) + newNodes = append(newNodes, items[i]) } + items = newNodes - // generate the resource + // generate the resource again n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1 kind: Service metadata: - name: %s + name: %s `, functionConfig.Spec.Name)) if err != nil { - return err + return nil, err } - newNodes = append(newNodes, n) - resourceList.Items = newNodes - return nil - }) - - // for testing purposes only -- normally read from stdin when Executing - cmd.SetIn(bytes.NewBufferString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -# items are provided as nodes -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo -# functionConfig is parsed into flags by framework.Command -functionConfig: - apiVersion: example.com/v1alpha1 - kind: ExampleServiceGenerator - spec: - name: bar -`)) - - // run the command - if err := cmd.Execute(); err != nil { - panic(err) + items = append(items, n) + return items, nil } - // Output: - // apiVersion: config.kubernetes.io/v1alpha1 - // kind: ResourceList - // items: - // - apiVersion: apps/v1 - // kind: Deployment - // metadata: - // name: foo - // - apiVersion: v1 - // kind: Service - // metadata: - // name: bar - // functionConfig: - // apiVersion: example.com/v1alpha1 - // kind: ExampleServiceGenerator - // spec: - // name: bar -} - -// ExampleResourceList_generateReplace generates a resource from a functionConfig. -// If the resource already exist s, it replaces the resource with a new copy. -func ExampleResourceList_generateReplace() { - input := bytes.NewBufferString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -# items are provided as nodes -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo -# functionConfig is parsed into flags by framework.Command -functionConfig: - apiVersion: example.com/v1alpha1 - kind: ExampleServiceGenerator - spec: - name: bar -`) - - // function API definition which will be parsed from the ResourceList.functionConfig - // read from stdin - type Spec struct { - Name string `yaml:"name,omitempty"` - } - type ExampleServiceGenerator struct { - Spec Spec `yaml:"spec,omitempty"` - } - functionConfig := &ExampleServiceGenerator{} - - rl := framework.ResourceList{ - FunctionConfig: functionConfig, - Reader: input, // for testing only - } - if err := rl.Read(); err != nil { - panic(err) - } - - // remove the last generated resource - var newNodes []*yaml.RNode - for i := range rl.Items { - meta, err := rl.Items[i].GetMeta() - if err != nil { - panic(err) - } - // something we already generated, remove it from the list so we regenerate it - if meta.Name == functionConfig.Spec.Name && - meta.Kind == service && - meta.APIVersion == "v1" { - continue - } - newNodes = append(newNodes, rl.Items[i]) - } - rl.Items = newNodes - - // generate the resource again - n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1 -kind: Service -metadata: - name: %s -`, functionConfig.Spec.Name)) + p := framework.SimpleProcessor{Config: functionConfig, Filter: kio.FilterFunc(fn)} + err := framework.Execute(p, &kio.ByteReadWriter{Reader: input}) if err != nil { panic(err) } - rl.Items = append(rl.Items, n) - - if err := rl.Write(); err != nil { - panic(err) - } // Output: // apiVersion: config.kubernetes.io/v1alpha1 @@ -334,239 +166,35 @@ metadata: // name: bar } -// ExampleCommand_generateUpdate generates a resource, updating the previously generated -// copy rather than replacing it. -// -// Note: This will keep manual edits to the previously generated copy. -func ExampleCommand_generateUpdate() { - // function API definition which will be parsed from the ResourceList.functionConfig - // read from stdin - type Spec struct { - Name string `yaml:"name,omitempty"` - Annotations map[string]string `yaml:"annotations,omitempty"` - } - type ExampleServiceGenerator struct { - Spec Spec `yaml:"spec,omitempty"` - } - functionConfig := &ExampleServiceGenerator{} - - // function implementation -- generate or update a Service resource - resourceList := &framework.ResourceList{FunctionConfig: functionConfig} - cmd := framework.Command(resourceList, func() error { - var found bool - for i := range resourceList.Items { - meta, err := resourceList.Items[i].GetMeta() - if err != nil { - return err - } - - // something we already generated, reconcile it to make sure it matches what - // is specified by the functionConfig - if meta.Name == functionConfig.Spec.Name && - meta.Kind == service && - meta.APIVersion == "v1" { - // set some values - for k, v := range functionConfig.Spec.Annotations { - err := resourceList.Items[i].PipeE(yaml.SetAnnotation(k, v)) - if err != nil { - return err - } - } - found = true - break - } - } - if found { - return nil - } - - // generate the resource if not found - n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1 -kind: Service -metadata: - name: %s -`, functionConfig.Spec.Name)) - if err != nil { - return err - } - for k, v := range functionConfig.Spec.Annotations { - err := n.PipeE(yaml.SetAnnotation(k, v)) - if err != nil { - return err - } - } - resourceList.Items = append(resourceList.Items, n) - - return nil +// ExampleTemplateProcessor provides an example for using the TemplateProcessor to add resources +// from templates defined inline +func ExampleTemplateProcessor_generate_inline() { + api := new(struct { + Key string `json:"key" yaml:"key"` + Value string `json:"value" yaml:"value"` }) - - // for testing purposes only -- normally read from stdin when Executing - cmd.SetIn(bytes.NewBufferString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -# items are provided as nodes -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo -- apiVersion: v1 - kind: Service - metadata: - name: bar -# functionConfig is parsed into flags by framework.Command -functionConfig: - apiVersion: example.com/v1alpha1 - kind: ExampleServiceGenerator - spec: - name: bar - annotations: - a: b -`)) - - // run the command - if err := cmd.Execute(); err != nil { - panic(err) - } - - // Output: - // apiVersion: config.kubernetes.io/v1alpha1 - // kind: ResourceList - // items: - // - apiVersion: apps/v1 - // kind: Deployment - // metadata: - // name: foo - // - apiVersion: v1 - // kind: Service - // metadata: - // name: bar - // annotations: - // a: 'b' - // functionConfig: - // apiVersion: example.com/v1alpha1 - // kind: ExampleServiceGenerator - // spec: - // name: bar - // annotations: - // a: b -} - -// ExampleCommand_validate validates that all Deployment resources have the replicas field set. -// If any Deployments do not contain spec.replicas, then the function will return results -// which will be set on ResourceList.results -func ExampleCommand_validate() { - resourceList := &framework.ResourceList{} - cmd := framework.Command(resourceList, func() error { - // validation results - var validationResults []framework.Item - - // validate that each Deployment resource has spec.replicas set - for i := range resourceList.Items { - // only check Deployment resources - meta, err := resourceList.Items[i].GetMeta() - if err != nil { - return err - } - if meta.Kind != "Deployment" { - continue - } - - // lookup replicas field - r, err := resourceList.Items[i].Pipe(yaml.Lookup("spec", "replicas")) - if err != nil { - return err - } - - // check replicas not specified - if r != nil { - continue - } - validationResults = append(validationResults, framework.Item{ - Severity: framework.Error, - Message: "missing replicas", - ResourceRef: meta, - Field: framework.Field{ - Path: "spec.field", - SuggestedValue: "1", - }, - }) - } - - if len(validationResults) > 0 { - resourceList.Result = &framework.Result{ - Name: "replicas-validator", - Items: validationResults, - } - } - - return resourceList.Result - }) - - // for testing purposes only -- normally read from stdin when Executing - cmd.SetIn(bytes.NewBufferString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -# items are provided as nodes -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: foo -`)) - - // run the command - if err := cmd.Execute(); err != nil { - // normally exit 1 here - } - - // Output: - // apiVersion: config.kubernetes.io/v1alpha1 - // kind: ResourceList - // items: - // - apiVersion: apps/v1 - // kind: Deployment - // metadata: - // name: foo - // results: - // name: replicas-validator - // items: - // - message: missing replicas - // severity: error - // resourceRef: - // apiVersion: apps/v1 - // kind: Deployment - // metadata: - // name: foo - // field: - // path: spec.field - // suggestedValue: "1" -} - -// ExampleTemplateCommand provides an example for using the TemplateCommand -func ExampleTemplateCommand() { // create the template - cmd := framework.TemplateCommand{ - // Template input - API: &struct { - Key string `json:"key" yaml:"key"` - Value string `json:"value" yaml:"value"` - }{}, - // Template - Template: template.Must(template.New("example").Parse(` + fn := framework.TemplateProcessor{ + // Templates input + TemplateData: api, + // Templates + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.StringTemplates(` apiVersion: apps/v1 kind: Deployment metadata: - name: foo - namespace: default - annotations: - {{ .Key }}: {{ .Value }} -`)), - }.GetCommand() + name: foo + namespace: default + annotations: + {{ .Key }}: {{ .Value }} +`)}}, + } + cmd := command.Build(fn, command.StandaloneEnabled, false) - cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")}) + // mimic standalone mode: testdata/template/config.yaml will be parsed into `api` + cmd.SetArgs([]string{filepath.Join("testdata", "example", "template", "config.yaml")}) if err := cmd.Execute(); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) } // Output: @@ -579,22 +207,29 @@ metadata: // a: b } -// ExampleTemplateCommand_files provides an example for using the TemplateCommand -func ExampleTemplateCommand_files() { +// ExampleTemplateProcessor_files provides an example for using the TemplateProcessor to add +// resources from templates defined in files. +func ExampleTemplateProcessor_generate_files() { + api := new(struct { + Key string `json:"key" yaml:"key"` + Value string `json:"value" yaml:"value"` + }) // create the template - cmd := framework.TemplateCommand{ - // Template input - API: &struct { - Key string `json:"key" yaml:"key"` - Value string `json:"value" yaml:"value"` - }{}, - // Template - TemplatesFiles: []string{filepath.Join("testdata", "templatefiles", "deployment.template")}, - }.GetCommand() - - cmd.SetArgs([]string{filepath.Join("testdata", "templatefiles", "config.yaml")}) + templateFn := framework.TemplateProcessor{ + // Templates input + TemplateData: api, + // Templates + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.TemplatesFromFile( + filepath.Join("testdata", "example", "templatefiles", "deployment.template"), + ), + }}, + } + cmd := command.Build(templateFn, command.StandaloneEnabled, false) + // mimic standalone mode: testdata/template/config.yaml will be parsed into `api` + cmd.SetArgs([]string{filepath.Join("testdata", "example", "templatefiles", "config.yaml")}) if err := cmd.Execute(); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) } // Output: @@ -607,52 +242,58 @@ func ExampleTemplateCommand_files() { // a: b } -// ExampleTemplateCommand_preprocess provides an example for using the TemplateCommand +// ExampleTemplateProcessor_preprocess provides an example for using the TemplateProcessor // with PreProcess to configure the template based on the input resources observed. -func ExampleTemplateCommand_preprocess() { - config := &struct { +func ExampleTemplateProcessor_preprocess() { + config := new(struct { Key string `json:"key" yaml:"key"` Value string `json:"value" yaml:"value"` Short bool - }{} + }) // create the template - cmd := framework.TemplateCommand{ - // Template input - API: config, - PreProcess: func(rl *framework.ResourceList) error { - config.Short = len(rl.Items) < 3 - return nil + fn := framework.TemplateProcessor{ + // Templates input + TemplateData: config, + PreProcessFilters: []kio.Filter{ + kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + config.Short = len(items) < 3 + return items, nil + }), }, - // Template - Template: template.Must(template.New("example").Parse(` + // Templates + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.StringTemplates(` apiVersion: apps/v1 kind: Deployment metadata: - name: foo - namespace: default - annotations: - {{ .Key }}: {{ .Value }} + name: foo + namespace: default + annotations: + {{ .Key }}: {{ .Value }} {{- if .Short }} - short: 'true' + short: 'true' {{- end }} --- apiVersion: apps/v1 kind: Deployment metadata: - name: bar - namespace: default - annotations: - {{ .Key }}: {{ .Value }} + name: bar + namespace: default + annotations: + {{ .Key }}: {{ .Value }} {{- if .Short }} - short: 'true' + short: 'true' {{- end }} -`)), - }.GetCommand() +`), + }}, + } - cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")}) + cmd := command.Build(fn, command.StandaloneEnabled, false) + // mimic standalone mode: testdata/template/config.yaml will be parsed into `api` + cmd.SetArgs([]string{filepath.Join("testdata", "example", "template", "config.yaml")}) if err := cmd.Execute(); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) } // Output: @@ -675,20 +316,20 @@ metadata: // short: 'true' } -// ExampleTemplateCommand_postprocess provides an example for using the TemplateCommand +// ExampleTemplateProcessor_postprocess provides an example for using the TemplateProcessor // with PostProcess to modify the results. -func ExampleTemplateCommand_postprocess() { - config := &struct { +func ExampleTemplateProcessor_postprocess() { + config := new(struct { Key string `json:"key" yaml:"key"` Value string `json:"value" yaml:"value"` - }{} + }) // create the template - cmd := framework.TemplateCommand{ - // Template input - API: config, - // Template - Template: template.Must(template.New("example").Parse(` + fn := framework.TemplateProcessor{ + // Templates input + TemplateData: config, + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.StringTemplates(` apiVersion: apps/v1 kind: Deployment metadata: @@ -704,17 +345,20 @@ metadata: namespace: default annotations: {{ .Key }}: {{ .Value }} -`)), - PostProcess: func(rl *framework.ResourceList) error { - // trim the first resources - rl.Items = rl.Items[1:] - return nil +`), + }}, + PostProcessFilters: []kio.Filter{ + kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + items = items[1:] + return items, nil + }), }, - }.GetCommand() + } + cmd := command.Build(fn, command.StandaloneEnabled, false) - cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")}) + cmd.SetArgs([]string{filepath.Join("testdata", "example", "template", "config.yaml")}) if err := cmd.Execute(); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) } // Output: @@ -727,18 +371,16 @@ metadata: // a: b } -// ExampleTemplateCommand_patch provides an example for using the TemplateCommand to -// create a function which patches resources. -func ExampleTemplateCommand_patch() { - // patch the foo resource only - s := framework.Selector{Names: []string{"foo"}} - - cmd := framework.TemplateCommand{ - API: &struct { +// ExampleTemplateProcessor_patch provides an example for using the TemplateProcessor to +// create a function that patches resources. +func ExampleTemplateProcessor_patch() { + fn := framework.TemplateProcessor{ + TemplateData: new(struct { Key string `json:"key" yaml:"key"` Value string `json:"value" yaml:"value"` - }{}, - Template: template.Must(template.New("example").Parse(` + }), + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.StringTemplates(` apiVersion: apps/v1 kind: Deployment metadata: @@ -754,21 +396,25 @@ metadata: namespace: default annotations: {{ .Key }}: {{ .Value }} -`)), +`), + }}, // PatchTemplates are applied to BOTH ResourceList input resources AND templated resources - PatchTemplates: []framework.PatchTemplate{{ - Selector: &s, - Template: template.Must(template.New("test").Parse(` + PatchTemplates: []framework.PatchTemplate{ + &framework.ResourcePatchTemplate{ + // patch the foo resource only + Selector: &framework.Selector{Names: []string{"foo"}}, + Templates: framework.StringTemplates(` metadata: annotations: patched: 'true' -`)), - }}, - }.GetCommand() +`), + }}, + } + cmd := command.Build(fn, command.StandaloneEnabled, false) - cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")}) + cmd.SetArgs([]string{filepath.Join("testdata", "example", "template", "config.yaml")}) if err := cmd.Execute(); err != nil { - fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) + _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err) } // Output: @@ -790,112 +436,214 @@ metadata: // a: b } +// ExampleTemplateProcessor_MergeResources provides an example for using the TemplateProcessor to +// create a function that treats incoming resources as potential patches +// for the resources the function generates itself. +func ExampleTemplateProcessor_MergeResources() { + p := framework.TemplateProcessor{ + TemplateData: new(struct { + Name string `json:"name" yaml:"name"` + }), + ResourceTemplates: []framework.ResourceTemplate{{ + // This is the generated resource the input will patch + Templates: framework.StringTemplates(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Name }} +spec: + replicas: 1 + selector: + app: foo + template: + spec: + containers: + - name: app + image: example.io/team/app +`), + }}, + MergeResources: true, + } + + // The second resource will be treated as a patch since its metadata matches the resource + // generated by ResourceTemplates and MergeResources is true. + rw := kio.ByteReadWriter{Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- kind: Deployment + apiVersion: apps/v1 + metadata: + name: custom + spec: + replicas: 6 + selector: + app: custom + template: + spec: + containers: + - name: app + image: example.io/team/custom +- kind: Deployment + apiVersion: apps/v1 + metadata: + name: mergeTest + spec: + replicas: 6 +functionConfig: + name: mergeTest +`)} + if err := framework.Execute(p, &rw); err != nil { + panic(err) + } + + // Output: + // apiVersion: config.kubernetes.io/v1alpha1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: mergeTest + // spec: + // replicas: 6 + // selector: + // app: foo + // template: + // spec: + // containers: + // - name: app + // image: example.io/team/app + // - kind: Deployment + // apiVersion: apps/v1 + // metadata: + // name: custom + // spec: + // replicas: 6 + // selector: + // app: custom + // template: + // spec: + // containers: + // - name: app + // image: example.io/team/custom + // functionConfig: + // name: mergeTest +} + +// ExampleSelector_templatizeKinds provides an example of using a template as a selector value, +// to dynamically match resources based on the functionConfig input. It also shows how Selector +// can be used with SimpleProcessor to implement a ResourceListProcessor the filters the input. func ExampleSelector_templatizeKinds() { type api struct { KindName string `yaml:"kindName"` } - rl := &framework.ResourceList{ - FunctionConfig: &api{KindName: "Deployment"}, + rw := &kio.ByteReadWriter{ Reader: bytes.NewBufferString(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: foo - namespace: default ---- -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: bar - namespace: default +apiVersion: config.kubernetes.io/v1beta1 +kind: ResourceList +functionConfig: + kindName: Deployment +items: +- 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) + config := &api{} + p := framework.SimpleProcessor{ + Config: config, + Filter: &framework.Selector{ + TemplateData: config, + Kinds: []string{"{{ .KindName }}"}, + }, } - var err error - s := &framework.Selector{ - TemplatizeValues: true, - Kinds: []string{"{{ .KindName }}"}, - } - rl.Items, err = s.GetMatches(rl) + err := framework.Execute(p, rw) 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' + // apiVersion: config.kubernetes.io/v1beta1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: foo + // namespace: default + // functionConfig: + // kindName: Deployment } +// ExampleSelector_templatizeKinds provides an example of using a template as a selector value, +// to dynamically match resources based on the functionConfig input. It also shows how Selector +// can be used with SimpleProcessor to implement a ResourceListProcessor the filters the input. func ExampleSelector_templatizeAnnotations() { type api struct { - Value string `yaml:"vaue"` + Value string `yaml:"value"` } - 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) + rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1beta1 +kind: ResourceList +functionConfig: + value: bar +items: +- 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 +`)} + config := &api{} + p := framework.SimpleProcessor{ + Config: config, + Filter: &framework.Selector{ + TemplateData: config, + Annotations: map[string]string{"key": "{{ .Value }}"}, + }, } - 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 { + if err := framework.Execute(p, rw); err != nil { panic(err) } // Output: - // apiVersion: apps/v1 - // kind: Deployment - // metadata: - // name: bar - // namespace: default - // annotations: - // key: bar - // config.kubernetes.io/index: '1' + // apiVersion: config.kubernetes.io/v1beta1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: bar + // namespace: default + // annotations: + // key: bar + // functionConfig: + // value: bar } -// ExamplePatchContainersWithString patches all containers. -func ExamplePatchContainersWithString() { - resources, err := kio.ParseAll(` +// ExampleTemplateProcessor_container_patch provides an example for using TemplateProcessor to +// patch all of the containers in the input. +func ExampleTemplateProcessor_container_patch() { + input := ` apiVersion: apps/v1 kind: Deployment metadata: @@ -937,22 +685,23 @@ metadata: spec: selector: foo: bar -`) - if err != nil { - log.Fatal(err) - } - - input := struct{ Value string }{Value: "new-value"} - err = framework.PatchContainersWithString(resources, ` +` + p := framework.TemplateProcessor{ + PatchTemplates: []framework.PatchTemplate{ + &framework.ContainerPatchTemplate{ + Templates: framework.StringTemplates(` env: - KEY: {{ .Value }} -`, input) +- name: KEY + value: {{ .Value }} +`), + TemplateData: struct{ Value string }{Value: "new-value"}, + }}, + } + err := framework.Execute(p, &kio.ByteReadWriter{Reader: bytes.NewBufferString(input)}) if err != nil { log.Fatal(err) } - fmt.Println(kio.StringAll(resources)) - // Output: // apiVersion: apps/v1 // kind: Deployment @@ -965,11 +714,13 @@ env: // - name: foo // image: a // env: - // KEY: new-value + // - name: KEY + // value: new-value // - name: bar // image: b // env: - // KEY: new-value + // - name: KEY + // value: new-value // --- // apiVersion: v1 // kind: Service @@ -990,11 +741,13 @@ env: // - name: foo // image: a // env: - // KEY: new-value + // - name: KEY + // value: new-value // - name: baz // image: b // env: - // KEY: new-value + // - name: KEY + // value: new-value // --- // apiVersion: v1 // kind: Service @@ -1003,13 +756,12 @@ env: // spec: // selector: // foo: bar - // } // PatchTemplateContainersWithString patches containers matching // a specific name. -func ExamplePatchContainersWithString_names() { - resources, err := kio.ParseAll(` +func ExampleTemplateProcessor_container_patch_by_name() { + input := ` apiVersion: apps/v1 kind: Deployment metadata: @@ -1020,6 +772,9 @@ spec: containers: - name: foo image: a + env: + - name: EXISTING + value: variable - name: bar image: b --- @@ -1051,22 +806,26 @@ metadata: spec: selector: foo: bar -`) - if err != nil { - log.Fatal(err) - } - - input := struct{ Value string }{Value: "new-value"} - err = framework.PatchContainersWithString(resources, ` +` + p := framework.TemplateProcessor{ + TemplateData: struct{ Value string }{Value: "new-value"}, + PatchTemplates: []framework.PatchTemplate{ + &framework.ContainerPatchTemplate{ + // Only patch containers named "foo" + ContainerMatcher: framework.ContainerNameMatcher("foo"), + Templates: framework.StringTemplates(` env: - KEY: {{ .Value }} -`, input, "foo") +- name: KEY + value: {{ .Value }} +`), + }}, + } + + err := framework.Execute(p, &kio.ByteReadWriter{Reader: bytes.NewBufferString(input)}) if err != nil { log.Fatal(err) } - fmt.Println(kio.StringAll(resources)) - // Output: // apiVersion: apps/v1 // kind: Deployment @@ -1079,7 +838,10 @@ env: // - name: foo // image: a // env: - // KEY: new-value + // - name: EXISTING + // value: variable + // - name: KEY + // value: new-value // - name: bar // image: b // --- @@ -1102,7 +864,8 @@ env: // - name: foo // image: a // env: - // KEY: new-value + // - name: KEY + // value: new-value // - name: baz // image: b // --- @@ -1113,5 +876,171 @@ env: // spec: // selector: // foo: bar - // +} + +type v1alpha1JavaSpringBoot struct { + Metadata Metadata `yaml:"metadata" json:"metadata"` + Spec v1alpha1JavaSpringBootSpec `yaml:"spec" json:"spec"` +} + +type Metadata struct { + Name string `yaml:"name" json:"name"` +} + +type v1alpha1JavaSpringBootSpec struct { + Replicas int `yaml:"replicas" json:"replicas"` + Domain string `yaml:"domain" json:"domain"` + Image string `yaml:"image" json:"image"` +} + +func (a v1alpha1JavaSpringBoot) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + filter := framework.TemplateProcessor{ + ResourceTemplates: []framework.ResourceTemplate{{ + TemplateData: &a, + Templates: framework.StringTemplates(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Metadata.Name }} + selector: + app: {{ .Metadata.Name }} +spec: + replicas: {{ .Spec.Replicas }} + template: + spec: + containers: + - name: app + image: {{ .Spec.Image }} + {{ if .Spec.Domain }} + ports: + - containerPort: 80 + name: http + {{ end }} + +{{ if .Spec.Domain }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ .Metadata.Name }}-svc +spec: + selector: + app: {{ .Metadata.Name }} + ports: + - protocol: TCP + port: 80 + targetPort: 80 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Metadata.Name }}-ingress +spec: + tls: + - hosts: + - {{ .Spec.Domain }} + secretName: secret-tls + defaultBackend: + service: + name: {{ .Metadata.Name }} + port: + number: 80 +{{ end }} +`), + }}, + } + return filter.Filter(items) +} + +func (a *v1alpha1JavaSpringBoot) Default() error { + if a.Spec.Replicas == 0 { + a.Spec.Replicas = 3 + } + return nil +} + +func (a *v1alpha1JavaSpringBoot) Validate() error { + if a.Metadata.Name == "" { + return errors.Errorf("Name is required") + } + return nil +} + +// ExampleVersionedAPIProcessor shows how to use the VersionedAPIProcessor and TemplateProcessor to +// build functions that implement complex multi-version APIs that require defaulting and validation. +func ExampleVersionedAPIProcessor() { + p := &framework.VersionedAPIProcessor{FilterProvider: framework.GVKFilterMap{ + "JavaSpringBoot": { + "example.com/v1alpha1": &v1alpha1JavaSpringBoot{}, + }}} + + source := &kio.ByteReadWriter{Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1beta1 +kind: ResourceList +functionConfig: + apiVersion: example.com/v1alpha1 + kind: JavaSpringBoot + metadata: + name: my-app + spec: + image: example.docker.com/team/app:1.0 + domain: demo.example.com +`)} + if err := framework.Execute(p, source); err != nil { + log.Fatal(err) + } + + // Output: + // apiVersion: config.kubernetes.io/v1beta1 + // kind: ResourceList + // items: + // - apiVersion: apps/v1 + // kind: Deployment + // metadata: + // name: my-app + // selector: + // app: my-app + // spec: + // replicas: 3 + // template: + // spec: + // containers: + // - name: app + // image: example.docker.com/team/app:1.0 + // ports: + // - containerPort: 80 + // name: http + // - apiVersion: v1 + // kind: Service + // metadata: + // name: my-app-svc + // spec: + // selector: + // app: my-app + // ports: + // - protocol: TCP + // port: 80 + // targetPort: 80 + // - apiVersion: networking.k8s.io/v1 + // kind: Ingress + // metadata: + // name: my-app-ingress + // spec: + // tls: + // - hosts: + // - demo.example.com + // secretName: secret-tls + // defaultBackend: + // service: + // name: my-app + // port: + // number: 80 + // functionConfig: + // apiVersion: example.com/v1alpha1 + // kind: JavaSpringBoot + // metadata: + // name: my-app + // spec: + // image: example.docker.com/team/app:1.0 + // domain: demo.example.com } diff --git a/kyaml/fn/framework/framework.go b/kyaml/fn/framework/framework.go index 26652cdd4..5902926c4 100644 --- a/kyaml/fn/framework/framework.go +++ b/kyaml/fn/framework/framework.go @@ -4,34 +4,22 @@ package framework import ( - "bytes" - "fmt" - "io" - "io/ioutil" "os" - "path/filepath" - "strings" - "text/template" - "github.com/spf13/cobra" - "github.com/spf13/pflag" "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/kio/filters" "sigs.k8s.io/kustomize/kyaml/yaml" ) -// ResourceList reads the function input and writes the function output. -// -// Adheres to the spec: https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +// ResourceList is a Kubernetes list type used as the primary data interchange format +// in the Configuration Functions Specification: +// https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +// This framework facilitates building functions that receive and emit ResourceLists, +// as required by the specification. type ResourceList struct { - // FunctionConfig is the ResourceList.functionConfig input value. If FunctionConfig - // is set to a value such as a struct or map[string]interface{} before ResourceList.Read() - // is called, then the functionConfig will be parsed into that value. - // If it is nil, the functionConfig will be set to a map[string]interface{} - // before it is parsed. + // FunctionConfig is the ResourceList.functionConfig input value. // - // e.g. given the function input: + // e.g. given the input: // // kind: ResourceList // functionConfig: @@ -39,11 +27,13 @@ type ResourceList struct { // spec: // foo: var // - // FunctionConfig will contain the Example unmarshalled into its value. - FunctionConfig interface{} + // FunctionConfig will contain the RNodes for the Example: + // kind: Example + // spec: + // foo: var + FunctionConfig *yaml.RNode `yaml:"functionConfig" json:"functionConfig"` - // Items is the ResourceList.items input and output value. Items will be set by - // ResourceList.Read() and written by ResourceList.Write(). + // Items is the ResourceList.items input and output value. // // e.g. given the function input: // @@ -55,565 +45,102 @@ type ResourceList struct { // ... // // Items will be a slice containing the Deployment and Service resources - Items []*yaml.RNode + // Mutating functions will alter this field during processing. + Items []*yaml.RNode `yaml:"items" json:"items"` - // Result is ResourceList.result output value. Result will be written by - // ResourceList.Write() - Result *Result - - // DisableStandalone if set will not support standalone mode - DisableStandalone bool - - // Args are the command args used for standalone mode - Args []string - - // Flags are an optional set of flags to parse the ResourceList.functionConfig.data. - // If non-nil, ResourceList.Read() will set the flag value for each flag name matching - // a ResourceList.functionConfig.data map entry. - // - // e.g. given the function input: - // - // kind: ResourceList - // functionConfig: - // data: - // foo: bar - // a: b - // - // The flags --a=b and --foo=bar will be set in Flags. - Flags *pflag.FlagSet - - // Reader is used to read the function input (ResourceList). - // Defaults to os.Stdin. - Reader io.Reader - - // Writer is used to write the function output (ResourceList) - // Defaults to os.Stdout. - Writer io.Writer - - // ReadWriter reads function input and writes function output - // If set, it will take precedence over the Reader and Writer fields - ReadWriter *kio.ByteReadWriter - - // NoPrintError if set will prevent the error from being printed - NoPrintError bool - - Command *cobra.Command + // Result is ResourceList.result output value. + // Validating functions can optionally use this field to communicate structured + // validation error data to downstream functions. + Result *Result `yaml:"results" json:"results"` } -func (r *ResourceList) defaultReadWriter() *kio.ByteReadWriter { - rw := kio.ByteReadWriter{ - KeepReaderAnnotations: true, - Reader: r.Reader, - Writer: r.Writer, - } - - // Default reader source precedence: r.Reader > r.Command.InOrStdin > os.Stdin - if rw.Reader == nil { - if r.Command != nil { - rw.Reader = r.Command.InOrStdin() - } else { - rw.Reader = os.Stdin - } - } - - // Default writer source precedence: r.Writer > r.Command.OutOrStdout > os.Stdout - if rw.Writer == nil { - if r.Command != nil { - rw.Writer = r.Command.OutOrStdout() - } else { - rw.Writer = os.Stdout - } - } - - return &rw -} - -// Read reads the ResourceList -func (r *ResourceList) Read() error { - // legacy kustomize plugin input style - legacyPlugin := os.Getenv("KUSTOMIZE_PLUGIN_CONFIG_STRING") - if legacyPlugin != "" && r.FunctionConfig != nil { - err := yaml.Unmarshal([]byte(legacyPlugin), r.FunctionConfig) - if err != nil { - return err - } - } - - // parse the inputs from the args - var readStdinStandalone bool - if len(r.Args) > 0 && !r.DisableStandalone { - // write the files as input - var buf bytes.Buffer - for i := range r.Args { - // the first argument is the resourceList.FunctionConfig and will be parsed - // separately later, the rest of the arguments are the resourceList.items - if i == 0 { - continue - } - if r.Args[i] == "-" { - // Read stdin separately - readStdinStandalone = true - continue - } - - b, err := ioutil.ReadFile(r.Args[i]) - if err != nil { - return errors.WrapPrefixf(err, "unable to read input file %s", r.Args[i]) - } - buf.WriteString(string(b)) - buf.WriteString("\n---\n") - } - r.Reader = &buf - } - - // Default the ReadWriter and ensure related fields are in a consistent state - if r.ReadWriter == nil { - r.ReadWriter = r.defaultReadWriter() - } - r.Reader = r.ReadWriter.Reader - r.Writer = r.ReadWriter.Writer - - // parse the resourceList.FunctionConfig from the first arg - if len(r.Args) > 0 && !r.DisableStandalone { - // Don't keep the reader annotations if we are in standalone mode - r.ReadWriter.KeepReaderAnnotations = false - // Don't wrap the resources in a resourceList -- we are in - // standalone mode and writing to stdout to be applied - r.ReadWriter.NoWrap = true - - b, err := ioutil.ReadFile(r.Args[0]) - if err != nil { - return errors.WrapPrefixf(err, "unable to read configuration file %s", r.Args[0]) - } - fc, err := yaml.Parse(string(b)) - if err != nil { - return errors.WrapPrefixf(err, "unable to parse configuration file %s", r.Args[0]) - } - // use this as the function config used to configure the function - r.ReadWriter.FunctionConfig = fc - } - - var err error - r.Items, err = r.ReadWriter.Read() - if err != nil { - return errors.Wrap(err) - } - - if readStdinStandalone { - var in io.Reader - if r.Command != nil { - in = r.Command.InOrStdin() - } else { - in = os.Stdin - } - br := kio.ByteReader{Reader: in} - items, err := br.Read() - if err != nil { - return errors.Wrap(err) - } - // stdin always comes first so files are patches - r.Items = append(items, r.Items...) - } - - // parse the functionConfig - return func() error { - if r.ReadWriter.FunctionConfig == nil { - // no function config exists - return nil - } - if r.FunctionConfig == nil { - // set directly from r.rw - r.FunctionConfig = r.ReadWriter.FunctionConfig - } else { - // unmarshal the functionConfig into the provided value - err := yaml.Unmarshal([]byte(r.ReadWriter.FunctionConfig.MustString()), r.FunctionConfig) - if err != nil { - return errors.Wrap(err) - } - } - - // set the functionConfig values as flags so they are easy to access - if r.Flags == nil || !r.Flags.HasFlags() { - return nil - } - // flags are always set from the "data" field - data, err := r.ReadWriter.FunctionConfig.Pipe(yaml.Lookup("data")) - if err != nil || data == nil { - return err - } - return data.VisitFields(func(node *yaml.MapNode) error { - f := r.Flags.Lookup(node.Key.YNode().Value) - if f == nil { - return nil - } - return f.Value.Set(node.Value.YNode().Value) - }) - }() -} - -// Write writes the ResourceList -func (r *ResourceList) Write() error { - // set the ResourceList.results for validating functions - if r.Result != nil { - if len(r.Result.Items) > 0 { - b, err := yaml.Marshal(r.Result) - if err != nil { - return errors.Wrap(err) - } - y, err := yaml.Parse(string(b)) - if err != nil { - return errors.Wrap(err) - } - r.ReadWriter.Results = y - } - } - - // write the results - return r.ReadWriter.Write(r.Items) -} - -// Command returns a cobra.Command to run a function. +// ResourceListProcessor is implemented by configuration functions built with this framework +// to conform to the Configuration Functions Specification: +// https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md +// To invoke a processor, pass it to framework.Execute, which will also handle ResourceList IO. // -// The cobra.Command will use the provided ResourceList to Read() the input, -// run the provided function, and then Write() the output. -// -// The returned cobra.Command will have a "gen" subcommand which can be used to generate -// a Dockerfile to build the function into a container image -// -// go run main.go gen DIR/ -func Command(resourceList *ResourceList, function Function) *cobra.Command { - cmd := cobra.Command{} - resourceList.Command = &cmd - AddGenerateDockerfile(&cmd) - var printStack bool - cmd.RunE = func(cmd *cobra.Command, args []string) error { - resourceList.Reader = cmd.InOrStdin() - resourceList.Writer = cmd.OutOrStdout() - resourceList.Flags = cmd.Flags() - resourceList.Args = args - err := WrapInResourceListIO(resourceList, function)() - if err != nil && !resourceList.NoPrintError { - fmt.Fprintf(cmd.ErrOrStderr(), "%v", err) - } - // print the stack if requested - if s := errors.GetStack(err); printStack && s != "" { - fmt.Fprintln(cmd.ErrOrStderr(), s) - } - return err - } - cmd.Flags().BoolVar(&printStack, "stack", false, "print the stack trace on failure") - cmd.Args = cobra.MinimumNArgs(0) - cmd.SilenceErrors = true - cmd.SilenceUsage = true - return &cmd +// This framework provides several ready-to-use ResourceListProcessors, including +// SimpleProcessor, VersionedAPIProcessor and TemplateProcessor. +// You can also build your own by implementing this interface. +type ResourceListProcessor interface { + Process(rl *ResourceList) error } -// TemplateCommand provides a cobra command to invoke a template -type TemplateCommand struct { - // API is the function API provide to the template as input - API interface{} +// ResourceListProcessorFunc converts a compatible function to a ResourceListProcessor. +type ResourceListProcessorFunc func(rl *ResourceList) error - // Template is a go template to render and is appended to Templates. - Template *template.Template - - // Templates is a list of templates to render. - Templates []*template.Template - - // TemplatesFn returns a list of templates - TemplatesFn func(*ResourceList) ([]*template.Template, error) - - // PatchTemplates is a list of templates to render into Patches and apply. - PatchTemplates []PatchTemplate - - // PatchTemplateFn returns a list of templates to render into Patches and apply. - // PatchTemplateFn is called after the ResourceList has been parsed. - PatchTemplatesFn func(*ResourceList) ([]PatchTemplate, error) - - // PatchContainerTemplates applies patches to matching container fields - PatchContainerTemplates []ContainerPatchTemplate - - // PatchContainerTemplates returns a list of PatchContainerTemplates - PatchContainerTemplatesFn func(*ResourceList) ([]ContainerPatchTemplate, error) - - // TemplateFiles list of templates to read from disk which are appended - // to Templates. - TemplatesFiles []string - - // MergeResources if set to true will apply input resources - // as patches to the templates - MergeResources bool - - // PreProcess is run on the ResourceList before the template is invoked - PreProcess func(*ResourceList) error - - PreProcessFilters []kio.Filter - - // PostProcess is run on the ResourceList after the template is invoked - PostProcess func(*ResourceList) error - - PostProcessFilters []kio.Filter +// Process makes ResourceListProcessorFunc implement the ResourceListProcessor interface. +func (p ResourceListProcessorFunc) Process(rl *ResourceList) error { + return p(rl) } -// ContainerPatchTemplate defines a patch to be applied to containers -type ContainerPatchTemplate struct { - PatchTemplate - - ContainerNames []string -} - -func (tc TemplateCommand) doTemplate(t *template.Template, rl *ResourceList) error { - // invoke the template - var b bytes.Buffer - err := t.Execute(&b, tc.API) - if err != nil { - return errors.WrapPrefixf(err, "failed to render template %v", t.DefinedTemplates()) - } - // split the resources so the error messaging is better - for _, s := range strings.Split(b.String(), "\n---\n") { - s = strings.TrimSpace(s) - if s == "" { - continue - } - nodes, err := (&kio.ByteReader{Reader: bytes.NewBufferString(s)}).Read() - if err != nil { - // create the debug string - lines := strings.Split(s, "\n") - for j := range lines { - lines[j] = fmt.Sprintf("%03d %s", j+1, lines[j]) - } - s = strings.Join(lines, "\n") - return errors.WrapPrefixf(err, "failed to parse rendered template into a resource:\n%s\n", s) - } - - if tc.MergeResources { - // apply inputs as patches -- add the - rl.Items = append(nodes, rl.Items...) - } else { - // add to the inputs list - rl.Items = append(rl.Items, nodes...) - } - } - return nil -} - -// Defaulter is implemented by APIs to have Default invoked +// Defaulter is implemented by APIs to have Default invoked. +// The standard application is to create a type to hold your FunctionConfig data, and +// implement Defaulter on that type. All of the framework's processors will invoke Default() +// on your type after unmarshalling the FunctionConfig data into it. type Defaulter interface { Default() error } -// Validator is implemented by APIs to have Validate invoked +// Validator is implemented by APIs to have Validate invoked. +// The standard application is to create a type to hold your FunctionConfig data, and +// implement Validator on that type. All of the framework's processors will invoke Validate() +// on your type after unmarshalling the FunctionConfig data into it. type Validator interface { Validate() error } -func (tc *TemplateCommand) doPreProcess(rl *ResourceList) error { - // do any preprocessing - if tc.PreProcess != nil { - if err := tc.PreProcess(rl); err != nil { - return err - } +// Execute is the entrypoint for invoking configuration functions built with this framework +// from code. See framework/command#Build for a Cobra-based command-line equivalent. +// Execute reads a ResourceList from the given source, passes it to a ResourceListProcessor, +// and then writes the result to the target. +// STDIN and STDOUT will be used if no reader or writer respectively is provided. +func Execute(p ResourceListProcessor, rlSource *kio.ByteReadWriter) error { + // Prepare the resource list source + if rlSource == nil { + rlSource = &kio.ByteReadWriter{KeepReaderAnnotations: true} + } + if rlSource.Reader == nil { + rlSource.Reader = os.Stdin + } + if rlSource.Writer == nil { + rlSource.Writer = os.Stdout } - // TODO: test this - if tc.PreProcessFilters != nil { - for i := range tc.PreProcessFilters { - fltr := tc.PreProcessFilters[i] - var err error - rl.Items, err = fltr.Filter(rl.Items) - if err != nil { - return err - } - } - } - return nil -} - -func (tc *TemplateCommand) doMerge(rl *ResourceList) error { + // Read the input + rl := ResourceList{} var err error - if tc.MergeResources { - rl.Items, err = filters.MergeFilter{}.Filter(rl.Items) + if rl.Items, err = rlSource.Read(); err != nil { + return errors.Wrap(err) } - return err -} + rl.FunctionConfig = rlSource.FunctionConfig -func (tc *TemplateCommand) doPostProcess(rl *ResourceList) error { - // finish up - if tc.PostProcess != nil { - if err := tc.PostProcess(rl); err != nil { - return err - } - } - // TODO: test this - if tc.PostProcessFilters != nil { - for i := range tc.PostProcessFilters { - fltr := tc.PostProcessFilters[i] - var err error - rl.Items, err = fltr.Filter(rl.Items) - if err != nil { - return err - } - } - } - return nil -} + retErr := p.Process(&rl) -func (tc *TemplateCommand) doTemplates(rl *ResourceList) error { - if tc.Template != nil { - tc.Templates = append(tc.Templates, tc.Template) - } - - // TODO: test this - if tc.TemplatesFn != nil { - t, err := tc.TemplatesFn(rl) + // Write the results + // Set the ResourceList.results for validating functions + if rl.Result != nil && len(rl.Result.Items) > 0 { + b, err := yaml.Marshal(rl.Result) if err != nil { - return err + return errors.Wrap(err) } - tc.Templates = append(tc.Templates, t...) - } - - for i := range tc.TemplatesFiles { - tbytes, err := ioutil.ReadFile(tc.TemplatesFiles[i]) + y, err := yaml.Parse(string(b)) if err != nil { - return errors.WrapPrefixf(err, "unable to read template file") + return errors.Wrap(err) } - t, err := template.New("files").Parse(string(tbytes)) - if err != nil { - return errors.WrapPrefixf(err, "unable to parse template files %v", tc.TemplatesFiles) - } - tc.Templates = append(tc.Templates, t) + rlSource.Results = y } - - for i := range tc.Templates { - if err := tc.doTemplate(tc.Templates[i], rl); err != nil { - return err - } - } - return nil -} - -func (tc *TemplateCommand) doPatchTemplates(rl *ResourceList) error { - if tc.PatchTemplatesFn != nil { - pt, err := tc.PatchTemplatesFn(rl) - if err != nil { - return err - } - tc.PatchTemplates = append(tc.PatchTemplates, pt...) - } - for i := range tc.PatchTemplates { - if err := tc.PatchTemplates[i].Apply(rl); err != nil { - return err - } - } - return nil -} - -func (tc *TemplateCommand) doPatchContainerTemplates(rl *ResourceList) error { - if tc.PatchContainerTemplatesFn != nil { - ct, err := tc.PatchContainerTemplatesFn(rl) - if err != nil { - return err - } - tc.PatchContainerTemplates = append(tc.PatchContainerTemplates, ct...) - } - for i := range tc.PatchContainerTemplates { - ct := tc.PatchContainerTemplates[i] - matches, err := ct.Selector.GetMatches(rl) - if err != nil { - return err - } - err = PatchContainersWithTemplate(matches, ct.Template, rl.FunctionConfig, ct.ContainerNames...) - if err != nil { - return err - } - } - return nil -} - -// GetCommand returns a new cobra command -func (tc TemplateCommand) GetCommand() *cobra.Command { - rl := &ResourceList{ - FunctionConfig: tc.API, - NoPrintError: true, - } - return Command(rl, func() error { return tc.Execute(rl) }) -} - -func (tc TemplateCommand) Execute(rl *ResourceList) error { - if d, ok := rl.FunctionConfig.(Defaulter); ok { - if err := d.Default(); err != nil { - return err - } - } - - if v, ok := rl.FunctionConfig.(Validator); ok { - if err := v.Validate(); err != nil { - return err - } - } - - if err := tc.doPreProcess(rl); err != nil { - return err - } - if err := tc.doTemplates(rl); err != nil { - return err - } - if err := tc.doPatchTemplates(rl); err != nil { - return err - } - if err := tc.doPatchContainerTemplates(rl); err != nil { - return err - } - if err := tc.doMerge(rl); err != nil { - return err - } - if err := tc.doPostProcess(rl); err != nil { + if err := rlSource.Write(rl.Items); err != nil { return err } - return nil + return retErr } -// AddGenerateDockerfile adds a "gen" subcommand to create a Dockerfile for building -// the function as a container. -func AddGenerateDockerfile(cmd *cobra.Command) { - gen := &cobra.Command{ - Use: "gen", - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return ioutil.WriteFile(filepath.Join(args[0], "Dockerfile"), []byte(`FROM golang:1.13-stretch -ENV CGO_ENABLED=0 -WORKDIR /go/src/ -COPY . . -RUN go build -v -o /usr/local/bin/function ./ - -FROM alpine:latest -COPY --from=0 /usr/local/bin/function /usr/local/bin/function -CMD ["function"] -`), 0600) - }, - } - cmd.AddCommand(gen) -} - -func WrapInResourceListIO(rl *ResourceList, function Function) Function { - return func() error { - if err := rl.Read(); err != nil { - return err - } - - retErr := function() - - if err := rl.Write(); err != nil { - return err - } - - return retErr - } -} - -// Filters returns a function which returns the provided Filters -func Filters(fltrs ...kio.Filter) func(*ResourceList) []kio.Filter { - return func(*ResourceList) []kio.Filter { - return fltrs - } +// Filter executes the given kio.Filter and replaces the ResourceList's items with the result. +// This can be used to help implement ResourceListProcessors. See SimpleProcessor for example. +func (rl *ResourceList) Filter(api kio.Filter) error { + var err error + rl.Items, err = api.Filter(rl.Items) + return errors.Wrap(err) } diff --git a/kyaml/fn/framework/framework_test.go b/kyaml/fn/framework/framework_test.go index e12292715..a69354435 100644 --- a/kyaml/fn/framework/framework_test.go +++ b/kyaml/fn/framework/framework_test.go @@ -5,362 +5,84 @@ package framework_test import ( "bytes" - "io/ioutil" - "os" - "path/filepath" "strings" "testing" - "text/template" - "github.com/spf13/cobra" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/kyaml/fn/framework" - "sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil" - "sigs.k8s.io/kustomize/kyaml/testutil" + "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) -func TestCommand_dockerfile(t *testing.T) { - d, err := ioutil.TempDir("", "kustomize") - if !assert.NoError(t, err) { - t.FailNow() - } - defer os.RemoveAll(d) - - // create a function - - resourceList := &framework.ResourceList{} - cmd := framework.Command(resourceList, func() error { return nil }) - - // generate the Dockerfile - cmd.SetArgs([]string{"gen", d}) - if !assert.NoError(t, cmd.Execute()) { - t.FailNow() - } - - b, err := ioutil.ReadFile(filepath.Join(d, "Dockerfile")) - if !assert.NoError(t, err) { - t.FailNow() - } - - expected := `FROM golang:1.13-stretch -ENV CGO_ENABLED=0 -WORKDIR /go/src/ -COPY . . -RUN go build -v -o /usr/local/bin/function ./ - -FROM alpine:latest -COPY --from=0 /usr/local/bin/function /usr/local/bin/function -CMD ["function"] -` - if !assert.Equal(t, expected, string(b)) { - t.FailNow() - } -} - -// TestCommand_standalone tests the framework works in standalone mode -func TestCommand_standalone(t *testing.T) { - // TODO: make this test pass on windows -- currently failure seems spurious - testutil.SkipWindows(t) - - type api = struct { - A string `json:"a" yaml:"a"` - } - var config api - - resourceList := &framework.ResourceList{FunctionConfig: &config} - cmdFn := func() *cobra.Command { - return framework.Command(resourceList, func() error { - resourceList.Items = append(resourceList.Items, yaml.MustParse(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 -`)) - for i := range resourceList.Items { - err := resourceList.Items[i].PipeE(yaml.SetAnnotation("a", config.A)) - if err != nil { - return err - } - } - - return nil - }) - } - - frameworktestutil.ResultsChecker{Command: cmdFn}.Assert(t) -} - -func TestCommand_standalonestdin(t *testing.T) { - // TODO: make this test pass on windows -- currently failure seems spurious - testutil.SkipWindows(t) - - type api = struct { - A string `json:"a" yaml:"a"` - } - var config api - - resourceList := &framework.ResourceList{FunctionConfig: &config} - cmd := framework.Command(resourceList, func() error { - resourceList.Items = append(resourceList.Items, yaml.MustParse(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bar2 - namespace: default - annotations: - foo: bar2 -`)) - for i := range resourceList.Items { - err := resourceList.Items[i].PipeE(yaml.SetAnnotation("a", config.A)) - if err != nil { - return err - } +func TestExecute_Result(t *testing.T) { + p := framework.ResourceListProcessorFunc(func(rl *framework.ResourceList) error { + err := &framework.Result{ + Name: "Incompatible config", + Items: []framework.ResultItem{{ + Message: "bad value for replicas", + Severity: framework.Error, + ResourceRef: yaml.ResourceMeta{ + TypeMeta: yaml.TypeMeta{APIVersion: "v1", Kind: "Deployment"}, + ObjectMeta: yaml.ObjectMeta{ + NameMeta: yaml.NameMeta{Name: "tester", Namespace: "default"}, + }, + }, + Field: framework.Field{ + Path: ".spec.Replicas", + CurrentValue: "0", + SuggestedValue: "3", + }, + File: framework.File{ + Path: "/path/to/deployment.yaml", + Index: 0, + }, + }}, } - - return nil + rl.Result = err + return err }) - cmd.SetIn(bytes.NewBufferString(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 -spec: - replicas: 1 -`)) - var out bytes.Buffer - cmd.SetOut(&out) - cmd.SetArgs([]string{filepath.Join("testdata", "command", "config.yaml"), "-"}) - - require.NoError(t, cmd.Execute()) - - require.Equal(t, strings.TrimSpace(` -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 - a: 'b' -spec: - replicas: 1 ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: bar2 - namespace: default - annotations: - foo: bar2 - a: 'b' -`), strings.TrimSpace(out.String())) -} - -func TestCommand_PatchTemplateFn(t *testing.T) { - // TODO: make this test pass on windows -- currently failure seems spurious - testutil.SkipWindows(t) - - type api = struct { - Spec struct { - A string `json:"a" yaml:"a"` - } `json:"spec" yaml:"spec"` - } - var config api - - cmd := framework.TemplateCommand{ - API: &config, - PatchTemplatesFn: func(_ *framework.ResourceList) ([]framework.PatchTemplate, error) { - return []framework.PatchTemplate{{ - Selector: &framework.Selector{Names: []string{config.Spec.A}}, - Template: template.Must(template.New("test").Parse(` -metadata: - annotations: - baz: buz -`)), - }}, nil - }, - }.GetCommand() - - cmd.SetIn(bytes.NewBufferString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 - spec: - replicas: 1 -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar2 - namespace: default - annotations: - foo: bar2 - spec: - replicas: 1 -functionConfig: - apiVersion: example.com/v1alpha1 - kind: Example - spec: - a: "bar1" -`)) - var out bytes.Buffer - cmd.SetOut(&out) - - require.NoError(t, cmd.Execute()) - - require.Equal(t, strings.TrimSpace(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 - baz: buz - config.kubernetes.io/index: '0' - spec: - replicas: 1 -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar2 - namespace: default - annotations: - foo: bar2 - spec: - replicas: 1 -functionConfig: - apiVersion: example.com/v1alpha1 - kind: Example - spec: - a: "bar1" -`), strings.TrimSpace(out.String())) -} - -func TestCommand_PatchContainerTemplatesFn(t *testing.T) { - // TODO: make this test pass on windows -- currently failure seems spurious - testutil.SkipWindows(t) - - type api = struct { - Spec struct { - A string `json:"a" yaml:"a"` - } `json:"spec" yaml:"spec"` - } - var config api - - cmd := framework.TemplateCommand{ - API: &config, - PatchContainerTemplatesFn: func(_ *framework.ResourceList) ([]framework.ContainerPatchTemplate, error) { - return []framework.ContainerPatchTemplate{{ - PatchTemplate: framework.PatchTemplate{ - Selector: &framework.Selector{Names: []string{config.Spec.A}}, - Template: template.Must(template.New("test").Parse(` -env: - key: Foo - value: Bar -`))}, - }}, nil - }, - }.GetCommand() - - cmd.SetIn(bytes.NewBufferString(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 - spec: - template: - spec: - containers: - - name: foo - - name: bar -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar2 - namespace: default - annotations: - foo: bar2 - spec: - template: - spec: - containers: - - name: foo - - name: bar -functionConfig: - apiVersion: example.com/v1alpha1 - kind: Example - spec: - a: "bar1" -`)) - var out bytes.Buffer - cmd.SetOut(&out) - - require.NoError(t, cmd.Execute()) - - require.Equal(t, strings.TrimSpace(` -apiVersion: config.kubernetes.io/v1alpha1 -kind: ResourceList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar1 - namespace: default - annotations: - foo: bar1 - spec: - template: - spec: - containers: - - name: foo - env: - key: Foo - value: Bar - - name: bar - env: - key: Foo - value: Bar -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: bar2 - namespace: default - annotations: - foo: bar2 - spec: - template: - spec: - containers: - - name: foo - - name: bar -functionConfig: - apiVersion: example.com/v1alpha1 - kind: Example - spec: - a: "bar1" -`), strings.TrimSpace(out.String())) + out := new(bytes.Buffer) + source := &kio.ByteReadWriter{Reader: bytes.NewBufferString(` +kind: ResourceList +apiVersion: config.kubernetes.io/v1alpha1 +items: +- kind: Deployment + apiVersion: v1 + metadata: + name: tester + namespace: default + spec: + replicas: 0 +`), Writer: out} + err := framework.Execute(p, source) + assert.EqualError(t, err, "[error] v1/Deployment/default/tester .spec."+ + "Replicas: bad value for replicas") + assert.Equal(t, 1, err.(*framework.Result).ExitCode()) + assert.Equal(t, `apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- kind: Deployment + apiVersion: v1 + metadata: + name: tester + namespace: default + spec: + replicas: 0 +results: + name: Incompatible config + items: + - message: bad value for replicas + severity: error + resourceRef: + apiVersion: v1 + kind: Deployment + metadata: + name: tester + namespace: default + field: + path: .spec.Replicas + currentValue: "0" + suggestedValue: "3" + file: + path: /path/to/deployment.yaml`, strings.TrimSpace(out.String())) } diff --git a/kyaml/fn/framework/frameworktestutil/frameworktestutil.go b/kyaml/fn/framework/frameworktestutil/frameworktestutil.go index 40cc5642f..c2d17d9b6 100644 --- a/kyaml/fn/framework/frameworktestutil/frameworktestutil.go +++ b/kyaml/fn/framework/frameworktestutil/frameworktestutil.go @@ -14,13 +14,17 @@ import ( "github.com/spf13/cobra" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" ) -// ResultsChecker tests a function by running it with predefined inputs and comparing +// CommandResultsChecker tests a function by running it with predefined inputs and comparing // the outputs to expected results. -type ResultsChecker struct { +type CommandResultsChecker struct { // TestDataDirectory is the directory containing the testdata subdirectories. - // ResultsChecker will recurse into each test directory and run the Command + // CommandResultsChecker will recurse into each test directory and run the Command // if the directory contains both the ConfigInputFilename and at least one // of ExpectedOutputFilname or ExpectedErrorFilename. // Defaults to "testdata" @@ -37,12 +41,12 @@ type ResultsChecker struct { // ExpectedOutputFilename is the file with the expected output of the function // Defaults to "expected.yaml". Directories containing neither this file - // nore ExpectedErrorFilename will be skipped. + // nor ExpectedErrorFilename will be skipped. ExpectedOutputFilename string // ExpectedErrorFilename is the file containing part of an expected error message // Defaults to "error.yaml". Directories containing neither this file - // nore ExpectedOutputFilname will be skipped. + // nor ExpectedOutputFilename will be skipped. ExpectedErrorFilename string // Command provides the function to run. @@ -51,10 +55,12 @@ type ResultsChecker struct { // UpdateExpectedFromActual if set to true will write the actual results to the // expected testdata files. This is useful for updating test data. UpdateExpectedFromActual bool + + testsCasesRun int } // Assert asserts the results for functions -func (rc ResultsChecker) Assert(t *testing.T) bool { +func (rc *CommandResultsChecker) Assert(t *testing.T) bool { if rc.TestDataDirectory == "" { rc.TestDataDirectory = "testdata" } @@ -71,10 +77,8 @@ func (rc ResultsChecker) Assert(t *testing.T) bool { rc.InputFilenameGlob = "input*.yaml" } - _ = filepath.Walk(rc.TestDataDirectory, func(path string, info os.FileInfo, err error) error { - if err != nil { - t.FailNow() - } + err := filepath.Walk(rc.TestDataDirectory, func(path string, info os.FileInfo, err error) error { + require.NoError(t, err) if !info.IsDir() { // skip non-directories return nil @@ -82,21 +86,21 @@ func (rc ResultsChecker) Assert(t *testing.T) bool { rc.compare(t, path) return nil }) + require.NoError(t, err) + + require.NotZero(t, rc.testsCasesRun, "No complete test cases found in %s", rc.TestDataDirectory) return true } -func (rc ResultsChecker) compare(t *testing.T, path string) { +func (rc *CommandResultsChecker) 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() - } + require.NoError(t, err) + + defer func() { require.NoError(t, os.Chdir(d)) }() + require.NoError(t, os.Chdir(path)) // make sure this directory contains test data _, err = os.Stat(rc.ConfigInputFilename) @@ -106,22 +110,19 @@ func (rc ResultsChecker) compare(t *testing.T, path string) { } args := []string{rc.ConfigInputFilename} - expectedOutput, expectedError := rc.getExpected(t) + expectedOutput, expectedError := getExpected(t, rc.ExpectedOutputFilename, rc.ExpectedErrorFilename) if expectedError == "" && expectedOutput == "" { // missing expected return } - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, err) // run the test t.Run(path, func(t *testing.T) { + rc.testsCasesRun += 1 if rc.InputFilenameGlob != "" { inputs, err := filepath.Glob(rc.InputFilenameGlob) - if !assert.NoError(t, err) { - t.FailNow() - } + require.NoError(t, err) args = append(args, inputs...) } @@ -133,64 +134,196 @@ func (rc ResultsChecker) compare(t *testing.T, path string) { err = cmd.Execute() - // Compae the results - if expectedError != "" && !assert.Error(t, err, actualOutput.String()) { - if !rc.UpdateExpectedFromActual { - t.FailNow() + // Update the fixtures if configured to + if rc.UpdateExpectedFromActual { + if actualError.String() != "" { + assert.NoError(t, ioutil.WriteFile(rc.ExpectedErrorFilename, actualError.Bytes(), 0600)) } + if actualOutput.String() != "" { + assert.NoError(t, ioutil.WriteFile(rc.ExpectedOutputFilename, actualOutput.Bytes(), 0600)) + } + return } - if expectedError == "" && !assert.NoError(t, err, actualError.String()) { - if !rc.UpdateExpectedFromActual { - t.FailNow() - } + + // Compare the results + if expectedError != "" { + // We expected an error, so make sure there was one and it matches + require.Error(t, err, actualOutput.String()) + require.Contains(t, + standardizeSpacing(actualError.String()), + standardizeSpacing(expectedError), actualOutput.String()) + } else { + // We didn't expect an error, and the output should match + require.NoError(t, err, actualError.String()) + require.Equal(t, + standardizeSpacing(expectedOutput), + standardizeSpacing(actualOutput.String()), actualError.String()) } - if !assert.Equal(t, - strings.TrimSpace(expectedOutput), - strings.TrimSpace(actualOutput.String()), actualError.String()) { - if !rc.UpdateExpectedFromActual { - t.FailNow() - } - // update test results - assert.NoError(t, ioutil.WriteFile(rc.ExpectedOutputFilename, actualOutput.Bytes(), 0600)) + }) +} + +func standardizeSpacing(s string) string { + // remove extra whitespace and convert Windows line endings + return strings.ReplaceAll(strings.TrimSpace(s), "\r\n", "\n") +} + +// ProcessorResultsChecker tests a function by running it with predefined inputs and comparing +// the outputs to expected results. +type ProcessorResultsChecker struct { + // TestDataDirectory is the directory containing the testdata subdirectories. + // CommandResultsChecker will recurse into each test directory and run the Processor + // if the directory contains both the InputFilename and at least one + // of ExpectedOutputFilename or ExpectedErrorFilename. + // Defaults to "testdata" + TestDataDirectory string + + // InputFilename is the name of the file containing the ResourceList input. + // Directories without this file will be skipped. + // Defaults to "input.yaml" + InputFilename string + + // ExpectedOutputFilename is the file with the expected output of the function + // Defaults to "expected.yaml". Directories containing neither this file + // nor ExpectedErrorFilename will be skipped. + ExpectedOutputFilename string + + // ExpectedErrorFilename is the file containing part of an expected error message + // Defaults to "error.yaml". Directories containing neither this file + // nor ExpectedOutputFilename will be skipped. + ExpectedErrorFilename string + + // Processor returns a ResourceListProcessor to run. + Processor func() framework.ResourceListProcessor + + // UpdateExpectedFromActual if set to true will write the actual results to the + // expected testdata files. This is useful for updating test data. + UpdateExpectedFromActual bool + + testsCasesRun int +} + +// Assert asserts the results for functions +func (rc *ProcessorResultsChecker) Assert(t *testing.T) bool { + if rc.TestDataDirectory == "" { + rc.TestDataDirectory = "testdata" + } + if rc.InputFilename == "" { + rc.InputFilename = "input.yaml" + } + if rc.ExpectedOutputFilename == "" { + rc.ExpectedOutputFilename = "expected.yaml" + } + if rc.ExpectedErrorFilename == "" { + rc.ExpectedErrorFilename = "error.yaml" + } + + err := filepath.Walk(rc.TestDataDirectory, func(path string, info os.FileInfo, err error) error { + require.NoError(t, err) + if !info.IsDir() { + // skip non-directories + return nil } - if !assert.Contains(t, - strings.TrimSpace(actualError.String()), - strings.TrimSpace(expectedError), actualOutput.String()) { - if !rc.UpdateExpectedFromActual { - t.FailNow() + rc.compare(t, path) + return nil + }) + require.NoError(t, err) + + require.NotZero(t, rc.testsCasesRun, "No complete test cases found in %s", rc.TestDataDirectory) + + return true +} + +func (rc *ProcessorResultsChecker) 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() + require.NoError(t, err) + + defer func() { require.NoError(t, os.Chdir(d)) }() + require.NoError(t, os.Chdir(path)) + + // make sure this directory contains test data + _, err = os.Stat(rc.InputFilename) + if os.IsNotExist(err) { + // missing input + return + } + require.NoError(t, err) + + expectedOutput, expectedError := getExpected(t, rc.ExpectedOutputFilename, rc.ExpectedErrorFilename) + if expectedError == "" && expectedOutput == "" { + // missing expected + return + } + + // run the test + t.Run(path, func(t *testing.T) { + rc.testsCasesRun += 1 + actualOutput := bytes.NewBuffer([]byte{}) + rlBytes, err := ioutil.ReadFile(rc.InputFilename) + require.NoError(t, err) + + rw := kio.ByteReadWriter{ + Reader: bytes.NewBuffer(rlBytes), + Writer: actualOutput, + } + + err = framework.Execute(rc.Processor(), &rw) + + // Update the fixtures if configured to + if rc.UpdateExpectedFromActual { + if err != nil { + require.NoError(t, ioutil.WriteFile(rc.ExpectedErrorFilename, []byte(err.Error()), 0600)) } - // update test results - assert.NoError(t, ioutil.WriteFile(rc.ExpectedErrorFilename, actualError.Bytes(), 0600)) + if len(actualOutput.String()) > 0 { + require.NoError(t, ioutil.WriteFile(rc.ExpectedOutputFilename, actualOutput.Bytes(), 0600)) + } + return + } + + // Compare the results + if expectedError != "" { + // We expected an error, so make sure there was one and it matches + require.Error(t, err, actualOutput.String()) + require.Contains(t, + standardizeSpacing(err.Error()), + standardizeSpacing(expectedError), actualOutput.String()) + } else { + // We didn't expect an error, and the output should match + require.NoError(t, err) + require.Equal(t, + standardizeSpacing(expectedOutput), + standardizeSpacing(actualOutput.String())) } }) } // getExpected reads the expected results and error files -func (rc ResultsChecker) getExpected(t *testing.T) (string, string) { +func getExpected(t *testing.T, expectedOutFilename, expectedErrFilename string) (string, string) { // read the expected results var expectedOutput, expectedError string - if rc.ExpectedOutputFilename != "" { - _, err := os.Stat(rc.ExpectedOutputFilename) + if expectedOutFilename != "" { + _, err := os.Stat(expectedOutFilename) if !os.IsNotExist(err) && err != nil { t.FailNow() } if err == nil { // only read the file if it exists - b, err := ioutil.ReadFile(rc.ExpectedOutputFilename) + b, err := ioutil.ReadFile(expectedOutFilename) if !assert.NoError(t, err) { t.FailNow() } expectedOutput = string(b) } } - if rc.ExpectedErrorFilename != "" { - _, err := os.Stat(rc.ExpectedErrorFilename) + if expectedErrFilename != "" { + _, err := os.Stat(expectedErrFilename) if !os.IsNotExist(err) && err != nil { t.FailNow() } if err == nil { // only read the file if it exists - b, err := ioutil.ReadFile(rc.ExpectedErrorFilename) + b, err := ioutil.ReadFile(expectedErrFilename) if !assert.NoError(t, err) { t.FailNow() } diff --git a/kyaml/fn/framework/matchers.go b/kyaml/fn/framework/matchers.go new file mode 100644 index 000000000..ad36adf90 --- /dev/null +++ b/kyaml/fn/framework/matchers.go @@ -0,0 +1,349 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package framework + +import ( + "bytes" + "strings" + "text/template" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/sets" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// ResourceMatcher is implemented by types designed for use in or as selectors. +type ResourceMatcher interface { + // kio.Filter applies the matcher to multiple resources. + // This makes individual matchers usable as selectors directly. + kio.Filter + // Match returns true if the given resource matches the matcher's configuration. + Match(node *yaml.RNode) bool +} + +// ResourceMatcherFunc converts a compliant function into a ResourceMatcher +type ResourceMatcherFunc func(node *yaml.RNode) bool + +// Match runs the ResourceMatcherFunc on the given node. +func (m ResourceMatcherFunc) Match(node *yaml.RNode) bool { + return m(node) +} + +// Filter applies ResourceMatcherFunc to a list of items, returning only those that match. +func (m ResourceMatcherFunc) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + // MatchAll or MatchAny doesn't really matter here since there is only one matcher (m). + return MatchAll(m).Filter(items) +} + +// ResourceTemplateMatcher is implemented by ResourceMatcher types that accept text templates as +// part of their configuration. +type ResourceTemplateMatcher interface { + // ResourceMatcher makes matchers usable in or as selectors. + ResourceMatcher + // DefaultTemplateData is used to pass default template values down a chain of matchers. + DefaultTemplateData(interface{}) + // InitTemplates is used to render the templates in selectors that support + // ResourceTemplateMatcher. The selector should call this exactly once per filter + // operation, before beginning match comparisons. + InitTemplates() error +} + +// ContainerNameMatcher returns a function that returns true if the "name" field +// of the provided container node matches one of the given container names. +// If no names are provided, the function always returns true. +// Note that this is not a ResourceMatcher, since the node it matches against must be +// container-level (e.g. "name", "env" and "image" would be top level fields). +func ContainerNameMatcher(names ...string) func(node *yaml.RNode) bool { + namesSet := sets.String{} + namesSet.Insert(names...) + return func(node *yaml.RNode) bool { + if len(namesSet) == 0 { + return true + } + f := node.Field("name") + if f == nil { + return false + } + return namesSet.Has(yaml.GetValue(f.Value)) + } +} + +// NameMatcher matches resources whose metadata.name is equal to one of the provided values. +// e.g. `NameMatcher("foo", "bar")` matches if `metadata.name` is either "foo" or "bar". +// +// NameMatcher supports templating. +// e.g. `NameMatcher("{{.AppName}}")` will match `metadata.name` "foo" if TemplateData is +// `struct{ AppName string }{ AppName: "foo" }` +func NameMatcher(names ...string) ResourceTemplateMatcher { + return &TemplatedMetaSliceMatcher{ + Templates: names, + MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { + return names.Has(meta.Name) + }, + } +} + +// NamespaceMatcher matches resources whose metadata.namespace is equal to one of the provided values. +// e.g. `NamespaceMatcher("foo", "bar")` matches if `metadata.namespace` is either "foo" or "bar". +// +// NamespaceMatcher supports templating. +// e.g. `NamespaceMatcher("{{.AppName}}")` will match `metadata.namespace` "foo" if TemplateData is +// `struct{ AppName string }{ AppName: "foo" }` +func NamespaceMatcher(names ...string) ResourceTemplateMatcher { + return &TemplatedMetaSliceMatcher{ + Templates: names, + MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { + return names.Has(meta.Namespace) + }, + } +} + +// KindMatcher matches resources whose kind is equal to one of the provided values. +// e.g. `KindMatcher("foo", "bar")` matches if `kind` is either "foo" or "bar". +// +// KindMatcher supports templating. +// e.g. `KindMatcher("{{.TargetKind}}")` will match `kind` "foo" if TemplateData is +// `struct{ TargetKind string }{ TargetKind: "foo" }` +func KindMatcher(names ...string) ResourceTemplateMatcher { + return &TemplatedMetaSliceMatcher{ + Templates: names, + MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { + return names.Has(meta.Kind) + }, + } +} + +// APIVersionMatcher matches resources whose kind is equal to one of the provided values. +// e.g. `APIVersionMatcher("foo/v1", "bar/v1")` matches if `apiVersion` is either "foo/v1" or +// "bar/v1". +// +// APIVersionMatcher supports templating. +// e.g. `APIVersionMatcher("{{.TargetAPI}}")` will match `apiVersion` "foo/v1" if TemplateData is +// `struct{ TargetAPI string }{ TargetAPI: "foo/v1" }` +func APIVersionMatcher(names ...string) ResourceTemplateMatcher { + return &TemplatedMetaSliceMatcher{ + Templates: names, + MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { + return names.Has(meta.APIVersion) + }, + } +} + +// GVKMatcher matches resources whose API group, version and kind match one of the provided values. +// e.g. `GVKMatcher("foo/v1/Widget", "bar/v1/App")` matches if `apiVersion` concatenated with `kind` +// is either "foo/v1/Widget" or "bar/v1/App". +// +// GVKMatcher supports templating. +// e.g. `GVKMatcher("{{.TargetAPI}}")` will match "foo/v1/Widget" if TemplateData is +// `struct{ TargetAPI string }{ TargetAPI: "foo/v1/Widget" }` +func GVKMatcher(names ...string) ResourceTemplateMatcher { + return &TemplatedMetaSliceMatcher{ + Templates: names, + MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { + gvk := strings.Join([]string{meta.APIVersion, meta.Kind}, "/") + return names.Has(gvk) + }, + } +} + +// TemplatedMetaSliceMatcher is a utility type for constructing matchers that compare resource +// metadata to a slice of (possibly templated) strings. +type TemplatedMetaSliceMatcher struct { + // Templates is the list of possibly templated strings to compare to. + Templates []string + // values is the set of final (possibly rendered) strings to compare to. + values sets.String + // TemplateData is the data to use in template rendering. + // Rendering will not take place if it is nil when InitTemplates is called. + TemplateData interface{} + // MetaMatcher is a function that returns true if the given resource metadata matches at + // least one of the given names. + // The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field. + MetaMatcher func(names sets.String, meta yaml.ResourceMeta) bool +} + +// Match parses the resource node's metadata and delegates matching logic to the provided +// MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaSliceMatcher to match +// against any field in resource metadata. +func (m *TemplatedMetaSliceMatcher) Match(node *yaml.RNode) bool { + var err error + meta, err := node.GetMeta() + if err != nil { + return false + } + return m.MetaMatcher(m.values, meta) +} + +// Filter applies the matcher to a list of items, returning only those that match. +func (m *TemplatedMetaSliceMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + // AndSelector or OrSelector doesn't really matter here since there is only one matcher (m). + s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData} + return s.Filter(items) +} + +// DefaultTemplateData sets TemplateData to the provided default values if it has not already +// been set. +func (m *TemplatedMetaSliceMatcher) DefaultTemplateData(data interface{}) { + if m.TemplateData == nil { + m.TemplateData = data + } +} + +// InitTemplates is used to render any templates the selector's list of strings may contain +// before the selector is applied. It should be called exactly once per filter +// operation, before beginning match comparisons. +func (m *TemplatedMetaSliceMatcher) InitTemplates() error { + values, err := templatizeSlice(m.Templates, m.TemplateData) + if err != nil { + return errors.Wrap(err) + } + m.values = sets.String{} + m.values.Insert(values...) + return nil +} + +var _ ResourceTemplateMatcher = &TemplatedMetaSliceMatcher{} + +// LabelMatcher matches resources that are labelled with all of the provided key-value pairs. +// e.g. `LabelMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources labelled +// app=foo AND env=prod. +// +// LabelMatcher supports templating. +// e.g. `LabelMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if +// TemplateData is `struct{ AppName string }{ AppName: "foo" }` +func LabelMatcher(labels map[string]string) ResourceTemplateMatcher { + return &TemplatedMetaMapMatcher{ + Templates: labels, + MetaMatcher: func(labels map[string]string, meta yaml.ResourceMeta) bool { + return compareMaps(labels, meta.Labels) + }, + } +} + +func compareMaps(desired, actual map[string]string) bool { + for k := range desired { + // actual either doesn't have the key or has the wrong value for it + if actual[k] != desired[k] { + return false + } + } + return true +} + +// AnnotationMatcher matches resources that are annotated with all of the provided key-value pairs. +// e.g. `AnnotationMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources +// annotated app=foo AND env=prod. +// +// AnnotationMatcher supports templating. +// e.g. `AnnotationMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if +// TemplateData is `struct{ AppName string }{ AppName: "foo" }` +func AnnotationMatcher(ann map[string]string) ResourceTemplateMatcher { + return &TemplatedMetaMapMatcher{ + Templates: ann, + MetaMatcher: func(ann map[string]string, meta yaml.ResourceMeta) bool { + return compareMaps(ann, meta.Annotations) + }, + } +} + +// TemplatedMetaMapMatcher is a utility type for constructing matchers that compare resource +// metadata to a map of (possibly templated) key-value pairs. +type TemplatedMetaMapMatcher struct { + // Templates is the list of possibly templated strings to compare to. + Templates map[string]string + // values is the map of final (possibly rendered) strings to compare to. + values map[string]string + // TemplateData is the data to use in template rendering. + // Rendering will not take place if it is nil when InitTemplates is called. + TemplateData interface{} + // MetaMatcher is a function that returns true if the given resource metadata matches at + // least one of the given names. + // The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field. + MetaMatcher func(names map[string]string, meta yaml.ResourceMeta) bool +} + +// Match parses the resource node's metadata and delegates matching logic to the provided +// MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaMapMatcher to match +// against any field in resource metadata. +func (m *TemplatedMetaMapMatcher) Match(node *yaml.RNode) bool { + var err error + meta, err := node.GetMeta() + if err != nil { + return false + } + + return m.MetaMatcher(m.values, meta) +} + +// DefaultTemplateData sets TemplateData to the provided default values if it has not already +// been set. +func (m *TemplatedMetaMapMatcher) DefaultTemplateData(data interface{}) { + if m.TemplateData == nil { + m.TemplateData = data + } +} + +// InitTemplates is used to render any templates the selector's key-value pairs may contain +// before the selector is applied. It should be called exactly once per filter +// operation, before beginning match comparisons. +func (m *TemplatedMetaMapMatcher) InitTemplates() error { + var err error + m.values, err = templatizeMap(m.Templates, m.TemplateData) + return errors.Wrap(err) +} + +// Filter applies the matcher to a list of items, returning only those that match. +func (m *TemplatedMetaMapMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + // AndSelector or OrSelector doesn't really matter here since there is only one matcher (m). + s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData} + return s.Filter(items) +} + +var _ ResourceTemplateMatcher = &TemplatedMetaMapMatcher{} + +func templatizeSlice(values []string, data interface{}) ([]string, error) { + if data == nil { + return values, nil + } + var err error + results := make([]string, len(values)) + for i := range values { + results[i], err = templatize(values[i], data) + if err != nil { + return nil, errors.WrapPrefixf(err, "unable to render template %s", values[i]) + } + } + return results, nil +} + +func templatizeMap(values map[string]string, data interface{}) (map[string]string, error) { + if data == nil { + return values, nil + } + var err error + results := make(map[string]string, len(values)) + + for k := range values { + results[k], err = templatize(values[k], data) + if err != nil { + return nil, errors.WrapPrefixf(err, "unable to render template for %s=%s", k, values[k]) + } + } + return results, nil +} + +// templatize renders the value as a template, using the provided data +func templatize(value string, data 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, data) + if err != nil { + return "", errors.Wrap(err) + } + return b.String(), nil +} diff --git a/kyaml/fn/framework/patch.go b/kyaml/fn/framework/patch.go index 2af0a7774..0ca7529c5 100644 --- a/kyaml/fn/framework/patch.go +++ b/kyaml/fn/framework/patch.go @@ -11,50 +11,205 @@ import ( "sigs.k8s.io/kustomize/kyaml/errors" "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/sets" + "sigs.k8s.io/kustomize/kyaml/openapi" "sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/kustomize/kyaml/yaml/merge2" + "sigs.k8s.io/kustomize/kyaml/yaml/walk" ) -// Applier applies some modification to a ResourceList -type Applier interface { - Apply(rl *ResourceList) error +// ResourcePatchTemplate applies a patch to a collection of resources +type ResourcePatchTemplate struct { + // Templates is a function that returns a list of templates to render into one or more patches. + Templates TemplatesFunc + + // Selector targets the rendered patches to specific resources. If no Selector is provided, + // all resources will be patched. + // + // Although any Filter can be used, this framework provides several especially for Selector use: + // framework.Selector, framework.AndSelector, framework.OrSelector. You can also use any of the + // framework's ResourceMatchers here directly. + Selector kio.Filter + + // TemplateData is the data to use when rendering the templates provided by the Templates field. + TemplateData interface{} } -var _ Applier = PatchTemplate{} - -// PatchTemplate applies a patch to a collection of Resources -type PatchTemplate struct { - // Template is a template to render into one or more patches. - Template *template.Template - - // Selector targets the rendered patch to specific resources. - Selector *Selector +// DefaultTemplateData sets TemplateData to the provided default values if it has not already +// been set. +func (t *ResourcePatchTemplate) DefaultTemplateData(data interface{}) { + if t.TemplateData == nil { + t.TemplateData = data + } } -// Apply applies the patch to all matching resources in the list. The rl.FunctionConfig -// is provided to the template as input. -func (p PatchTemplate) Apply(rl *ResourceList) error { - if p.Selector == nil { - // programming error -- user shouldn't see this - return errors.Errorf("must specify PatchTemplate.Selector") +// Filter applies the ResourcePatchTemplate to the appropriate resources in the input. +// First, it applies the Selector to identify target resources. Then, it renders the Templates +// into patches using TemplateData. Finally, it identifies applies the patch to each resource. +func (t ResourcePatchTemplate) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + var err error + target := items + if t.Selector != nil { + target, err = t.Selector.Filter(items) + if err != nil { + return nil, err + } + } + if len(target) == 0 { + // nothing to do + return items, nil } - matches, err := p.Selector.GetMatches(rl) + if err := t.apply(target); err != nil { + return nil, errors.Wrap(err) + } + return items, nil +} + +func (t *ResourcePatchTemplate) apply(matches []*yaml.RNode) error { + templates, err := t.Templates() if err != nil { - return err + return errors.Wrap(err) } - if len(matches) == 0 { - return nil + var patches []*yaml.RNode + for i := range templates { + newP, err := renderPatches(templates[i], t.TemplateData) + if err != nil { + return errors.Wrap(err) + } + patches = append(patches, newP...) } - return p.apply(rl, p.Template, matches) + + // apply the patches to the matching resources + for j := range matches { + for i := range patches { + matches[j], err = merge2.Merge(patches[i], matches[j], yaml.MergeOptions{}) + if err != nil { + return errors.WrapPrefixf(err, "failed to apply templated patch") + } + } + } + return nil } -func (p *PatchTemplate) apply(rl *ResourceList, t *template.Template, matches []*yaml.RNode) error { +// ContainerPatchTemplate defines a patch to be applied to containers +type ContainerPatchTemplate struct { + // Templates is a function that returns a list of templates to render into one or more + // patches that apply at the container level. For example, "name", "env" and "image" would be + // top-level fields in container patches. + Templates TemplatesFunc + + // Selector targets the rendered patches to containers within specific resources. + // If no Selector is provided, all resources with containers will be patched (subject to + // ContainerMatcher, if provided). + // + // Although any Filter can be used, this framework provides several especially for Selector use: + // framework.Selector, framework.AndSelector, framework.OrSelector. You can also use any of the + // framework's ResourceMatchers here directly. + Selector kio.Filter + + // TemplateData is the data to use when rendering the templates provided by the Templates field. + TemplateData interface{} + + // ContainerMatcher targets the rendered patch to only those containers it matches. + // For example, it can be used with ContainerNameMatcher to patch only containers with + // specific names. If no ContainerMatcher is provided, all containers will be patched. + // + // The node passed to ContainerMatcher will be container-level, not a full resource node. + // For example, "name", "env" and "image" would be top level fields. + // To filter based on resource-level context, use the Selector field. + ContainerMatcher func(node *yaml.RNode) bool +} + +// DefaultTemplateData sets TemplateData to the provided default values if it has not already +// been set. +func (cpt *ContainerPatchTemplate) DefaultTemplateData(data interface{}) { + if cpt.TemplateData == nil { + cpt.TemplateData = data + } +} + +// Filter applies the ContainerPatchTemplate to the appropriate resources in the input. +// First, it applies the Selector to identify target resources. Then, it renders the Templates +// into patches using TemplateData. Finally, it identifies target containers and applies the +// patches. +func (cpt ContainerPatchTemplate) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + var err error + target := items + if cpt.Selector != nil { + target, err = cpt.Selector.Filter(items) + if err != nil { + return nil, err + } + } + if len(target) == 0 { + // nothing to do + return items, nil + } + + if err := cpt.apply(target); err != nil { + return nil, err + } + + return items, nil +} + +// PatchContainers applies the patch to each matching container in each resource. +func (cpt ContainerPatchTemplate) apply(matches []*yaml.RNode) error { + templates, err := cpt.Templates() + if err != nil { + return errors.Wrap(err) + } + var patches []*yaml.RNode + for i := range templates { + newP, err := renderPatches(templates[i], cpt.TemplateData) + if err != nil { + return errors.Wrap(err) + } + patches = append(patches, newP...) + } + + for i := range matches { + // TODO(knverey): Make this work for more Kinds and expose the helper for doing so. + containers, err := matches[i].Pipe(yaml.Lookup("spec", "template", "spec", "containers")) + if err != nil { + return errors.Wrap(err) + } + if containers == nil { + continue + } + err = containers.VisitElements(func(node *yaml.RNode) error { + if cpt.ContainerMatcher != nil && !cpt.ContainerMatcher(node) { + return nil + } + for j := range patches { + merger := walk.Walker{ + Sources: []*yaml.RNode{node, patches[j]}, // dest, src + Visitor: merge2.Merger{}, + MergeOptions: yaml.MergeOptions{}, + Schema: openapi.SchemaForResourceType(yaml.TypeMeta{ + APIVersion: "v1", + Kind: "Pod", + }).Lookup("spec", "containers").Elements(), + } + _, err = merger.Walk() + if err != nil { + return errors.WrapPrefixf(err, "failed to apply templated patch") + } + } + return nil + }) + if err != nil { + return errors.Wrap(err) + } + } + return nil +} + +func renderPatches(t *template.Template, data interface{}) ([]*yaml.RNode, error) { // render the patches var b bytes.Buffer - if err := t.Execute(&b, rl.FunctionConfig); err != nil { - return errors.WrapPrefixf(err, "failed to render patch template %v", t.DefinedTemplates()) + if err := t.Execute(&b, data); err != nil { + return nil, errors.WrapPrefixf(err, "failed to render patch template %v", t.DefinedTemplates()) } // parse the patches into RNodes @@ -64,201 +219,25 @@ func (p *PatchTemplate) apply(rl *ResourceList, t *template.Template, matches [] if s == "" { continue } - newNodes, err := (&kio.ByteReader{Reader: bytes.NewBufferString(s)}).Read() + r := &kio.ByteReader{Reader: bytes.NewBufferString(s), OmitReaderAnnotations: true} + newNodes, err := r.Read() if err != nil { - // create the debug string - lines := strings.Split(s, "\n") - for j := range lines { - lines[j] = fmt.Sprintf("%03d %s", j+1, lines[j]) - } - s = strings.Join(lines, "\n") - return errors.WrapPrefixf(err, "failed to parse rendered patch template into a resource:\n%s\n", s) + return nil, errors.WrapPrefixf(err, + "failed to parse rendered patch template into a resource:\n%s\n", addLineNumbers(s)) + } + if err := yaml.ErrorIfAnyInvalidAndNonNull(yaml.MappingNode, newNodes...); err != nil { + return nil, errors.WrapPrefixf(err, + "failed to parse rendered patch template into a resource:\n%s\n", addLineNumbers(s)) } nodes = append(nodes, newNodes...) } - - // apply the patches to the matching resources - var err error - for j := range matches { - for i := range nodes { - matches[j], err = merge2.Merge(nodes[i], p.Selector.matches[j], yaml.MergeOptions{}) - if err != nil { - return errors.WrapPrefixf(err, "failed to merge templated patch") - } - } - } - return nil + return nodes, nil } -// Selector matches resources. A resource matches if and only if ALL of the Selector fields -// match the resource. An empty Selector matches all resources. -type Selector struct { - // Names is a list of metadata.names to match. If empty match all names. - // e.g. Names: ["foo", "bar"] matches if `metadata.name` is either "foo" or "bar". - Names []string `json:"names" yaml:"names"` - - namesSet sets.String - - // Namespaces is a list of metadata.namespaces to match. If empty match all namespaces. - // e.g. Namespaces: ["foo", "bar"] matches if `metadata.namespace` is either "foo" or "bar". - Namespaces []string `json:"namespaces" yaml:"namespaces"` - - namespaceSet sets.String - - // Kinds is a list of kinds to match. If empty match all kinds. - // e.g. Kinds: ["foo", "bar"] matches if `kind` is either "foo" or "bar". - Kinds []string `json:"kinds" yaml:"kinds"` - - kindsSet sets.String - - // APIVersions is a list of apiVersions to match. If empty apply match all apiVersions. - // e.g. APIVersions: ["foo/v1", "bar/v1"] matches if `apiVersion` is either "foo/v1" or "bar/v1". - APIVersions []string `json:"apiVersions" yaml:"apiVersions"` - - apiVersionsSet sets.String - - // Labels is a collection of labels to match. All labels must match exactly. - // e.g. Labels: {"foo": "bar", "baz": "buz"] matches if BOTH "foo" and "baz" labels match. - Labels map[string]string `json:"labels" yaml:"labels"` - - // Annotations is a collection of annotations to match. All annotations must match exactly. - // e.g. Annotations: {"foo": "bar", "baz": "buz"] matches if BOTH "foo" and "baz" annotations match. - Annotations map[string]string `json:"annotations" yaml:"annotations"` - - // Filter is an arbitrary filter function to match a resource. - // Selector matches if the function returns true. - Filter func(*yaml.RNode) bool - - // 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 -func (s *Selector) GetMatches(rl *ResourceList) ([]*yaml.RNode, error) { - if err := s.init(rl); err != nil { - return nil, err - } - 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{} - s.kindsSet.Insert(s.Kinds...) - s.apiVersionsSet = sets.String{} - s.apiVersionsSet.Insert(s.APIVersions...) - s.namesSet = sets.String{} - s.namesSet.Insert(s.Names...) - s.namespaceSet = sets.String{} - s.namespaceSet.Insert(s.Namespaces...) - - // 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 - } else if !match { - continue - } - s.matches = append(s.matches, rl.Items[i]) - } - return nil -} - -// isMatch returns true if r matches the patch selector -func (s *Selector) isMatch(r *yaml.RNode) (bool, error) { - m, err := r.GetMeta() - if err != nil { - return false, errors.Wrap(err) - } - if s.kindsSet.Len() > 0 && !s.kindsSet.Has(m.Kind) { - return false, nil - } - if s.apiVersionsSet.Len() > 0 && !s.apiVersionsSet.Has(m.APIVersion) { - return false, nil - } - if s.namesSet.Len() > 0 && !s.namesSet.Has(m.Name) { - return false, nil - } - if s.namespaceSet.Len() > 0 && !s.namespaceSet.Has(m.Namespace) { - return false, nil - } - for k := range s.Labels { - if m.Labels == nil || m.Labels[k] != s.Labels[k] { - return false, nil - } - } - for k := range s.Annotations { - if m.Annotations == nil || m.Annotations[k] != s.Annotations[k] { - return false, nil - } - } - if s.Filter != nil { - if match := s.Filter(r); !match { - return false, nil - } - } - return true, nil +func addLineNumbers(s string) string { + lines := strings.Split(s, "\n") + for j := range lines { + lines[j] = fmt.Sprintf("%03d %s", j+1, lines[j]) + } + return strings.Join(lines, "\n") } diff --git a/kyaml/fn/framework/patch_test.go b/kyaml/fn/framework/patch_test.go index 3af567889..8dd63f542 100644 --- a/kyaml/fn/framework/patch_test.go +++ b/kyaml/fn/framework/patch_test.go @@ -4,23 +4,18 @@ 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/command" "sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil" - "sigs.k8s.io/kustomize/kyaml/testutil" + "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/yaml" ) -func TestPatchTemplate(t *testing.T) { - // TODO: make this test pass on windows -- current failure seems spurious - testutil.SkipWindows(t) - +func TestResourcePatchTemplate_ComplexSelectors(t *testing.T) { cmdFn := func() *cobra.Command { type api struct { Selector framework.Selector `json:"selector" yaml:"selector"` @@ -33,22 +28,14 @@ func TestPatchTemplate(t *testing.T) { filter := framework.Selector{ // this is a special manual filter for the Selector for when the built-in matchers // are insufficient - Filter: func(rn *yaml.RNode) bool { + ResourceMatcher: func(rn *yaml.RNode) bool { m, _ := rn.GetMeta() return config.Special != "" && m.Annotations["foo"] == config.Special }, } - return framework.TemplateCommand{ - API: &config, - PreProcess: func(rl *framework.ResourceList) error { - // do some extra processing based on the inputs - config.LongList = len(rl.Items) > 2 - return nil - }, - PatchTemplates: []framework.PatchTemplate{ - { - // Apply these rendered patches - Template: template.Must(template.New("test").Parse(` + pt1 := framework.ResourcePatchTemplate{ + // Apply these rendered patches + Templates: framework.StringTemplates(` spec: template: spec: @@ -62,211 +49,36 @@ metadata: {{- if .LongList }} long: 'true' {{- end }} -`)), - // Use the selector from the input - Selector: &config.Selector, - }, - { - // Apply these rendered patches - Template: template.Must(template.New("test").Parse(` +`), + // Use the selector from the input + Selector: &config.Selector, + } + + pt2 := framework.ResourcePatchTemplate{ + // Apply these rendered patches + Templates: framework.StringTemplates(` metadata: annotations: filterPatched: '{{ .A }}' -`)), - // Use an explicit selector - Selector: &filter, - }, - }, - }.GetCommand() +`), + // Use an explicit selector + Selector: &filter, + } + + fn := framework.TemplateProcessor{ + TemplateData: &config, + PreProcessFilters: []kio.Filter{kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + // do some extra processing based on the inputs + config.LongList = len(items) > 2 + return items, nil + })}, + PatchTemplates: []framework.PatchTemplate{&pt1, &pt2}, + } + + return command.Build(fn, command.StandaloneEnabled, false) } - 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() - } - }) - } + tc := frameworktestutil.CommandResultsChecker{Command: cmdFn, + TestDataDirectory: "testdata/patch-selector"} + tc.Assert(t) } diff --git a/kyaml/fn/framework/pkger.go b/kyaml/fn/framework/pkger.go deleted file mode 100644 index 69cc7359e..000000000 --- a/kyaml/fn/framework/pkger.go +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package framework - -import ( - "io/ioutil" - "os" - "path" - "strings" - "text/template" - - "github.com/markbates/pkger" -) - -type TemplatesFn func(*ResourceList) ([]*template.Template, error) - -// TemplatesFromDir applies a directory of templates as generated resources. -func TemplatesFromDir(dirs ...pkger.Dir) TemplatesFn { - return func(_ *ResourceList) ([]*template.Template, error) { - var pt []*template.Template - for i := range dirs { - d := dirs[i] - err := pkger.Walk(string(d), func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !strings.HasSuffix(info.Name(), ".template.yaml") { - return nil - } - name := path.Join(string(d), info.Name()) - f, err := pkger.Open(name) - if err != nil { - return err - } - b, err := ioutil.ReadAll(f) - if err != nil { - return err - } - t, err := template.New(info.Name()).Parse(string(b)) - if err != nil { - return err - } - - pt = append(pt, t) - return nil - }) - if err != nil { - return nil, err - } - } - return pt, nil - } -} - -// PatchTemplatesFn returns a slice of PatchTemplate -type PatchTemplatesFn func(*ResourceList) ([]PatchTemplate, error) - -// PT applies a directory of patches using the Selector -type PT struct { - Selector func() *Selector - Dir pkger.Dir -} - -// PatchTemplatesFromDir applies a directory of templates as patches. -func PatchTemplatesFromDir(templates ...PT) PatchTemplatesFn { - return func(*ResourceList) ([]PatchTemplate, error) { - var pt []PatchTemplate - for i := range templates { - v := templates[i] - err := pkger.Walk(string(v.Dir), func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - name := path.Join(string(v.Dir), info.Name()) - - if !strings.HasSuffix(info.Name(), ".template.yaml") { - return nil - } - f, err := pkger.Open(name) - if err != nil { - return err - } - b, err := ioutil.ReadAll(f) - if err != nil { - return err - } - t, err := template.New(info.Name()).Parse(string(b)) - if err != nil { - return err - } - - pt = append(pt, PatchTemplate{Template: t, Selector: v.Selector()}) - return nil - }) - if err != nil { - return nil, err - } - } - return pt, nil - } -} - -// ContainerPatchTemplateFn returns a slice of ContainerPatchTemplate -type ContainerPatchTemplateFn func(*ResourceList) ([]ContainerPatchTemplate, error) - -// CPT applies a directory of container patches using the Selector -type CPT struct { - Selector func() *Selector - Dir pkger.Dir - Names []string -} - -// ContainerPatchTemplatesFromDir applies a directory of templates as container patches. -func ContainerPatchTemplatesFromDir(templates ...CPT) ContainerPatchTemplateFn { - return func(*ResourceList) ([]ContainerPatchTemplate, error) { - var cpt []ContainerPatchTemplate - for i := range templates { - v := templates[i] - err := pkger.Walk(string(v.Dir), func(p string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if !strings.HasSuffix(info.Name(), ".template.yaml") { - return nil - } - - name := path.Join(string(v.Dir), info.Name()) - f, err := pkger.Open(name) - if err != nil { - return err - } - b, err := ioutil.ReadAll(f) - if err != nil { - return err - } - t, err := template.New(info.Name()).Parse(string(b)) - if err != nil { - return err - } - - cpt = append(cpt, ContainerPatchTemplate{ - PatchTemplate: PatchTemplate{Template: t, Selector: v.Selector()}, - ContainerNames: v.Names, - }) - return nil - }) - if err != nil { - return nil, err - } - } - return cpt, nil - } -} diff --git a/kyaml/fn/framework/processors.go b/kyaml/fn/framework/processors.go new file mode 100644 index 000000000..14e787581 --- /dev/null +++ b/kyaml/fn/framework/processors.go @@ -0,0 +1,410 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package framework + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + "strings" + "text/template" + + "github.com/markbates/pkger" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/kio/filters" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// SimpleProcessor processes a ResourceList by loading the FunctionConfig into +// the given Config type and then running the provided Filter on the ResourceList. +// The provided Config MAY implement Defaulter and Validator to have Default and Validate +// respectively called between unmarshalling and filter execution. +// +// Typical uses include functions that do not actually require config, and simple functions built +// with a filter that closes over the Config instance to access ResourceList.functionConfig values. +type SimpleProcessor struct { + // Filter is the kio.Filter that will be used to process the ResourceList's items. + // Note that kio.FilterFunc is available to transform a compatible func into a kio.Filter. + Filter kio.Filter + // Config must be a struct capable of receiving the data from ResourceList.functionConfig. + // Filter functions may close over this struct to access its data. + Config interface{} +} + +// Process makes SimpleProcessor implement the ResourceListProcessor interface. +// It loads the ResourceList.functionConfig into the provided Config type, applying +// defaulting and validation if supported by Config. It then executes the processor's filter. +func (p SimpleProcessor) Process(rl *ResourceList) error { + if err := LoadFunctionConfig(rl.FunctionConfig, p.Config); err != nil { + return errors.Wrap(err) + } + return errors.Wrap(rl.Filter(p.Filter)) +} + +// GVKFilterMap is a FilterProvider that resolves Filters through a simple lookup in a map. +// It is intended for use in VersionedAPIProcessor. +type GVKFilterMap map[string]map[string]kio.Filter + +// ProviderFor makes GVKFilterMap implement the FilterProvider interface. +// It uses the given apiVersion and kind to do a simple lookup in the map and +// returns an error if no exact match is found. +func (m GVKFilterMap) ProviderFor(apiVersion, kind string) (kio.Filter, error) { + if kind == "" { + return nil, errors.Errorf("kind is required") + } + if apiVersion == "" { + return nil, errors.Errorf("apiVersion is required") + } + + var ok bool + var versionMap map[string]kio.Filter + if versionMap, ok = m[kind]; !ok { + return nil, errors.Errorf("kind %q is not supported", kind) + } + + var p kio.Filter + if p, ok = versionMap[apiVersion]; !ok { + return nil, errors.Errorf("apiVersion %q is not supported for kind %q", apiVersion, kind) + } + return p, nil +} + +// FilterProvider is implemented by types that provide a way to look up which Filter +// should be used to process a ResourceList based on the ApiVersion and Kind of the +// ResourceList.functionConfig in the input. FilterProviders are intended to be used +// as part of VersionedAPIProcessor. +type FilterProvider interface { + // ProviderFor returns the appropriate filter for the given APIVersion and Kind. + ProviderFor(apiVersion, kind string) (kio.Filter, error) +} + +// FilterProviderFunc converts a compatible function to a FilterProvider. +type FilterProviderFunc func(apiVersion, kind string) (kio.Filter, error) + +// ProviderFor makes FilterProviderFunc implement FilterProvider. +func (f FilterProviderFunc) ProviderFor(apiVersion, kind string) (kio.Filter, error) { + return f(apiVersion, kind) +} + +// VersionedAPIProcessor selects the appropriate kio.Filter based on the ApiVersion +// and Kind of the ResourceList.functionConfig in the input. +// It can be used to implement configuration function APIs that evolve over time, +// or create processors that support multiple configuration APIs with a single entrypoint. +// All provided Filters MUST be structs capable of receiving ResourceList.functionConfig data. +// Provided Filters MAY implement Defaulter and Validator to have Default and Validate +// respectively called between unmarshalling and filter execution. +type VersionedAPIProcessor struct { + // FilterProvider resolves a kio.Filter for each supported API, based on its APIVersion and Kind. + // GVKFilterMap is a simple FilterProvider implementation for use here. + FilterProvider FilterProvider +} + +// Process makes VersionedAPIProcessor implement the ResourceListProcessor interface. +// It looks up the configuration object to use based on the ApiVersion and Kind of the +// input ResourceList.functionConfig, loads ResourceList.functionConfig into that object, +// invokes Validate and Default if supported, and finally invokes Filter. +func (p *VersionedAPIProcessor) Process(rl *ResourceList) error { + api, err := p.FilterProvider.ProviderFor(extractGVK(rl.FunctionConfig)) + if err != nil { + return errors.WrapPrefixf(err, "unable to identify provider for resource") + } + if err := LoadFunctionConfig(rl.FunctionConfig, api); err != nil { + return errors.Wrap(err) + } + return errors.Wrap(rl.Filter(api)) +} + +// extractGVK returns the apiVersion and kind fields from the given RNodes if it contains +// valid TypeMeta. It returns an empty string if a value is not found. +func extractGVK(src *yaml.RNode) (apiVersion, kind string) { + if src == nil { + return "", "" + } + if versionNode := src.Field("apiVersion"); versionNode != nil { + if a, err := versionNode.Value.String(); err == nil { + apiVersion = strings.TrimSpace(a) + } + } + if kindNode := src.Field("kind"); kindNode != nil { + if k, err := kindNode.Value.String(); err == nil { + kind = strings.TrimSpace(k) + } + } + return apiVersion, kind +} + +// LoadFunctionConfig reads a configuration resource from YAML into the provided data structure +// and then prepares it for use by running defaulting and validation on it, if supported. +// ResourceListProcessors should use this function to load ResourceList.functionConfig. +func LoadFunctionConfig(src *yaml.RNode, api interface{}) error { + if api == nil { + return nil + } + if err := yaml.Unmarshal([]byte(src.MustString()), api); err != nil { + return errors.Wrap(err) + } + + if d, ok := api.(Defaulter); ok { + if err := d.Default(); err != nil { + return err + } + } + + if v, ok := api.(Validator); ok { + return v.Validate() + } + return nil +} + +// TemplateProcessor is a ResourceListProcessor based on rendering templates with the data in +// ResourceList.functionConfig. It works as follows: +// - loads ResourceList.functionConfig into TemplateData +// - runs PreProcessFilters +// - renders ResourceTemplates and adds them to ResourceList.items +// - renders PatchTemplates and applies them to ResourceList.items +// - executes a merge on ResourceList.items if configured to +// - runs PostProcessFilters +// The TemplateData struct MAY implement Defaulter and Validator to have Default and Validate +// respectively called between unmarshalling and filter execution. +// +// TemplateProcessor also implements kio.Filter directly and can be used in the construction of +// higher-level processors. For example, you might use TemplateProcessors as the filters for each +// API supported by a VersionedAPIProcessor (see VersionedAPIProcessor examples). +type TemplateProcessor struct { + // TemplateData will will be exposed to all the templates in the processor (unless explicitly + // overridden for a template). + // If TemplateProcessor is used directly as a ResourceListProcessor, TemplateData will contain the + // value of ResourceList.functionConfig. + TemplateData interface{} + + // ResourceTemplates returns a list of templates to render into resources. + // If MergeResources is set, any matching resources in ResourceList.items will be used as patches + // modifying the rendered templates. Otherwise, the rendered resources will be appended to + // the input resources as-is. + ResourceTemplates []ResourceTemplate + + // PatchTemplates is a list of templates to render into patches that apply to ResourceList.items. + // ResourcePatchTemplate can be used here to patch entire resources. + // ContainerPatchTemplate can be used here to patch specific containers within resources. + PatchTemplates []PatchTemplate + + // MergeResources, if set to true, will cause the resources in ResourceList.items to be + // will be applied as patches on any matching resources generated by ResourceTemplates. + MergeResources bool + + // PreProcessFilters provides a hook to manipulate the ResourceList's items or config after + // TemplateData has been populated but before template-based filters are applied. + PreProcessFilters []kio.Filter + + // PostProcessFilters provides a hook to manipulate the ResourceList's items after template + // filters are applied. + PostProcessFilters []kio.Filter +} + +// Filter implements the kio.Filter interface, enabling you to use TemplateProcessor +// as part of a higher-level ResourceListProcessor like VersionedAPIProcessor. +// It sets up all the features of TemplateProcessors as a pipeline of filters and executes them. +func (tp TemplateProcessor) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + buf := &kio.PackageBuffer{Nodes: items} + pipeline := kio.Pipeline{ + Inputs: []kio.Reader{buf}, + Filters: []kio.Filter{ + kio.FilterFunc(tp.doPreProcess), + kio.FilterFunc(tp.doResourceTemplates), + kio.FilterFunc(tp.doPatchTemplates), + kio.FilterFunc(tp.doMerge), + kio.FilterFunc(tp.doPostProcess), + }, + Outputs: []kio.Writer{buf}, + ContinueOnEmptyResult: true, + } + if err := pipeline.Execute(); err != nil { + return nil, err + } + + return buf.Nodes, nil +} + +// Process implements the ResourceListProcessor interface, enabling you to use TemplateProcessor +// directly as a processor. As a Processor, it loads the ResourceList.functionConfig into the +// TemplateData field, exposing it to all templates by default. +func (tp TemplateProcessor) Process(rl *ResourceList) error { + if err := LoadFunctionConfig(rl.FunctionConfig, tp.TemplateData); err != nil { + return errors.Wrap(err) + } + return errors.Wrap(rl.Filter(tp)) +} + +// TemplatesFunc is a function that provides a list of templates. +// TemplateProcessor uses this to defer loading of templates to the point where they are used. +type TemplatesFunc func() ([]*template.Template, error) + +// PatchTemplate is implemented by kio.Filters that work by rendering patches and applying them to +// the given resource nodes. +type PatchTemplate interface { + // Filter is a kio.Filter-compliant function that applies PatchTemplate's templates as patches + // on the given resource nodes. + Filter(items []*yaml.RNode) ([]*yaml.RNode, error) + // DefaultTemplateData accepts default data to be used in template rendering when no template + // data was explicitly provided to the PatchTemplate. + DefaultTemplateData(interface{}) +} + +func (tp *TemplateProcessor) doPreProcess(items []*yaml.RNode) ([]*yaml.RNode, error) { + if tp.PreProcessFilters == nil { + return items, nil + } + for i := range tp.PreProcessFilters { + filter := tp.PreProcessFilters[i] + var err error + items, err = filter.Filter(items) + if err != nil { + return nil, err + } + } + return items, nil +} + +func (tp *TemplateProcessor) doMerge(items []*yaml.RNode) ([]*yaml.RNode, error) { + var err error + if tp.MergeResources { + items, err = filters.MergeFilter{}.Filter(items) + } + return items, err +} + +func (tp *TemplateProcessor) doPostProcess(items []*yaml.RNode) ([]*yaml.RNode, error) { + if tp.PostProcessFilters == nil { + return items, nil + } + for i := range tp.PostProcessFilters { + filter := tp.PostProcessFilters[i] + var err error + items, err = filter.Filter(items) + if err != nil { + return nil, err + } + } + return items, nil +} + +func (tp *TemplateProcessor) doResourceTemplates(items []*yaml.RNode) ([]*yaml.RNode, error) { + if tp.ResourceTemplates == nil { + return items, nil + } + + for i := range tp.ResourceTemplates { + tp.ResourceTemplates[i].DefaultTemplateData(tp.TemplateData) + newItems, err := tp.ResourceTemplates[i].Render() + if err != nil { + return nil, err + } + if tp.MergeResources { + // apply inputs as patches -- add the new items to the front of the list + items = append(newItems, items...) + } else { + // assume these are new unique resources--append to the list + items = append(items, newItems...) + } + } + return items, nil +} + +func (tp *TemplateProcessor) doPatchTemplates(items []*yaml.RNode) ([]*yaml.RNode, error) { + if tp.PatchTemplates == nil { + return items, nil + } + + for i := range tp.PatchTemplates { + // Default the template data for the patch to the processor's data + tp.PatchTemplates[i].DefaultTemplateData(tp.TemplateData) + var err error + if items, err = tp.PatchTemplates[i].Filter(items); err != nil { + return nil, err + } + } + return items, nil +} + +// StringTemplates returns a TemplatesFunc that will generate templates from the provided strings. +// This is a helper to facilitate providing ResourceTemplates, PatchTemplates and +// ContainerPatchTemplates to a TemplateProcessor. +func StringTemplates(data ...string) TemplatesFunc { + return func() ([]*template.Template, error) { + var templates []*template.Template + for i := range data { + t, err := template.New(fmt.Sprintf("inline%d", i)).Parse(data[i]) + if err != nil { + return nil, err + } + templates = append(templates, t) + } + return templates, nil + } +} + +// TemplatesFromFile returns a TemplatesFunc that will generate templates from the provided files. +// This is a helper to facilitate providing ResourceTemplates, PatchTemplates and +// ContainerPatchTemplates to a TemplateProcessor. +func TemplatesFromFile(files ...string) TemplatesFunc { + return func() ([]*template.Template, error) { + var templates []*template.Template + for i := range files { + n := filepath.Base(files[i]) + t, err := template.New(n).ParseFiles(files[i]) + if err != nil { + return nil, err + } + templates = append(templates, t) + } + return templates, nil + } +} + +// TemplatesFromDir returns a TemplatesFunc that will generate templates from the provided +// directories. Only files suffixed with .template.yaml will be included. +// This is a helper to facilitate providing ResourceTemplates, PatchTemplates and +// ContainerPatchTemplates to a TemplateProcessor. +func TemplatesFromDir(dirs ...pkger.Dir) TemplatesFunc { + return func() ([]*template.Template, error) { + var pt []*template.Template + for i := range dirs { + dir := string(dirs[i]) + err := pkger.Walk(dir, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !strings.HasSuffix(info.Name(), ".template.yaml") { + return nil + } + name := path.Join(dir, info.Name()) + f, err := pkger.Open(name) + if err != nil { + return err + } + defer f.Close() + + b, err := ioutil.ReadAll(f) + if err != nil { + return err + } + t, err := template.New(info.Name()).Parse(string(b)) + if err != nil { + return err + } + + pt = append(pt, t) + return nil + }) + if err != nil { + return nil, err + } + } + return pt, nil + } +} diff --git a/kyaml/fn/framework/processors_test.go b/kyaml/fn/framework/processors_test.go new file mode 100644 index 000000000..458168a36 --- /dev/null +++ b/kyaml/fn/framework/processors_test.go @@ -0,0 +1,527 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package framework_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/markbates/pkger" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/yaml" + + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +func TestTemplateProcessor_ResourceTemplates(t *testing.T) { + type API struct { + Image string `json:"image" yaml:"image"` + } + + p := framework.TemplateProcessor{ + TemplateData: &API{}, + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.TemplatesFromDir(pkger.Dir( + "/fn/framework/testdata/template-processor/templates")), + }}, + } + + out := new(bytes.Buffer) + rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service +functionConfig: + image: baz +`), + Writer: out} + + require.NoError(t, framework.Execute(p, rw)) + require.Equal(t, strings.TrimSpace(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: v1 + kind: Service +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo + namespace: bar + spec: + template: + spec: + containers: + - name: foo + image: baz +functionConfig: + image: baz +`), strings.TrimSpace(out.String())) +} + +func TestTemplateProcessor_PatchTemplates(t *testing.T) { + type API struct { + Spec struct { + Replicas int `json:"replicas" yaml:"replicas"` + A string `json:"a" yaml:"a"` + } `json:"spec" yaml:"spec"` + } + + config := &API{} + p := framework.TemplateProcessor{ + TemplateData: config, + PatchTemplates: []framework.PatchTemplate{ + // Patch from dir with no selector templating + &framework.ResourcePatchTemplate{ + Templates: framework.TemplatesFromDir(pkger.Dir( + "/fn/framework/testdata/template-processor/patches")), + Selector: &framework.Selector{Names: []string{"foo"}}, + }, + // Patch from string with selector templating + &framework.ResourcePatchTemplate{ + Selector: &framework.Selector{Names: []string{"{{.Spec.A}}"}, TemplateData: &config}, + Templates: framework.StringTemplates(` +metadata: + annotations: + baz: buz +`)}, + }, + } + out := new(bytes.Buffer) + + rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo + spec: + template: + spec: + containers: + - name: foo + image: baz +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: bar + spec: + template: + spec: + containers: + - name: foo + image: baz +functionConfig: + spec: + replicas: 5 + a: bar +`), + Writer: out} + + require.NoError(t, framework.Execute(p, rw)) + require.Equal(t, strings.TrimSpace(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo + spec: + template: + spec: + containers: + - name: foo + image: baz + replicas: 5 +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: bar + annotations: + baz: buz + spec: + template: + spec: + containers: + - name: foo + image: baz +functionConfig: + spec: + replicas: 5 + a: bar +`), strings.TrimSpace(out.String())) +} + +func TestTemplateProcessor_ContainerPatchTemplates(t *testing.T) { + type API struct { + Spec struct { + Key string `json:"key" yaml:"key"` + Value string `json:"value" yaml:"value"` + A string `json:"a" yaml:"a"` + } + } + + config := &API{} + p := framework.TemplateProcessor{ + TemplateData: config, + PatchTemplates: []framework.PatchTemplate{ + // patch from dir with no selector templating + &framework.ContainerPatchTemplate{ + Templates: framework.TemplatesFromDir(pkger.Dir( + "/fn/framework/testdata/template-processor/container-patches")), + Selector: &framework.Selector{Names: []string{"foo"}}, + }, + // patch from string with selector templating + &framework.ContainerPatchTemplate{ + Selector: &framework.Selector{Names: []string{"{{.Spec.A}}"}, TemplateData: &config}, + Templates: framework.StringTemplates(` +env: +- name: Foo + value: Bar +`)}, + }, + } + + out := new(bytes.Buffer) + rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo + spec: + template: + spec: + containers: + - name: a + env: + - name: EXISTING + value: variable + - name: b + - name: c +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: bar + spec: + template: + spec: + containers: + - name: foo + image: baz +functionConfig: + spec: + key: Hello + value: World + a: bar +`), + Writer: out} + + require.NoError(t, framework.Execute(p, rw)) + require.Equal(t, strings.TrimSpace(` +apiVersion: config.kubernetes.io/v1alpha1 +kind: ResourceList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: foo + spec: + template: + spec: + containers: + - name: a + env: + - name: EXISTING + value: variable + - name: Hello + value: World + - name: b + env: + - name: Hello + value: World + - name: c + env: + - name: Hello + value: World +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: bar + spec: + template: + spec: + containers: + - name: foo + image: baz + env: + - name: Foo + value: Bar +functionConfig: + spec: + key: Hello + value: World + a: bar +`), strings.TrimSpace(out.String())) +} + +func TestSimpleProcessor_Process_loads_config(t *testing.T) { + cfg := new(struct { + Value string `yaml:"value"` + }) + p := framework.SimpleProcessor{ + Filter: kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + if cfg.Value != "dataFromResourceList" { + return nil, errors.Errorf("got incorrect config value %q", cfg.Value) + } + return items, nil + }), + Config: &cfg, + } + rl := framework.ResourceList{ + FunctionConfig: yaml.NewMapRNode(&map[string]string{ + "value": "dataFromResourceList", + }), + } + require.NoError(t, p.Process(&rl)) +} + +func TestSimpleProcessor_Process_Error(t *testing.T) { + tests := []struct { + name string + filter kio.Filter + config interface{} + wantErr string + }{ + { + name: "error when given func as Config", + config: func() {}, + wantErr: "cannot unmarshal !!map into func()", + }, + { + name: "error in filter", + wantErr: "err from filter", + filter: kio.FilterFunc(func(_ []*yaml.RNode) ([]*yaml.RNode, error) { + return nil, errors.Errorf("err from filter") + }), + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + p := framework.SimpleProcessor{ + Filter: tt.filter, + Config: tt.config, + } + rl := framework.ResourceList{ + FunctionConfig: yaml.NewMapRNode(&map[string]string{ + "value": "dataFromResourceList", + }), + } + err := p.Process(&rl) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + }) + } +} + +func TestVersionedAPIProcessor_Process_Error(t *testing.T) { + tests := []struct { + name string + filterProvider framework.FilterProvider + apiVersion string + kind string + wantErr string + }{ + { + name: "error when given FilterFunc as Filter", + filterProvider: framework.FilterProviderFunc(func(_, _ string) (kio.Filter, error) { + return kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) { + return items, nil + }), nil + }), + wantErr: "cannot unmarshal !!map into kio.FilterFunc", + }, + { + name: "error in filter", + filterProvider: framework.FilterProviderFunc(func(_, _ string) (kio.Filter, error) { + return &framework.AndSelector{FailOnEmptyMatch: true}, nil + }), + wantErr: "selector did not select any items", + }, + { + name: "error GVKFilterMap no filter for kind", + filterProvider: framework.GVKFilterMap{ + "puppy": { + "pets.example.com/v1beta1": &framework.Selector{}, + }, + }, + kind: "kitten", + apiVersion: "pets.example.com/v1beta1", + wantErr: "kind \"kitten\" is not supported", + }, + { + name: "error GVKFilterMap no filter for version", + filterProvider: framework.GVKFilterMap{ + "kitten": { + "pets.example.com/v1alpha1": &framework.Selector{}, + }, + }, + kind: "kitten", + apiVersion: "pets.example.com/v1beta1", + wantErr: "apiVersion \"pets.example.com/v1beta1\" is not supported for kind \"kitten\"", + }, + { + name: "error GVKFilterMap blank kind", + filterProvider: framework.GVKFilterMap{}, + kind: "", + apiVersion: "pets.example.com/v1beta1", + wantErr: "unable to identify provider for resource: kind is required", + }, + { + name: "error GVKFilterMap blank version", + filterProvider: framework.GVKFilterMap{}, + kind: "kitten", + apiVersion: "", + wantErr: "unable to identify provider for resource: apiVersion is required", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + p := framework.VersionedAPIProcessor{ + FilterProvider: tt.filterProvider, + } + rl := framework.ResourceList{ + FunctionConfig: yaml.NewMapRNode(&map[string]string{ + "apiVersion": tt.apiVersion, + "kind": tt.kind, + }), + } + err := p.Process(&rl) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + }) + } +} + +func TestTemplateProcessor_Process_Error(t *testing.T) { + tests := []struct { + name string + processor framework.TemplateProcessor + wantErr string + }{ + { + name: "ResourcePatchTemplate is not a resource", + processor: framework.TemplateProcessor{ + PatchTemplates: []framework.PatchTemplate{ + &framework.ResourcePatchTemplate{ + Templates: framework.StringTemplates(`aString +another`), + }}, + }, + wantErr: `failed to parse rendered patch template into a resource: +001 aString +002 another +: wrong Node Kind for expected: MappingNode was ScalarNode: value: {aString another}`, + }, + { + name: "ResourcePatchTemplate is invalid template", + processor: framework.TemplateProcessor{ + PatchTemplates: []framework.PatchTemplate{ + &framework.ResourcePatchTemplate{ + Templates: framework.StringTemplates("foo: {{ .OOPS }}"), + }}, + }, + wantErr: "can't evaluate field OOPS", + }, + { + name: "ContainerPatchTemplate is not a resource", + processor: framework.TemplateProcessor{ + PatchTemplates: []framework.PatchTemplate{ + &framework.ContainerPatchTemplate{ + Templates: framework.StringTemplates(`aString +another`), + }}, + }, + wantErr: `failed to parse rendered patch template into a resource: +001 aString +002 another +: wrong Node Kind for expected: MappingNode was ScalarNode: value: {aString another}`, + }, + { + name: "ContainerPatchTemplate is invalid template", + processor: framework.TemplateProcessor{ + PatchTemplates: []framework.PatchTemplate{ + &framework.ContainerPatchTemplate{ + Templates: framework.StringTemplates("foo: {{ .OOPS }}"), + }}, + }, + wantErr: "can't evaluate field OOPS", + }, + { + name: "ResourceTemplate is not a resource", + processor: framework.TemplateProcessor{ + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.StringTemplates(`aString +another`), + }}, + }, + wantErr: `failed to parse rendered template into a resource: +001 aString +002 another +: wrong Node Kind for expected: MappingNode was ScalarNode: value: {aString another}`, + }, + { + name: "ResourceTemplate is invalid template", + processor: framework.TemplateProcessor{ + ResourceTemplates: []framework.ResourceTemplate{{ + Templates: framework.StringTemplates("foo: {{ .OOPS }}"), + }}, + }, + wantErr: "can't evaluate field OOPS", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + rl := framework.ResourceList{ + Items: []*yaml.RNode{ + yaml.MustParse(` +kind: Deployment +apiVersion: apps/v1 +metadata: + name: foo +spec: + replicas: 5 + template: + spec: + containers: + - name: foo +`), + }, + FunctionConfig: yaml.NewMapRNode(&map[string]string{ + "value": "dataFromResourceList", + }), + } + tt.processor.TemplateData = new(struct { + Value string `yaml:"value"` + }) + err := tt.processor.Process(&rl) + require.Error(t, err) + assert.Contains(t, err.Error(), tt.wantErr) + }) + } +} diff --git a/kyaml/fn/framework/types.go b/kyaml/fn/framework/result.go similarity index 68% rename from kyaml/fn/framework/types.go rename to kyaml/fn/framework/result.go index f734e19ba..b397fd3cd 100644 --- a/kyaml/fn/framework/types.go +++ b/kyaml/fn/framework/result.go @@ -4,25 +4,13 @@ package framework import ( + "fmt" "strings" "sigs.k8s.io/kustomize/kyaml/yaml" ) -// Function defines a function which mutates or validates a collection of configuration -// To create a structured validation result, return a Result as the error. -type Function func() error - -// Result defines a function result which will be set on the emitted ResourceList -type Result struct { - // Name is the name of the function creating the result - Name string `yaml:"name,omitempty"` - - // Items are the individual results - Items []Item `yaml:"items,omitempty"` -} - -// Severity indicates the severity of the result +// Severity indicates the severity of the Result type Severity string const ( @@ -34,22 +22,31 @@ const ( Info Severity = "info" ) -// Item defines a validation result -type Item struct { +// ResultItem defines a validation result +type ResultItem struct { // Message is a human readable message Message string `yaml:"message,omitempty"` - // Severity is the severity of the + // Severity is the severity of this result Severity Severity `yaml:"severity,omitempty"` // ResourceRef is a reference to a resource ResourceRef yaml.ResourceMeta `yaml:"resourceRef,omitempty"` + // Field is a reference to the field in a resource this result refers to Field Field `yaml:"field,omitempty"` + // File references a file containing the resource this result refers to File File `yaml:"file,omitempty"` } +// String provides a human-readable message for the result item +func (i ResultItem) String() string { + identifier := i.ResourceRef.GetIdentifier() + idString := strings.Join([]string{identifier.GetAPIVersion(), identifier.GetKind(), identifier.GetNamespace(), identifier.GetName()}, "/") + return fmt.Sprintf("[%s] %s %s: %s", i.Severity, idString, i.Field.Path, i.Message) +} + // File references a file containing a resource type File struct { // Path is relative path to the file containing the resource @@ -72,16 +69,25 @@ type Field struct { SuggestedValue string `yaml:"suggestedValue,omitempty"` } -// Error implement error +// Result defines a function result which will be set on the emitted ResourceList +type Result struct { + // Name is the name of the function creating the result + Name string `yaml:"name,omitempty"` + + // Items are the individual results + Items []ResultItem `yaml:"items,omitempty"` +} + +// Error enables a Result to be returned as an error func (e Result) Error() string { var msgs []string for _, i := range e.Items { - msgs = append(msgs, i.Message) + msgs = append(msgs, i.String()) } return strings.Join(msgs, "\n\n") } -// ExitCode provides the exit code based on the result +// ExitCode provides the exit code based on the result's severity func (e Result) ExitCode() int { for _, i := range e.Items { if i.Severity == Error { diff --git a/kyaml/fn/framework/selector.go b/kyaml/fn/framework/selector.go new file mode 100644 index 000000000..4da97e665 --- /dev/null +++ b/kyaml/fn/framework/selector.go @@ -0,0 +1,219 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package framework + +import ( + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// Selector matches resources. A resource matches if and only if ALL of the Selector fields +// match the resource. An empty Selector matches all resources. +type Selector struct { + // Names is a list of metadata.names to match. If empty match all names. + // e.g. Names: ["foo", "bar"] matches if `metadata.name` is either "foo" or "bar". + Names []string `json:"names" yaml:"names"` + + // Namespaces is a list of metadata.namespaces to match. If empty match all namespaces. + // e.g. Namespaces: ["foo", "bar"] matches if `metadata.namespace` is either "foo" or "bar". + Namespaces []string `json:"namespaces" yaml:"namespaces"` + + // Kinds is a list of kinds to match. If empty match all kinds. + // e.g. Kinds: ["foo", "bar"] matches if `kind` is either "foo" or "bar". + Kinds []string `json:"kinds" yaml:"kinds"` + + // APIVersions is a list of apiVersions to match. If empty apply match all apiVersions. + // e.g. APIVersions: ["foo/v1", "bar/v1"] matches if `apiVersion` is either "foo/v1" or "bar/v1". + APIVersions []string `json:"apiVersions" yaml:"apiVersions"` + + // Labels is a collection of labels to match. All labels must match exactly. + // e.g. Labels: {"foo": "bar", "baz": "buz"] matches if BOTH "foo" and "baz" labels match. + Labels map[string]string `json:"labels" yaml:"labels"` + + // Annotations is a collection of annotations to match. All annotations must match exactly. + // e.g. Annotations: {"foo": "bar", "baz": "buz"] matches if BOTH "foo" and "baz" annotations match. + Annotations map[string]string `json:"annotations" yaml:"annotations"` + + // ResourceMatcher is an arbitrary function used to match resources. + // Selector matches if the function returns true. + ResourceMatcher func(*yaml.RNode) bool + + // TemplateData if present will cause the selector values to be parsed as templates + // and rendered using TemplateData before they are used. + TemplateData interface{} + + // FailOnEmptyMatch makes the selector return an error when no items are selected. + FailOnEmptyMatch bool +} + +// Filter implements kio.Filter, returning only those items from the list that the selector +// matches. +func (s *Selector) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + andSel := AndSelector{TemplateData: s.TemplateData, FailOnEmptyMatch: s.FailOnEmptyMatch} + if s.Names != nil { + andSel.Matchers = append(andSel.Matchers, NameMatcher(s.Names...)) + } + if s.Namespaces != nil { + andSel.Matchers = append(andSel.Matchers, NamespaceMatcher(s.Namespaces...)) + } + if s.Kinds != nil { + andSel.Matchers = append(andSel.Matchers, KindMatcher(s.Kinds...)) + } + if s.APIVersions != nil { + andSel.Matchers = append(andSel.Matchers, APIVersionMatcher(s.APIVersions...)) + } + if s.Labels != nil { + andSel.Matchers = append(andSel.Matchers, LabelMatcher(s.Labels)) + } + if s.Annotations != nil { + andSel.Matchers = append(andSel.Matchers, AnnotationMatcher(s.Annotations)) + } + if s.ResourceMatcher != nil { + andSel.Matchers = append(andSel.Matchers, ResourceMatcherFunc(s.ResourceMatcher)) + } + return andSel.Filter(items) +} + +// MatchAll is a shorthand for building an AndSelector from a list of ResourceMatchers. +func MatchAll(matchers ...ResourceMatcher) *AndSelector { + return &AndSelector{Matchers: matchers} +} + +// MatchAny is a shorthand for building an OrSelector from a list of ResourceMatchers. +func MatchAny(matchers ...ResourceMatcher) *OrSelector { + return &OrSelector{Matchers: matchers} +} + +// OrSelector is a kio.Filter that selects resources when that match at least one of its embedded +// matchers. +type OrSelector struct { + // Matchers is the list of ResourceMatchers to try on the input resources. + Matchers []ResourceMatcher + // TemplateData, if present, is used to initialize any matchers that implement + // ResourceTemplateMatcher. + TemplateData interface{} + // FailOnEmptyMatch makes the selector return an error when no items are selected. + FailOnEmptyMatch bool +} + +// Match implements ResourceMatcher so that OrSelectors can be composed +func (s *OrSelector) Match(item *yaml.RNode) bool { + for _, matcher := range s.Matchers { + if matcher.Match(item) { + return true + } + } + return false +} + +// Filter implements kio.Filter, returning only those items from the list that the selector +// matches. +func (s *OrSelector) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + if err := initMatcherTemplates(s.Matchers, s.TemplateData); err != nil { + return nil, err + } + + var selectedItems []*yaml.RNode + for i := range items { + for _, matcher := range s.Matchers { + if matcher.Match(items[i]) { + selectedItems = append(selectedItems, items[i]) + break + } + } + } + if s.FailOnEmptyMatch && len(selectedItems) == 0 { + return nil, errors.Errorf("selector did not select any items") + } + return selectedItems, nil +} + +// DefaultTemplateData makes OrSelector a ResourceTemplateMatcher. +// Although it does not contain templates itself, this allows it to support ResourceTemplateMatchers +// when being used as a matcher itself. +func (s *OrSelector) DefaultTemplateData(data interface{}) { + if s.TemplateData == nil { + s.TemplateData = data + } +} + +func (s *OrSelector) InitTemplates() error { + return initMatcherTemplates(s.Matchers, s.TemplateData) +} + +func initMatcherTemplates(matchers []ResourceMatcher, data interface{}) error { + for _, matcher := range matchers { + if tm, ok := matcher.(ResourceTemplateMatcher); ok { + tm.DefaultTemplateData(data) + if err := tm.InitTemplates(); err != nil { + return err + } + } + } + return nil +} + +var _ ResourceTemplateMatcher = &OrSelector{} + +// OrSelector is a kio.Filter that selects resources when that match all of its embedded +// matchers. +type AndSelector struct { + // Matchers is the list of ResourceMatchers to try on the input resources. + Matchers []ResourceMatcher + // TemplateData, if present, is used to initialize any matchers that implement + // ResourceTemplateMatcher. + TemplateData interface{} + // FailOnEmptyMatch makes the selector return an error when no items are selected. + FailOnEmptyMatch bool +} + +// Match implements ResourceMatcher so that AndSelectors can be composed +func (s *AndSelector) Match(item *yaml.RNode) bool { + for _, matcher := range s.Matchers { + if !matcher.Match(item) { + return false + } + } + return true +} + +// Filter implements kio.Filter, returning only those items from the list that the selector +// matches. +func (s *AndSelector) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { + if err := initMatcherTemplates(s.Matchers, s.TemplateData); err != nil { + return nil, err + } + var selectedItems []*yaml.RNode + for i := range items { + isSelected := true + for _, matcher := range s.Matchers { + if !matcher.Match(items[i]) { + isSelected = false + break + } + } + if isSelected { + selectedItems = append(selectedItems, items[i]) + } + } + if s.FailOnEmptyMatch && len(selectedItems) == 0 { + return nil, errors.Errorf("selector did not select any items") + } + return selectedItems, nil +} + +// DefaultTemplateData makes AndSelector a ResourceTemplateMatcher. +// Although it does not contain templates itself, this allows it to support ResourceTemplateMatchers +// when being used as a matcher itself. +func (s *AndSelector) DefaultTemplateData(data interface{}) { + if s.TemplateData == nil { + s.TemplateData = data + } +} + +func (s *AndSelector) InitTemplates() error { + return initMatcherTemplates(s.Matchers, s.TemplateData) +} + +var _ ResourceTemplateMatcher = &AndSelector{} diff --git a/kyaml/fn/framework/selector_test.go b/kyaml/fn/framework/selector_test.go new file mode 100644 index 000000000..208a9161a --- /dev/null +++ b/kyaml/fn/framework/selector_test.go @@ -0,0 +1,465 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package framework_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/yaml" + + "sigs.k8s.io/kustomize/kyaml/fn/framework" + "sigs.k8s.io/kustomize/kyaml/kio" +) + +func TestSelector(t *testing.T) { + type Test struct { + // Name is the name of the test + Name string + + // Filter 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 + { + Name: "apiVersion", + 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 + rw := &kio.ByteReadWriter{ + Reader: bytes.NewBufferString(input), + Writer: &out, + KeepReaderAnnotations: true, + } + p := func(rl *framework.ResourceList) error { + s := &framework.Selector{TemplateData: test} + test.Fn(s) + rl.Items, err = s.Filter(rl.Items) + return err + } + + require.NoError(t, framework.Execute(framework.ResourceListProcessorFunc(p), rw)) + require.Equal(t, strings.TrimSpace(expectedFoo), strings.TrimSpace(out.String())) + }) + } + + // 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 + rw := &kio.ByteReadWriter{ + Reader: bytes.NewBufferString(input), + Writer: &out, + KeepReaderAnnotations: true, + } + + p := func(rl *framework.ResourceList) error { + s := &framework.Selector{TemplateData: test} + test.Fn(s) + rl.Items, err = s.Filter(rl.Items) + return err + } + require.NoError(t, framework.Execute(framework.ResourceListProcessorFunc(p), rw)) + require.Equal(t, strings.TrimSpace(expectedBar), strings.TrimSpace(out.String())) + }) + } +} + +func TestAndOrSelector_Composition(t *testing.T) { + // This selector should pick the "prime-target" deployment by name + // as well as any resources with the given labels or annotations regardless of kind + s := framework.MatchAny( + framework.MatchAll( + framework.GVKMatcher("apps/v1/Deployment"), + framework.NameMatcher("prime-target"), + ), + framework.MatchAny( + framework.LabelMatcher(map[string]string{ + "select": "yes", + }), + framework.AnnotationMatcher(map[string]string{ + "example.io/select": "yes", + }), + ), + ) + + input, err := kio.FromBytes([]byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prime-target +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: exclude-one +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: exclude-two + labels: + select: no +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: extra-target + labels: + select: yes +--- +apiVersion: apps/v1 +kind: ConfigMap +metadata: + name: prime-target +data: + shouldSelect: false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-one + labels: + select: yes +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-two + annotations: + example.io/select: yes +`)) + require.NoError(t, err) + result, err := s.Filter(input) + require.NoError(t, err) + + expected := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prime-target +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: extra-target + labels: + select: yes +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-one + labels: + select: yes +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-two + annotations: + example.io/select: yes +` + resultStr, err := kio.StringAll(result) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr)) +} + +func TestAndOrSelector_CompositionTemplated(t *testing.T) { + // This selector should pick the "prime-target" deployment by name + // as well as any resources with the given labels or annotations regardless of kind + // Note: very similar to above test, but uses verbose expression to access templating + type templateStruct struct { + GVK string + Name string + LabelValue string + AnnotationValue string + } + + s := framework.OrSelector{ + // This should get propagated to matchers without explicit data + TemplateData: &templateStruct{ + GVK: "apps/v1/Oops", + Name: "extra-target", + LabelValue: "yes", + AnnotationValue: "yes", + }, + Matchers: []framework.ResourceMatcher{ + &framework.AndSelector{ + TemplateData: &templateStruct{ + GVK: "apps/v1/Deployment", + Name: "prime-target", + }, + Matchers: []framework.ResourceMatcher{ + framework.GVKMatcher("{{.GVK}}"), + framework.NameMatcher("{{.Name}}"), + }, + }, + &framework.OrSelector{ + Matchers: []framework.ResourceMatcher{ + framework.LabelMatcher(map[string]string{ + "select": "{{.LabelValue}}", + }), + framework.AnnotationMatcher(map[string]string{ + "example.io/select": "{{.AnnotationValue}}", + }), + }, + }, + }, + } + + input, err := kio.FromBytes([]byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prime-target +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: exclude-one +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: exclude-two + labels: + select: no +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: extra-target + labels: + select: yes +--- +apiVersion: apps/v1 +kind: ConfigMap +metadata: + name: prime-target +data: + shouldSelect: false +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-one + labels: + select: yes +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-two + annotations: + example.io/select: yes +`)) + require.NoError(t, err) + result, err := s.Filter(input) + require.NoError(t, err) + + expected := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: prime-target +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: extra-target + labels: + select: yes +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-one + labels: + select: yes +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: extra-target-two + annotations: + example.io/select: yes +` + resultStr, err := kio.StringAll(result) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr)) +} + +func TestMatchersAsFilters(t *testing.T) { + input, err := kio.FromBytes([]byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: target + labels: + select: me +--- +apiVersion: extensions/v1beta2 +kind: Deployment +metadata: + name: exclude + labels: + select: no +`)) + require.NoError(t, err) + + expected := ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: target + labels: + select: me +` + matchers := map[string]framework.ResourceMatcher{ + "slice": framework.NameMatcher("target"), + "map": framework.LabelMatcher(map[string]string{"select": "me"}), + "func": framework.ResourceMatcherFunc(func(node *yaml.RNode) bool { + v := node.Field("apiVersion").Value + return strings.TrimSpace(v.MustString()) == "apps/v1" + }), + } + for desc, m := range matchers { + matcher := m + t.Run(desc, func(t *testing.T) { + result, err := matcher.Filter(input) + require.NoError(t, err) + resultStr, err := kio.StringAll(result) + require.NoError(t, err) + assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(resultStr)) + }) + } +} diff --git a/kyaml/fn/framework/template.go b/kyaml/fn/framework/template.go new file mode 100644 index 000000000..ee7546d9d --- /dev/null +++ b/kyaml/fn/framework/template.go @@ -0,0 +1,80 @@ +// Copyright 2021 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package framework + +import ( + "bytes" + "strings" + "text/template" + + "sigs.k8s.io/kustomize/kyaml/errors" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// ResourceTemplate generates resources from templates. +type ResourceTemplate struct { + // Templates is a function that returns a list of templates to render into one or more resources. + Templates TemplatesFunc + + // TemplateData is the data to use when rendering the templates provided by the Templates field. + TemplateData interface{} +} + +// DefaultTemplateData sets TemplateData to the provided default values if it has not already +// been set. +func (rt *ResourceTemplate) DefaultTemplateData(data interface{}) { + if rt.TemplateData == nil { + rt.TemplateData = data + } +} + +// Render renders the Templates into resource nodes using TemplateData. +func (rt *ResourceTemplate) Render() ([]*yaml.RNode, error) { + var items []*yaml.RNode + + if rt.Templates == nil { + return items, nil + } + + templates, err := rt.Templates() + if err != nil { + return nil, errors.WrapPrefixf(err, "failed to retrieve ResourceTemplates") + } + + for i := range templates { + newItems, err := rt.doTemplate(templates[i]) + if err != nil { + return nil, err + } + items = append(items, newItems...) + } + return items, nil +} + +func (rt *ResourceTemplate) doTemplate(t *template.Template) ([]*yaml.RNode, error) { + // invoke the template + var b bytes.Buffer + err := t.Execute(&b, rt.TemplateData) + if err != nil { + return nil, errors.WrapPrefixf(err, "failed to render template %v", t.DefinedTemplates()) + } + var items []*yaml.RNode + + // split the resources so the error messaging is better + for _, s := range strings.Split(b.String(), "\n---\n") { + s = strings.TrimSpace(s) + if s == "" { + continue + } + newItems, err := (&kio.ByteReader{Reader: bytes.NewBufferString(s)}).Read() + if err != nil { + return nil, errors.WrapPrefixf(err, + "failed to parse rendered template into a resource:\n%s\n", addLineNumbers(s)) + } + + items = append(items, newItems...) + } + return items, nil +} diff --git a/kyaml/fn/framework/testdata/template/config.yaml b/kyaml/fn/framework/testdata/example/template/config.yaml similarity index 100% rename from kyaml/fn/framework/testdata/template/config.yaml rename to kyaml/fn/framework/testdata/example/template/config.yaml diff --git a/kyaml/fn/framework/testdata/templatefiles/config.yaml b/kyaml/fn/framework/testdata/example/templatefiles/config.yaml similarity index 100% rename from kyaml/fn/framework/testdata/templatefiles/config.yaml rename to kyaml/fn/framework/testdata/example/templatefiles/config.yaml diff --git a/kyaml/fn/framework/testdata/templatefiles/deployment.template b/kyaml/fn/framework/testdata/example/templatefiles/deployment.template similarity index 100% rename from kyaml/fn/framework/testdata/templatefiles/deployment.template rename to kyaml/fn/framework/testdata/example/templatefiles/deployment.template diff --git a/kyaml/fn/framework/patchtestdata/annotationselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/annotationselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/annotationselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/annotationselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/annotationselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/annotationselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/annotationselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/annotationselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/annotationselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/annotationselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/annotationselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/annotationselector/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/apiversionselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/apiversionselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/apiversionselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/apiversionselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/apiversionselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/apiversionselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/apiversionselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/apiversionselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/apiversionselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/apiversionselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/apiversionselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/apiversionselector/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/filterselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/filterselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/filterselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/filterselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/filterselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/filterselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/filterselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/filterselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/filterselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/filterselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/filterselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/filterselector/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/kindselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/kindselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/kindselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/kindselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/kindselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/kindselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/kindselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/kindselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/kindselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/kindselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/kindselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/kindselector/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/labelselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/labelselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/labelselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/labelselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/labelselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/labelselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/labelselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/labelselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/labelselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/labelselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/labelselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/labelselector/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/longlistpreprocess/config.yaml b/kyaml/fn/framework/testdata/patch-selector/longlistpreprocess/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/longlistpreprocess/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/longlistpreprocess/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/longlistpreprocess/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/longlistpreprocess/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/longlistpreprocess/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/longlistpreprocess/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/longlistpreprocess/input.yaml b/kyaml/fn/framework/testdata/patch-selector/longlistpreprocess/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/longlistpreprocess/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/longlistpreprocess/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/nameselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/nameselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/nameselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/nameselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/nameselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/nameselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/nameselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/nameselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/nameselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/nameselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/nameselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/nameselector/input.yaml diff --git a/kyaml/fn/framework/patchtestdata/namespaceselector/config.yaml b/kyaml/fn/framework/testdata/patch-selector/namespaceselector/config.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/namespaceselector/config.yaml rename to kyaml/fn/framework/testdata/patch-selector/namespaceselector/config.yaml diff --git a/kyaml/fn/framework/patchtestdata/namespaceselector/expected.yaml b/kyaml/fn/framework/testdata/patch-selector/namespaceselector/expected.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/namespaceselector/expected.yaml rename to kyaml/fn/framework/testdata/patch-selector/namespaceselector/expected.yaml diff --git a/kyaml/fn/framework/patchtestdata/namespaceselector/input.yaml b/kyaml/fn/framework/testdata/patch-selector/namespaceselector/input.yaml similarity index 100% rename from kyaml/fn/framework/patchtestdata/namespaceselector/input.yaml rename to kyaml/fn/framework/testdata/patch-selector/namespaceselector/input.yaml diff --git a/kyaml/fn/framework/example2/data/container-patches/container.template.yaml b/kyaml/fn/framework/testdata/template-processor/container-patches/container.template.yaml similarity index 62% rename from kyaml/fn/framework/example2/data/container-patches/container.template.yaml rename to kyaml/fn/framework/testdata/template-processor/container-patches/container.template.yaml index fe54a6035..c00bbe7ef 100644 --- a/kyaml/fn/framework/example2/data/container-patches/container.template.yaml +++ b/kyaml/fn/framework/testdata/template-processor/container-patches/container.template.yaml @@ -2,5 +2,5 @@ # SPDX-License-Identifier: Apache-2.0 env: - key: {{ .Key }} - value: {{ .Value }} \ No newline at end of file +- name: {{ .Spec.Key }} + value: {{ .Spec.Value }} diff --git a/kyaml/fn/framework/example2/data/patches/patch.template.yaml b/kyaml/fn/framework/testdata/template-processor/patches/patch.template.yaml similarity index 72% rename from kyaml/fn/framework/example2/data/patches/patch.template.yaml rename to kyaml/fn/framework/testdata/template-processor/patches/patch.template.yaml index ce2bec2c3..1ec6a3323 100644 --- a/kyaml/fn/framework/example2/data/patches/patch.template.yaml +++ b/kyaml/fn/framework/testdata/template-processor/patches/patch.template.yaml @@ -2,4 +2,4 @@ # SPDX-License-Identifier: Apache-2.0 spec: - replicas: {{ .Replicas }} \ No newline at end of file + replicas: {{ .Spec.Replicas }} diff --git a/kyaml/fn/framework/example2/data/templates/deploy.template.yaml b/kyaml/fn/framework/testdata/template-processor/templates/deploy.template.yaml similarity index 100% rename from kyaml/fn/framework/example2/data/templates/deploy.template.yaml rename to kyaml/fn/framework/testdata/template-processor/templates/deploy.template.yaml diff --git a/kyaml/go.mod b/kyaml/go.mod index 6d37891a3..a5fa20709 100644 --- a/kyaml/go.mod +++ b/kyaml/go.mod @@ -14,7 +14,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/sergi/go-diff v1.1.0 github.com/spf13/cobra v1.0.0 - github.com/spf13/pflag v1.0.5 + github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.4.0 github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 diff --git a/kyaml/go.sum b/kyaml/go.sum index 4ed6e9256..873afe46c 100644 --- a/kyaml/go.sum +++ b/kyaml/go.sum @@ -34,6 +34,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -117,6 +118,7 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= @@ -132,6 +134,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -142,6 +145,7 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7 github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno= github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -150,6 +154,7 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -174,14 +179,18 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -241,6 +250,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c h1:Vco5b+cuG5NNfORVxZy6bYZQ7rsigisU1WQFkvQ0L5E= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=