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 <mheon@redhat.com>
This commit is contained in:
Matt Heon
2024-04-17 08:23:38 -04:00
parent 693ae0ebc6
commit 30e2c923d6
6 changed files with 131 additions and 3 deletions

View File

@ -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

View File

@ -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
)

View File

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

View File

@ -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, "=")

View File

@ -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())

View File

@ -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
}