From 633ae014e6945670676e4118356d09418c678138 Mon Sep 17 00:00:00 2001 From: Jordan Williams Date: Wed, 17 Mar 2021 13:39:33 -0500 Subject: [PATCH] Generate Kubernetes PersistentVolumeClaims from named volumes Fixes #5788 This commit adds support for named volumes in podman-generate-kube. Named volumes are output in the YAML as PersistentVolumeClaims. To avoid naming conflicts, the volume name is suffixed with "-pvc". This commit adds a corresponding suffix for host path mounts. Host path volumes are suffixed with "-host". Signed-off-by: Jordan Williams --- .../source/markdown/podman-generate-kube.1.md | 115 +++++++++++++++++- docs/source/markdown/podman-play-kube.1.md | 4 +- libpod/kube.go | 32 ++++- test/e2e/generate_kube_test.go | 30 +++++ 4 files changed, 174 insertions(+), 7 deletions(-) diff --git a/docs/source/markdown/podman-generate-kube.1.md b/docs/source/markdown/podman-generate-kube.1.md index 019bae5398..0e5a31ae63 100644 --- a/docs/source/markdown/podman-generate-kube.1.md +++ b/docs/source/markdown/podman-generate-kube.1.md @@ -6,10 +6,14 @@ podman-generate-kube - Generate Kubernetes YAML based on a pod or container **podman generate kube** [*options*] *container...* | *pod* ## DESCRIPTION -**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from Podman one or more containers or a single pod. Whether +**podman generate kube** will generate Kubernetes Pod YAML (v1 specification) from Podman from one or more containers or a single pod. Whether the input is for containers or a pod, Podman will always generate the specification as a Pod. The input may be in the form of a pod or one or more container names or IDs. +Volumes appear in the generated YAML according to two different volume types. Bind-mounted volumes become *hostPath* volume types and named volumes become *persistentVolumeClaim* volume types. Generated *hostPath* volume types will be one of three subtypes depending on the state of the host path: *DirectoryOrCreate* when no file or directory exists at the host, *Directory* when host path is a directory, or *File* when host path is a file. The value for *claimName* for a *persistentVolumeClaim* is the name of the named volume registered in Podman. + +Potential name conflicts between volumes are avoided by using a standard naming scheme for each volume type. The *hostPath* volume types are named according to the path on the host machine, replacing forward slashes with hyphens less any leading and trailing forward slashes. The special case of the filesystem root, `/`, translates to the name `root`. Additionally, the name is suffixed with `-host` to avoid naming conflicts with *persistentVolumeClaim* volumes. Each *persistentVolumeClaim* volume type uses the name of its associated named volume suffixed with `-pvc`. + Note that the generated Kubernetes YAML file can be used to re-run the deployment via podman-play-kube(1). ## OPTIONS @@ -25,7 +29,7 @@ random port is assigned by Podman in the specification. ## EXAMPLES -Create Kubernetes Pod YAML for a container called `some-mariadb` . +Create Kubernetes Pod YAML for a container called `some-mariadb`. ``` $ sudo podman generate kube some-mariadb # Generation of Kubernetes YAML is still under development! @@ -81,6 +85,113 @@ spec: status: {} ``` +Create Kubernetes Pod YAML for a container with the directory `/home/user/my-data` on the host bind-mounted in the container to `/volume`. +``` +$ podman generate kube my-container-with-bind-mounted-data +# Generation of Kubernetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-3.1.0-dev +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2021-03-18T16:26:08Z" + labels: + app: my-container-with-bind-mounted-data + name: my-container-with-bind-mounted-data +spec: + containers: + - command: + - /bin/sh + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: container + value: podman + image: docker.io/library/alpine:latest + name: test-bind-mount + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + volumeMounts: + - mountPath: /volume + name: home-user-my-data-host + workingDir: / + dnsConfig: {} + restartPolicy: Never + volumes: + - hostPath: + path: /home/user/my-data + type: Directory + name: home-user-my-data-host +status: {} +``` + +Create Kubernetes Pod YAML for a container with the named volume `priceless-data` mounted in the container at `/volume`. +``` +$ podman generate kube my-container-using-priceless-data +# Generation of Kubernetes YAML is still under development! +# +# Save the output of this file and use kubectl create -f to import +# it into Kubernetes. +# +# Created with podman-3.1.0-dev +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: "2021-03-18T16:26:08Z" + labels: + app: my-container-using-priceless-data + name: my-container-using-priceless-data +spec: + containers: + - command: + - /bin/sh + env: + - name: PATH + value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + - name: TERM + value: xterm + - name: container + value: podman + image: docker.io/library/alpine:latest + name: test-bind-mount + resources: {} + securityContext: + allowPrivilegeEscalation: true + capabilities: + drop: + - CAP_MKNOD + - CAP_NET_RAW + - CAP_AUDIT_WRITE + privileged: false + readOnlyRootFilesystem: false + seLinuxOptions: {} + volumeMounts: + - mountPath: /volume + name: priceless-data-pvc + workingDir: / + dnsConfig: {} + restartPolicy: Never + volumes: + - name: priceless-data-pvc + persistentVolumeClaim: + claimName: priceless-data +status: {} +``` + Create Kubernetes Pod YAML for a pod called `demoweb` and include a service. ``` $ sudo podman generate kube -s demoweb diff --git a/docs/source/markdown/podman-play-kube.1.md b/docs/source/markdown/podman-play-kube.1.md index 0a34a622f7..1be597b94a 100644 --- a/docs/source/markdown/podman-play-kube.1.md +++ b/docs/source/markdown/podman-play-kube.1.md @@ -11,7 +11,9 @@ podman-play-kube - Create pods and containers based on Kubernetes YAML Ideally the input file would be one created by Podman (see podman-generate-kube(1)). This would guarantee a smooth import and expected results. -Note: HostPath volume types created by play kube will be given an SELinux private label (Z) +Only two volume types are supported by play kube, the *hostPath* and *persistentVolumeClaim* volume types. For the *hostPath* volume type, only the *default (empty)*, *DirectoryOrCreate*, *Directory*, *FileOrCreate*, *File*, and *Socket* subtypes are supported. The *CharDevice* and *BlockDevice* subtypes are not supported. Podman interprets the value of *hostPath* *path* as a file path when it contains at least one forward slash, otherwise Podman treats the value as the name of a named volume. When using a *persistentVolumeClaim*, the value for *claimName* is the name for the Podman named volume. + +Note: *hostPath* volume types created by play kube will be given an SELinux private label (Z) Note: If the `:latest` tag is used, Podman will attempt to pull the image from a registry. If the image was built locally with Podman or Buildah, it will have `localhost` as the domain, in that case, Podman will use the image from the local store even if it has the `:latest` tag. diff --git a/libpod/kube.go b/libpod/kube.go index 407c4ae002..b4dd4f10a8 100644 --- a/libpod/kube.go +++ b/libpod/kube.go @@ -330,8 +330,6 @@ func containerToV1Container(c *Container) (v1.Container, []v1.Volume, *v1.PodDNS } if len(c.config.UserVolumes) > 0 { - // TODO When we until we can resolve what the volume name should be, this is disabled - // Volume names need to be coordinated "globally" in the kube files. volumeMounts, volumes, err := libpodMountsToKubeVolumeMounts(c) if err != nil { return kubeContainer, kubeVolumes, nil, err @@ -493,8 +491,7 @@ func libpodEnvVarsToKubeEnvVars(envs []string) ([]v1.EnvVar, error) { // libpodMountsToKubeVolumeMounts converts the containers mounts to a struct kube understands func libpodMountsToKubeVolumeMounts(c *Container) ([]v1.VolumeMount, []v1.Volume, error) { - // TODO when named volumes are supported in play kube, also parse named volumes here - _, mounts := c.sortUserVolumes(c.config.Spec) + namedVolumes, mounts := c.sortUserVolumes(c.config.Spec) vms := make([]v1.VolumeMount, 0, len(mounts)) vos := make([]v1.Volume, 0, len(mounts)) for _, m := range mounts { @@ -505,9 +502,34 @@ func libpodMountsToKubeVolumeMounts(c *Container) ([]v1.VolumeMount, []v1.Volume vms = append(vms, vm) vos = append(vos, vo) } + for _, v := range namedVolumes { + vm, vo := generateKubePersistentVolumeClaim(v) + vms = append(vms, vm) + vos = append(vos, vo) + } return vms, vos, nil } +// generateKubePersistentVolumeClaim converts a ContainerNamedVolume to a Kubernetes PersistentVolumeClaim +func generateKubePersistentVolumeClaim(v *ContainerNamedVolume) (v1.VolumeMount, v1.Volume) { + ro := util.StringInSlice("ro", v.Options) + + // To avoid naming conflicts with any host path mounts, add a unique suffix to the volume's name. + name := v.Name + "-pvc" + + vm := v1.VolumeMount{} + vm.Name = name + vm.MountPath = v.Dest + vm.ReadOnly = ro + + pvc := v1.PersistentVolumeClaimVolumeSource{ClaimName: v.Name, ReadOnly: ro} + vs := v1.VolumeSource{} + vs.PersistentVolumeClaim = &pvc + vo := v1.Volume{Name: name, VolumeSource: vs} + + return vm, vo +} + // generateKubeVolumeMount takes a user specified mount and returns // a kubernetes VolumeMount (to be added to the container) and a kubernetes Volume // (to be added to the pod) @@ -519,6 +541,8 @@ func generateKubeVolumeMount(m specs.Mount) (v1.VolumeMount, v1.Volume, error) { if err != nil { return vm, vo, err } + // To avoid naming conflicts with any persistent volume mounts, add a unique suffix to the volume's name. + name += "-host" vm.Name = name vm.MountPath = m.Destination if util.StringInSlice("ro", m.Options) { diff --git a/test/e2e/generate_kube_test.go b/test/e2e/generate_kube_test.go index e741927462..1c53307bd0 100644 --- a/test/e2e/generate_kube_test.go +++ b/test/e2e/generate_kube_test.go @@ -495,6 +495,36 @@ var _ = Describe("Podman generate kube", func() { Expect(inspect.OutputToString()).To(ContainSubstring(vol1)) }) + It("podman generate kube with persistent volume claim", func() { + vol := "vol-test-persistent-volume-claim" + + // we need a container name because IDs don't persist after rm/play + ctrName := "test-persistent-volume-claim" + ctrNameInKubePod := "test1-test-persistent-volume-claim" + + session := podmanTest.Podman([]string{"run", "-d", "--pod", "new:test1", "--name", ctrName, "-v", vol + ":/volume/:z", "alpine", "top"}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(Equal(0)) + + outputFile := filepath.Join(podmanTest.RunRoot, "pod.yaml") + kube := podmanTest.Podman([]string{"generate", "kube", "test1", "-f", outputFile}) + kube.WaitWithDefaultTimeout() + Expect(kube.ExitCode()).To(Equal(0)) + + rm := podmanTest.Podman([]string{"pod", "rm", "-f", "test1"}) + rm.WaitWithDefaultTimeout() + Expect(rm.ExitCode()).To(Equal(0)) + + play := podmanTest.Podman([]string{"play", "kube", outputFile}) + play.WaitWithDefaultTimeout() + Expect(play.ExitCode()).To(Equal(0)) + + inspect := podmanTest.Podman([]string{"inspect", ctrNameInKubePod}) + inspect.WaitWithDefaultTimeout() + Expect(inspect.ExitCode()).To(Equal(0)) + Expect(inspect.OutputToString()).To(ContainSubstring(vol)) + }) + It("podman generate kube sharing pid namespace", func() { podName := "test" podSession := podmanTest.Podman([]string{"pod", "create", "--name", podName, "--share", "pid"})