From 7c5c3e339aa1b8a76d90a3dd987c1f2fdd0d2d8e Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 7 Mar 2024 08:35:50 -0500 Subject: [PATCH 1/4] Allow "subpath" in volume options The code enabling volume subpath mounts has been merged in containers/podman#17992 but no user-facing change was made. The documentation was also not updated. Signed-off-by: Vasyl Gello --- vendor/github.com/containers/common/pkg/parse/parse.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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": From 454eb680edbe6f3bd8813baf1ffdacbcc4beac8d Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Tue, 12 Mar 2024 12:48:23 -0400 Subject: [PATCH 2/4] Allow 'subpath' in mount options Signed-off-by: Vasyl Gello --- pkg/specgenutil/volumes.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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) From 7a1a9e3052b158c2d2cafc08d62300a69b15f5d1 Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 7 Mar 2024 08:49:29 -0500 Subject: [PATCH 3/4] Documentation change for subpath Signed-off-by: Vasyl Gello --- docs/source/markdown/options/mount.md | 2 ++ docs/source/markdown/options/volume.md | 6 ++++++ 2 files changed, 8 insertions(+) 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. From 6bbfcae6b199f0b255772bcb5566393fa755b06a Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Thu, 14 Mar 2024 06:35:14 -0400 Subject: [PATCH 4/4] Add tests for volume subpath mounts Signed-off-by: Vasyl Gello --- test/e2e/run_volume_test.go | 82 +++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) 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")) + }) })