play kube: support optional/mandatory env var from config map

In K8S the pod creation fails if an env var reference a non existing
config map key. It can be marked as optional, but per default it is
mandatory. Podman on the other hand always treat such references as
optional.

Rework envVarsFrom() and envVarValue() to additionaly return an error
and add support for the optional attribute in configMapRef and
configMapKeyRef.

Signed-off-by: Alban Bedel <albeu@free.fr>
This commit is contained in:
Alban Bedel
2021-03-26 10:54:26 +01:00
parent 9f92b8b0d8
commit e5ff694855
3 changed files with 235 additions and 21 deletions

View File

@ -210,12 +210,18 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener
} }
for _, env := range opts.Container.Env { for _, env := range opts.Container.Env {
value := envVarValue(env, opts) value, err := envVarValue(env, opts)
if err != nil {
return nil, err
}
envs[env.Name] = value envs[env.Name] = value
} }
for _, envFrom := range opts.Container.EnvFrom { for _, envFrom := range opts.Container.EnvFrom {
cmEnvs := envVarsFrom(envFrom, opts) cmEnvs, err := envVarsFrom(envFrom, opts)
if err != nil {
return nil, err
}
for k, v := range cmEnvs { for k, v := range cmEnvs {
envs[k] = v envs[k] = v
@ -326,39 +332,54 @@ func quantityToInt64(quantity *resource.Quantity) (int64, error) {
} }
// envVarsFrom returns all key-value pairs as env vars from a configMap that matches the envFrom setting of a container // envVarsFrom returns all key-value pairs as env vars from a configMap that matches the envFrom setting of a container
func envVarsFrom(envFrom v1.EnvFromSource, opts *CtrSpecGenOptions) map[string]string { func envVarsFrom(envFrom v1.EnvFromSource, opts *CtrSpecGenOptions) (map[string]string, error) {
envs := map[string]string{} envs := map[string]string{}
if envFrom.ConfigMapRef != nil { if envFrom.ConfigMapRef != nil {
cmName := envFrom.ConfigMapRef.Name cmRef := envFrom.ConfigMapRef
err := errors.Errorf("Configmap %v not found", cmRef.Name)
for _, c := range opts.ConfigMaps { for _, c := range opts.ConfigMaps {
if cmName == c.Name { if cmRef.Name == c.Name {
envs = c.Data envs = c.Data
err = nil
break break
} }
} }
if err != nil && (cmRef.Optional == nil || !*cmRef.Optional) {
return nil, err
}
} }
return envs return envs, nil
} }
// envVarValue returns the environment variable value configured within the container's env setting. // envVarValue returns the environment variable value configured within the container's env setting.
// It gets the value from a configMap if specified, otherwise returns env.Value // It gets the value from a configMap if specified, otherwise returns env.Value
func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) string { func envVarValue(env v1.EnvVar, opts *CtrSpecGenOptions) (string, error) {
if env.ValueFrom != nil { if env.ValueFrom != nil {
if env.ValueFrom.ConfigMapKeyRef != nil { if env.ValueFrom.ConfigMapKeyRef != nil {
cmKeyRef := env.ValueFrom.ConfigMapKeyRef
err := errors.Errorf("Cannot set env %v: configmap %v not found", env.Name, cmKeyRef.Name)
for _, c := range opts.ConfigMaps { for _, c := range opts.ConfigMaps {
if env.ValueFrom.ConfigMapKeyRef.Name == c.Name { if cmKeyRef.Name == c.Name {
if value, ok := c.Data[env.ValueFrom.ConfigMapKeyRef.Key]; ok { if value, ok := c.Data[cmKeyRef.Key]; ok {
return value return value, nil
}
err = errors.Errorf("Cannot set env %v: key %s not found in configmap %v", env.Name, cmKeyRef.Key, cmKeyRef.Name)
break
} }
} }
if cmKeyRef.Optional == nil || !*cmKeyRef.Optional {
return "", err
} }
return "", nil
} }
} }
return env.Value return env.Value, nil
} }
// getPodPorts converts a slice of kube container descriptions to an // getPodPorts converts a slice of kube container descriptions to an

View File

@ -13,6 +13,7 @@ func TestEnvVarsFrom(t *testing.T) {
name string name string
envFrom v1.EnvFromSource envFrom v1.EnvFromSource
options CtrSpecGenOptions options CtrSpecGenOptions
succeed bool
expected map[string]string expected map[string]string
}{ }{
{ {
@ -27,6 +28,7 @@ func TestEnvVarsFrom(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: configMapList, ConfigMaps: configMapList,
}, },
true,
map[string]string{ map[string]string{
"myvar": "foo", "myvar": "foo",
}, },
@ -43,6 +45,23 @@ func TestEnvVarsFrom(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: configMapList, ConfigMaps: configMapList,
}, },
false,
nil,
},
{
"OptionalConfigMapDoesNotExist",
v1.EnvFromSource{
ConfigMapRef: &v1.ConfigMapEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
Optional: &optional,
},
},
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
true,
map[string]string{}, map[string]string{},
}, },
{ {
@ -57,6 +76,23 @@ func TestEnvVarsFrom(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: []v1.ConfigMap{}, ConfigMaps: []v1.ConfigMap{},
}, },
false,
nil,
},
{
"OptionalEmptyConfigMapList",
v1.EnvFromSource{
ConfigMapRef: &v1.ConfigMapEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
Optional: &optional,
},
},
CtrSpecGenOptions{
ConfigMaps: []v1.ConfigMap{},
},
true,
map[string]string{}, map[string]string{},
}, },
} }
@ -64,7 +100,8 @@ func TestEnvVarsFrom(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
result := envVarsFrom(test.envFrom, &test.options) result, err := envVarsFrom(test.envFrom, &test.options)
assert.Equal(t, err == nil, test.succeed)
assert.Equal(t, test.expected, result) assert.Equal(t, test.expected, result)
}) })
} }
@ -75,6 +112,7 @@ func TestEnvVarValue(t *testing.T) {
name string name string
envVar v1.EnvVar envVar v1.EnvVar
options CtrSpecGenOptions options CtrSpecGenOptions
succeed bool
expected string expected string
}{ }{
{ {
@ -93,6 +131,7 @@ func TestEnvVarValue(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: configMapList, ConfigMaps: configMapList,
}, },
true,
"foo", "foo",
}, },
{ {
@ -111,6 +150,27 @@ func TestEnvVarValue(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: configMapList, ConfigMaps: configMapList,
}, },
false,
"",
},
{
"OptionalContainerKeyDoesNotExistInConfigMap",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
Key: "doesnotexist",
Optional: &optional,
},
},
},
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
true,
"", "",
}, },
{ {
@ -129,6 +189,27 @@ func TestEnvVarValue(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: configMapList, ConfigMaps: configMapList,
}, },
false,
"",
},
{
"OptionalConfigMapDoesNotExist",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
Key: "myvar",
Optional: &optional,
},
},
},
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
true,
"", "",
}, },
{ {
@ -147,6 +228,27 @@ func TestEnvVarValue(t *testing.T) {
CtrSpecGenOptions{ CtrSpecGenOptions{
ConfigMaps: []v1.ConfigMap{}, ConfigMaps: []v1.ConfigMap{},
}, },
false,
"",
},
{
"OptionalEmptyConfigMapList",
v1.EnvVar{
Name: "FOO",
ValueFrom: &v1.EnvVarSource{
ConfigMapKeyRef: &v1.ConfigMapKeySelector{
LocalObjectReference: v1.LocalObjectReference{
Name: "foo",
},
Key: "myvar",
Optional: &optional,
},
},
},
CtrSpecGenOptions{
ConfigMaps: []v1.ConfigMap{},
},
true,
"", "",
}, },
} }
@ -154,7 +256,8 @@ func TestEnvVarValue(t *testing.T) {
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
result := envVarValue(test.envVar, &test.options) result, err := envVarValue(test.envVar, &test.options)
assert.Equal(t, err == nil, test.succeed)
assert.Equal(t, test.expected, result) assert.Equal(t, test.expected, result)
}) })
} }
@ -184,3 +287,5 @@ var configMapList = []v1.ConfigMap{
}, },
}, },
} }
var optional = true

