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 {
value := envVarValue(env, opts)
value, err := envVarValue(env, opts)
if err != nil {
return nil, err
}
envs[env.Name] = value
}
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 {
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
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{}
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 {
if cmName == c.Name {
if cmRef.Name == c.Name {
envs = c.Data
err = nil
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.
// 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.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 {
if env.ValueFrom.ConfigMapKeyRef.Name == c.Name {
if value, ok := c.Data[env.ValueFrom.ConfigMapKeyRef.Key]; ok {
return value
if cmKeyRef.Name == c.Name {
if value, ok := c.Data[cmKeyRef.Key]; ok {
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

View File

@ -13,6 +13,7 @@ func TestEnvVarsFrom(t *testing.T) {
name string
envFrom v1.EnvFromSource
options CtrSpecGenOptions
succeed bool
expected map[string]string
}{
{
@ -27,6 +28,7 @@ func TestEnvVarsFrom(t *testing.T) {
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
true,
map[string]string{
"myvar": "foo",
},
@ -43,6 +45,23 @@ func TestEnvVarsFrom(t *testing.T) {
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
false,
nil,
},
{
"OptionalConfigMapDoesNotExist",
v1.EnvFromSource{
ConfigMapRef: &v1.ConfigMapEnvSource{
LocalObjectReference: v1.LocalObjectReference{
Name: "doesnotexist",
},
Optional: &optional,
},
},
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
true,
map[string]string{},
},
{
@ -57,6 +76,23 @@ func TestEnvVarsFrom(t *testing.T) {
CtrSpecGenOptions{
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{},
},
}
@ -64,7 +100,8 @@ func TestEnvVarsFrom(t *testing.T) {
for _, test := range tests {
test := test
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)
})
}
@ -75,6 +112,7 @@ func TestEnvVarValue(t *testing.T) {
name string
envVar v1.EnvVar
options CtrSpecGenOptions
succeed bool
expected string
}{
{
@ -93,6 +131,7 @@ func TestEnvVarValue(t *testing.T) {
CtrSpecGenOptions{
ConfigMaps: configMapList,
},
true,
"foo",
},
{
@ -111,6 +150,27 @@ func TestEnvVarValue(t *testing.T) {
CtrSpecGenOptions{
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{
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{
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 {
test := test
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)
})
}
@ -184,3 +287,5 @@ var configMapList = []v1.ConfigMap{
},
},
}
var optional = true

View File

@ -141,6 +141,7 @@ spec:
configMapKeyRef:
name: {{ .RefName }}
key: {{ .RefKey }}
optional: {{ .Optional }}
{{ else }}
value: {{ .Value }}
{{ end }}
@ -151,6 +152,7 @@ spec:
{{ if (eq .From "configmap") }}
- configMapRef:
name: {{ .Name }}
optional: {{ .Optional }}
{{ 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) {
e := Env{
Name: name,
@ -754,17 +756,19 @@ func withEnv(name, value, valueFrom, refName, refKey string) ctrOption {
ValueFrom: valueFrom,
RefName: refName,
RefKey: refKey,
Optional: optional,
}
c.Env = append(c.Env, e)
}
}
func withEnvFrom(name, from string) ctrOption {
func withEnvFrom(name, from string, optional bool) ctrOption {
return func(c *Ctr) {
e := EnvFrom{
Name: name,
From: from,
Name: name,
From: from,
Optional: optional,
}
c.EnvFrom = append(c.EnvFrom, e)
@ -822,11 +826,13 @@ type Env struct {
ValueFrom string
RefName string
RefKey string
Optional bool
}
type EnvFrom struct {
Name string
From string
Name string
From string
Optional bool
}
func milliCPUToQuota(milliCPU string) int {
@ -1062,7 +1068,7 @@ var _ = Describe("Podman play kube", func() {
err := generateKubeYaml("configmap", cm, cmYamlPathname)
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)
Expect(err).To(BeNil())
@ -1076,6 +1082,68 @@ var _ = Describe("Podman play kube", func() {
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() {
SkipIfRemote("configmap list is not supported as a param")
cmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml")
@ -1083,7 +1151,7 @@ var _ = Describe("Podman play kube", func() {
err := generateKubeYaml("configmap", cm, cmYamlPathname)
Expect(err).To(BeNil())
pod := getPod(withCtr(getCtr(withEnvFrom("foo", "configmap"))))
pod := getPod(withCtr(getCtr(withEnvFrom("foo", "configmap", false))))
err = generateKubeYaml("pod", pod, kubeYaml)
Expect(err).To(BeNil())
@ -1098,6 +1166,26 @@ var _ = Describe("Podman play kube", func() {
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() {
pod := getPod()
err := generateKubeYaml("pod", pod, kubeYaml)