From 3e48d74c838f694d56942ce500d28e51a32cdc12 Mon Sep 17 00:00:00 2001 From: Charlie Doern Date: Fri, 16 Dec 2022 20:26:54 -0500 Subject: [PATCH] Add support for hostPath and configMap subpath usage podman play kube now supports and has tests for the subpath field when using a hostPath volume type and a configMap volume type. The hostpath works similarly to the named volume, allowing a user to specify a whole directory but also a specific file or subdir within that mount. Config Maps operate the same way but specifically allow users to mount specific data in a subpath alongside the existing data resolves #16828 Signed-off-by: Charlie Doern --- pkg/specgen/generate/kube/kube.go | 16 +++-- test/e2e/play_kube_test.go | 104 ++++++++++++++++++++++++------ 2 files changed, 98 insertions(+), 22 deletions(-) diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index fa76b55438..84ac4ce187 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -8,6 +8,7 @@ import ( "math" "net" "os" + "path/filepath" "regexp" "runtime" "strconv" @@ -370,6 +371,10 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener } volume.MountPath = dest + path := volumeSource.Source + if len(volume.SubPath) > 0 { + path = filepath.Join(path, volume.SubPath) + } switch volumeSource.Type { case KubeVolumeTypeBindMount: // If the container has bind mounts, we need to check if @@ -378,14 +383,14 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener // Make sure the z/Z option is not already there (from editing the YAML) if k == define.BindMountPrefix { lastIndex := strings.LastIndex(v, ":") - if v[:lastIndex] == volumeSource.Source && !cutil.StringInSlice("z", options) && !cutil.StringInSlice("Z", options) { + if v[:lastIndex] == path && !cutil.StringInSlice("z", options) && !cutil.StringInSlice("Z", options) { options = append(options, v[lastIndex+1:]) } } } mount := spec.Mount{ Destination: volume.MountPath, - Source: volumeSource.Source, + Source: path, Type: "bind", Options: options, } @@ -403,13 +408,14 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener Dest: volume.MountPath, Name: volumeSource.Source, Options: options, + SubPath: volume.SubPath, } s.Volumes = append(s.Volumes, &cmVolume) case KubeVolumeTypeCharDevice: // We are setting the path as hostPath:mountPath to comply with pkg/specgen/generate.DeviceFromPath. // The type is here just to improve readability as it is not taken into account when the actual device is created. device := spec.LinuxDevice{ - Path: fmt.Sprintf("%s:%s", volumeSource.Source, volume.MountPath), + Path: fmt.Sprintf("%s:%s", path, volume.MountPath), Type: "c", } s.Devices = append(s.Devices, device) @@ -417,7 +423,7 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener // We are setting the path as hostPath:mountPath to comply with pkg/specgen/generate.DeviceFromPath. // The type is here just to improve readability as it is not taken into account when the actual device is created. device := spec.LinuxDevice{ - Path: fmt.Sprintf("%s:%s", volumeSource.Source, volume.MountPath), + Path: fmt.Sprintf("%s:%s", path, volume.MountPath), Type: "b", } s.Devices = append(s.Devices, device) @@ -428,6 +434,7 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener Dest: volume.MountPath, Name: volumeSource.Source, Options: options, + SubPath: volume.SubPath, } s.Volumes = append(s.Volumes, &secretVolume) case KubeVolumeTypeEmptyDir: @@ -436,6 +443,7 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener Name: volumeSource.Source, Options: options, IsAnonymous: true, + SubPath: volume.SubPath, } s.Volumes = append(s.Volumes, &emptyDirVolume) default: diff --git a/test/e2e/play_kube_test.go b/test/e2e/play_kube_test.go index 6d752279fc..93e1abfd7c 100644 --- a/test/e2e/play_kube_test.go +++ b/test/e2e/play_kube_test.go @@ -247,7 +247,7 @@ spec: - containerPort: 80 ` -var subpathTest = ` +var subpathTestNamedVolume = ` apiVersion: v1 kind: Pod metadata: @@ -614,6 +614,7 @@ spec: {{ if .VolumeMount }} - name: {{.VolumeName}} mountPath: {{ .VolumeMountPath }} + subPath: {{ .VolumeSubPath }} readonly: {{.VolumeReadOnly}} {{ end }} {{ end }} @@ -1169,6 +1170,7 @@ type Ctr struct { VolumeMount bool VolumeMountPath string VolumeName string + VolumeSubPath string VolumeReadOnly bool Env []Env EnvFrom []EnvFrom @@ -1196,6 +1198,7 @@ func getCtr(options ...ctrOption) *Ctr { VolumeMountPath: "", VolumeName: "", VolumeReadOnly: false, + VolumeSubPath: "", Env: []Env{}, EnvFrom: []EnvFrom{}, InitCtrType: "", @@ -1307,12 +1310,15 @@ func withHostIP(ip string, port string) ctrOption { } } -func withVolumeMount(mountPath string, readonly bool) ctrOption { +func withVolumeMount(mountPath, subpath string, readonly bool) ctrOption { return func(c *Ctr) { c.VolumeMountPath = mountPath c.VolumeName = defaultVolName c.VolumeReadOnly = readonly c.VolumeMount = true + if len(subpath) > 0 { + c.VolumeSubPath = subpath + } } } @@ -1404,7 +1410,7 @@ func getPersistentVolumeClaimVolume(vName string) *Volume { // getConfigMap returns a new ConfigMap Volume given the name and items // of the ConfigMap. -func getConfigMapVolume(vName string, items []map[string]string, optional bool) *Volume { +func getConfigMapVolume(vName string, items []map[string]string, optional bool) *Volume { //nolint:unparam return &Volume{ VolumeType: "ConfigMap", Name: defaultVolName, @@ -2862,7 +2868,7 @@ spec: Expect(err).ToNot(HaveOccurred()) f.Close() - ctr := getCtr(withVolumeMount(hostPathLocation, true), withImage(BB)) + ctr := getCtr(withVolumeMount(hostPathLocation, "", true), withImage(BB)) pod := getPod(withVolume(getHostPathVolume("File", hostPathLocation)), withCtr(ctr)) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -2902,7 +2908,7 @@ VOLUME %s`, ALPINE, hostPathDir+"/") podmanTest.BuildImage(containerfile, image, "false") // Create and play kube pod - ctr := getCtr(withVolumeMount(hostPathDir+"/", false), withImage(image)) + ctr := getCtr(withVolumeMount(hostPathDir+"/", "", false), withImage(image)) pod := getPod(withCtr(ctr), withVolume(getHostPathVolume("Directory", hostPathDir+"/"))) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -2930,7 +2936,7 @@ VOLUME %s`, ALPINE, hostPathDir+"/") It("podman play kube test with PersistentVolumeClaim volume", func() { volumeName := "namedVolume" - ctr := getCtr(withVolumeMount("/test", false), withImage(BB)) + ctr := getCtr(withVolumeMount("/test", "", false), withImage(BB)) pod := getPod(withVolume(getPersistentVolumeClaimVolume(volumeName)), withCtr(ctr)) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -2953,7 +2959,7 @@ VOLUME %s`, ALPINE, hostPathDir+"/") cmYaml, err := getKubeYaml("configmap", cm) Expect(err).ToNot(HaveOccurred()) - ctr := getCtr(withVolumeMount("/test", false), withImage(BB)) + ctr := getCtr(withVolumeMount("/test", "", false), withImage(BB)) pod := getPod(withVolume(getConfigMapVolume(volumeName, []map[string]string{}, false)), withCtr(ctr)) podYaml, err := getKubeYaml("pod", pod) Expect(err).ToNot(HaveOccurred()) @@ -2981,7 +2987,7 @@ VOLUME %s`, ALPINE, hostPathDir+"/") "path": "BAR", }} - ctr := getCtr(withVolumeMount("/test", false), withImage(BB)) + ctr := getCtr(withVolumeMount("/test", "", false), withImage(BB)) pod := getPod(withVolume(getConfigMapVolume(volumeName, volumeContents, false)), withCtr(ctr)) podYaml, err := getKubeYaml("pod", pod) Expect(err).ToNot(HaveOccurred()) @@ -3006,7 +3012,7 @@ VOLUME %s`, ALPINE, hostPathDir+"/") It("podman play kube with a missing optional ConfigMap volume", func() { volumeName := "cmVol" - ctr := getCtr(withVolumeMount("/test", false), withImage(BB)) + ctr := getCtr(withVolumeMount("/test", "", false), withImage(BB)) pod := getPod(withVolume(getConfigMapVolume(volumeName, []map[string]string{}, true)), withCtr(ctr)) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -3020,8 +3026,8 @@ VOLUME %s`, ALPINE, hostPathDir+"/") podName := "test-pod" ctrName1 := "vol-test-ctr" ctrName2 := "vol-test-ctr-2" - ctr1 := getCtr(withVolumeMount("/test-emptydir", false), withImage(BB), withName(ctrName1)) - ctr2 := getCtr(withVolumeMount("/test-emptydir-2", false), withImage(BB), withName(ctrName2)) + ctr1 := getCtr(withVolumeMount("/test-emptydir", "", false), withImage(BB), withName(ctrName1)) + ctr2 := getCtr(withVolumeMount("/test-emptydir-2", "", false), withImage(BB), withName(ctrName2)) pod := getPod(withPodName(podName), withVolume(getEmptyDirVolume()), withCtr(ctr1), withCtr(ctr2)) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -3346,7 +3352,7 @@ spec: deploymentName := "multiFoo" podName := "multiFoo" ctrName := "ctr-01" - ctr := getCtr(withVolumeMount("/test", false)) + ctr := getCtr(withVolumeMount("/test", "", false)) ctr.Name = ctrName pod := getPod(withPodName(podName), withVolume(getPersistentVolumeClaimVolume(volName)), withCtr(ctr)) deployment := getDeployment(withPod(pod)) @@ -4233,7 +4239,7 @@ ENV OPENJ9_JAVA_OPTIONS=%q blockVolume := getHostPathVolume("BlockDevice", devicePath) - pod := getPod(withVolume(blockVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, false)))) + pod := getPod(withVolume(blockVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, "", false)))) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -4272,7 +4278,7 @@ ENV OPENJ9_JAVA_OPTIONS=%q charVolume := getHostPathVolume("CharDevice", devicePath) - pod := getPod(withVolume(charVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, false)))) + pod := getPod(withVolume(charVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, "", false)))) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -4300,7 +4306,7 @@ ENV OPENJ9_JAVA_OPTIONS=%q blockVolume := getHostPathVolume("BlockDevice", devicePath) - pod := getPod(withVolume(blockVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, false)))) + pod := getPod(withVolume(blockVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, "", false)))) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -4326,7 +4332,7 @@ ENV OPENJ9_JAVA_OPTIONS=%q charVolume := getHostPathVolume("BlockDevice", devicePath) - pod := getPod(withVolume(charVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, false)))) + pod := getPod(withVolume(charVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, "", false)))) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -4351,7 +4357,7 @@ ENV OPENJ9_JAVA_OPTIONS=%q charVolume := getHostPathVolume("CharDevice", devicePath) - pod := getPod(withVolume(charVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, false)))) + pod := getPod(withVolume(charVolume), withCtr(getCtr(withImage(REGISTRY_IMAGE), withCmd(nil), withArg(nil), withVolumeMount(devicePath, "", false)))) err = generateKubeYaml("pod", pod, kubeYaml) Expect(err).ToNot(HaveOccurred()) @@ -4494,7 +4500,7 @@ spec: volumeImp.WaitWithDefaultTimeout() Expect(volumeImp).Should(Exit(0)) - err = writeYaml(subpathTest, kubeYaml) + err = writeYaml(subpathTestNamedVolume, kubeYaml) Expect(err).ToNot(HaveOccurred()) playKube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) @@ -4506,4 +4512,66 @@ spec: Expect(exec).Should(Exit(0)) Expect(exec.OutputToString()).Should(Equal("hi")) }) + + It("podman play kube with hostPath subpaths", func() { + if !Containerized() { + Skip("something is wrong with file permissions in CI or in the yaml creation. cannot ls or cat the fs unless in a container") + } + + hostPathLocation := podmanTest.TempDir + Expect(os.MkdirAll(filepath.Join(hostPathLocation, "testing", "onlythis"), 0755)).To(Succeed()) + file, err := os.Create(filepath.Join(hostPathLocation, "testing", "onlythis", "123.txt")) + Expect(err).ToNot(HaveOccurred()) + + _, err = file.Write([]byte("hi")) + Expect(err).ToNot(HaveOccurred()) + + err = file.Close() + Expect(err).ToNot(HaveOccurred()) + + pod := getPod(withPodName("testpod"), withCtr(getCtr(withImage(ALPINE), withName("testctr"), withCmd([]string{"top"}), withVolumeMount("/var", "testing/onlythis", false))), withVolume(getHostPathVolume("DirectoryOrCreate", hostPathLocation))) + + err = generateKubeYaml("pod", pod, kubeYaml) + Expect(err).To(Not(HaveOccurred())) + playKube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + playKube.WaitWithDefaultTimeout() + Expect(playKube).Should(Exit(0)) + exec := podmanTest.Podman([]string{"exec", "-it", "testpod-testctr", "ls", "/var"}) + exec.WaitWithDefaultTimeout() + Expect(exec).Should(Exit(0)) + Expect(exec.OutputToString()).Should(ContainSubstring("123.txt")) + }) + + It("podman play kube with configMap subpaths", func() { + volumeName := "cmVol" + cm := getConfigMap(withConfigMapName(volumeName), withConfigMapData("FOO", "foobar")) + cmYaml, err := getKubeYaml("configmap", cm) + Expect(err).ToNot(HaveOccurred()) + volumeContents := []map[string]string{{ + "key": "FOO", + "path": "BAR", + }} + + ctr := getCtr(withPullPolicy("always"), withName("testctr"), withCmd([]string{"top"}), withVolumeMount("/etc/BAR", "BAR", false), withImage(ALPINE)) + pod := getPod(withPodName("testpod"), withVolume(getConfigMapVolume(volumeName, volumeContents, false)), withCtr(ctr)) + + podYaml, err := getKubeYaml("pod", pod) + Expect(err).ToNot(HaveOccurred()) + + yamls := []string{cmYaml, podYaml} + err = generateMultiDocKubeYaml(yamls, kubeYaml) + Expect(err).ToNot(HaveOccurred()) + + out, _ := os.ReadFile(kubeYaml) + kube := podmanTest.Podman([]string{"play", "kube", kubeYaml}) + kube.WaitWithDefaultTimeout() + Expect(kube).Should(Exit(0), string(out)) + + exec := podmanTest.Podman([]string{"exec", "-it", "testpod-testctr", "ls", "/etc/"}) + exec.WaitWithDefaultTimeout() + Expect(exec).Should(Exit(0)) + Expect(exec.OutputToString()).ShouldNot(HaveLen(3)) + Expect(exec.OutputToString()).Should(ContainSubstring("BAR")) + // we want to check that we can mount a subpath but not replace the entire dir + }) })