Improvements to kyaml fn framework

This commit creates a new version of the alpha configuration functions framework. Goals include:
- Make it easy to build multi-version APIs with the framework (not previously facilitated at all).
- Simplify the framework's APIs where redundant configuration options exist (leaving the most powerful, replacing others with helpers to maintain usability they provided).
- Make the Framework's APIs more consistent (e.g. between the various template types, usage of kio.Filter, field names)
- Decouple responsibilities (e.g. command creation, resource list processing, generation of templating functions).
- Make the framework even more powerfully pluggable (e.g. any kio.Filter can be a selector, and the selector the framework provides is itself a filter built from reusable abstractions).
- Improve documentation.
- Make container patches merge fields (notably list fields like `env`) correctly.
This commit is contained in:
Katrina Verey
2021-01-21 17:52:45 -08:00
parent 1d524b6fbe
commit 5c4b5b1bf0
61 changed files with 4059 additions and 2624 deletions

View File

@@ -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)
}

View File

@@ -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()))
}

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -1,4 +0,0 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package example2

View File

@@ -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()))
}

View File

@@ -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`)))

File diff suppressed because it is too large Load Diff

View File

@@ -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)
}

View File

@@ -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()))
}

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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")
}

View File

@@ -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)
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
})
}
}

View File

@@ -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 {

View File

@@ -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{}

View File

@@ -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))
})
}
}

View File

@@ -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
}

View File

@@ -2,5 +2,5 @@
# SPDX-License-Identifier: Apache-2.0
env:
key: {{ .Key }}
value: {{ .Value }}
- name: {{ .Spec.Key }}
value: {{ .Spec.Value }}

View File

@@ -2,4 +2,4 @@
# SPDX-License-Identifier: Apache-2.0
spec:
replicas: {{ .Replicas }}
replicas: {{ .Spec.Replicas }}

View File

@@ -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

View File

@@ -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=