mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-05-17 18:25:26 +00:00
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:
166
kyaml/fn/framework/command/command.go
Normal file
166
kyaml/fn/framework/command/command.go
Normal 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)
|
||||
}
|
||||
164
kyaml/fn/framework/command/command_test.go
Normal file
164
kyaml/fn/framework/command/command_test.go
Normal 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()))
|
||||
}
|
||||
90
kyaml/fn/framework/command/doc.go
Normal file
90
kyaml/fn/framework/command/doc.go
Normal 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
|
||||
382
kyaml/fn/framework/command/example_test.go
Normal file
382
kyaml/fn/framework/command/example_test.go
Normal 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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
9
kyaml/fn/framework/example/input/cm.yaml
Normal file
9
kyaml/fn/framework/example/input/cm.yaml
Normal 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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package example2
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
349
kyaml/fn/framework/matchers.go
Normal file
349
kyaml/fn/framework/matchers.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
410
kyaml/fn/framework/processors.go
Normal file
410
kyaml/fn/framework/processors.go
Normal 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
|
||||
}
|
||||
}
|
||||
527
kyaml/fn/framework/processors_test.go
Normal file
527
kyaml/fn/framework/processors_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
219
kyaml/fn/framework/selector.go
Normal file
219
kyaml/fn/framework/selector.go
Normal 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{}
|
||||
465
kyaml/fn/framework/selector_test.go
Normal file
465
kyaml/fn/framework/selector_test.go
Normal 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))
|
||||
})
|
||||
}
|
||||
}
|
||||
80
kyaml/fn/framework/template.go
Normal file
80
kyaml/fn/framework/template.go
Normal 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
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
env:
|
||||
key: {{ .Key }}
|
||||
value: {{ .Value }}
|
||||
- name: {{ .Spec.Key }}
|
||||
value: {{ .Spec.Value }}
|
||||
@@ -2,4 +2,4 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
spec:
|
||||
replicas: {{ .Replicas }}
|
||||
replicas: {{ .Spec.Replicas }}
|
||||
@@ -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
|
||||
|
||||
10
kyaml/go.sum
10
kyaml/go.sum
@@ -34,6 +34,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
@@ -117,6 +118,7 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
@@ -132,6 +134,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@@ -142,6 +145,7 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
|
||||
github.com/markbates/pkger v0.17.1 h1:/MKEtWqtc0mZvu9OinB9UzVN9iYCwLWuyUv4Bw+PCno=
|
||||
github.com/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
@@ -150,6 +154,7 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -174,14 +179,18 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -241,6 +250,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c h1:Vco5b+cuG5NNfORVxZy6bYZQ7rsigisU1WQFkvQ0L5E=
|
||||
golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
|
||||
Reference in New Issue
Block a user