From cc7a71c288ccf98dfbaa8c05efa3dfed66c2e8ac Mon Sep 17 00:00:00 2001 From: Matthias Riegler Date: Sun, 16 Feb 2025 22:28:21 +0100 Subject: [PATCH] feat: add exec-plugin argument and environment support (#5316) * feat: add exec-plugin argument and environment support Previously, the documentation lead to think that this is working, but it's not been implemented. This PR is fixing this Signed-off-by: Matthias Riegler * chore: disable linting for env var split Signed-off-by: Matthias Riegler --------- Signed-off-by: Matthias Riegler --- kyaml/fn/runtime/exec/exec.go | 4 ++ kyaml/fn/runtime/exec/exec_test.go | 7 +-- kyaml/fn/runtime/runtimeutil/functiontypes.go | 6 +++ kyaml/runfn/runfn.go | 28 +++++++++- kyaml/runfn/runfn_test.go | 52 +++++++++++++++++++ 5 files changed, 92 insertions(+), 5 deletions(-) diff --git a/kyaml/fn/runtime/exec/exec.go b/kyaml/fn/runtime/exec/exec.go index 8bb3fe12e6..562dab30f7 100644 --- a/kyaml/fn/runtime/exec/exec.go +++ b/kyaml/fn/runtime/exec/exec.go @@ -21,6 +21,9 @@ type Filter struct { // Args are the arguments to the executable Args []string `yaml:"args,omitempty"` + // Env is exposed to the environment + Env []string `yaml:"env,omitempty"` + // WorkingDir is the working directory that the executable // should run in WorkingDir string @@ -35,6 +38,7 @@ func (c *Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) { func (c *Filter) Run(reader io.Reader, writer io.Writer) error { cmd := exec.Command(c.Path, c.Args...) //nolint:gosec + cmd.Env = append(os.Environ(), c.Env...) cmd.Stdin = reader cmd.Stdout = writer cmd.Stderr = os.Stderr diff --git a/kyaml/fn/runtime/exec/exec_test.go b/kyaml/fn/runtime/exec/exec_test.go index 1e9ff2d700..0ea2992264 100644 --- a/kyaml/fn/runtime/exec/exec_test.go +++ b/kyaml/fn/runtime/exec/exec_test.go @@ -17,7 +17,7 @@ import ( func TestFunctionFilter_Filter(t *testing.T) { wd, err := os.Getwd() require.NoError(t, err) - var tests = []struct { + tests := []struct { name string input []string functionConfig string @@ -57,8 +57,9 @@ metadata: }, expectedError: "", instance: exec.Filter{ - Path: "sed", - Args: []string{"s/Deployment/StatefulSet/g"}, + Path: "sh", + Env: []string{"TARGET=StatefulSet"}, + Args: []string{"-c", `sed "s/Deployment/$TARGET/g"`}, WorkingDir: wd, }, }, diff --git a/kyaml/fn/runtime/runtimeutil/functiontypes.go b/kyaml/fn/runtime/runtimeutil/functiontypes.go index f56962c146..cd6cb8e28e 100644 --- a/kyaml/fn/runtime/runtimeutil/functiontypes.go +++ b/kyaml/fn/runtime/runtimeutil/functiontypes.go @@ -138,6 +138,12 @@ type FunctionSpec struct { type ExecSpec struct { Path string `json:"path,omitempty" yaml:"path,omitempty"` + + // Args is a slice of args that will be passed as arguments to script + Args []string `json:"args,omitempty" yaml:"args,omitempty"` + + // Env is a slice of env string that will be exposed to container + Env []string `json:"envs,omitempty" yaml:"envs,omitempty"` } // ContainerSpec defines a spec for running a function as a container diff --git a/kyaml/runfn/runfn.go b/kyaml/runfn/runfn.go index 777d3f87cc..bf8863c572 100644 --- a/kyaml/runfn/runfn.go +++ b/kyaml/runfn/runfn.go @@ -281,8 +281,8 @@ 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. +// mergeContainerEnv is container-specific and 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) @@ -297,6 +297,28 @@ func (r RunFns) mergeContainerEnv(envs []string) []string { return declarative.Raw() } +// mergeExecEnv 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) mergeExecEnv(envs []string) []string { + envMap := map[string]string{} + + for _, env := range append(envs, r.Env...) { + res := strings.Split(env, "=") + //nolint:gomnd + if len(res) == 2 { + envMap[res[0]] = res[1] + } + } + + mergedEnv := []string{} + for key, value := range envMap { + mergedEnv = append(mergedEnv, fmt.Sprintf("%s=%s", key, value)) + } + // Sort the envs to make the output deterministic + sort.Strings(mergedEnv) + return mergedEnv +} + func (r RunFns) getFunctionFilters(global bool, fns ...*yaml.RNode) ( []kio.Filter, error) { var fltrs []kio.Filter @@ -494,6 +516,8 @@ func (r *RunFns) ffp(spec runtimeutil.FunctionSpec, api *yaml.RNode, currentUser if r.EnableExec && spec.Exec.Path != "" { ef := &exec.Filter{ Path: spec.Exec.Path, + Args: spec.Exec.Args, + Env: r.mergeExecEnv(spec.Exec.Env), WorkingDir: r.WorkingDir, } diff --git a/kyaml/runfn/runfn_test.go b/kyaml/runfn/runfn_test.go index 2b360873ef..7b1fbc057c 100644 --- a/kyaml/runfn/runfn_test.go +++ b/kyaml/runfn/runfn_test.go @@ -1282,3 +1282,55 @@ func TestRunFns_mergeContainerEnv(t *testing.T) { }) } } + +func TestRunFns_mergeExecEnv(t *testing.T) { + testcases := []struct { + name string + instance RunFns + inputEnvs []string + expect []string + }{ + { + name: "all empty", + instance: RunFns{}, + expect: []string{}, + }, + { + name: "empty command line envs", + instance: RunFns{}, + inputEnvs: []string{"foo=bar"}, + expect: []string{"foo=bar"}, + }, + { + name: "empty declarative envs", + instance: RunFns{ + Env: []string{"foo=bar"}, + }, + expect: []string{"foo=bar"}, + }, + { + name: "same key", + instance: RunFns{ + Env: []string{"foo=bar"}, + }, + inputEnvs: []string{"foo=bar1"}, + expect: []string{"foo=bar"}, + }, + { + name: "same exported key", + instance: RunFns{ + Env: []string{"foo=bar"}, + }, + inputEnvs: []string{"foo1=bar1"}, + expect: []string{"foo1=bar1", "foo=bar"}, + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(tc.name, func(t *testing.T) { + envs := tc.instance.mergeExecEnv(tc.inputEnvs) + assert.Equal(t, tc.expect, envs) + }) + } +}