Files
kustomize/kyaml/fn/framework/example_test.go
Phillip Wittrock f2706dce68 Improved fn framework support for patching
- Generate patches with a func
- Generate patches for containers
2020-11-24 12:02:55 -08:00

1118 lines
24 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework_test
import (
"bytes"
"fmt"
"log"
"os"
"path/filepath"
"text/template"
"github.com/spf13/pflag"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const service = "Service"
// ExampleResourceList_modify implements a function that sets an annotation on each resource.
// The annotation value is configured via a flag value parsed from ResourceList.functionConfig.data
func ExampleResourceList_modify() {
// for testing purposes only -- normally read from stdin when Executing
input := 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 is parsed into flags by framework.Command
functionConfig:
apiVersion: v1
kind: ConfigMap
data:
value: baz
`)
// configure the annotation value using a flag parsed from
// ResourceList.functionConfig.data.value
fs := pflag.NewFlagSet("tests", pflag.ContinueOnError)
value := fs.String("value", "", "annotation value")
rl := framework.ResourceList{
Flags: fs,
Reader: input, // for testing only
}
if err := rl.Read(); err != nil {
panic(err)
}
for i := range rl.Items {
// set the annotation on each resource item
if err := rl.Items[i].PipeE(yaml.SetAnnotation("value", *value)); err != nil {
panic(err)
}
}
if err := rl.Write(); 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
}
// ExampleCommand_modify implements a function that sets an annotation on each resource.
// The annotation value is configured via a flag value parsed from
// ResourceList.functionConfig.data
func ExampleCommand_modify() {
// configure the annotation value using a flag parsed from
// ResourceList.functionConfig.data.value
resourceList := framework.ResourceList{}
var value string
cmd := framework.Command(&resourceList, func() error {
for i := range resourceList.Items {
// set the annotation on each resource item
err := resourceList.Items[i].PipeE(yaml.SetAnnotation("value", value))
if err != nil {
return err
}
}
return nil
})
cmd.Flags().StringVar(&value, "value", "", "annotation value")
// for testing purposes only -- normally read from stdin when Executing
cmd.SetIn(bytes.NewBufferString(`
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 is parsed into flags by framework.Command
functionConfig:
apiVersion: v1
kind: ConfigMap
data:
value: baz
`))
// run the command
if err := cmd.Execute(); err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1alpha1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// annotations:
// value: 'baz'
// - apiVersion: v1
// kind: Service
// metadata:
// name: foo
// annotations:
// value: 'baz'
// functionConfig:
// apiVersion: v1
// kind: ConfigMap
// data:
// value: baz
}
// ExampleCommand_generateReplace generates a resource from a functionConfig.
// If the resource already exist s, it replaces the resource with a new copy.
func ExampleCommand_generateReplace() {
// function API definition which will be parsed from the ResourceList.functionConfig
// 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
resourceList := &framework.ResourceList{FunctionConfig: functionConfig}
cmd := framework.Command(resourceList, func() error {
var newNodes []*yaml.RNode
for i := range resourceList.Items {
meta, err := resourceList.Items[i].GetMeta()
if err != nil {
return err
}
// something we already generated, remove it from the list so we regenerate it
if meta.Name == functionConfig.Spec.Name &&
meta.Kind == service &&
meta.APIVersion == "v1" {
continue
}
newNodes = append(newNodes, resourceList.Items[i])
}
// generate the resource
n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
name: %s
`, functionConfig.Spec.Name))
if err != nil {
return err
}
newNodes = append(newNodes, n)
resourceList.Items = newNodes
return nil
})
// for testing purposes only -- normally read from stdin when Executing
cmd.SetIn(bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1alpha1
kind: ResourceList
# items are provided as nodes
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
# functionConfig is parsed into flags by framework.Command
functionConfig:
apiVersion: example.com/v1alpha1
kind: ExampleServiceGenerator
spec:
name: bar
`))
// run the command
if err := cmd.Execute(); err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1alpha1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// - apiVersion: v1
// kind: Service
// metadata:
// name: bar
// functionConfig:
// apiVersion: example.com/v1alpha1
// kind: ExampleServiceGenerator
// spec:
// name: bar
}
// ExampleResourceList_generateReplace generates a resource from a functionConfig.
// If the resource already exist s, it replaces the resource with a new copy.
func ExampleResourceList_generateReplace() {
input := bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1alpha1
kind: ResourceList
# items are provided as nodes
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
# functionConfig is parsed into flags by framework.Command
functionConfig:
apiVersion: example.com/v1alpha1
kind: ExampleServiceGenerator
spec:
name: bar
`)
// function API definition which will be parsed from the ResourceList.functionConfig
// read from stdin
type Spec struct {
Name string `yaml:"name,omitempty"`
}
type ExampleServiceGenerator struct {
Spec Spec `yaml:"spec,omitempty"`
}
functionConfig := &ExampleServiceGenerator{}
rl := framework.ResourceList{
FunctionConfig: functionConfig,
Reader: input, // for testing only
}
if err := rl.Read(); err != nil {
panic(err)
}
// remove the last generated resource
var newNodes []*yaml.RNode
for i := range rl.Items {
meta, err := rl.Items[i].GetMeta()
if err != nil {
panic(err)
}
// something we already generated, remove it from the list so we regenerate it
if meta.Name == functionConfig.Spec.Name &&
meta.Kind == service &&
meta.APIVersion == "v1" {
continue
}
newNodes = append(newNodes, rl.Items[i])
}
rl.Items = newNodes
// generate the resource again
n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
name: %s
`, functionConfig.Spec.Name))
if err != nil {
panic(err)
}
rl.Items = append(rl.Items, n)
if err := rl.Write(); err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1alpha1
// 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
}
// ExampleCommand_generateUpdate generates a resource, updating the previously generated
// copy rather than replacing it.
//
// Note: This will keep manual edits to the previously generated copy.
func ExampleCommand_generateUpdate() {
// function API definition which will be parsed from the ResourceList.functionConfig
// read from stdin
type Spec struct {
Name string `yaml:"name,omitempty"`
Annotations map[string]string `yaml:"annotations,omitempty"`
}
type ExampleServiceGenerator struct {
Spec Spec `yaml:"spec,omitempty"`
}
functionConfig := &ExampleServiceGenerator{}
// function implementation -- generate or update a Service resource
resourceList := &framework.ResourceList{FunctionConfig: functionConfig}
cmd := framework.Command(resourceList, func() error {
var found bool
for i := range resourceList.Items {
meta, err := resourceList.Items[i].GetMeta()
if err != nil {
return err
}
// something we already generated, reconcile it to make sure it matches what
// is specified by the functionConfig
if meta.Name == functionConfig.Spec.Name &&
meta.Kind == service &&
meta.APIVersion == "v1" {
// set some values
for k, v := range functionConfig.Spec.Annotations {
err := resourceList.Items[i].PipeE(yaml.SetAnnotation(k, v))
if err != nil {
return err
}
}
found = true
break
}
}
if found {
return nil
}
// generate the resource if not found
n, err := yaml.Parse(fmt.Sprintf(`apiVersion: v1
kind: Service
metadata:
name: %s
`, functionConfig.Spec.Name))
if err != nil {
return err
}
for k, v := range functionConfig.Spec.Annotations {
err := n.PipeE(yaml.SetAnnotation(k, v))
if err != nil {
return err
}
}
resourceList.Items = append(resourceList.Items, n)
return nil
})
// for testing purposes only -- normally read from stdin when Executing
cmd.SetIn(bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1alpha1
kind: ResourceList
# items are provided as nodes
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
- apiVersion: v1
kind: Service
metadata:
name: bar
# functionConfig is parsed into flags by framework.Command
functionConfig:
apiVersion: example.com/v1alpha1
kind: ExampleServiceGenerator
spec:
name: bar
annotations:
a: b
`))
// run the command
if err := cmd.Execute(); err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1alpha1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// - apiVersion: v1
// kind: Service
// metadata:
// name: bar
// annotations:
// a: 'b'
// functionConfig:
// apiVersion: example.com/v1alpha1
// kind: ExampleServiceGenerator
// spec:
// name: bar
// annotations:
// a: b
}
// ExampleCommand_validate validates that all Deployment resources have the replicas field set.
// If any Deployments do not contain spec.replicas, then the function will return results
// which will be set on ResourceList.results
func ExampleCommand_validate() {
resourceList := &framework.ResourceList{}
cmd := framework.Command(resourceList, func() error {
// validation results
var validationResults []framework.Item
// validate that each Deployment resource has spec.replicas set
for i := range resourceList.Items {
// only check Deployment resources
meta, err := resourceList.Items[i].GetMeta()
if err != nil {
return err
}
if meta.Kind != "Deployment" {
continue
}
// lookup replicas field
r, err := resourceList.Items[i].Pipe(yaml.Lookup("spec", "replicas"))
if err != nil {
return err
}
// check replicas not specified
if r != nil {
continue
}
validationResults = append(validationResults, framework.Item{
Severity: framework.Error,
Message: "missing replicas",
ResourceRef: meta,
Field: framework.Field{
Path: "spec.field",
SuggestedValue: "1",
},
})
}
if len(validationResults) > 0 {
resourceList.Result = &framework.Result{
Name: "replicas-validator",
Items: validationResults,
}
}
return resourceList.Result
})
// for testing purposes only -- normally read from stdin when Executing
cmd.SetIn(bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1alpha1
kind: ResourceList
# items are provided as nodes
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
`))
// run the command
if err := cmd.Execute(); err != nil {
// normally exit 1 here
}
// Output:
// apiVersion: config.kubernetes.io/v1alpha1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// results:
// name: replicas-validator
// items:
// - message: missing replicas
// severity: error
// resourceRef:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// field:
// path: spec.field
// suggestedValue: "1"
}
// ExampleTemplateCommand provides an example for using the TemplateCommand
func ExampleTemplateCommand() {
// create the template
cmd := framework.TemplateCommand{
// Template input
API: &struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
}{},
// Template
Template: template.Must(template.New("example").Parse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
`)),
}.GetCommand()
cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")})
if err := cmd.Execute(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// a: b
}
// ExampleTemplateCommand_files provides an example for using the TemplateCommand
func ExampleTemplateCommand_files() {
// create the template
cmd := framework.TemplateCommand{
// Template input
API: &struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
}{},
// Template
TemplatesFiles: []string{filepath.Join("testdata", "templatefiles", "deployment.template")},
}.GetCommand()
cmd.SetArgs([]string{filepath.Join("testdata", "templatefiles", "config.yaml")})
if err := cmd.Execute(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// a: b
}
// ExampleTemplateCommand_preprocess provides an example for using the TemplateCommand
// with PreProcess to configure the template based on the input resources observed.
func ExampleTemplateCommand_preprocess() {
config := &struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
Short bool
}{}
// create the template
cmd := framework.TemplateCommand{
// Template input
API: config,
PreProcess: func(rl *framework.ResourceList) error {
config.Short = len(rl.Items) < 3
return nil
},
// Template
Template: template.Must(template.New("example").Parse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
{{- if .Short }}
short: 'true'
{{- end }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
{{- if .Short }}
short: 'true'
{{- end }}
`)),
}.GetCommand()
cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")})
if err := cmd.Execute(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// a: b
// short: 'true'
// ---
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// namespace: default
// annotations:
// a: b
// short: 'true'
}
// ExampleTemplateCommand_postprocess provides an example for using the TemplateCommand
// with PostProcess to modify the results.
func ExampleTemplateCommand_postprocess() {
config := &struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
}{}
// create the template
cmd := framework.TemplateCommand{
// Template input
API: config,
// Template
Template: template.Must(template.New("example").Parse(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
`)),
PostProcess: func(rl *framework.ResourceList) error {
// trim the first resources
rl.Items = rl.Items[1:]
return nil
},
}.GetCommand()
cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")})
if err := cmd.Execute(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// namespace: default
// annotations:
// a: b
}
// ExampleTemplateCommand_patch provides an example for using the TemplateCommand to
// create a function which patches resources.
func ExampleTemplateCommand_patch() {
// patch the foo resource only
s := framework.Selector{Names: []string{"foo"}}
cmd := framework.TemplateCommand{
API: &struct {
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 }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
`)),
// PatchTemplates are applied to BOTH ResourceList input resources AND templated resources
PatchTemplates: []framework.PatchTemplate{{
Selector: &s,
Template: template.Must(template.New("test").Parse(`
metadata:
annotations:
patched: 'true'
`)),
}},
}.GetCommand()
cmd.SetArgs([]string{filepath.Join("testdata", "template", "config.yaml")})
if err := cmd.Execute(); err != nil {
fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// a: b
// patched: 'true'
// ---
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// namespace: default
// annotations:
// a: b
}
func ExampleSelector_templatizeKinds() {
type api struct {
KindName string `yaml:"kindName"`
}
rl := &framework.ResourceList{
FunctionConfig: &api{KindName: "Deployment"},
Reader: bytes.NewBufferString(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: bar
namespace: default
`),
Writer: os.Stdout,
}
if err := rl.Read(); err != nil {
panic(err)
}
var err error
s := &framework.Selector{
TemplatizeValues: true,
Kinds: []string{"{{ .KindName }}"},
}
rl.Items, err = s.GetMatches(rl)
if err != nil {
panic(err)
}
if err := rl.Write(); err != nil {
panic(err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// config.kubernetes.io/index: '0'
}
func ExampleSelector_templatizeAnnotations() {
type api struct {
Value string `yaml:"vaue"`
}
rl := &framework.ResourceList{
FunctionConfig: &api{Value: "bar"},
Reader: bytes.NewBufferString(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
annotations:
key: foo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
annotations:
key: bar
`),
Writer: os.Stdout,
}
if err := rl.Read(); err != nil {
panic(err)
}
var err error
s := &framework.Selector{
TemplatizeValues: true,
Annotations: map[string]string{"key": "{{ .Value }}"},
}
rl.Items, err = s.GetMatches(rl)
if err != nil {
panic(err)
}
if err := rl.Write(); err != nil {
panic(err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// namespace: default
// annotations:
// key: bar
// config.kubernetes.io/index: '1'
}
// ExamplePatchContainersWithString patches all containers.
func ExamplePatchContainersWithString() {
resources, err := kio.ParseAll(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
spec:
template:
spec:
containers:
- name: foo
image: a
- name: bar
image: b
---
apiVersion: v1
kind: Service
metadata:
name: foo
spec:
selector:
foo: bar
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
spec:
template:
spec:
containers:
- name: foo
image: a
- name: baz
image: b
---
apiVersion: v1
kind: Service
metadata:
name: bar
spec:
selector:
foo: bar
`)
if err != nil {
log.Fatal(err)
}
input := struct{ Value string }{Value: "new-value"}
err = framework.PatchContainersWithString(resources, `
env:
KEY: {{ .Value }}
`, input)
if err != nil {
log.Fatal(err)
}
fmt.Println(kio.StringAll(resources))
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// spec:
// template:
// spec:
// containers:
// - name: foo
// image: a
// env:
// KEY: new-value
// - name: bar
// image: b
// env:
// KEY: new-value
// ---
// apiVersion: v1
// kind: Service
// metadata:
// name: foo
// spec:
// selector:
// foo: bar
// ---
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// spec:
// template:
// spec:
// containers:
// - name: foo
// image: a
// env:
// KEY: new-value
// - name: baz
// image: b
// env:
// KEY: new-value
// ---
// apiVersion: v1
// kind: Service
// metadata:
// name: bar
// spec:
// selector:
// foo: bar
// <nil>
}
// PatchTemplateContainersWithString patches containers matching
// a specific name.
func ExamplePatchContainersWithString_names() {
resources, err := kio.ParseAll(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
spec:
template:
spec:
containers:
- name: foo
image: a
- name: bar
image: b
---
apiVersion: v1
kind: Service
metadata:
name: foo
spec:
selector:
foo: bar
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
spec:
template:
spec:
containers:
- name: foo
image: a
- name: baz
image: b
---
apiVersion: v1
kind: Service
metadata:
name: bar
spec:
selector:
foo: bar
`)
if err != nil {
log.Fatal(err)
}
input := struct{ Value string }{Value: "new-value"}
err = framework.PatchContainersWithString(resources, `
env:
KEY: {{ .Value }}
`, input, "foo")
if err != nil {
log.Fatal(err)
}
fmt.Println(kio.StringAll(resources))
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// spec:
// template:
// spec:
// containers:
// - name: foo
// image: a
// env:
// KEY: new-value
// - name: bar
// image: b
// ---
// apiVersion: v1
// kind: Service
// metadata:
// name: foo
// spec:
// selector:
// foo: bar
// ---
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// spec:
// template:
// spec:
// containers:
// - name: foo
// image: a
// env:
// KEY: new-value
// - name: baz
// image: b
// ---
// apiVersion: v1
// kind: Service
// metadata:
// name: bar
// spec:
// selector:
// foo: bar
// <nil>
}