From 0fefcf8a4f78f47928ec4fb67feada91f70bbaa8 Mon Sep 17 00:00:00 2001 From: Daniel J Walsh Date: Wed, 26 Jul 2023 09:23:37 -0400 Subject: [PATCH] Add glob support to podman run/create --mount HPC Community asked for this support specifically for using GPUs within containers. Nvidia requires the correct shared library to to be present in the directory that matches the device mounted into the container. These libraries have random suffixes based on versions of the installed libraries on the host. podman run --mount type=glob:src=/usr/lib64/nvidia\*:ro=true. This helps quadlets be more portable for this use case. Signed-off-by: Daniel J Walsh --- docs/source/markdown/options/mount.md | 10 ++-- docs/source/markdown/podman-create.1.md.in | 6 +++ docs/source/markdown/podman-run.1.md.in | 6 +++ pkg/specgenutil/volumes.go | 56 +++++++++++++++++++++- pkg/systemd/quadlet/quadlet.go | 2 +- test/system/060-mount.bats | 39 +++++++++++++++ 6 files changed, 113 insertions(+), 6 deletions(-) diff --git a/docs/source/markdown/options/mount.md b/docs/source/markdown/options/mount.md index ac8e1b058d..cc53331587 100644 --- a/docs/source/markdown/options/mount.md +++ b/docs/source/markdown/options/mount.md @@ -6,7 +6,7 @@ Attach a filesystem mount to the container -Current supported mount TYPEs are **bind**, **volume**, **image**, **tmpfs** and **devpts**. [[1]](#Footnote1) +Current supported mount TYPEs are **bind**, **devpts**, **glob**, **image**, **tmpfs** and **volume**. [[1]](#Footnote1) e.g. @@ -16,6 +16,8 @@ Current supported mount TYPEs are **bind**, **volume**, **image**, **tmpfs** and type=bind,src=/path/on/host,dst=/path/in/container,relabel=shared,U=true + type=glob,src=/usr/lib/libfoo*,destination=/usr/lib,ro=true + type=volume,source=vol1,destination=/path/in/container,ro=true type=tmpfs,tmpfs-size=512M,destination=/path/in/container @@ -26,10 +28,12 @@ Current supported mount TYPEs are **bind**, **volume**, **image**, **tmpfs** and Common Options: - · src, source: mount source spec for bind and volume. Mandatory for bind. + · src, source: mount source spec for bind, glob, and volume. Mandatory for bind and glob. · dst, destination, target: mount destination spec. + Paths matching globs, are mounted on the destination directory with the identical name inside the container. + Options specific to volume: · ro, readonly: true or false (default). @@ -47,7 +51,7 @@ Current supported mount TYPEs are **bind**, **volume**, **image**, **tmpfs** and · rw, readwrite: true or false (default). - Options specific to bind: + Options specific to bind and glob: · ro, readonly: true or false (default). diff --git a/docs/source/markdown/podman-create.1.md.in b/docs/source/markdown/podman-create.1.md.in index b247ce1e5c..0576675c5a 100644 --- a/docs/source/markdown/podman-create.1.md.in +++ b/docs/source/markdown/podman-create.1.md.in @@ -443,6 +443,12 @@ $ podman create --name container3 --requires container1,container2 -t -i fedora $ podman start --attach container3 ``` +### Exposing shared libraries inside of container as read-only using a glob + +``` +$ podman create --mount type=glob,src=/usr/lib64/libnvidia\*,ro -i -t fedora /bin/bash +``` + ### Configure keep supplemental groups for access to volume ``` diff --git a/docs/source/markdown/podman-run.1.md.in b/docs/source/markdown/podman-run.1.md.in index 95f3632996..a42ddd67f7 100644 --- a/docs/source/markdown/podman-run.1.md.in +++ b/docs/source/markdown/podman-run.1.md.in @@ -468,6 +468,12 @@ $ podman run --read-only -i -t fedora /bin/bash $ podman run --read-only --read-only-tmpfs=false --tmpfs /run -i -t fedora /bin/bash ``` +### Exposing shared libraries inside of container as read-only using a glob + +``` +$ podman run --mount type=glob,src=/usr/lib64/libnvidia\*,ro=true -i -t fedora /bin/bash +``` + ### Exposing log messages from the container to the host's log Bind mount the _/dev/log_ directory to have messages that are logged in the container show up in the host's diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go index 72859cc727..d3751cb29d 100644 --- a/pkg/specgenutil/volumes.go +++ b/pkg/specgenutil/volumes.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "path" + "path/filepath" "strings" "github.com/containers/common/pkg/config" @@ -18,7 +19,7 @@ import ( var ( errOptionArg = errors.New("must provide an argument for option") errNoDest = errors.New("must set volume destination") - errInvalidSyntax = errors.New("incorrect mount format: should be --mount type=,[src=,]target=[,options]") + errInvalidSyntax = errors.New("incorrect mount format: should be --mount type=,[src=,]target=[,options]") ) // Parse all volume-related options in the create config into a set of mounts @@ -196,6 +197,20 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest) } finalMounts[mount.Destination] = mount + case "glob": + mounts, err := getGlobMounts(tokens) + if err != nil { + return err + } + for _, mount := range mounts { + if _, ok := finalMounts[mount.Destination]; ok { + if ignoreDup { + continue + } + return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest) + } + finalMounts[mount.Destination] = mount + } case define.TypeTmpfs: mount, err := getTmpfsMount(tokens) if err != nil { @@ -438,12 +453,49 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) { return nil, fmt.Errorf("%s: %w", kv[0], util.ErrBadMntOption) } } - if len(mnt.Destination) == 0 { + if mountType != "glob" && len(mnt.Destination) == 0 { return nil, errNoDest } return &mnt, nil } +// Parse glob mounts entry from the --mount flag. +func getGlobMounts(args []string) ([]spec.Mount, error) { + mounts := []spec.Mount{} + + mnt, err := parseMountOptions("glob", args) + if err != nil { + return nil, err + } + + globs, err := filepath.Glob(mnt.Source) + if err != nil { + return nil, err + } + if len(globs) == 0 { + return nil, fmt.Errorf("no file paths matching glob %q", mnt.Source) + } + + options, err := parse.ValidateVolumeOpts(mnt.Options) + if err != nil { + return nil, err + } + for _, src := range globs { + var newMount spec.Mount + newMount.Type = define.TypeBind + newMount.Options = options + newMount.Source = src + if len(mnt.Destination) == 0 { + newMount.Destination = src + } else { + newMount.Destination = filepath.Join(mnt.Destination, filepath.Base(src)) + } + mounts = append(mounts, newMount) + } + + return mounts, nil +} + // Parse a single bind mount entry from the --mount flag. func getBindMount(args []string) (spec.Mount, error) { newMount := spec.Mount{ diff --git a/pkg/systemd/quadlet/quadlet.go b/pkg/systemd/quadlet/quadlet.go index dd823f79c6..d593bf8e24 100644 --- a/pkg/systemd/quadlet/quadlet.go +++ b/pkg/systemd/quadlet/quadlet.go @@ -664,7 +664,7 @@ func ConvertContainer(container *parser.UnitFile, names map[string]string, isUse paramsMap[kv[0]] = kv[1] } if paramType, ok := paramsMap["type"]; ok { - if paramType == "volume" || paramType == "bind" { + if paramType == "volume" || paramType == "bind" || paramType == "glob" { var err error if paramSource, ok := paramsMap["source"]; ok { paramsMap["source"], err = handleStorageSource(container, service, paramSource, names) diff --git a/test/system/060-mount.bats b/test/system/060-mount.bats index c59b3e1d05..918b5e1189 100644 --- a/test/system/060-mount.bats +++ b/test/system/060-mount.bats @@ -247,4 +247,43 @@ EOF buildah rm $external_cid } +@test "podman volume globs" { + v1a=v1_$(random_string) + v1b=v1_$(random_string) + v2=v2_$(random_string) + vol1a=${PODMAN_TMPDIR}/$v1a + vol1b=${PODMAN_TMPDIR}/$v1b + vol2=${PODMAN_TMPDIR}/$v2 + touch $vol1a $vol1b $vol2 + + # if volumes source and dest match then pass + run_podman run --rm --mount type=glob,src=${PODMAN_TMPDIR}/v1\*,ro $IMAGE ls $vol1a $vol1b + run_podman 1 run --rm --mount source=${PODMAN_TMPDIR}/v1\*,type=glob,ro $IMAGE ls $vol2 + is "$output" ".*No such file or directory" "$vol2 should not be mounted in the container" + + run_podman 125 run --rm --mount source=${PODMAN_TMPDIR}/v3\*,type=glob,ro $IMAGE ls $vol2 + is "$output" "Error: no file paths matching glob \"${PODMAN_TMPDIR}/v3\*\"" "Glob does not match so should throw error" + + run_podman 1 run --rm --mount source=${PODMAN_TMPDIR}/v2\*,type=glob,ro,Z $IMAGE touch $vol2 + is "$output" "touch: $vol2: Read-only file system" "Mount should be read-only" + + run_podman run --rm --mount source=${PODMAN_TMPDIR}/v2\*,type=glob,ro=false,Z $IMAGE touch $vol2 + + run_podman run --rm --mount type=glob,src=${PODMAN_TMPDIR}/v1\*,destination=/non/existing/directory,ro $IMAGE ls /non/existing/directory + is "$output" ".*$v1a" "podman images --inspect should include $v1a" + is "$output" ".*$v1b" "podman images --inspect should include $v1b" + + run_podman create --rm --mount type=glob,src=${PODMAN_TMPDIR}/v1\*,ro $IMAGE ls $vol1a $vol1b + cid=$output + run_podman container inspect $output + is "$output" ".*$vol1a" "podman images --inspect should include $vol1a" + is "$output" ".*$vol1b" "podman images --inspect should include $vol1b" + + run_podman 125 run --rm --mount source=${PODMAN_TMPDIR}/v2\*,type=bind,ro=false $IMAGE touch $vol2 + is "$output" "Error: must set volume destination" "Bind mounts require destination" + + run_podman 125 run --rm --mount source=${PODMAN_TMPDIR}/v2\*,destination=/tmp/foobar, ro=false $IMAGE touch $vol2 + is "$output" "Error: invalid reference format" "Default mounts don not support globs" +} + # vim: filetype=sh