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 9a14b39fd1b9..7114d74952da 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 00616ccfa373..8e188b33c48a 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 2ec7c0040e92..78dd31b39cd4 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 f4f03f341c07..c90d779bdeb9 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 0887eb4c8c5e..8d4029114bc1 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 075711138cca..d2c1e5487672 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 c4818671636e..510b11254b80 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 2cd1881e8e5c..07cfca1fb337 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 ea2ebf4a045f..21a929a1373b 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 a9d4031ae278..ac1956f56b7a 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 f1202dfce631..fa7ffb8eabb1 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 ec5cc104209a..1328323cebc1 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 07cfca1fb337..4dda259cd78b 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 174db05145ae..144177574827 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 +}