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

661 lines
16 KiB
Go

// Copyright 2021 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package framework_test
import (
"bytes"
"regexp"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/framework/frameworktestutil"
"sigs.k8s.io/kustomize/kyaml/fn/framework/parser"
"sigs.k8s.io/kustomize/kyaml/openapi"
"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: parser.TemplateFiles("testdata/template-processor/templates/basic"),
}},
}
out := new(bytes.Buffer)
rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
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/v1
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: parser.TemplateFiles("testdata/template-processor/patches/basic"),
Selector: &framework.Selector{Names: []string{"foo"}},
},
// Patch from string with selector templating
&framework.ResourcePatchTemplate{
Selector: &framework.Selector{Names: []string{"{{.Spec.A}}"}, TemplateData: &config},
Templates: parser.TemplateStrings(`
metadata:
annotations:
baz: buz
`)},
},
}
out := new(bytes.Buffer)
rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
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/v1
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: parser.TemplateFiles("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: parser.TemplateStrings(`
env:
- name: Foo
value: Bar
`)},
},
}
out := new(bytes.Buffer)
rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
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/v1
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 TestTemplateProcessor_ContainerPatchTemplates_MultipleWorkloadKinds(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,
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateFiles("testdata/template-processor/templates/container-sources"),
}},
PatchTemplates: []framework.PatchTemplate{
&framework.ContainerPatchTemplate{
Templates: parser.TemplateFiles("testdata/template-processor/container-patches"),
},
},
}
out := new(bytes.Buffer)
rw := &kio.ByteReadWriter{Writer: out, Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items: []
functionConfig:
spec:
key: Hello
value: World
a: bar
`)}
require.NoError(t, framework.Execute(p, rw))
resources, err := (&kio.ByteReader{Reader: out}).Read()
require.NoError(t, err)
envRegex := regexp.MustCompile(strings.TrimSpace(`
\s+ env:
\s+ - name: EXISTING
\s+ value: variable
\s+ - name: Hello
\s+ value: World
`))
require.Equal(t, 9, len(resources))
for i, r := range resources {
t.Run(r.GetKind(), func(t *testing.T) {
assert.Regexp(t, envRegex, resources[i].MustString())
})
}
}
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: parser.TemplateStrings(`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: parser.TemplateStrings("foo: {{ .OOPS }}"),
}},
},
wantErr: "can't evaluate field OOPS",
},
{
name: "ContainerPatchTemplate is not a resource",
processor: framework.TemplateProcessor{
PatchTemplates: []framework.PatchTemplate{
&framework.ContainerPatchTemplate{
Templates: parser.TemplateStrings(`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: parser.TemplateStrings("foo: {{ .OOPS }}"),
}},
},
wantErr: "can't evaluate field OOPS",
},
{
name: "ResourceTemplate is not a resource",
processor: framework.TemplateProcessor{
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateStrings(`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: parser.TemplateStrings("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)
})
}
}
func TestTemplateProcessor_AdditionalSchemas(t *testing.T) {
p := framework.TemplateProcessor{
AdditionalSchemas: parser.SchemaFiles("testdata/template-processor/schemas"),
ResourceTemplates: []framework.ResourceTemplate{{
Templates: parser.TemplateFiles("testdata/template-processor/templates/custom-resource/foo.template.yaml"),
}},
PatchTemplates: []framework.PatchTemplate{
&framework.ResourcePatchTemplate{
Templates: parser.TemplateFiles("testdata/template-processor/patches/custom-resource/patch.template.yaml")},
},
}
out := new(bytes.Buffer)
rw := &kio.ByteReadWriter{Reader: bytes.NewBufferString(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- apiVersion: example.com/v1
kind: Foo
metadata:
name: source
spec:
targets:
- app: C
size: medium
`),
Writer: out}
require.NoError(t, framework.Execute(p, rw))
require.Equal(t, strings.TrimSpace(`
apiVersion: config.kubernetes.io/v1
kind: ResourceList
items:
- apiVersion: example.com/v1
kind: Foo
metadata:
name: source
spec:
targets:
- app: C
size: large
type: Ruby
- app: B
size: small
- apiVersion: example.com/v1
kind: Foo
metadata:
name: example
spec:
targets:
- app: A
type: Go
size: small
- app: B
type: Go
size: small
- app: C
type: Ruby
size: large
`), strings.TrimSpace(out.String()))
found := openapi.SchemaForResourceType(yaml.TypeMeta{
APIVersion: "example.com/v1",
Kind: "Foo",
})
require.Nil(t, found, "openAPI schema was not reset")
}
func TestTemplateProcessor_Validator(t *testing.T) {
// This test proves the Validate method is called when implemented
// and demonstrates the use of ProcessorResultsChecker's error matching
p := func() framework.ResourceListProcessor {
return &framework.VersionedAPIProcessor{FilterProvider: framework.GVKFilterMap{
"JavaSpringBoot": {
"example.com/v1alpha1": &v1alpha1JavaSpringBoot{},
}}}
}
c := frameworktestutil.ProcessorResultsChecker{
TestDataDirectory: "testdata/validation",
Processor: p,
}
c.Assert(t)
}