Files
kustomize/kyaml/fn/framework/example_test.go
2021-10-19 16:15:27 -07:00

1067 lines
24 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework_test
import (
"bytes"
"fmt"
"log"
"path/filepath"
"strings"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/framework"
"sigs.k8s.io/kustomize/kyaml/fn/framework/command"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
const service = "Service"
// ExampleSimpleProcessor_modify implements a function that sets an annotation on each resource.
func ExampleSimpleProcessor_modify() {
input := bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
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
`)
config := new(struct {
Data map[string]string `yaml:"data" json:"data"`
})
fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) {
for i := range items {
// set the annotation on each resource item
if err := items[i].PipeE(yaml.SetAnnotation("value", config.Data["value"])); err != nil {
return nil, err
}
}
return items, nil
}
err := framework.Execute(framework.SimpleProcessor{Config: config, Filter: kio.FilterFunc(fn)}, &kio.ByteReadWriter{Reader: input})
if err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1
// 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
}
// ExampleSimpleProcessor_generateReplace generates a resource from a FunctionConfig.
// If the resource already exists, it replaces the resource with a new copy.
func ExampleSimpleProcessor_generateReplace() {
input := bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
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
`)
// 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{}
fn := func(items []*yaml.RNode) ([]*yaml.RNode, error) {
// remove the last generated resource
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])
}
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 {
return nil, err
}
items = append(items, n)
return items, nil
}
p := framework.SimpleProcessor{Config: functionConfig, Filter: kio.FilterFunc(fn)}
err := framework.Execute(p, &kio.ByteReadWriter{Reader: input})
if err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1
// 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
}
// ExampleTemplateProcessor provides an example for using the TemplateProcessor to add resources
// from templates defined inline
func ExampleTemplateProcessor_generate_inline() {
api := new(struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
})
// create the template
fn := framework.TemplateProcessor{
// Templates input
TemplateData: api,
// Templates
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateStrings(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
annotations:
{{ .Key }}: {{ .Value }}
`)}},
}
cmd := command.Build(fn, command.StandaloneEnabled, false)
// mimic standalone mode: testdata/template/config.yaml will be parsed into `api`
cmd.SetArgs([]string{filepath.Join("testdata", "example", "template", "config.yaml")})
if err := cmd.Execute(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// a: b
}
// ExampleTemplateProcessor_files provides an example for using the TemplateProcessor to add
// resources from templates defined in files.
func ExampleTemplateProcessor_generate_files() {
api := new(struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
})
// create the template
templateFn := framework.TemplateProcessor{
// Templates input
TemplateData: api,
// Templates
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateFiles("testdata/example/templatefiles/deployment.template.yaml"),
}},
}
cmd := command.Build(templateFn, command.StandaloneEnabled, false)
// mimic standalone mode: testdata/template/config.yaml will be parsed into `api`
cmd.SetArgs([]string{filepath.Join("testdata", "example", "templatefiles", "config.yaml")})
if err := cmd.Execute(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// Output:
// # Copyright 2021 The Kubernetes Authors.
// # SPDX-License-Identifier: Apache-2.0
//
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// annotations:
// a: b
}
// ExampleTemplateProcessor_preprocess provides an example for using the TemplateProcessor
// with PreProcess to configure the template based on the input resources observed.
func ExampleTemplateProcessor_preprocess() {
config := new(struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
Short bool
})
// create the template
fn := framework.TemplateProcessor{
// Templates input
TemplateData: config,
PreProcessFilters: []kio.Filter{
kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) {
config.Short = len(items) < 3
return items, nil
}),
},
// Templates
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateStrings(`
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 }}
`),
}},
}
cmd := command.Build(fn, command.StandaloneEnabled, false)
// mimic standalone mode: testdata/template/config.yaml will be parsed into `api`
cmd.SetArgs([]string{filepath.Join("testdata", "example", "template", "config.yaml")})
if err := cmd.Execute(); err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", err)
}
// 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'
}
// ExampleTemplateProcessor_postprocess provides an example for using the TemplateProcessor
// with PostProcess to modify the results.
func ExampleTemplateProcessor_postprocess() {
config := new(struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
})
// create the template
fn := framework.TemplateProcessor{
// Templates input
TemplateData: config,
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateStrings(`
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 }}
`),
}},
PostProcessFilters: []kio.Filter{
kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) {
items = items[1:]
return items, nil
}),
},
}
cmd := command.Build(fn, command.StandaloneEnabled, false)
cmd.SetArgs([]string{filepath.Join("testdata", "example", "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
}
// ExampleTemplateProcessor_patch provides an example for using the TemplateProcessor to
// create a function that patches resources.
func ExampleTemplateProcessor_patch() {
fn := framework.TemplateProcessor{
TemplateData: new(struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
}),
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateStrings(`
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{
&framework.ResourcePatchTemplate{
// patch the foo resource only
Selector: &framework.Selector{Names: []string{"foo"}},
Templates: parser.TemplateStrings(`
metadata:
annotations:
patched: 'true'
`),
}},
}
cmd := command.Build(fn, command.StandaloneEnabled, false)
cmd.SetArgs([]string{filepath.Join("testdata", "example", "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
}
// ExampleTemplateProcessor_MergeResources provides an example for using the TemplateProcessor to
// create a function that treats incoming resources as potential patches
// for the resources the function generates itself.
func ExampleTemplateProcessor_MergeResources() {
p := framework.TemplateProcessor{
TemplateData: new(struct {
Name string `json:"name" yaml:"name"`
}),
ResourceTemplates: []framework.ResourceTemplate{{
// This is the generated resource the input will patch
Templates: parser.TemplateStrings(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Name }}
spec:
replicas: 1
selector:
app: foo
template:
spec:
containers:
- name: app
image: example.io/team/app
`),
}},
MergeResources: true,
}
// The second resource will be treated as a patch since its metadata matches the resource
// generated by ResourceTemplates and MergeResources is true.
rw := kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- kind: Deployment
apiVersion: apps/v1
metadata:
name: custom
spec:
replicas: 6
selector:
app: custom
template:
spec:
containers:
- name: app
image: example.io/team/custom
- kind: Deployment
apiVersion: apps/v1
metadata:
name: mergeTest
spec:
replicas: 6
functionConfig:
name: mergeTest
`)}
if err := framework.Execute(p, &rw); err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: mergeTest
// spec:
// replicas: 6
// selector:
// app: foo
// template:
// spec:
// containers:
// - name: app
// image: example.io/team/app
// - kind: Deployment
// apiVersion: apps/v1
// metadata:
// name: custom
// spec:
// replicas: 6
// selector:
// app: custom
// template:
// spec:
// containers:
// - name: app
// image: example.io/team/custom
// functionConfig:
// name: mergeTest
}
// ExampleSelector_templatizeKinds provides an example of using a template as a selector value,
// to dynamically match resources based on the functionConfig input. It also shows how Selector
// can be used with SimpleProcessor to implement a ResourceListProcessor the filters the input.
func ExampleSelector_templatizeKinds() {
type api struct {
KindName string `yaml:"kindName"`
}
rw := &kio.ByteReadWriter{
Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
functionConfig:
kindName: Deployment
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
- apiVersion: apps/v1
kind: StatefulSet
metadata:
name: bar
namespace: default
`),
}
config := &api{}
p := framework.SimpleProcessor{
Config: config,
Filter: &framework.Selector{
TemplateData: config,
Kinds: []string{"{{ .KindName }}"},
},
}
err := framework.Execute(p, rw)
if err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// namespace: default
// functionConfig:
// kindName: Deployment
}
// ExampleSelector_templatizeKinds provides an example of using a template as a selector value,
// to dynamically match resources based on the functionConfig input. It also shows how Selector
// can be used with SimpleProcessor to implement a ResourceListProcessor the filters the input.
func ExampleSelector_templatizeAnnotations() {
type api struct {
Value string `yaml:"value"`
}
rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
functionConfig:
value: bar
items:
- apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
namespace: default
annotations:
key: foo
- apiVersion: apps/v1
kind: Deployment
metadata:
name: bar
namespace: default
annotations:
key: bar
`)}
config := &api{}
p := framework.SimpleProcessor{
Config: config,
Filter: &framework.Selector{
TemplateData: config,
Annotations: map[string]string{"key": "{{ .Value }}"},
},
}
if err := framework.Execute(p, rw); err != nil {
panic(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: bar
// namespace: default
// annotations:
// key: bar
// functionConfig:
// value: bar
}
// ExampleTemplateProcessor_container_patch provides an example for using TemplateProcessor to
// patch all of the containers in the input.
func ExampleTemplateProcessor_container_patch() {
input := `
apiVersion: apps/v1
kind: Deployment
metadata:
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
`
p := framework.TemplateProcessor{
PatchTemplates: []framework.PatchTemplate{
&framework.ContainerPatchTemplate{
Templates: parser.TemplateStrings(`
env:
- name: KEY
value: {{ .Value }}
`),
TemplateData: struct{ Value string }{Value: "new-value"},
}},
}
err := framework.Execute(p, &kio.ByteReadWriter{Reader: bytes.NewBufferString(input)})
if err != nil {
log.Fatal(err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// spec:
// template:
// spec:
// containers:
// - name: foo
// image: a
// env:
// - name: KEY
// value: new-value
// - name: bar
// image: b
// env:
// - name: KEY
// value: 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:
// - name: KEY
// value: new-value
// - name: baz
// image: b
// env:
// - name: KEY
// value: new-value
// ---
// apiVersion: v1
// kind: Service
// metadata:
// name: bar
// spec:
// selector:
// foo: bar
}
// PatchTemplateContainersWithString patches containers matching
// a specific name.
func ExampleTemplateProcessor_container_patch_by_name() {
input := `
apiVersion: apps/v1
kind: Deployment
metadata:
name: foo
spec:
template:
spec:
containers:
- name: foo
image: a
env:
- name: EXISTING
value: variable
- 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
`
p := framework.TemplateProcessor{
TemplateData: struct{ Value string }{Value: "new-value"},
PatchTemplates: []framework.PatchTemplate{
&framework.ContainerPatchTemplate{
// Only patch containers named "foo"
ContainerMatcher: framework.ContainerNameMatcher("foo"),
Templates: parser.TemplateStrings(`
env:
- name: KEY
value: {{ .Value }}
`),
}},
}
err := framework.Execute(p, &kio.ByteReadWriter{Reader: bytes.NewBufferString(input)})
if err != nil {
log.Fatal(err)
}
// Output:
// apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: foo
// spec:
// template:
// spec:
// containers:
// - name: foo
// image: a
// env:
// - name: EXISTING
// value: variable
// - name: KEY
// value: 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:
// - name: KEY
// value: new-value
// - name: baz
// image: b
// ---
// apiVersion: v1
// kind: Service
// metadata:
// name: bar
// spec:
// selector:
// foo: bar
}
type v1alpha1JavaSpringBoot struct {
Metadata Metadata `yaml:"metadata" json:"metadata"`
Spec v1alpha1JavaSpringBootSpec `yaml:"spec" json:"spec"`
}
type Metadata struct {
Name string `yaml:"name" json:"name"`
}
type v1alpha1JavaSpringBootSpec struct {
Replicas int `yaml:"replicas" json:"replicas"`
Domain string `yaml:"domain" json:"domain"`
Image string `yaml:"image" json:"image"`
}
func (a v1alpha1JavaSpringBoot) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) {
filter := framework.TemplateProcessor{
ResourceTemplates: []framework.ResourceTemplate{{
TemplateData: &a,
Templates: parser.TemplateStrings(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Metadata.Name }}
selector:
app: {{ .Metadata.Name }}
spec:
replicas: {{ .Spec.Replicas }}
template:
spec:
containers:
- name: app
image: {{ .Spec.Image }}
{{ if .Spec.Domain }}
ports:
- containerPort: 80
name: http
{{ end }}
{{ if .Spec.Domain }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Metadata.Name }}-svc
spec:
selector:
app: {{ .Metadata.Name }}
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .Metadata.Name }}-ingress
spec:
tls:
- hosts:
- {{ .Spec.Domain }}
secretName: secret-tls
defaultBackend:
service:
name: {{ .Metadata.Name }}
port:
number: 80
{{ end }}
`),
}},
}
return filter.Filter(items)
}
func (a *v1alpha1JavaSpringBoot) Default() error {
if a.Spec.Replicas == 0 {
a.Spec.Replicas = 3
}
return nil
}
func (a *v1alpha1JavaSpringBoot) Validate() error {
var messages []string
if a.Metadata.Name == "" {
messages = append(messages, "name is required")
}
if a.Spec.Replicas > 10 {
messages = append(messages, "replicas must be less than 10")
}
if !strings.HasSuffix(a.Spec.Domain, "example.com") {
messages = append(messages, "domain must be a subdomain of example.com")
}
if strings.HasSuffix(a.Spec.Image, ":latest") {
messages = append(messages, "image should not have latest tag")
}
if len(messages) == 0 {
return nil
}
errMsg := fmt.Sprintf("JavaSpringBoot had %d errors:\n", len(messages))
for i, msg := range messages {
errMsg += fmt.Sprintf(" [%d] %s\n", i+1, msg)
}
return errors.Errorf(errMsg)
}
// ExampleVersionedAPIProcessor shows how to use the VersionedAPIProcessor and TemplateProcessor to
// build functions that implement complex multi-version APIs that require defaulting and validation.
func ExampleVersionedAPIProcessor() {
p := &framework.VersionedAPIProcessor{FilterProvider: framework.GVKFilterMap{
"JavaSpringBoot": {
"example.com/v1alpha1": &v1alpha1JavaSpringBoot{},
}}}
source := &kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
functionConfig:
apiVersion: example.com/v1alpha1
kind: JavaSpringBoot
metadata:
name: my-app
spec:
image: example.docker.com/team/app:1.0
domain: demo.example.com
`)}
if err := framework.Execute(p, source); err != nil {
log.Fatal(err)
}
// Output:
// apiVersion: config.kubernetes.io/v1
// kind: ResourceList
// items:
// - apiVersion: apps/v1
// kind: Deployment
// metadata:
// name: my-app
// selector:
// app: my-app
// spec:
// replicas: 3
// template:
// spec:
// containers:
// - name: app
// image: example.docker.com/team/app:1.0
// ports:
// - containerPort: 80
// name: http
// - apiVersion: v1
// kind: Service
// metadata:
// name: my-app-svc
// spec:
// selector:
// app: my-app
// ports:
// - protocol: TCP
// port: 80
// targetPort: 80
// - apiVersion: networking.k8s.io/v1
// kind: Ingress
// metadata:
// name: my-app-ingress
// spec:
// tls:
// - hosts:
// - demo.example.com
// secretName: secret-tls
// defaultBackend:
// service:
// name: my-app
// port:
// number: 80
// functionConfig:
// apiVersion: example.com/v1alpha1
// kind: JavaSpringBoot
// metadata:
// name: my-app
// spec:
// image: example.docker.com/team/app:1.0
// domain: demo.example.com
}