
Allows to limit execution time of KRM function. Change-Id: I7fd8d97492512c6c5033375429906a1268f3818e Signed-off-by: Ruslan Aliev <raliev@mirantis.com> Closes: #544 Closes: #545 Closes: #597
1269 lines
28 KiB
Go
1269 lines
28 KiB
Go
/*
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
https://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package runfn
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"sigs.k8s.io/kustomize/kyaml/copyutil"
|
|
"sigs.k8s.io/kustomize/kyaml/errors"
|
|
"sigs.k8s.io/kustomize/kyaml/fn/runtime/container"
|
|
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
|
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
|
"sigs.k8s.io/kustomize/kyaml/kio/filters"
|
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
|
)
|
|
|
|
const (
|
|
ValueReplacerYAMLData = `apiVersion: v1
|
|
kind: ValueReplacer
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: gcr.io/example.com/image:version
|
|
config.kubernetes.io/local-config: "true"
|
|
stringMatch: Deployment
|
|
replace: StatefulSet
|
|
`
|
|
)
|
|
|
|
func currentUser() (*user.User, error) {
|
|
return &user.User{
|
|
Uid: "1",
|
|
Gid: "2",
|
|
}, nil
|
|
}
|
|
|
|
func TestRunFns_init(t *testing.T) {
|
|
instance := RunFns{noCmdSet: true}
|
|
instance.init()
|
|
if !assert.Equal(t, instance.Input, os.Stdin) {
|
|
t.FailNow()
|
|
}
|
|
if !assert.Equal(t, instance.Output, os.Stdout) {
|
|
t.FailNow()
|
|
}
|
|
|
|
api, err := yaml.Parse(`apiVersion: apps/v1
|
|
kind:
|
|
`)
|
|
spec := runtimeutil.FunctionSpec{
|
|
Container: runtimeutil.ContainerSpec{
|
|
Image: "example.com:version",
|
|
},
|
|
}
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
filter, err := instance.functionFilterProvider(spec, api, currentUser)
|
|
assert.NoError(t, err)
|
|
c := container.NewContainer(runtimeutil.ContainerSpec{Image: "example.com:version"}, "nobody")
|
|
cf := &c
|
|
cf.Exec.FunctionConfig = api
|
|
assert.Equal(t, cf, filter)
|
|
}
|
|
|
|
func TestRunFns_initAsCurrentUser(t *testing.T) {
|
|
instance := RunFns{
|
|
AsCurrentUser: true,
|
|
noCmdSet: true,
|
|
}
|
|
instance.init()
|
|
if !assert.Equal(t, instance.Input, os.Stdin) {
|
|
t.FailNow()
|
|
}
|
|
if !assert.Equal(t, instance.Output, os.Stdout) {
|
|
t.FailNow()
|
|
}
|
|
|
|
api, err := yaml.Parse(`apiVersion: apps/v1
|
|
kind:
|
|
`)
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
spec := runtimeutil.FunctionSpec{
|
|
Container: runtimeutil.ContainerSpec{
|
|
Image: "example.com:version",
|
|
},
|
|
}
|
|
|
|
filter, err := instance.functionFilterProvider(spec, api, currentUser)
|
|
assert.NoError(t, err)
|
|
c := container.NewContainer(runtimeutil.ContainerSpec{Image: "example.com:version"}, "1:2")
|
|
cf := &c
|
|
cf.Exec.FunctionConfig = api
|
|
assert.Equal(t, cf, filter)
|
|
}
|
|
|
|
func TestRunFns_Execute__initGlobalScope(t *testing.T) {
|
|
instance := RunFns{GlobalScope: true, noCmdSet: true}
|
|
instance.init()
|
|
if !assert.Equal(t, instance.Input, os.Stdin) {
|
|
t.FailNow()
|
|
}
|
|
if !assert.Equal(t, instance.Output, os.Stdout) {
|
|
t.FailNow()
|
|
}
|
|
api, err := yaml.Parse(`apiVersion: apps/v1
|
|
kind:
|
|
`)
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
|
|
spec := runtimeutil.FunctionSpec{
|
|
Container: runtimeutil.ContainerSpec{
|
|
Image: "example.com:version",
|
|
},
|
|
}
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
filter, err := instance.functionFilterProvider(spec, api, currentUser)
|
|
assert.NoError(t, err)
|
|
c := container.NewContainer(runtimeutil.ContainerSpec{Image: "example.com:version"}, "nobody")
|
|
cf := &c
|
|
cf.Exec.FunctionConfig = api
|
|
cf.Exec.GlobalScope = true
|
|
assert.Equal(t, cf, filter)
|
|
}
|
|
|
|
func TestRunFns_Execute__initDefault(t *testing.T) {
|
|
b := &bytes.Buffer{}
|
|
var tests = []struct {
|
|
instance RunFns
|
|
expected RunFns
|
|
name string
|
|
}{
|
|
{
|
|
instance: RunFns{},
|
|
name: "empty",
|
|
expected: RunFns{Output: os.Stdout, Input: os.Stdin, NoFunctionsFromInput: getFalse()},
|
|
},
|
|
{
|
|
name: "explicit output",
|
|
instance: RunFns{Output: b},
|
|
expected: RunFns{Output: b, Input: os.Stdin, NoFunctionsFromInput: getFalse()},
|
|
},
|
|
{
|
|
name: "explicit input",
|
|
instance: RunFns{Input: b},
|
|
expected: RunFns{Output: os.Stdout, Input: b, NoFunctionsFromInput: getFalse()},
|
|
},
|
|
{
|
|
name: "explicit functions -- no functions from input",
|
|
instance: RunFns{Functions: []*yaml.RNode{{}}},
|
|
expected: RunFns{Output: os.Stdout, Input: os.Stdin, NoFunctionsFromInput: getTrue(), Functions: []*yaml.RNode{{}}},
|
|
},
|
|
{
|
|
name: "explicit functions -- yes functions from input",
|
|
instance: RunFns{Functions: []*yaml.RNode{{}}, NoFunctionsFromInput: getFalse()},
|
|
expected: RunFns{Output: os.Stdout, Input: os.Stdin, NoFunctionsFromInput: getFalse(), Functions: []*yaml.RNode{{}}},
|
|
},
|
|
{
|
|
name: "explicit functions in paths -- no functions from input",
|
|
instance: RunFns{FunctionPaths: []string{"foo"}},
|
|
expected: RunFns{
|
|
Output: os.Stdout,
|
|
Input: os.Stdin,
|
|
NoFunctionsFromInput: getTrue(),
|
|
FunctionPaths: []string{"foo"},
|
|
},
|
|
},
|
|
{
|
|
name: "functions in paths -- yes functions from input",
|
|
instance: RunFns{FunctionPaths: []string{"foo"}, NoFunctionsFromInput: getFalse()},
|
|
expected: RunFns{
|
|
Output: os.Stdout,
|
|
Input: os.Stdin,
|
|
NoFunctionsFromInput: getFalse(),
|
|
FunctionPaths: []string{"foo"},
|
|
},
|
|
},
|
|
{
|
|
name: "explicit directories in mounts",
|
|
instance: RunFns{StorageMounts: []runtimeutil.StorageMount{{MountType: "volume", Src: "myvol", DstPath: "/local/"}}},
|
|
expected: RunFns{
|
|
Output: os.Stdout,
|
|
Input: os.Stdin,
|
|
NoFunctionsFromInput: getFalse(),
|
|
StorageMounts: []runtimeutil.StorageMount{{MountType: "volume", Src: "myvol", DstPath: "/local/"}},
|
|
},
|
|
},
|
|
}
|
|
for i := range tests {
|
|
tt := tests[i]
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
(&tt.instance).init()
|
|
(&tt.instance).functionFilterProvider = nil
|
|
if !assert.Equal(t, tt.expected, tt.instance) {
|
|
t.FailNow()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func getTrue() *bool {
|
|
t := true
|
|
return &t
|
|
}
|
|
|
|
func getFalse() *bool {
|
|
f := false
|
|
return &f
|
|
}
|
|
|
|
type f struct {
|
|
// path to function file and string value to write
|
|
path, value string
|
|
// if true, create the function in a separate directory from
|
|
// the config, and provide it through FunctionPaths
|
|
outOfPackage bool
|
|
|
|
// if true, create the function as an explicit Functions input
|
|
explicitFunction bool
|
|
|
|
// if true and outOfPackage is true, create a new directory
|
|
// for this function separate from the previous one. If
|
|
// false and outOfPackage is true, create the function in
|
|
// the directory created for the last outOfPackage function.
|
|
newFnPath bool
|
|
}
|
|
|
|
// TestRunFns_getFilters tests how filters are found and sorted
|
|
func TestRunFns_getFilters(t *testing.T) {
|
|
var tests = []struct {
|
|
// function files to write
|
|
in []f
|
|
// images to be run in a specific order
|
|
out []string
|
|
|
|
// images to be run in a specific order -- computed from directory path
|
|
outFn func(string) []string
|
|
|
|
// expected Error
|
|
error string
|
|
|
|
// name of the test
|
|
name string
|
|
// value to set for NoFunctionsFromInput
|
|
noFunctionsFromInput *bool
|
|
}{
|
|
// Test
|
|
//
|
|
//
|
|
{name: "single implicit function",
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "bar.yaml"),
|
|
value: `
|
|
apiVersion: example.com/v1alpha1
|
|
kind: ExampleFunction
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: gcr.io/example.com/image:v1.0.0
|
|
config.kubernetes.io/local-config: "true"
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"gcr.io/example.com/image:v1.0.0"},
|
|
},
|
|
|
|
{name: "no function spec",
|
|
in: []f{
|
|
{
|
|
explicitFunction: true,
|
|
value: `
|
|
foo: bar
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "defer_failure",
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "bar.yaml"),
|
|
value: `
|
|
apiVersion: example.com/v1alpha1
|
|
kind: ExampleFunction
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
deferFailure: true
|
|
container:
|
|
image: gcr.io/example.com/image:v1.0.0
|
|
config.kubernetes.io/local-config: "true"
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"gcr.io/example.com/image:v1.0.0 deferFailure: true"},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "sort functions -- deepest first",
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("a.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("foo", "b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"b", "a"},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "sort functions -- skip implicit with output of package",
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "a.yaml"),
|
|
outOfPackage: true, // out of package is run last
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"a"},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "sort functions -- skip implicit",
|
|
noFunctionsFromInput: getTrue(),
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "a.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: nil,
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "sort functions -- include implicit",
|
|
noFunctionsFromInput: getFalse(),
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "a.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"a", "b"},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "sort functions -- implicit first",
|
|
noFunctionsFromInput: getFalse(),
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "a.yaml"),
|
|
outOfPackage: true, // out of package is run last
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"b", "a"},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "explicit functions",
|
|
in: []f{
|
|
{
|
|
explicitFunction: true,
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: c
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"c"},
|
|
},
|
|
|
|
// Test
|
|
//
|
|
//
|
|
{name: "sort functions -- implicit first",
|
|
noFunctionsFromInput: getFalse(),
|
|
in: []f{
|
|
{
|
|
explicitFunction: true,
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: c
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("foo", "a.yaml"),
|
|
outOfPackage: true, // out of package is run last
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`,
|
|
},
|
|
{
|
|
path: filepath.Join("b.yaml"),
|
|
value: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`,
|
|
},
|
|
},
|
|
out: []string{"b", "a", "c"},
|
|
},
|
|
|
|
{name: "starlark-function-disabled",
|
|
in: []f{
|
|
{
|
|
path: filepath.Join("foo", "bar.yaml"),
|
|
value: `
|
|
apiVersion: example.com/v1alpha1
|
|
kind: ExampleFunction
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
starlark:
|
|
path: a/b/c
|
|
`,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for i := range tests {
|
|
tt := tests[i]
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// setup the test directory
|
|
d := setupTest(t)
|
|
defer os.RemoveAll(d)
|
|
|
|
// write the functions to files
|
|
var fnPaths []string
|
|
var parsedFns []*yaml.RNode
|
|
var fnPath string
|
|
var err error
|
|
for _, f := range tt.in {
|
|
getFileLocation(t, f, &d, &fnPath, &fnPaths, &parsedFns)
|
|
}
|
|
defer os.RemoveAll(fnPath)
|
|
|
|
// init the instance
|
|
r := &RunFns{
|
|
FunctionPaths: fnPaths,
|
|
Functions: parsedFns,
|
|
Path: d,
|
|
NoFunctionsFromInput: tt.noFunctionsFromInput,
|
|
}
|
|
r.init()
|
|
|
|
// get the filters which would be run
|
|
var results []string
|
|
_, fltrs, _, err := r.getNodesAndFilters()
|
|
|
|
if tt.error != "" {
|
|
if !assert.EqualError(t, err, tt.error) {
|
|
t.FailNow()
|
|
}
|
|
return
|
|
}
|
|
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
for _, f := range fltrs {
|
|
results = append(results, strings.TrimSpace(fmt.Sprintf("%v", f)))
|
|
}
|
|
|
|
// compare the actual ordering to the expected ordering
|
|
if tt.outFn != nil {
|
|
if !assert.Equal(t, tt.outFn(d), results) {
|
|
t.FailNow()
|
|
}
|
|
} else {
|
|
if !assert.Equal(t, tt.out, results) {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func getFileLocation(t *testing.T, f f, d *string, fnPath *string, fnPaths *[]string, parsedFns *[]*yaml.RNode) {
|
|
// get the location for the file
|
|
var dir string
|
|
switch {
|
|
case f.outOfPackage:
|
|
// if out of package, write to a separate temp directory
|
|
if f.newFnPath || *fnPath == "" {
|
|
// create a new fn directory
|
|
var err error
|
|
*fnPath, err = ioutil.TempDir("", "kustomize-test")
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
|
|
*fnPaths = append(*fnPaths, *fnPath)
|
|
}
|
|
dir = *fnPath
|
|
case f.explicitFunction:
|
|
*parsedFns = append(*parsedFns, yaml.MustParse(f.value))
|
|
default:
|
|
// if in package, write to the dir containing the configs
|
|
dir = *d
|
|
}
|
|
|
|
if !f.explicitFunction {
|
|
// create the parent dir and write the file
|
|
err := os.MkdirAll(filepath.Join(dir, filepath.Dir(f.path)), 0700)
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
err = ioutil.WriteFile(filepath.Join(dir, f.path), []byte(f.value), 0600)
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunFns_sortFns(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
nodes []*yaml.RNode
|
|
expectedImages []string
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "multiple functions in the same file are ordered by index",
|
|
nodes: []*yaml.RNode{
|
|
yaml.MustParse(`
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/path: functions.yaml
|
|
config.kubernetes.io/index: 1
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`),
|
|
yaml.MustParse(`
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/path: functions.yaml
|
|
config.kubernetes.io/index: 0
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`),
|
|
},
|
|
expectedImages: []string{"b", "a"},
|
|
},
|
|
{
|
|
name: "non-integer value in index annotation is an error",
|
|
nodes: []*yaml.RNode{
|
|
yaml.MustParse(`
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/path: functions.yaml
|
|
config.kubernetes.io/index: 0
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
`),
|
|
yaml.MustParse(`
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/path: functions.yaml
|
|
config.kubernetes.io/index: abc
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: b
|
|
`),
|
|
},
|
|
expectedErrMsg: "strconv.Atoi: parsing \"abc\": invalid syntax",
|
|
},
|
|
}
|
|
|
|
for i := range testCases {
|
|
test := testCases[i]
|
|
t.Run(test.name, func(t *testing.T) {
|
|
packageBuff := &kio.PackageBuffer{
|
|
Nodes: test.nodes,
|
|
}
|
|
|
|
err := sortFns(packageBuff)
|
|
if test.expectedErrMsg != "" {
|
|
if !assert.Error(t, err) {
|
|
t.FailNow()
|
|
}
|
|
assert.Equal(t, test.expectedErrMsg, err.Error())
|
|
return
|
|
}
|
|
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
|
|
var images []string
|
|
for _, n := range packageBuff.Nodes {
|
|
spec := runtimeutil.GetFunctionSpec(n)
|
|
images = append(images, spec.Container.Image)
|
|
}
|
|
|
|
assert.Equal(t, test.expectedImages, images)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunFns_network(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
network bool
|
|
expectNetwork bool
|
|
error string
|
|
}{
|
|
{
|
|
name: "imperative false, declarative false",
|
|
input: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
network: false
|
|
`,
|
|
network: false,
|
|
expectNetwork: false,
|
|
},
|
|
{
|
|
name: "imperative true, declarative false",
|
|
input: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
network: false
|
|
`,
|
|
network: true,
|
|
expectNetwork: false,
|
|
},
|
|
{
|
|
name: "imperative true, declarative true",
|
|
input: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
network: true
|
|
`,
|
|
network: true,
|
|
expectNetwork: true,
|
|
},
|
|
{
|
|
name: "imperative false, declarative true",
|
|
input: `
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: a
|
|
network: true
|
|
`,
|
|
network: false,
|
|
error: "network required but not enabled with --network",
|
|
},
|
|
}
|
|
|
|
for i := range tests {
|
|
tt := tests[i]
|
|
fn := yaml.MustParse(tt.input)
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// init the instance
|
|
r := &RunFns{
|
|
Functions: []*yaml.RNode{fn},
|
|
Network: tt.network,
|
|
}
|
|
r.init()
|
|
|
|
_, fltrs, _, err := r.getNodesAndFilters()
|
|
if tt.error != "" {
|
|
if !assert.EqualError(t, err, tt.error) {
|
|
t.FailNow()
|
|
}
|
|
return
|
|
}
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
|
|
fltr, ok := fltrs[0].(*container.Filter)
|
|
assert.True(t, ok)
|
|
if !assert.Equal(t, tt.expectNetwork, fltr.Network) {
|
|
t.FailNow()
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCmd_Execute(t *testing.T) {
|
|
dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// write a test filter to the directory of configuration
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
|
|
return
|
|
}
|
|
|
|
instance := RunFns{Path: dir, functionFilterProvider: getFilterProvider(t)}
|
|
if !assert.NoError(t, instance.Execute()) {
|
|
t.FailNow()
|
|
}
|
|
b, err := ioutil.ReadFile(
|
|
filepath.Join(dir, "java", "java-deployment.resource.yaml"))
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
assert.Contains(t, string(b), "kind: StatefulSet")
|
|
}
|
|
|
|
type TestFilter struct {
|
|
invoked bool
|
|
Exit error
|
|
}
|
|
|
|
func (f *TestFilter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
f.invoked = true
|
|
return input, nil
|
|
}
|
|
|
|
func (f *TestFilter) GetExit() error {
|
|
return f.Exit
|
|
}
|
|
|
|
func TestCmd_Execute_deferFailure(t *testing.T) {
|
|
dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// write a test filter to the directory of configuration
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter1.yaml"), []byte(`apiVersion: v1
|
|
kind: ValueReplacer
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: 1
|
|
config.kubernetes.io/local-config: "true"
|
|
stringMatch: Deployment
|
|
replace: StatefulSet
|
|
`), 0600)) {
|
|
t.FailNow()
|
|
}
|
|
|
|
// write a test filter to the directory of configuration
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter2.yaml"), []byte(`apiVersion: v1
|
|
kind: ValueReplacer
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: 2
|
|
config.kubernetes.io/local-config: "true"
|
|
stringMatch: Deployment
|
|
replace: StatefulSet
|
|
`), 0600)) {
|
|
t.FailNow()
|
|
}
|
|
|
|
var fltrs []*TestFilter
|
|
instance := RunFns{
|
|
Path: dir,
|
|
functionFilterProvider: func(
|
|
f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
|
|
tf := &TestFilter{
|
|
Exit: errors.Errorf("message: %s", f.Container.Image),
|
|
}
|
|
fltrs = append(fltrs, tf)
|
|
return tf, nil
|
|
},
|
|
}
|
|
instance.init()
|
|
|
|
err := instance.Execute()
|
|
|
|
// make sure all filters were run
|
|
if !assert.Equal(t, 2, len(fltrs)) {
|
|
t.FailNow()
|
|
}
|
|
for i := range fltrs {
|
|
if !assert.True(t, fltrs[i].invoked) {
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
if !assert.EqualError(t, err, "message: 1\n---\nmessage: 2") {
|
|
t.FailNow()
|
|
}
|
|
b, err := ioutil.ReadFile(
|
|
filepath.Join(dir, "java", "java-deployment.resource.yaml"))
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
// files weren't changed because there was an error
|
|
assert.Contains(t, string(b), "kind: Deployment")
|
|
}
|
|
|
|
// TestCmd_Execute_setOutput tests the execution of a filter reading and writing to a dir
|
|
func TestCmd_Execute_setFunctionPaths(t *testing.T) {
|
|
dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// write a test filter to a separate directory
|
|
tmpF, err := ioutil.TempFile("", "filter*.yaml")
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
os.RemoveAll(tmpF.Name())
|
|
if !assert.NoError(t, ioutil.WriteFile(tmpF.Name(), []byte(ValueReplacerYAMLData), 0600)) {
|
|
return
|
|
}
|
|
|
|
// run the functions, providing the path to the directory of filters
|
|
instance := RunFns{
|
|
FunctionPaths: []string{tmpF.Name()},
|
|
Path: dir,
|
|
functionFilterProvider: getFilterProvider(t),
|
|
}
|
|
// initialize the defaults
|
|
instance.init()
|
|
|
|
err = instance.Execute()
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
b, err := ioutil.ReadFile(
|
|
filepath.Join(dir, "java", "java-deployment.resource.yaml"))
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
assert.Contains(t, string(b), "kind: StatefulSet")
|
|
}
|
|
|
|
// TestCmd_Execute_setOutput tests the execution of a filter using an io.Writer as output
|
|
func TestCmd_Execute_setOutput(t *testing.T) {
|
|
dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// write a test filter
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
|
|
return
|
|
}
|
|
|
|
out := &bytes.Buffer{}
|
|
instance := RunFns{
|
|
Output: out, // write to out
|
|
Path: dir,
|
|
functionFilterProvider: getFilterProvider(t),
|
|
}
|
|
// initialize the defaults
|
|
instance.init()
|
|
|
|
if !assert.NoError(t, instance.Execute()) {
|
|
return
|
|
}
|
|
b, err := ioutil.ReadFile(
|
|
filepath.Join(dir, "java", "java-deployment.resource.yaml"))
|
|
if !assert.NoError(t, err) {
|
|
return
|
|
}
|
|
assert.NotContains(t, string(b), "kind: StatefulSet")
|
|
assert.Contains(t, out.String(), "kind: StatefulSet")
|
|
}
|
|
|
|
// TestCmd_Execute_setInput tests the execution of a filter using an io.Reader as input
|
|
func TestCmd_Execute_setInput(t *testing.T) {
|
|
dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
|
|
return
|
|
}
|
|
|
|
read, err := kio.LocalPackageReader{PackagePath: dir}.Read()
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
input := &bytes.Buffer{}
|
|
if !assert.NoError(t, kio.ByteWriter{Writer: input}.Write(read)) {
|
|
t.FailNow()
|
|
}
|
|
|
|
outDir, err := ioutil.TempDir("", "kustomize-test")
|
|
defer os.RemoveAll(outDir)
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
|
|
return
|
|
}
|
|
|
|
instance := RunFns{
|
|
Input: input, // read from input
|
|
Path: outDir,
|
|
functionFilterProvider: getFilterProvider(t),
|
|
}
|
|
// initialize the defaults
|
|
instance.init()
|
|
|
|
if !assert.NoError(t, instance.Execute()) {
|
|
return
|
|
}
|
|
b, err := ioutil.ReadFile(
|
|
filepath.Join(outDir, "java", "java-deployment.resource.yaml"))
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
assert.Contains(t, string(b), "kind: StatefulSet")
|
|
}
|
|
|
|
// TestCmd_Execute_enableLogSteps tests the execution of a filter with LogSteps enabled.
|
|
func TestCmd_Execute_enableLogSteps(t *testing.T) {
|
|
dir := setupTest(t)
|
|
defer os.RemoveAll(dir)
|
|
|
|
// write a test filter to the directory of configuration
|
|
if !assert.NoError(t, ioutil.WriteFile(
|
|
filepath.Join(dir, "filter.yaml"), []byte(ValueReplacerYAMLData), 0600)) {
|
|
return
|
|
}
|
|
|
|
logs := &bytes.Buffer{}
|
|
instance := RunFns{
|
|
Path: dir,
|
|
functionFilterProvider: getFilterProvider(t),
|
|
LogSteps: true,
|
|
LogWriter: logs,
|
|
}
|
|
if !assert.NoError(t, instance.Execute()) {
|
|
t.FailNow()
|
|
}
|
|
b, err := ioutil.ReadFile(
|
|
filepath.Join(dir, "java", "java-deployment.resource.yaml"))
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
assert.Contains(t, string(b), "kind: StatefulSet")
|
|
assert.Equal(t, "Running unknown-type function\n", logs.String())
|
|
}
|
|
|
|
func getGeneratorFilterProvider(t *testing.T) func(
|
|
runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) {
|
|
return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
|
|
return kio.FilterFunc(func(items []*yaml.RNode) ([]*yaml.RNode, error) {
|
|
if f.Container.Image == "generate" {
|
|
node, err := yaml.Parse("kind: generated")
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
return append(items, node), nil
|
|
}
|
|
return items, nil
|
|
}), nil
|
|
}
|
|
}
|
|
func TestRunFns_ContinueOnEmptyResult(t *testing.T) {
|
|
fn1, err := yaml.Parse(`
|
|
kind: fakefn
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: pass
|
|
`)
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
fn2, err := yaml.Parse(`
|
|
kind: fakefn
|
|
metadata:
|
|
annotations:
|
|
config.kubernetes.io/function: |
|
|
container:
|
|
image: generate
|
|
`)
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
|
|
var test = []struct {
|
|
ContinueOnEmptyResult bool
|
|
ExpectedOutput string
|
|
}{
|
|
{
|
|
ContinueOnEmptyResult: false,
|
|
ExpectedOutput: "",
|
|
},
|
|
{
|
|
ContinueOnEmptyResult: true,
|
|
ExpectedOutput: "kind: generated\n",
|
|
},
|
|
}
|
|
for i := range test {
|
|
ouputBuffer := bytes.Buffer{}
|
|
instance := RunFns{
|
|
Input: bytes.NewReader([]byte{}),
|
|
Output: &ouputBuffer,
|
|
Functions: []*yaml.RNode{fn1, fn2},
|
|
functionFilterProvider: getGeneratorFilterProvider(t),
|
|
ContinueOnEmptyResult: test[i].ContinueOnEmptyResult,
|
|
}
|
|
if !assert.NoError(t, instance.Execute()) {
|
|
t.FailNow()
|
|
}
|
|
assert.Equal(t, test[i].ExpectedOutput, ouputBuffer.String())
|
|
}
|
|
}
|
|
|
|
// setupTest initializes a temp test directory containing test data
|
|
func setupTest(t *testing.T) string {
|
|
dir, err := ioutil.TempDir("", "kustomize-kyaml-test")
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
if !assert.True(t, ok) {
|
|
t.FailNow()
|
|
}
|
|
ds, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "test", "testdata"))
|
|
if !assert.NoError(t, err) {
|
|
t.FailNow()
|
|
}
|
|
if !assert.NoError(t, copyutil.CopyDir(ds, dir)) {
|
|
t.FailNow()
|
|
}
|
|
if !assert.NoError(t, os.Chdir(filepath.Dir(dir))) {
|
|
t.FailNow()
|
|
}
|
|
return dir
|
|
}
|
|
|
|
// getFilterProvider fakes the creation of a filter, replacing the ContainerFiler with
|
|
// a filter to s/kind: Deployment/kind: StatefulSet/g.
|
|
// this can be used to simulate running a filter.
|
|
func getFilterProvider(t *testing.T) func(runtimeutil.FunctionSpec, *yaml.RNode, currentUserFunc) (kio.Filter, error) {
|
|
return func(f runtimeutil.FunctionSpec, node *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) {
|
|
// parse the filter from the input
|
|
filter := yaml.YFilter{}
|
|
b := &bytes.Buffer{}
|
|
e := yaml.NewEncoder(b)
|
|
if !assert.NoError(t, e.Encode(node.YNode())) {
|
|
t.FailNow()
|
|
}
|
|
e.Close()
|
|
d := yaml.NewDecoder(b)
|
|
if !assert.NoError(t, d.Decode(&filter)) {
|
|
t.FailNow()
|
|
}
|
|
|
|
return filters.Modifier{
|
|
Filters: []yaml.YFilter{{Filter: yaml.Lookup("kind")}, filter},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func TestRunFns_mergeContainerEnv(t *testing.T) {
|
|
testcases := []struct {
|
|
name string
|
|
instance RunFns
|
|
inputEnvs []string
|
|
expect runtimeutil.ContainerEnv
|
|
}{
|
|
{
|
|
name: "all empty",
|
|
instance: RunFns{},
|
|
expect: *runtimeutil.NewContainerEnv(),
|
|
},
|
|
{
|
|
name: "empty command line envs",
|
|
instance: RunFns{},
|
|
inputEnvs: []string{"foo=bar"},
|
|
expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar"}),
|
|
},
|
|
{
|
|
name: "empty declarative envs",
|
|
instance: RunFns{
|
|
Env: []string{"foo=bar"},
|
|
},
|
|
expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar"}),
|
|
},
|
|
{
|
|
name: "same key",
|
|
instance: RunFns{
|
|
Env: []string{"foo=bar", "foo"},
|
|
},
|
|
inputEnvs: []string{"foo=bar1", "bar"},
|
|
expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar", "bar", "foo"}),
|
|
},
|
|
{
|
|
name: "same exported key",
|
|
instance: RunFns{
|
|
Env: []string{"foo=bar", "foo"},
|
|
},
|
|
inputEnvs: []string{"foo1=bar1", "foo"},
|
|
expect: *runtimeutil.NewContainerEnvFromStringSlice([]string{"foo=bar", "foo1=bar1", "foo"}),
|
|
},
|
|
}
|
|
|
|
for i := range testcases {
|
|
tc := testcases[i]
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
envs := tc.instance.mergeContainerEnv(tc.inputEnvs)
|
|
assert.Equal(t, tc.expect.GetDockerFlags(), runtimeutil.NewContainerEnvFromStringSlice(envs).GetDockerFlags())
|
|
})
|
|
}
|
|
}
|