Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing user-facing part to allow volume subpath mounts #22040

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/markdown/options/mount.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*).
Expand Down
6 changes: 6 additions & 0 deletions docs/source/markdown/options/volume.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** <sup>[[1]](#Footnote1)</sup>
* **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.
Expand Down Expand Up @@ -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.
15 changes: 14 additions & 1 deletion pkg/specgenutil/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
82 changes: 82 additions & 0 deletions test/e2e/run_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
})
})
10 changes: 9 additions & 1 deletion vendor/github.com/containers/common/pkg/parse/parse.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.