Skip to content

Commit

Permalink
Merge pull request #22410 from mheon/automount_images_k8s
Browse files Browse the repository at this point in the history
Add the ability to automount images as volumes via play
  • Loading branch information
openshift-merge-bot[bot] authored Apr 25, 2024
2 parents 2730201 + 30e2c92 commit 15cc886
Show file tree
Hide file tree
Showing 13 changed files with 175 additions and 2 deletions.
2 changes: 2 additions & 0 deletions docs/source/markdown/options/mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*).
Expand Down
11 changes: 11 additions & 0 deletions docs/source/markdown/podman-kube-play.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions libpod/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions libpod/container_internal_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions libpod/define/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
1 change: 1 addition & 0 deletions libpod/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,7 @@ func WithImageVolumes(volumes []*ContainerImageVolume) CtrCreateOption {
Dest: vol.Dest,
Source: vol.Source,
ReadWrite: vol.ReadWrite,
SubPath: vol.SubPath,
})
}

Expand Down
60 changes: 60 additions & 0 deletions pkg/domain/infra/abi/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
}

Expand Down
1 change: 1 addition & 0 deletions pkg/specgen/generate/container_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions pkg/specgen/generate/kube/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, "=")
Expand Down
3 changes: 3 additions & 0 deletions pkg/specgen/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions pkg/specgenutil/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
21 changes: 21 additions & 0 deletions test/e2e/run_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -934,4 +934,25 @@ 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() {
podmanTest.AddImageToRWStore(ALPINE)

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, pathInCtr)
run2 := podmanTest.Podman(run2Cmd)
run2.WaitWithDefaultTimeout()
Expect(run2).Should(ExitCleanly())

Expect(run1.OutputToString()).Should(Equal(run2.OutputToString()))
})
})
45 changes: 45 additions & 0 deletions test/system/700-play.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 <<EOF
FROM $IMAGE
RUN mkdir /test1 /test2
RUN touch /test1/a /test1/b /test1/c
RUN touch /test2/asdf /test2/ejgre /test2/lteghe
VOLUME /test1
VOLUME /test2
EOF

run_podman build -t automount_test -f $PODMAN_TMPDIR/Containerfile

fname="/tmp/play_kube_wait_$(random_string 6).yaml"
echo "
apiVersion: v1
kind: Pod
metadata:
labels:
app: test
name: test_pod
spec:
restartPolicy: Never
containers:
- name: testctr
image: $IMAGE
command:
- top
" > $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
}

1 comment on commit 15cc886

@packit-as-a-service
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

podman-next COPR build failed. @containers/packit-build please check.

Please sign in to comment.