diff --git a/kyaml/fn/runtime/container/container.go b/kyaml/fn/runtime/container/container.go index 8e9bf52f8..efe15681f 100644 --- a/kyaml/fn/runtime/container/container.go +++ b/kyaml/fn/runtime/container/container.go @@ -5,8 +5,6 @@ package container import ( "fmt" - "os" - "strings" runtimeexec "sigs.k8s.io/kustomize/kyaml/fn/runtime/exec" "sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil" @@ -177,19 +175,7 @@ func (c *Filter) getCommand() (string, []string) { args = append(args, "--mount", storageMount.String()) } - // TODO: put these env processes into a separate function and call it in the outside of - // getCommand - os.Setenv("LOG_TO_STDERR", "true") - os.Setenv("STRUCTURED_RESULTS", "true") - - // export the local environment vars to the container - for _, pair := range os.Environ() { - items := strings.Split(pair, "=") - if items[0] == "" || items[1] == "" || items[0] == tmpDirEnvKey { - continue - } - args = append(args, "-e", items[0]) - } + args = append(args, c.Envs.GetDockerFlags()...) a := append(args, c.Image) return "docker", a } diff --git a/kyaml/fn/runtime/container/container_test.go b/kyaml/fn/runtime/container/container_test.go index c0833f5b2..754b274d9 100644 --- a/kyaml/fn/runtime/container/container_test.go +++ b/kyaml/fn/runtime/container/container_test.go @@ -7,7 +7,6 @@ import ( "bytes" "fmt" "os" - "strings" "testing" "github.com/stretchr/testify/assert" @@ -137,20 +136,13 @@ metadata: } tt.instance.Exec.FunctionConfig = cfg - os.Setenv("KYAML_TEST", "FOO") - tt.instance.setupExec() + tt.instance.Envs.AddKeyValue("KYAML_TEST", "FOO") + tt.expectedArgs = append(tt.expectedArgs, tt.instance.Envs.GetDockerFlags()...) - // configure expected env - for _, e := range os.Environ() { - // the process env - parts := strings.Split(e, "=") - if parts[0] == "" || parts[1] == "" || parts[0] == tmpDirEnvKey { - continue - } - tt.expectedArgs = append(tt.expectedArgs, "-e", parts[0]) - } tt.expectedArgs = append(tt.expectedArgs, tt.instance.Image) + tt.instance.setupExec() + if !assert.Equal(t, "docker", tt.instance.Exec.Path) { t.FailNow() } diff --git a/kyaml/fn/runtime/runtimeutil/functiontypes.go b/kyaml/fn/runtime/runtimeutil/functiontypes.go index 869d27d20..5c193dba9 100644 --- a/kyaml/fn/runtime/runtimeutil/functiontypes.go +++ b/kyaml/fn/runtime/runtimeutil/functiontypes.go @@ -6,6 +6,7 @@ package runtimeutil import ( "fmt" "os" + "sort" "strings" "sigs.k8s.io/kustomize/kyaml/yaml" @@ -40,6 +41,77 @@ const ( NetworkNameNone ContainerNetworkName = "none" NetworkNameEmpty ContainerNetworkName = "" ) +const defaultEnvValue string = "true" + +// ContainerEnvs contains the environment variables for the container +type ContainerEnvs struct { + // EnvsMap is a key-value map that will be set as env in container + EnvsMap map[string]string `json:"envsMap,omitempty" yaml:"envsMap,omitempty"` + + // ExportKeys are only env key. Value will be the value in the host system + ExportKeys []string `json:"exportKeys,omitempty" yaml:"exportKeys,omitempty"` +} + +// GetDockerFlags returns docker run style env flags +func (ce *ContainerEnvs) GetDockerFlags() []string { + envs := ce.EnvsMap + if envs == nil { + envs = make(map[string]string) + } + + // default envs + envs["LOG_TO_STDERR"] = defaultEnvValue + envs["STRUCTURED_RESULTS"] = defaultEnvValue + + flags := []string{} + // return in order to keep consistent among different runs + keys := []string{} + for k := range envs { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { + flags = append(flags, "-e", key+"="+envs[key]) + } + + for _, key := range ce.ExportKeys { + flags = append(flags, "-e", key) + } + + return flags +} + +// AddKeyValue adds a key-value pair into the envs +func (ce *ContainerEnvs) AddKeyValue(key, value string) { + if ce.EnvsMap == nil { + ce.EnvsMap = make(map[string]string) + } + ce.EnvsMap[key] = value +} + +// HasExportedKey returns true if the key is a exported key +func (ce *ContainerEnvs) HasExportedKey(key string) bool { + for _, k := range ce.ExportKeys { + if k == key { + return true + } + } + return false +} + +// AddKey adds a key into the envs +func (ce *ContainerEnvs) AddKey(key string) { + if !ce.HasExportedKey(key) { + ce.ExportKeys = append(ce.ExportKeys, key) + } +} + +// NewContainerEnvs returns an empty instance of ContainerEnvs +func NewContainerEnvs() ContainerEnvs { + var ce ContainerEnvs + ce.EnvsMap = make(map[string]string) + return ce +} // FunctionSpec defines a spec for running a function type FunctionSpec struct { @@ -75,6 +147,9 @@ type ContainerSpec struct { // User is the username/uid that application runs as in continer User ContainerUser `json:"user,omitempty" yaml:"user,omitempty"` + + // Envs are environment variables that will be exported to container + Envs ContainerEnvs `json:"envs,omitempty" yaml:"envs,omitempty"` } // ContainerNetwork diff --git a/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go b/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go index 89b728cf1..be62f8202 100644 --- a/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go +++ b/kyaml/fn/runtime/runtimeutil/runtimeutil_test.go @@ -1428,3 +1428,43 @@ func Test_StringToStorageMount(t *testing.T) { assert.Equal(t, tc.expectedOut, (&s).String()) } } + +func TestContainerEnvs(t *testing.T) { + tests := []struct { + input ContainerEnvs + output []string + }{ + { + input: ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + }, + output: []string{"-e", "LOG_TO_STDERR=true", "-e", "STRUCTURED_RESULTS=true", "-e", "foo=bar"}, + }, + { + input: ContainerEnvs{ + ExportKeys: []string{"foo"}, + }, + output: []string{"-e", "LOG_TO_STDERR=true", "-e", "STRUCTURED_RESULTS=true", "-e", "foo"}, + }, + { + input: ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + ExportKeys: []string{"baz"}, + }, + output: []string{"-e", "LOG_TO_STDERR=true", "-e", "STRUCTURED_RESULTS=true", "-e", "foo=bar", "-e", "baz"}, + }, + { + input: ContainerEnvs{}, + output: []string{"-e", "LOG_TO_STDERR=true", "-e", "STRUCTURED_RESULTS=true"}, + }, + } + + for _, tc := range tests { + flags := tc.input.GetDockerFlags() + assert.Equal(t, tc.output, flags) + } +} diff --git a/kyaml/runfn/runfn.go b/kyaml/runfn/runfn.go index 4b743df2b..4030492fb 100644 --- a/kyaml/runfn/runfn.go +++ b/kyaml/runfn/runfn.go @@ -90,6 +90,9 @@ type RunFns struct { // User username used to run the application in container, User runtimeutil.ContainerUser + + // Envs are environment variables that will be exported to container + Envs runtimeutil.ContainerEnvs } // Execute runs the command @@ -269,6 +272,20 @@ func (r RunFns) getFunctionsFromFunctions() ([]kio.Filter, error) { return r.getFunctionFilters(true, r.Functions...) } +// mergeContainerEnvs 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) mergeContainerEnvs(envs runtimeutil.ContainerEnvs) runtimeutil.ContainerEnvs { + for key, value := range r.Envs.EnvsMap { + envs.AddKeyValue(key, value) + } + + for _, key := range r.Envs.ExportKeys { + envs.AddKey(key) + } + + return envs +} + func (r RunFns) getFunctionFilters(global bool, fns ...*yaml.RNode) ( []kio.Filter, error) { var fltrs []kio.Filter @@ -282,10 +299,11 @@ func (r RunFns) getFunctionFilters(global bool, fns ...*yaml.RNode) ( } spec.Container.Network.Name = runtimeutil.ContainerNetworkName(r.NetworkName) } - // command line username has higher priority - if r.User != "" { + // command line username and envs has higher priority + if !r.User.IsEmpty() { spec.Container.User = r.User } + spec.Container.Envs = r.mergeContainerEnvs(spec.Container.Envs) c, err := r.functionFilterProvider(*spec, api) if err != nil { @@ -394,6 +412,7 @@ func (r *RunFns) ffp(spec runtimeutil.FunctionSpec, api *yaml.RNode) (kio.Filter Network: spec.Container.Network, StorageMounts: r.StorageMounts, User: spec.Container.User, + Envs: spec.Container.Envs, }) cf := &c cf.Exec.FunctionConfig = api diff --git a/kyaml/runfn/runfn_test.go b/kyaml/runfn/runfn_test.go index 55cb2d429..491372097 100644 --- a/kyaml/runfn/runfn_test.go +++ b/kyaml/runfn/runfn_test.go @@ -986,3 +986,104 @@ func getFilterProvider(t *testing.T) func(runtimeutil.FunctionSpec, *yaml.RNode) }, nil } } + +func TestRunfns_mergeContainerEnvs(t *testing.T) { + testcases := []struct { + name string + instance RunFns + inputEnvs runtimeutil.ContainerEnvs + expect runtimeutil.ContainerEnvs + }{ + { + name: "all empty", + instance: RunFns{}, + inputEnvs: runtimeutil.NewContainerEnvs(), + expect: runtimeutil.NewContainerEnvs(), + }, + { + name: "empty command line envs", + instance: RunFns{}, + inputEnvs: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + }, + expect: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + }, + }, + { + name: "empty declarative envs", + instance: RunFns{ + Envs: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + }, + }, + inputEnvs: runtimeutil.NewContainerEnvs(), + expect: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + }, + }, + { + name: "same key", + instance: RunFns{ + Envs: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + ExportKeys: []string{"foo"}, + }, + }, + inputEnvs: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar1", + }, + ExportKeys: []string{"bar"}, + }, + expect: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + ExportKeys: []string{"bar", "foo"}, + }, + }, + { + name: "same exported key", + instance: RunFns{ + Envs: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + }, + ExportKeys: []string{"foo"}, + }, + }, + inputEnvs: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo1": "bar1", + }, + ExportKeys: []string{"foo"}, + }, + expect: runtimeutil.ContainerEnvs{ + EnvsMap: map[string]string{ + "foo": "bar", + "foo1": "bar1", + }, + ExportKeys: []string{"foo"}, + }, + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(tc.name, func(t *testing.T) { + envs := tc.instance.mergeContainerEnvs(tc.inputEnvs) + assert.Equal(t, tc.expect.GetDockerFlags(), envs.GetDockerFlags()) + }) + } +}