View File

@ -141,6 +141,7 @@ spec:
configMapKeyRef: configMapKeyRef:
name: {{ .RefName }} name: {{ .RefName }}
key: {{ .RefKey }} key: {{ .RefKey }}
optional: {{ .Optional }}
{{ else }} {{ else }}
value: {{ .Value }} value: {{ .Value }}
{{ end }} {{ end }}
@ -151,6 +152,7 @@ spec:
{{ if (eq .From "configmap") }} {{ if (eq .From "configmap") }}
- configMapRef: - configMapRef:
name: {{ .Name }} name: {{ .Name }}
optional: {{ .Optional }}
{{ end }} {{ end }}
{{ end }} {{ end }}
{{ end }} {{ end }}
@ -746,7 +748,7 @@ func withVolumeMount(mountPath string, readonly bool) ctrOption {
} }
} }
func withEnv(name, value, valueFrom, refName, refKey string) ctrOption { func withEnv(name, value, valueFrom, refName, refKey string, optional bool) ctrOption {
return func(c *Ctr) { return func(c *Ctr) {
e := Env{ e := Env{
Name: name, Name: name,
@ -754,17 +756,19 @@ func withEnv(name, value, valueFrom, refName, refKey string) ctrOption {
ValueFrom: valueFrom, ValueFrom: valueFrom,
RefName: refName, RefName: refName,
RefKey: refKey, RefKey: refKey,
Optional: optional,
} }
c.Env = append(c.Env, e) c.Env = append(c.Env, e)
} }
} }
func withEnvFrom(name, from string) ctrOption { func withEnvFrom(name, from string, optional bool) ctrOption {
return func(c *Ctr) { return func(c *Ctr) {
e := EnvFrom{ e := EnvFrom{
Name: name, Name: name,
From: from, From: from,
Optional: optional,
} }
c.EnvFrom = append(c.EnvFrom, e) c.EnvFrom = append(c.EnvFrom, e)
@ -822,11 +826,13 @@ type Env struct {
ValueFrom string ValueFrom string
RefName string RefName string
RefKey string RefKey string
Optional bool
} }
type EnvFrom struct { type EnvFrom struct {
Name string Name string
From string From string
Optional bool
} }
func milliCPUToQuota(milliCPU string) int { func milliCPUToQuota(milliCPU string) int {
@ -1062,7 +1068,7 @@ var _ = Describe("Podman play kube", func() {
err := generateKubeYaml("configmap", cm, cmYamlPathname) err := generateKubeYaml("configmap", cm, cmYamlPathname)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "FOO")))) pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "FOO", false))))
err = generateKubeYaml("pod", pod, kubeYaml) err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -1076,6 +1082,68 @@ var _ = Describe("Podman play kube", func() {
Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`)) Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`))
}) })
It("podman play kube test required env value from configmap with missing key", func() {
SkipIfRemote("configmap list is not supported as a param")
cmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml")
cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo"))
err := generateKubeYaml("configmap", cm, cmYamlPathname)
Expect(err).To(BeNil())
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "MISSING_KEY", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml, "--configmap", cmYamlPathname})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
It("podman play kube test required env value from missing configmap", func() {
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "missing_cm", "FOO", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
It("podman play kube test optional env value from configmap with missing key", func() {
SkipIfRemote("configmap list is not supported as a param")
cmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml")
cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo"))
err := generateKubeYaml("configmap", cm, cmYamlPathname)
Expect(err).To(BeNil())
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "MISSING_KEY", true))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml, "--configmap", cmYamlPathname})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ range .Config.Env }}[{{ . }}]{{end}}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`[FOO=]`))
})
It("podman play kube test optional env value from missing configmap", func() {
pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "missing_cm", "FOO", true))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ range .Config.Env }}[{{ . }}]{{end}}'"})
inspect.WaitWithDefaultTimeout()
Expect(inspect.ExitCode()).To(Equal(0))
Expect(inspect.OutputToString()).To(ContainSubstring(`[FOO=]`))
})
It("podman play kube test get all key-value pairs from configmap as envs", func() { It("podman play kube test get all key-value pairs from configmap as envs", func() {
SkipIfRemote("configmap list is not supported as a param") SkipIfRemote("configmap list is not supported as a param")
cmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml") cmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml")
@ -1083,7 +1151,7 @@ var _ = Describe("Podman play kube", func() {
err := generateKubeYaml("configmap", cm, cmYamlPathname) err := generateKubeYaml("configmap", cm, cmYamlPathname)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
pod := getPod(withCtr(getCtr(withEnvFrom("foo", "configmap")))) pod := getPod(withCtr(getCtr(withEnvFrom("foo", "configmap", false))))
err = generateKubeYaml("pod", pod, kubeYaml) err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil()) Expect(err).To(BeNil())
@ -1098,6 +1166,26 @@ var _ = Describe("Podman play kube", func() {
Expect(inspect.OutputToString()).To(ContainSubstring(`FOO2=foo2`)) Expect(inspect.OutputToString()).To(ContainSubstring(`FOO2=foo2`))
}) })
It("podman play kube test get all key-value pairs from required configmap as envs", func() {
pod := getPod(withCtr(getCtr(withEnvFrom("missing_cm", "configmap", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Not(Equal(0)))
})
It("podman play kube test get all key-value pairs from optional configmap as envs", func() {
pod := getPod(withCtr(getCtr(withEnvFrom("missing_cm", "configmap", true))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
kube := podmanTest.Podman([]string{"play", "kube", kubeYaml})
kube.WaitWithDefaultTimeout()
Expect(kube.ExitCode()).To(Equal(0))
})
It("podman play kube test hostname", func() { It("podman play kube test hostname", func() {
pod := getPod() pod := getPod()
err := generateKubeYaml("pod", pod, kubeYaml) err := generateKubeYaml("pod", pod, kubeYaml)