diff --git a/docs/source/markdown/options/mount.md b/docs/source/markdown/options/mount.md index 9a14b39fd1..35e6d27579 100644 --- a/docs/source/markdown/options/mount.md +++ b/docs/source/markdown/options/mount.md @@ -37,6 +37,8 @@ Options specific to type=**volume**: Multiple ranges are separated with #. If the specified mapping is prepended with a '@' then the mapping is considered relative to the container user namespace. The host ID for the mapping is changed to account for the relative position of the container user in the container user namespace. +- *subpath*: If specified, mount specific file or directory from volume into the container. + Options specific to type=**image**: - *rw*, *readwrite*: *true* or *false* (default if unspecified: *false*). diff --git a/docs/source/markdown/options/volume.md b/docs/source/markdown/options/volume.md index 116ed4c078..481c368011 100644 --- a/docs/source/markdown/options/volume.md +++ b/docs/source/markdown/options/volume.md @@ -28,6 +28,7 @@ The _OPTIONS_ is a comma-separated list and can be one or more of: * [**r**]**bind** * [**r**]**shared**|[**r**]**slave**|[**r**]**private**[**r**]**unbindable** [[1]](#Footnote1) * **idmap**[=**options**] +* **subpath**[=**/path/inside/volume**] The `CONTAINER-DIR` must be an absolute path such as `/src/docs`. The volume is mounted into the container at this directory. @@ -204,3 +205,8 @@ For each triplet, the first value is the start of the backing file system IDs that are mapped to the second value on the host. The length of this mapping is given in the third value. Multiple ranges are separated with #. + +`Volume sub-path mount` + +If `subpath` is specified, mount only specified file or directory from the volume +into the container. diff --git a/pkg/specgenutil/volumes.go b/pkg/specgenutil/volumes.go index c481867163..0b30679e49 100644 --- a/pkg/specgenutil/volumes.go +++ b/pkg/specgenutil/volumes.go @@ -254,7 +254,7 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m } func parseMountOptions(mountType string, args []string) (*spec.Mount, error) { - var setTmpcopyup, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership, setSwap bool + var setTmpcopyup, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership, setSubpath, setSwap bool mnt := spec.Mount{} for _, arg := range args { @@ -371,6 +371,19 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) { return nil, fmt.Errorf("host directory cannot be empty: %w", errOptionArg) } mnt.Source = value + case "subpath": + if setSubpath { + return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg) + } + setSubpath = true + if mountType != define.TypeVolume { + return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType) + } + if hasValue { + mnt.Options = append(mnt.Options, fmt.Sprintf("subpath=%s", value)) + } else { + return nil, fmt.Errorf("%v: %w", name, errOptionArg) + } case "target", "dst", "destination": if mnt.Destination != "" { return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg) diff --git a/test/e2e/run_volume_test.go b/test/e2e/run_volume_test.go index 4e777d62ef..2da99c589f 100644 --- a/test/e2e/run_volume_test.go +++ b/test/e2e/run_volume_test.go @@ -962,4 +962,86 @@ USER testuser`, CITEST_IMAGE) Expect(run).Should(ExitCleanly()) Expect(run.OutputToString()).Should(ContainSubstring(strings.TrimLeft("/vol/", f.Name()))) }) + + It("podman works with mounted subpath of a named volume", func() { + // Create named volume + volName := "testVol" + volCreate := podmanTest.Podman([]string{"volume", "create", volName}) + volCreate.WaitWithDefaultTimeout() + Expect(volCreate).Should(ExitCleanly()) + + // Populate volume with sub-directories and files + volMount := podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test", volName), ALPINE, "/bin/sh", "-c", "mkdir /test/subdir-ro && mkdir /test/subdir-rw && touch /test/subdir-ro/readonlyfile.txt && touch /test/subdir-rw/writablefile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(ExitCleanly()) + + // Mount subdir-ro as readonly + mountinfo := getMountInfo(volName + ":/test:subpath=subdir-ro,ro") + Expect(mountinfo[5]).To(ContainSubstring("ro")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test:subpath=subdir-ro,ro", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(ExitCleanly()) + Expect(volMount.OutputToString()).To(Equal("0")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test:subpath=subdir-ro,ro", volName), ALPINE, "stat", "-c", "%s", "/test/writablefile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(1)) + Expect(volMount.ErrorToString()).To(ContainSubstring("such file or")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/test,subpath=subdir-ro,ro", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(ExitCleanly()) + Expect(volMount.OutputToString()).To(Equal("0")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/test,subpath=subdir-ro,ro", volName), ALPINE, "stat", "-c", "%s", "/test/writablefile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(1)) + Expect(volMount.ErrorToString()).To(ContainSubstring("such file or")) + + // Mount subdir-rw as readwrite + mountinfo = getMountInfo(volName + ":/test:subpath=subdir-rw,rw") + Expect(mountinfo[5]).To(ContainSubstring("rw")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test:subpath=subdir-rw,rw", volName), ALPINE, "stat", "-c", "%s", "/test/writablefile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(ExitCleanly()) + Expect(volMount.OutputToString()).To(Equal("0")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test:subpath=subdir-rw,rw", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(1)) + Expect(volMount.ErrorToString()).To(ContainSubstring("such file or")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/test,subpath=subdir-rw,rw", volName), ALPINE, "stat", "-c", "%s", "/test/writablefile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(ExitCleanly()) + Expect(volMount.OutputToString()).To(Equal("0")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/test,subpath=subdir-rw,rw", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(1)) + Expect(volMount.ErrorToString()).To(ContainSubstring("such file or")) + + // Prevent directory traversal vulnerabilities in subpath + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test:subpath=../../../../../../../../../../../../../../etc,rw", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(126)) + Expect(volMount.ErrorToString()).Should(ContainSubstring("is outside")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "-v", fmt.Sprintf("%s:/test:subpath=/../../../../../../../../../../../../../../etc,rw", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(126)) + Expect(volMount.ErrorToString()).Should(ContainSubstring("is outside")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/test,subpath=../../../../../../../../../../../../../../etc,rw", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(126)) + Expect(volMount.ErrorToString()).Should(ContainSubstring("is outside")) + + volMount = podmanTest.Podman([]string{"run", "--rm", "--mount", fmt.Sprintf("type=volume,src=%s,dst=/test,subpath=/../../../../../../../../../../../../../../etc,rw", volName), ALPINE, "stat", "-c", "%s", "/test/readonlyfile.txt"}) + volMount.WaitWithDefaultTimeout() + Expect(volMount).Should(Exit(126)) + Expect(volMount.ErrorToString()).Should(ContainSubstring("is outside")) + }) }) diff --git a/vendor/github.com/containers/common/pkg/parse/parse.go b/vendor/github.com/containers/common/pkg/parse/parse.go index 284751e523..0c04c9d243 100644 --- a/vendor/github.com/containers/common/pkg/parse/parse.go +++ b/vendor/github.com/containers/common/pkg/parse/parse.go @@ -14,7 +14,7 @@ import ( // ValidateVolumeOpts validates a volume's options func ValidateVolumeOpts(options []string) ([]string, error) { - var foundRootPropagation, foundRWRO, foundLabelChange, bindType, foundExec, foundDev, foundSuid, foundChown, foundUpperDir, foundWorkDir, foundCopy, foundCopySymlink int + var foundRootPropagation, foundRWRO, foundLabelChange, bindType, foundExec, foundDev, foundSuid, foundChown, foundUpperDir, foundWorkDir, foundCopy, foundCopySymlink, foundVolumeSubpath int finalOpts := make([]string, 0, len(options)) for _, opt := range options { // support advanced options like upperdir=/path, workdir=/path @@ -38,6 +38,14 @@ func ValidateVolumeOpts(options []string) ([]string, error) { finalOpts = append(finalOpts, opt) continue } + if strings.HasPrefix(opt, "subpath") { + foundVolumeSubpath++ + if foundVolumeSubpath > 1 { + return nil, fmt.Errorf("invalid options %q, can only specify 1 subpath per mount", strings.Join(options, ", ")) + } + finalOpts = append(finalOpts, opt) + continue + } switch opt { case "noexec", "exec":