From 693ae0ebc60d648b870eb25c6d2fb155c280da61 Mon Sep 17 00:00:00 2001 From: Matt Heon Date: Thu, 18 Apr 2024 14:56:19 -0400 Subject: [PATCH 1/2] Add support for image volume subpaths Image volumes (the `--mount type=image,...` kind, not the `podman volume create --driver image ...` kind - it's strange that we have two) are needed for our automount scheme, but the request is that we mount only specific subpaths from the image into the container. To do that, we need image volume subpath support. Not that difficult code-wise, mostly just plumbing. Also, add support to the CLI; not strictly necessary, but it doesn't hurt anything and will make testing easier. Signed-off-by: Matt Heon --- docs/source/markdown/options/mount.md | 2 ++ libpod/container.go | 2 ++ libpod/container_internal_common.go | 16 ++++++++++++++-- libpod/options.go | 1 + pkg/specgen/generate/container_create.go | 1 + pkg/specgen/volumes.go | 3 +++ pkg/specgenutil/volumes.go | 8 ++++++++ test/e2e/run_volume_test.go | 16 ++++++++++++++++ 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/source/markdown/options/mount.md b/docs/source/markdown/options/mount.md index 9a14b39fd1..7114d74952 100644 --- a/docs/source/markdown/options/mount.md +++ b/docs/source/markdown/options/mount.md @@ -41,6 +41,8 @@ Options specific to type=**image**: - *rw*, *readwrite*: *true* or *false* (default if unspecified: *false*). +- *subpath*: Mount only a specific path within the image, instead of the whole image. + Options specific to **bind** and **glob**: - *ro*, *readonly*: *true* or *false* (default if unspecified: *false*). diff --git a/libpod/container.go b/libpod/container.go index 00616ccfa3..8e188b33c4 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -275,6 +275,8 @@ type ContainerImageVolume struct { Dest string `json:"dest"` // ReadWrite sets the volume writable. ReadWrite bool `json:"rw"` + // SubPath determines which part of the image will be mounted into the container. + SubPath string `json:"subPath,omitempty"` } // ContainerSecret is a secret that is mounted in a container diff --git a/libpod/container_internal_common.go b/libpod/container_internal_common.go index 2ec7c0040e..78dd31b39c 100644 --- a/libpod/container_internal_common.go +++ b/libpod/container_internal_common.go @@ -459,11 +459,23 @@ func (c *Container) generateSpec(ctx context.Context) (s *spec.Spec, cleanupFunc return nil, nil, fmt.Errorf("failed to create TempDir in the %s directory: %w", c.config.StaticDir, err) } + imagePath := mountPoint + if volume.SubPath != "" { + safeMount, err := c.safeMountSubPath(mountPoint, volume.SubPath) + if err != nil { + return nil, nil, err + } + + safeMounts = append(safeMounts, safeMount) + + imagePath = safeMount.mountPoint + } + var overlayMount spec.Mount if volume.ReadWrite { - overlayMount, err = overlay.Mount(contentDir, mountPoint, volume.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions()) + overlayMount, err = overlay.Mount(contentDir, imagePath, volume.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions()) } else { - overlayMount, err = overlay.MountReadOnly(contentDir, mountPoint, volume.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions()) + overlayMount, err = overlay.MountReadOnly(contentDir, imagePath, volume.Dest, c.RootUID(), c.RootGID(), c.runtime.store.GraphOptions()) } if err != nil { return nil, nil, fmt.Errorf("creating overlay mount for image %q failed: %w", volume.Source, err) diff --git a/libpod/options.go b/libpod/options.go index f4f03f341c..c90d779bde 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -1474,6 +1474,7 @@ func WithImageVolumes(volumes []*ContainerImageVolume) CtrCreateOption { Dest: vol.Dest, Source: vol.Source, ReadWrite: vol.ReadWrite, + SubPath: vol.SubPath, }) } diff --git a/pkg/specgen/generate/container_create.go b/pkg/specgen/generate/container_create.go index 0887eb4c8c..8d4029114b 100644 --- a/pkg/specgen/generate/container_create.go +++ b/pkg/specgen/generate/container_create.go @@ -501,6 +501,7 @@ func createContainerOptions(rt *libpod.Runtime, s *specgen.SpecGenerator, pod *l Dest: v.Destination, Source: v.Source, ReadWrite: v.ReadWrite, + SubPath: v.SubPath, }) } options = append(options, libpod.WithImageVolumes(vols)) diff --git a/pkg/specgen/volumes.go b/pkg/specgen/volumes.go index 075711138c..d2c1e54876 100644 --- a/pkg/specgen/volumes.go +++ b/pkg/specgen/volumes.go @@ -53,6 +53,9 @@ type ImageVolume struct { Destination string // ReadWrite sets the volume writable. ReadWrite bool + // SubPath mounts a particular path within the image. + // If empty, the whole image is mounted. + SubPath string `json:"subPath,omitempty"` } // GenVolumeMounts parses user input into mounts, volumes and overlay volumes diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go index c481867163..510b11254b 100644 --- a/pkg/specgenutil/volumes.go +++ b/pkg/specgenutil/volumes.go @@ -611,6 +611,14 @@ func getImageVolume(args []string) (*specgen.ImageVolume, error) { default: return nil, fmt.Errorf("invalid rw value %q: %w", value, util.ErrBadMntOption) } + case "subpath": + if !hasValue { + return nil, fmt.Errorf("%v: %w", name, errOptionArg) + } + if !filepath.IsAbs(value) { + return nil, fmt.Errorf("volume subpath %q must be an absolute path", value) + } + newVolume.SubPath = value case "consistency": // Often used on MACs and mistakenly on Linux platforms. // Since Docker ignores this option so shall we. diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go index 2cd1881e8e..07cfca1fb3 100644 --- a/test/e2e/run_volume_test.go +++ b/test/e2e/run_volume_test.go @@ -934,4 +934,20 @@ USER testuser`, CITEST_IMAGE) Expect(run).Should(ExitCleanly()) Expect(run.OutputToString()).Should(ContainSubstring(strings.TrimLeft("/vol/", f.Name()))) }) + + It("podman run --mount type=image with subpath", func() { + ctrCommand := []string{"run", "--mount", fmt.Sprintf("type=image,source=%s,dest=/mnt,subpath=/etc", ALPINE), ALPINE, "ls"} + + run1Cmd := append(ctrCommand, "/etc") + run1 := podmanTest.Podman(run1Cmd) + run1.WaitWithDefaultTimeout() + Expect(run1).Should(ExitCleanly()) + + run2Cmd := append(ctrCommand, "/mnt") + run2 := podmanTest.Podman(run2Cmd) + run2.WaitWithDefaultTimeout() + Expect(run2).Should(ExitCleanly()) + + Expect(run1.OutputToString()).Should(Equal(run2.OutputToString())) + }) }) From 30e2c923d6f0b7e9fc46e353288804e3fbbbad67 Mon Sep 17 00:00:00 2001 From: Matt Heon Date: Wed, 17 Apr 2024 08:23:38 -0400 Subject: [PATCH 2/2] Add the ability to automount images as volumes via play Effectively, this is an ability to take an image already pulled to the system, and automatically mount it into one or more containers defined in Kubernetes YAML accepted by `podman play`. Requirements: - The image must already exist in storage. - The image must have at least 1 volume directive. - The path given by the volume directive will be mounted from the image into the container. For example, an image with a volume at `/test/test_dir` will have `/test/test_dir` in the image mounted to `/test/test_dir` in the container. - Multiple images can be specified. If multiple images have a volume at a specific path, the last image specified trumps. - The images are always mounted read-only. - Images to mount are defined in the annotation "io.podman.annotations.kube.image.automount/$ctrname" as a semicolon-separated list. They are mounted into a single container in the pod, not the whole pod. As we're using a nonstandard annotation, this is Podman only, any Kubernetes install will just ignore this. Underneath, this compiles down to an image volume (`podman run --mount type=image,...`) with subpaths to specify what bits we want to mount into the container. Signed-off-by: Matt Heon --- docs/source/markdown/podman-kube-play.1.md.in | 11 ++++ libpod/define/annotations.go | 3 + pkg/domain/infra/abi/play.go | 60 +++++++++++++++++++ pkg/specgen/generate/kube/kube.go | 4 ++ test/e2e/run_volume_test.go | 11 +++- test/system/700-play.bats | 45 ++++++++++++++ 6 files changed, 131 insertions(+), 3 deletions(-) diff --git a/docs/source/markdown/podman-kube-play.1.md.in b/docs/source/markdown/podman-kube-play.1.md.in index ea2ebf4a04..21a929a137 100644 --- a/docs/source/markdown/podman-kube-play.1.md.in +++ b/docs/source/markdown/podman-kube-play.1.md.in @@ -158,6 +158,17 @@ spec: and as a result environment variable `FOO` is set to `bar` for container `container-1`. +`Automounting Volumes` + +An image can be automatically mounted into a container if the annotation `io.podman.annotations.kube.image.automount/$ctrname` is given. The following rules apply: + +- The image must already exist locally. +- The image must have at least 1 volume directive. +- The path given by the volume directive will be mounted from the image into the container. For example, an image with a volume at `/test/test_dir` will have `/test/test_dir` in the image mounted to `/test/test_dir` in the container. +- Multiple images can be specified. If multiple images have a volume at a specific path, the last image specified trumps. +- The images are always mounted read-only. +- Images to mount are defined in the annotation "io.podman.annotations.kube.image.automount/$ctrname" as a semicolon-separated list. They are mounted into a single container in the pod, not the whole pod. The annotation can be specified for additional containers if additional mounts are required. + ## OPTIONS @@option annotation.container diff --git a/libpod/define/annotations.go b/libpod/define/annotations.go index a9d4031ae2..ac1956f56b 100644 --- a/libpod/define/annotations.go +++ b/libpod/define/annotations.go @@ -160,6 +160,9 @@ const ( // the k8s behavior of waiting for the intialDelaySeconds to be over before updating the status KubeHealthCheckAnnotation = "io.podman.annotations.kube.health.check" + // KubeImageAutomountAnnotation + KubeImageAutomountAnnotation = "io.podman.annotations.kube.image.volumes.mount" + // TotalAnnotationSizeLimitB is the max length of annotations allowed by Kubernetes. TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB ) diff --git a/pkg/domain/infra/abi/play.go b/pkg/domain/infra/abi/play.go index f1202dfce6..fa7ffb8eab 100644 --- a/pkg/domain/infra/abi/play.go +++ b/pkg/domain/infra/abi/play.go @@ -126,6 +126,54 @@ func (ic *ContainerEngine) createServiceContainer(ctx context.Context, name stri return ctr, nil } +func (ic *ContainerEngine) prepareAutomountImages(ctx context.Context, forContainer string, annotations map[string]string) ([]*specgen.ImageVolume, error) { + volMap := make(map[string]*specgen.ImageVolume) + + ctrAnnotation := define.KubeImageAutomountAnnotation + "/" + forContainer + + automount, ok := annotations[ctrAnnotation] + if !ok || automount == "" { + return nil, nil + } + + for _, imageName := range strings.Split(automount, ";") { + img, fullName, err := ic.Libpod.LibimageRuntime().LookupImage(imageName, nil) + if err != nil { + return nil, fmt.Errorf("image %s from container %s does not exist in local storage, cannot automount: %w", imageName, forContainer, err) + } + + logrus.Infof("Resolved image name %s to %s for automount into container %s", imageName, fullName, forContainer) + + inspect, err := img.Inspect(ctx, nil) + if err != nil { + return nil, fmt.Errorf("cannot inspect image %s to automount into container %s: %w", fullName, forContainer, err) + } + + volumes := inspect.Config.Volumes + + for path := range volumes { + if oldPath, ok := volMap[path]; ok && oldPath != nil { + logrus.Warnf("Multiple volume mounts to %q requested, overriding image %q with image %s", path, oldPath.Source, fullName) + } + + imgVol := new(specgen.ImageVolume) + imgVol.Source = fullName + imgVol.Destination = path + imgVol.ReadWrite = false + imgVol.SubPath = path + + volMap[path] = imgVol + } + } + + toReturn := make([]*specgen.ImageVolume, 0, len(volMap)) + for _, vol := range volMap { + toReturn = append(toReturn, vol) + } + + return toReturn, nil +} + func prepareVolumesFrom(forContainer, podName string, ctrNames, annotations map[string]string) ([]string, error) { annotationVolsFrom := define.VolumesFromAnnotation + "/" + forContainer @@ -829,6 +877,11 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY initCtrType = define.OneShotInitContainer } + automountImages, err := ic.prepareAutomountImages(ctx, initCtr.Name, annotations) + if err != nil { + return nil, nil, err + } + var volumesFrom []string if list, err := prepareVolumesFrom(initCtr.Name, podName, ctrNames, annotations); err != nil { return nil, nil, err @@ -857,6 +910,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY UserNSIsHost: p.Userns.IsHost(), Volumes: volumes, VolumesFrom: volumesFrom, + ImageVolumes: automountImages, UtsNSIsHost: p.UtsNs.IsHost(), } specGen, err := kube.ToSpecGen(ctx, &specgenOpts) @@ -913,6 +967,11 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY labels[k] = v } + automountImages, err := ic.prepareAutomountImages(ctx, container.Name, annotations) + if err != nil { + return nil, nil, err + } + var volumesFrom []string if list, err := prepareVolumesFrom(container.Name, podName, ctrNames, annotations); err != nil { return nil, nil, err @@ -942,6 +1001,7 @@ func (ic *ContainerEngine) playKubePod(ctx context.Context, podName string, podY UserNSIsHost: p.Userns.IsHost(), Volumes: volumes, VolumesFrom: volumesFrom, + ImageVolumes: automountImages, UtsNSIsHost: p.UtsNs.IsHost(), } diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index ec5cc10420..1328323ceb 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -142,6 +142,8 @@ type CtrSpecGenOptions struct { Volumes map[string]*KubeVolume // VolumesFrom for all containers VolumesFrom []string + // Image Volumes for this container + ImageVolumes []*specgen.ImageVolume // PodID of the parent pod PodID string // PodName of the parent pod @@ -223,6 +225,8 @@ func ToSpecGen(ctx context.Context, opts *CtrSpecGenOptions) (*specgen.SpecGener Driver: opts.LogDriver, } + s.ImageVolumes = opts.ImageVolumes + s.LogConfiguration.Options = make(map[string]string) for _, o := range opts.LogOptions { opt, val, hasVal := strings.Cut(o, "=") diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go index 07cfca1fb3..4dda259cd7 100644 --- a/test/e2e/run_volume_test.go +++ b/test/e2e/run_volume_test.go @@ -936,14 +936,19 @@ USER testuser`, CITEST_IMAGE) }) It("podman run --mount type=image with subpath", func() { - ctrCommand := []string{"run", "--mount", fmt.Sprintf("type=image,source=%s,dest=/mnt,subpath=/etc", ALPINE), ALPINE, "ls"} + podmanTest.AddImageToRWStore(ALPINE) - run1Cmd := append(ctrCommand, "/etc") + pathToCheck := "/sbin" + pathInCtr := "/mnt" + + ctrCommand := []string{"run", "--mount", fmt.Sprintf("type=image,source=%s,dst=%s,subpath=%s", ALPINE, pathInCtr, pathToCheck), ALPINE, "ls"} + + run1Cmd := append(ctrCommand, pathToCheck) run1 := podmanTest.Podman(run1Cmd) run1.WaitWithDefaultTimeout() Expect(run1).Should(ExitCleanly()) - run2Cmd := append(ctrCommand, "/mnt") + run2Cmd := append(ctrCommand, pathInCtr) run2 := podmanTest.Podman(run2Cmd) run2.WaitWithDefaultTimeout() Expect(run2).Should(ExitCleanly()) diff --git a/test/system/700-play.bats b/test/system/700-play.bats index 174db05145..1441775748 100644 --- a/test/system/700-play.bats +++ b/test/system/700-play.bats @@ -981,3 +981,48 @@ _EOF run_podman pod rm -t 0 -f test_pod run_podman rmi -f userimage:latest $from_image } + +@test "podman play with automount volume" { + cat >$PODMAN_TMPDIR/Containerfile < $fname + + run_podman kube play --annotation "io.podman.annotations.kube.image.volumes.mount/testctr=automount_test" $fname + + run_podman run --rm automount_test ls /test1 + run_out_test1="$output" + run_podman exec test_pod-testctr ls /test1 + assert "$output" = "$run_out_test1" "matching ls run/exec volume path test1" + + run_podman run --rm automount_test ls /test2 + run_out_test2="$output" + run_podman exec test_pod-testctr ls /test2 + assert "$output" = "$run_out_test2" "matching ls run/exec volume path test2" + + run_podman rm -f -t 0 -a + run_podman rmi automount_test +}