Merge pull request #22410 from mheon/automount_images_k8s
Add the ability to automount images as volumes via play
This commit is contained in:
commit
15cc8862c1
|
|
@ -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*).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1474,6 +1474,7 @@ func WithImageVolumes(volumes []*ContainerImageVolume) CtrCreateOption {
|
|||
Dest: vol.Dest,
|
||||
Source: vol.Source,
|
||||
ReadWrite: vol.ReadWrite,
|
||||
SubPath: vol.SubPath,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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, "=")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue