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:
Katrina Verey
2021-01-21 17:52:45 -08:00
parent 1d524b6fbe
commit 5c4b5b1bf0
61 changed files with 4059 additions and 2624 deletions

View File

@@ -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()))
}