diff --git a/manifests/function/airshipctl-schemas/airshipit.org_genericcontainers.yaml b/manifests/function/airshipctl-schemas/airshipit.org_genericcontainers.yaml index 367676189..6c186b19f 100644 --- a/manifests/function/airshipctl-schemas/airshipit.org_genericcontainers.yaml +++ b/manifests/function/airshipctl-schemas/airshipit.org_genericcontainers.yaml @@ -149,6 +149,12 @@ spec: parameter is specified. Else it will write output to STDOUT. This path relative to current site root. type: string + timeout: + description: Timeout is the maximum amount of time (in seconds) for + container execution if not specified (0) no timeout will be set + and container could run indefinitely + format: int64 + type: integer type: description: Supported types are "airship" and "krm" type: string diff --git a/pkg/api/v1alpha1/genericcontainer_types.go b/pkg/api/v1alpha1/genericcontainer_types.go index 8d499ab79..744746f3f 100644 --- a/pkg/api/v1alpha1/genericcontainer_types.go +++ b/pkg/api/v1alpha1/genericcontainer_types.go @@ -95,6 +95,10 @@ type GenericContainerSpec struct { // Mounts are the storage or directories to mount into the container StorageMounts []StorageMount `json:"mounts,omitempty" yaml:"mounts,omitempty"` + + // Timeout is the maximum amount of time (in seconds) for container execution + // if not specified (0) no timeout will be set and container could run indefinitely + Timeout uint64 `json:"timeout,omitempty"` } // AirshipContainerSpec airship container settings diff --git a/pkg/container/api.go b/pkg/container/api.go index 78dc8fe84..670128d1a 100644 --- a/pkg/container/api.go +++ b/pkg/container/api.go @@ -28,11 +28,11 @@ import ( "github.com/ahmetb/dlog" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/runfn" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" "sigs.k8s.io/yaml" "opendev.org/airship/airshipctl/pkg/api/v1alpha1" + "opendev.org/airship/airshipctl/pkg/container/runfn" "opendev.org/airship/airshipctl/pkg/log" "opendev.org/airship/airshipctl/pkg/util" ) @@ -228,6 +228,7 @@ func (c *V1Alpha1) runKRM() error { Output: c.output, StorageMounts: mounts, ContinueOnEmptyResult: true, + Timeout: c.conf.Spec.Timeout, } function, err := kyaml.Parse(c.conf.Config) if err != nil { diff --git a/pkg/container/runfn/runfn.go b/pkg/container/runfn/runfn.go new file mode 100644 index 000000000..a304f952b --- /dev/null +++ b/pkg/container/runfn/runfn.go @@ -0,0 +1,532 @@ +/* + 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 ( + "fmt" + "io" + "os" + "os/user" + "path" + "path/filepath" + "sort" + "strconv" + "strings" + "sync/atomic" + + "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/kioutil" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +// RunFns runs the set of configuration functions in a local directory against +// the Resources in that directory +type RunFns struct { + StorageMounts []runtimeutil.StorageMount + + // Path is the path to the directory containing functions + Path string + + // FunctionPaths Paths allows functions to be specified outside the configuration + // directory. + // Functions provided on FunctionPaths are globally scoped. + // If FunctionPaths length is > 0, then NoFunctionsFromInput defaults to true + FunctionPaths []string + + // Functions is an explicit list of functions to run against the input. + // Functions provided on Functions are globally scoped. + // If Functions length is > 0, then NoFunctionsFromInput defaults to true + Functions []*yaml.RNode + + // GlobalScope if true, functions read from input will be scoped globally rather + // than only to Resources under their subdirs. + GlobalScope bool + + // Input can be set to read the Resources from Input rather than from a directory + Input io.Reader + + // Network enables network access for functions that declare it + Network bool + + // Output can be set to write the result to Output rather than back to the directory + Output io.Writer + + // NoFunctionsFromInput if set to true will not read any functions from the input, + // and only use explicit sources + NoFunctionsFromInput *bool + + // ResultsDir is where to write each functions results + ResultsDir string + + // LogSteps enables logging the function that is running. + LogSteps bool + + // LogWriter can be set to write the logs to LogWriter rather than stderr if LogSteps is enabled. + LogWriter io.Writer + + // resultsCount is used to generate the results filename for each container + resultsCount uint32 + + // functionFilterProvider provides a filter to perform the function. + // this is a variable so it can be mocked in tests + functionFilterProvider func( + filter runtimeutil.FunctionSpec, api *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) + + // noCmdSet is used in unit test to bypass setting cmd and args for docker, false by default + noCmdSet bool + + // AsCurrentUser is a boolean to indicate whether docker container should use + // the uid and gid that run the command + AsCurrentUser bool + + // Env contains environment variables that will be exported to container + Env []string + + // ContinueOnEmptyResult configures what happens when the underlying pipeline + // returns an empty result. + // If it is false (default), subsequent functions will be skipped and the + // result will be returned immediately. + // If it is true, the empty result will be provided as input to the next + // function in the list. + ContinueOnEmptyResult bool + + // Timeout is the maximum amount of time (in seconds) for KRM function execution + Timeout uint64 +} + +// Execute runs the command +func (r RunFns) Execute() error { + // make the path absolute so it works on mac + var err error + r.Path, err = filepath.Abs(r.Path) + if err != nil { + return errors.Wrap(err) + } + + // default the containerFilterProvider if it hasn't been override. Split out for testing. + (&r).init() + nodes, fltrs, output, err := r.getNodesAndFilters() + if err != nil { + return err + } + return r.runFunctions(nodes, output, fltrs) +} + +func (r RunFns) getNodesAndFilters() ( + *kio.PackageBuffer, []kio.Filter, *kio.LocalPackageReadWriter, error) { + // Read Resources from Directory or Input + buff := &kio.PackageBuffer{} + p := kio.Pipeline{Outputs: []kio.Writer{buff}} + // save the output dir because we will need it to write back + // the same one for reading must be used for writing if deleting Resources + var outputPkg *kio.LocalPackageReadWriter + if r.Path != "" { + outputPkg = &kio.LocalPackageReadWriter{PackagePath: r.Path, MatchFilesGlob: kio.MatchAll} + } + + if r.Input == nil { + p.Inputs = []kio.Reader{outputPkg} + } else { + p.Inputs = []kio.Reader{&kio.ByteReader{Reader: r.Input}} + } + if err := p.Execute(); err != nil { + return nil, nil, outputPkg, err + } + + fltrs, err := r.getFilters(buff.Nodes) + if err != nil { + return nil, nil, outputPkg, err + } + return buff, fltrs, outputPkg, nil +} + +func (r RunFns) getFilters(nodes []*yaml.RNode) ([]kio.Filter, error) { + var fltrs []kio.Filter + + // fns from annotations on the input resources + f, err := r.getFunctionsFromInput(nodes) + if err != nil { + return nil, err + } + fltrs = append(fltrs, f...) + + // fns from directories specified on the struct + f, err = r.getFunctionsFromFunctionPaths() + if err != nil { + return nil, err + } + fltrs = append(fltrs, f...) + + // explicit fns specified on the struct + f, err = r.getFunctionsFromFunctions() + if err != nil { + return nil, err + } + fltrs = append(fltrs, f...) + + return fltrs, nil +} + +// runFunctions runs the fltrs against the input and writes to either r.Output or output +func (r RunFns) runFunctions( + input kio.Reader, output kio.Writer, fltrs []kio.Filter) error { + // use the previously read Resources as input + var outputs []kio.Writer + if r.Output == nil { + // write back to the package + outputs = append(outputs, output) + } else { + // write to the output instead of the directory if r.Output is specified or + // the output is nil (reading from Input) + outputs = append(outputs, kio.ByteWriter{Writer: r.Output}) + } + + var err error + pipeline := kio.Pipeline{ + Inputs: []kio.Reader{input}, + Filters: fltrs, + Outputs: outputs, + ContinueOnEmptyResult: r.ContinueOnEmptyResult, + } + if r.LogSteps { + err = pipeline.ExecuteWithCallback(func(op kio.Filter) { + var identifier string + + switch filter := op.(type) { + case *container.Filter: + identifier = filter.Image + default: + identifier = "unknown-type function" + } + + _, _ = fmt.Fprintf(r.LogWriter, "Running %s\n", identifier) + }) + } else { + err = pipeline.Execute() + } + if err != nil { + return err + } + + // check for deferred function errors + var errs []string + for i := range fltrs { + cf, ok := fltrs[i].(runtimeutil.DeferFailureFunction) + if !ok { + continue + } + if cf.GetExit() != nil { + errs = append(errs, cf.GetExit().Error()) + } + } + if len(errs) > 0 { + return fmt.Errorf(strings.Join(errs, "\n---\n")) + } + return nil +} + +// getFunctionsFromInput scans the input for functions and runs them +func (r RunFns) getFunctionsFromInput(nodes []*yaml.RNode) ([]kio.Filter, error) { + if *r.NoFunctionsFromInput { + return nil, nil + } + + buff := &kio.PackageBuffer{} + err := kio.Pipeline{ + Inputs: []kio.Reader{&kio.PackageBuffer{Nodes: nodes}}, + Filters: []kio.Filter{&runtimeutil.IsReconcilerFilter{}}, + Outputs: []kio.Writer{buff}, + }.Execute() + if err != nil { + return nil, err + } + err = sortFns(buff) + if err != nil { + return nil, err + } + return r.getFunctionFilters(false, buff.Nodes...) +} + +// getFunctionsFromFunctionPaths returns the set of functions read from r.FunctionPaths +// as a slice of Filters +func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) { + buff := &kio.PackageBuffer{} + for i := range r.FunctionPaths { + err := kio.Pipeline{ + Inputs: []kio.Reader{ + kio.LocalPackageReader{PackagePath: r.FunctionPaths[i]}, + }, + Outputs: []kio.Writer{buff}, + }.Execute() + if err != nil { + return nil, err + } + } + return r.getFunctionFilters(true, buff.Nodes...) +} + +// getFunctionsFromFunctions returns the set of explicitly provided functions as +// Filters +func (r RunFns) getFunctionsFromFunctions() ([]kio.Filter, error) { + return r.getFunctionFilters(true, r.Functions...) +} + +// mergeContainerEnv will merge the envs specified by command line (imperative) and config +// file (declarative). If they have same key, the imperative value will be respected. +func (r RunFns) mergeContainerEnv(envs []string) []string { + imperative := runtimeutil.NewContainerEnvFromStringSlice(r.Env) + declarative := runtimeutil.NewContainerEnvFromStringSlice(envs) + for key, value := range imperative.EnvVars { + declarative.AddKeyValue(key, value) + } + + for _, key := range imperative.VarsToExport { + declarative.AddKey(key) + } + + return declarative.Raw() +} + +func (r RunFns) getFunctionFilters(global bool, fns ...*yaml.RNode) ( + []kio.Filter, error) { + fltrs := make([]kio.Filter, 0) + for i := range fns { + api := fns[i] + spec := runtimeutil.GetFunctionSpec(api) + if spec == nil { + // resource doesn't have function spec + continue + } + if spec.Container.Network && !r.Network { + // TODO(eddiezane): Provide error info about which function needs the network + return fltrs, errors.Errorf("network required but not enabled with --network") + } + // merge envs from imperative and declarative + spec.Container.Env = r.mergeContainerEnv(spec.Container.Env) + + c, err := r.functionFilterProvider(*spec, api, user.Current) + if err != nil { + return nil, err + } + + if c == nil { + continue + } + cf, ok := c.(*container.Filter) + if global && ok { + cf.Exec.GlobalScope = true + } + fltrs = append(fltrs, c) + } + return fltrs, nil +} + +// sortFns sorts functions so that functions with the longest paths come first +func sortFns(buff *kio.PackageBuffer) error { + var outerErr error + // sort the nodes so that we traverse them depth first + // functions deeper in the file system tree should be run first + sort.Slice(buff.Nodes, func(i, j int) bool { + mi, err := buff.Nodes[i].GetMeta() + if err != nil { + outerErr = err + return false + } + + pi := filepath.ToSlash(mi.Annotations[kioutil.PathAnnotation]) + + mj, err := buff.Nodes[j].GetMeta() + if err != nil { + outerErr = err + return false + } + pj := filepath.ToSlash(mj.Annotations[kioutil.PathAnnotation]) + + // If the path is the same, we decide the ordering based on the + // index annotation. + if pi == pj { + iIndex, err := strconv.Atoi(mi.Annotations[kioutil.IndexAnnotation]) + if err != nil { + outerErr = err + return false + } + jIndex, err := strconv.Atoi(mj.Annotations[kioutil.IndexAnnotation]) + if err != nil { + outerErr = err + return false + } + return iIndex < jIndex + } + + if filepath.Base(path.Dir(pi)) == "functions" { + // don't count the functions dir, the functions are scoped 1 level above + pi = filepath.Dir(path.Dir(pi)) + } else { + pi = filepath.Dir(pi) + } + + if filepath.Base(path.Dir(pj)) == "functions" { + // don't count the functions dir, the functions are scoped 1 level above + pj = filepath.Dir(path.Dir(pj)) + } else { + pj = filepath.Dir(pj) + } + + // i is "less" than j (comes earlier) if its depth is greater -- e.g. run + // i before j if it is deeper in the directory structure + li := len(strings.Split(pi, "/")) + if pi == "." { + // local dir should have 0 path elements instead of 1 + li = 0 + } + lj := len(strings.Split(pj, "/")) + if pj == "." { + // local dir should have 0 path elements instead of 1 + lj = 0 + } + if li != lj { + // use greater-than because we want to sort with the longest + // paths FIRST rather than last + return li > lj + } + + // sort by path names if depths are equal + return pi < pj + }) + return outerErr +} + +// init initializes the RunFns with a containerFilterProvider. +func (r *RunFns) init() { + if r.NoFunctionsFromInput == nil { + // default no functions from input if any function sources are explicitly provided + nfn := len(r.FunctionPaths) > 0 || len(r.Functions) > 0 + r.NoFunctionsFromInput = &nfn + } + + // if no path is specified, default reading from stdin and writing to stdout + if r.Path == "" { + if r.Output == nil { + r.Output = os.Stdout + } + if r.Input == nil { + r.Input = os.Stdin + } + } + + // functionFilterProvider set the filter provider + if r.functionFilterProvider == nil { + r.functionFilterProvider = r.ffp + } + + // if LogSteps is enabled and LogWriter is not specified, use stderr + if r.LogSteps && r.LogWriter == nil { + r.LogWriter = os.Stderr + } +} + +type currentUserFunc func() (*user.User, error) + +// getUIDGID will return "nobody" if asCurrentUser is false. Otherwise +// return "uid:gid" according to the return from currentUser function. +func getUIDGID(asCurrentUser bool, currentUser currentUserFunc) (string, error) { + if !asCurrentUser { + return "nobody", nil + } + + u, err := currentUser() + if err != nil { + return "", err + } + return fmt.Sprintf("%s:%s", u.Uid, u.Gid), nil +} + +// ffp provides function filters +func (r *RunFns) ffp(spec runtimeutil.FunctionSpec, api *yaml.RNode, currentUser currentUserFunc) (kio.Filter, error) { + var resultsFile string + if r.ResultsDir != "" { + resultsFile = filepath.Join(r.ResultsDir, fmt.Sprintf( + "results-%v.yaml", r.resultsCount)) + atomic.AddUint32(&r.resultsCount, 1) + } + if spec.Container.Image != "" { + // TODO: Add a test for this behavior + uidgid, err := getUIDGID(r.AsCurrentUser, currentUser) + if err != nil { + return nil, err + } + c := container.NewContainer( + runtimeutil.ContainerSpec{ + Image: spec.Container.Image, + Network: spec.Container.Network, + StorageMounts: r.StorageMounts, + Env: spec.Container.Env, + }, + uidgid, + ) + cf := &c + cf.Exec.FunctionConfig = api + cf.Exec.GlobalScope = r.GlobalScope + cf.Exec.ResultsFile = resultsFile + cf.Exec.DeferFailure = spec.DeferFailure + + if !r.noCmdSet { + cmd, args := r.getCommand(c) + cf.Exec.Path = cmd + cf.Exec.Args = args + } + + return cf, nil + } + + return nil, nil +} + +// getArgs returns the command + args to run to spawn the container +func (r *RunFns) getCommand(c container.Filter) (string, []string) { + network := runtimeutil.NetworkNameNone + if c.ContainerSpec.Network { + network = runtimeutil.NetworkNameHost + } + // run the container using docker. this is simpler than using the docker + // libraries, and ensures things like auth work the same as if the container + // was run from the cli. + args := []string{"run", + "--rm", // delete the container afterward + "-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", // attach stdin, stdout, stderr + "--network", string(network), + + // added security options + "--user", c.UIDGID, + "--security-opt=no-new-privileges", // don't allow the user to escalate privileges + // note: don't make fs readonly because things like heredoc rely on writing tmp files + } + + // TODO(joncwong): Allow StorageMount fields to have default values. + for _, storageMount := range c.StorageMounts { + args = append(args, "--mount", storageMount.String()) + } + + args = append(args, runtimeutil.NewContainerEnvFromStringSlice(c.Env).GetDockerFlags()...) + a := append(args, c.Image) + if r.Timeout > 0 { + a = append([]string{"-v", "-s9", fmt.Sprintf("%d", r.Timeout), "docker"}, a...) + return "timeout", a + } + return "docker", a +} diff --git a/pkg/container/runfn/runfn_test.go b/pkg/container/runfn/runfn_test.go new file mode 100644 index 000000000..f3e79d04f --- /dev/null +++ b/pkg/container/runfn/runfn_test.go @@ -0,0 +1,1268 @@ +/* + 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()) + }) + } +} diff --git a/pkg/container/runfn/test/testdata/java/java-configmap.resource.yaml b/pkg/container/runfn/test/testdata/java/java-configmap.resource.yaml new file mode 100644 index 000000000..d01363d10 --- /dev/null +++ b/pkg/container/runfn/test/testdata/java/java-configmap.resource.yaml @@ -0,0 +1,10 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config + labels: + app.kubernetes.io/component: undefined + app.kubernetes.io/instance: undefined +data: {} diff --git a/pkg/container/runfn/test/testdata/java/java-deployment.resource.yaml b/pkg/container/runfn/test/testdata/java/java-deployment.resource.yaml new file mode 100644 index 000000000..d03d3a48d --- /dev/null +++ b/pkg/container/runfn/test/testdata/java/java-deployment.resource.yaml @@ -0,0 +1,36 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +apiVersion: apps/v1 +kind: Deployment +metadata: + name: app + labels: + app: java +spec: + selector: + matchLabels: + app: java + template: + metadata: + labels: + app: java + spec: + restartPolicy: Always + containers: + - name: app + image: gcr.io/project/app:version + command: + - java + - -jar + - /app.jar + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: app-config + env: + - name: JAVA_OPTS + value: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap + -Djava.security.egd=file:/dev/./urandom + imagePullPolicy: Always + minReadySeconds: 5 diff --git a/pkg/container/runfn/test/testdata/java/java-service.resource.yaml b/pkg/container/runfn/test/testdata/java/java-service.resource.yaml new file mode 100644 index 000000000..f8d7b08d2 --- /dev/null +++ b/pkg/container/runfn/test/testdata/java/java-service.resource.yaml @@ -0,0 +1,15 @@ +# Copyright 2019 The Kubernetes Authors. +# SPDX-License-Identifier: Apache-2.0 +apiVersion: v1 +kind: Service +metadata: + name: app + labels: + app: java +spec: + selector: + app: java + ports: + - name: "8080" + port: 8080 + targetPort: 8080