mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-10 08:20:59 +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"
|
||||
}
|
||||
6
kyaml/fn/framework/command/testdata/standalone/config.yaml
vendored
Normal file
6
kyaml/fn/framework/command/testdata/standalone/config.yaml
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright 2019 The Kubernetes Authors.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
apiVersion: example.com/v1alpha1
|
||||
kind: Foo
|
||||
a: b
|
||||
20
kyaml/fn/framework/command/testdata/standalone/expected.yaml
vendored
Normal file
20
kyaml/fn/framework/command/testdata/standalone/expected.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2019 The Kubernetes Authors.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bar2
|
||||
namespace: default
|
||||
annotations:
|
||||
foo: bar2
|
||||
a: 'b'
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bar1
|
||||
namespace: default
|
||||
annotations:
|
||||
foo: bar1
|
||||
a: 'b'
|
||||
10
kyaml/fn/framework/command/testdata/standalone/input.yaml
vendored
Normal file
10
kyaml/fn/framework/command/testdata/standalone/input.yaml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Copyright 2019 The Kubernetes Authors.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bar2
|
||||
namespace: default
|
||||
annotations:
|
||||
foo: bar2
|
||||
Reference in New Issue
Block a user