From dd80635df06c1ef5e231f06ea8ca6bd2ad9edb5a Mon Sep 17 00:00:00 2001 From: Jakub Dzon Date: Fri, 19 Nov 2021 18:00:14 +0100 Subject: Support env variables based on ConfigMaps sent in payload Fixes #12363 Signed-off-by: Jakub Dzon --- docs/source/markdown/podman-play-kube.1.md | 35 ++++ pkg/domain/infra/abi/play.go | 28 ++- test/e2e/play_kube_test.go | 313 +++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+), 6 deletions(-) diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index 2438f7ad8..2a511f4bd 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -16,6 +16,7 @@ Currently, the supported Kubernetes kinds are: - Pod - Deployment - PersistentVolumeClaim +- ConfigMap `Kubernetes Pods or Deployments` @@ -68,6 +69,40 @@ like: The build will consider `foobar` to be the context directory for the build. If there is an image in local storage called `foobar`, the image will not be built unless the `--build` flag is used. +`Kubernetes ConfigMap` + +Kubernetes ConfigMap can be referred as a source of environment variables in Pods or Deployments. + +For example ConfigMap defined in following YAML: +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + FOO: bar +``` + +can be referred in a Pod in following way: +``` +apiVersion: v1 +kind: Pod +metadata: +... +spec: + containers: + - command: + - top + name: container-1 + image: foobar + envFrom: + - configMapRef: + name: foo + optional: false +``` + +and as a result environment variable `FOO` will be set to `bar` for container `container-1`. + ## OPTIONS #### **--authfile**=*path* diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index d2bb95f7c..bdf22cf0c 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -57,6 +57,8 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en ipIndex := 0 + var configMaps []v1.ConfigMap + // create pod on each document if it is a pod or deployment // any other kube kind will be skipped for _, document := range documentList { @@ -77,7 +79,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en podTemplateSpec.ObjectMeta = podYAML.ObjectMeta podTemplateSpec.Spec = podYAML.Spec - r, err := ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options, &ipIndex, podYAML.Annotations) + r, err := ic.playKubePod(ctx, podTemplateSpec.ObjectMeta.Name, &podTemplateSpec, options, &ipIndex, podYAML.Annotations, configMaps) if err != nil { return nil, err } @@ -91,7 +93,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return nil, errors.Wrapf(err, "unable to read YAML %q as Kube Deployment", path) } - r, err := ic.playKubeDeployment(ctx, &deploymentYAML, options, &ipIndex) + r, err := ic.playKubeDeployment(ctx, &deploymentYAML, options, &ipIndex, configMaps) if err != nil { return nil, err } @@ -112,6 +114,13 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en report.Volumes = append(report.Volumes, r.Volumes...) validKinds++ + case "ConfigMap": + var configMap v1.ConfigMap + + if err := yaml.Unmarshal(document, &configMap); err != nil { + return nil, errors.Wrapf(err, "unable to read YAML %q as Kube ConfigMap", path) + } + configMaps = append(configMaps, configMap) default: logrus.Infof("Kube kind %s not supported", kind) continue @@ -125,7 +134,7 @@ func (ic *ContainerEngine) PlayKube(ctx context.Context, path string, options en return report, nil } -func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAML *v1apps.Deployment, options entities.PlayKubeOptions, ipIndex *int) (*entities.PlayKubeReport, error) { +func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAML *v1apps.Deployment, options entities.PlayKubeOptions, ipIndex *int, configMaps []v1.ConfigMap) (*entities.PlayKubeReport, error) { var ( deploymentName string podSpec v1.PodTemplateSpec @@ -147,7 +156,7 @@ func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAM // create "replicas" number of pods for i = 0; i < numReplicas; i++ { podName := fmt.Sprintf("%s-pod-%d", deploymentName, i) - podReport, err := ic.playKubePod(ctx, podName, &podSpec, options, ipIndex, deploymentYAML.Annotations) + podReport, err := ic.playKubePod(ctx, podName, &podSpec, options, ipIndex, deploymentYAML.Annotations, configMaps) if err != nil { return nil, errors.Wrapf(err, "error encountered while bringing up pod %s", podName) } @@ -156,7 +165,7 @@ func (ic *ContainerEngine) playKubeDeployment(ctx context.Context, deploymentYAM return &report, nil } -func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec, options entities.PlayKubeOptions, ipIndex *int, annotations map[string]string) (*entities.PlayKubeReport, error) { +func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podYAML *v1.PodTemplateSpec, options entities.PlayKubeOptions, ipIndex *int, annotations map[string]string, configMaps []v1.ConfigMap) (*entities.PlayKubeReport, error) { var ( writer io.Writer playKubePod entities.PlayKubePod @@ -252,7 +261,10 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY ctrRestartPolicy = define.RestartPolicyAlways } - configMaps := []v1.ConfigMap{} + configMapIndex := make(map[string]struct{}) + for _, configMap := range configMaps { + configMapIndex[configMap.Name] = struct{}{} + } for _, p := range options.ConfigMaps { f, err := os.Open(p) if err != nil { @@ -265,6 +277,10 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY return nil, errors.Wrapf(err, "%q", p) } + if _, present := configMapIndex[cm.Name]; present { + return nil, errors.Errorf("ambiguous configuration: the same config map %s is present in YAML and in --configmaps %s file", cm.Name, p) + } + configMaps = append(configMaps, cm) } diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 64b46756f..1a3b5f8df 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -2,8 +2,13 @@ package integration import ( "bytes" + "context" "fmt" + "github.com/containers/podman/v3/pkg/bindings" + "github.com/containers/podman/v3/pkg/bindings/play" "io/ioutil" + "net" + "net/url" "os" "path/filepath" "strconv" @@ -436,6 +441,41 @@ spec: {{ end }} env: - name: HOSTNAME + {{ range .Env }} + - name: {{ .Name }} + {{ if (eq .ValueFrom "configmap") }} + valueFrom: + configMapKeyRef: + name: {{ .RefName }} + key: {{ .RefKey }} + optional: {{ .Optional }} + {{ end }} + {{ if (eq .ValueFrom "secret") }} + valueFrom: + secretKeyRef: + name: {{ .RefName }} + key: {{ .RefKey }} + optional: {{ .Optional }} + {{ end }} + {{ if (eq .ValueFrom "") }} + value: {{ .Value }} + {{ end }} + {{ end }} + {{ with .EnvFrom}} + envFrom: + {{ range . }} + {{ if (eq .From "configmap") }} + - configMapRef: + name: {{ .Name }} + optional: {{ .Optional }} + {{ end }} + {{ if (eq .From "secret") }} + - secretRef: + name: {{ .Name }} + optional: {{ .Optional }} + {{ end }} + {{ end }} + {{ end }} image: {{ .Image }} name: {{ .Name }} imagePullPolicy: {{ .PullPolicy }} @@ -2908,4 +2948,277 @@ ENV OPENJ9_JAVA_OPTIONS=%q }) }) + Context("with configmap in multi-doc yaml", func() { + It("podman play kube uses env value", func() { + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "FOO", false)))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`)) + }) + + It("podman play kube fails for required env value with missing key", func() { + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "MISSING_KEY", false)))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).To(ExitWithError()) + }) + + It("podman play kube succeeds for optional env value with missing key", func() { + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "MISSING_KEY", true)))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ range .Config.Env }}[{{ . }}]{{end}}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(`[FOO=]`)) + }) + + It("podman play kube uses all key-value pairs as envs", func() { + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO1", "foo1"), withConfigMapData("FOO2", "foo2")) + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr(withEnvFrom("foo", "configmap", false)))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(`FOO1=foo1`)) + Expect(inspect.OutputToString()).To(ContainSubstring(`FOO2=foo2`)) + }) + + It("podman play kube deployment uses variable from config map", func() { + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "FOO", false)))) + + deployment := getDeployment(withPod(pod)) + deploymentYaml, err := getKubeYaml("deployment", deployment) + yamls := []string{cmYaml, deploymentYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", fmt.Sprintf("%s-%s-%s", deployment.Name, "pod-0", defaultCtrName), "--format", "'{{ .Config }}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`)) + + }) + + It("podman play kube uses env value from configmap for HTTP API client", func() { + SkipIfRemote("cannot run in a remote setup") + address := url.URL{ + Scheme: "tcp", + Host: net.JoinHostPort("localhost", randomPort()), + } + + session := podmanTest.Podman([]string{ + "system", "service", "--log-level=debug", "--time=0", address.String(), + }) + defer session.Kill() + + WaitForService(address) + + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr(withEnv("FOO", "", "configmap", "foo", "FOO", false)))) + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + podmanConnection, err := bindings.NewConnection(context.Background(), address.String()) + Expect(err).ToNot(HaveOccurred()) + + _, err = play.Kube(podmanConnection, kubeYaml, nil) + Expect(err).ToNot(HaveOccurred()) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(`FOO=foo`)) + }) + }) + + Context("with configmap in multi-doc yaml and files", func() { + It("podman play kube uses env values from both sources", func() { + SkipIfRemote("--configmaps is not supported for remote") + + fsCmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml") + fsCm := getConfigMap(withConfigMapName("fooFs"), withConfigMapData("FOO_FS", "fooFS")) + err := generateKubeYaml("configmap", fsCm, fsCmYamlPathname) + Expect(err).To(BeNil()) + + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr( + withEnv("FOO_FS", "", "configmap", "fooFs", "FOO_FS", false), + withEnv("FOO", "", "configmap", "foo", "FOO", false), + ))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml, "--configmap", fsCmYamlPathname}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(And( + ContainSubstring(`FOO=foo`), + ContainSubstring(`FOO_FS=fooFS`), + )) + }) + + It("podman play kube uses all env values from both sources", func() { + SkipIfRemote("--configmaps is not supported for remote") + + fsCmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml") + fsCm := getConfigMap(withConfigMapName("fooFs"), + withConfigMapData("FOO_FS_1", "fooFS1"), + withConfigMapData("FOO_FS_2", "fooFS2")) + err := generateKubeYaml("configmap", fsCm, fsCmYamlPathname) + Expect(err).To(BeNil()) + + cm := getConfigMap(withConfigMapName("foo"), + withConfigMapData("FOO_1", "foo1"), + withConfigMapData("FOO_2", "foo2"), + ) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr( + withEnvFrom("foo", "configmap", false), + withEnvFrom("fooFs", "configmap", false), + ))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml, "--configmap", fsCmYamlPathname}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0)) + + inspect := podmanTest.Podman([]string{"inspect", getCtrNameInPod(pod), "--format", "'{{ .Config.Env }}'"}) + inspect.WaitWithDefaultTimeout() + Expect(inspect).Should(Exit(0)) + Expect(inspect.OutputToString()).To(And( + ContainSubstring(`FOO_1=foo1`), + ContainSubstring(`FOO_2=foo2`), + ContainSubstring(`FOO_FS_1=fooFS1`), + ContainSubstring(`FOO_FS_2=fooFS2`), + )) + }) + + It("podman play kube reports error when the same configmap name is present in both sources", func() { + SkipIfRemote("--configmaps is not supported for remote") + + fsCmYamlPathname := filepath.Join(podmanTest.TempDir, "foo-cm.yaml") + fsCm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "fooFS")) + err := generateKubeYaml("configmap", fsCm, fsCmYamlPathname) + Expect(err).To(BeNil()) + + cm := getConfigMap(withConfigMapName("foo"), withConfigMapData("FOO", "foo")) + + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).To(BeNil()) + + pod := getPod(withCtr(getCtr( + withEnv("FOO", "", "configmap", "foo", "FOO", false), + ))) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).To(BeNil()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).To(BeNil()) + + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml, "--configmap", fsCmYamlPathname}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(125)) + Expect(kube.ErrorToString()).To(ContainSubstring("ambiguous configuration: the same config map foo is present in YAML and in --configmaps")) + }) + }) }) -- cgit v1.2.3-54-g00ecf