Skip to content
Merged
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: 1 addition & 1 deletion libpod/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ type ContainerNamedVolume struct {
// This is used for emptyDir volumes from a kube yaml
IsAnonymous bool `json:"setAnonymous,omitempty"`
// SubPath determines which part of the Source will be mounted in the container
SubPath string
SubPath string `json:",omitempty"`
}

// ContainerOverlayVolume is an overlay volume that will be mounted into the
Expand Down
87 changes: 53 additions & 34 deletions pkg/specgenutil/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ var (
errNoDest = errors.New("must set volume destination")
)

type universalMount struct {
mount spec.Mount
// Used only with Named Volume type mounts
subPath string
}

// Parse all volume-related options in the create config into a set of mounts
// and named volumes to add to the container.
// Handles --volumes, --mount, and --tmpfs flags.
Expand Down Expand Up @@ -253,18 +259,18 @@ func Mounts(mountFlag []string, configMounts []string) (map[string]spec.Mount, m
return finalMounts, finalNamedVolumes, finalImageVolumes, nil
}

func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
func parseMountOptions(mountType string, args []string) (*universalMount, error) {
var setTmpcopyup, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership, setSwap bool

mnt := spec.Mount{}
mnt := new(universalMount)
for _, arg := range args {
name, value, hasValue := strings.Cut(arg, "=")
switch name {
case "bind-nonrecursive":
if mountType != define.TypeBind {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
}
mnt.Options = append(mnt.Options, define.TypeBind)
mnt.mount.Options = append(mnt.mount.Options, define.TypeBind)
case "bind-propagation":
if mountType != define.TypeBind {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
Expand All @@ -278,16 +284,16 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
default:
return nil, fmt.Errorf("invalid value %q", arg)
}
mnt.Options = append(mnt.Options, value)
mnt.mount.Options = append(mnt.mount.Options, value)
case "consistency":
// Often used on MACs and mistakenly on Linux platforms.
// Since Docker ignores this option so shall we.
continue
case "idmap":
if hasValue {
mnt.Options = append(mnt.Options, fmt.Sprintf("idmap=%s", value))
mnt.mount.Options = append(mnt.mount.Options, fmt.Sprintf("idmap=%s", value))
} else {
mnt.Options = append(mnt.Options, "idmap")
mnt.mount.Options = append(mnt.mount.Options, "idmap")
}
case "readonly", "ro", "rw":
if setRORW {
Expand All @@ -307,35 +313,35 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
if hasValue {
switch strings.ToLower(value) {
case "true":
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "false":
// Set the opposite only for rw
// ro's opposite is the default
if name == "rw" {
mnt.Options = append(mnt.Options, "ro")
mnt.mount.Options = append(mnt.mount.Options, "ro")
}
}
} else {
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
}
case "nodev", "dev":
if setDev {
return nil, fmt.Errorf("cannot pass 'nodev' and 'dev' mnt.Options more than once: %w", errOptionArg)
}
setDev = true
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "noexec", "exec":
if setExec {
return nil, fmt.Errorf("cannot pass 'noexec' and 'exec' mnt.Options more than once: %w", errOptionArg)
}
setExec = true
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "nosuid", "suid":
if setSuid {
return nil, fmt.Errorf("cannot pass 'nosuid' and 'suid' mnt.Options more than once: %w", errOptionArg)
}
setSuid = true
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "noswap":
if setSwap {
return nil, fmt.Errorf("cannot pass 'noswap' mnt.Options more than once: %w", errOptionArg)
Expand All @@ -344,7 +350,7 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
return nil, fmt.Errorf("the 'noswap' option is only allowed with rootful tmpfs mounts: %w", errOptionArg)
}
setSwap = true
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "relabel":
if setRelabel {
return nil, fmt.Errorf("cannot pass 'relabel' option more than once: %w", errOptionArg)
Expand All @@ -355,19 +361,19 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
}
switch value {
case "private":
mnt.Options = append(mnt.Options, "Z")
mnt.mount.Options = append(mnt.mount.Options, "Z")
case "shared":
mnt.Options = append(mnt.Options, "z")
mnt.mount.Options = append(mnt.mount.Options, "z")
default:
return nil, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", name, util.ErrBadMntOption)
}
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "unbindable", "runbindable", "Z", "z", "no-dereference":
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "src", "source":
if mountType == define.TypeTmpfs {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
}
if mnt.Source != "" {
if mnt.mount.Source != "" {
return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
}
if !hasValue {
Expand All @@ -376,9 +382,17 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
if len(value) == 0 {
return nil, fmt.Errorf("host directory cannot be empty: %w", errOptionArg)
}
mnt.Source = value
mnt.mount.Source = value
case "subpath", "volume-subpath":
if mountType != define.TypeVolume {
return nil, fmt.Errorf("cannot set option %q on non-volume mounts", name)
}
if !hasValue {
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
}
mnt.subPath = value
case "target", "dst", "destination":
if mnt.Destination != "" {
if mnt.mount.Destination != "" {
return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
}
if !hasValue {
Expand All @@ -387,7 +401,7 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
if err := parse.ValidateVolumeCtrDir(value); err != nil {
return nil, err
}
mnt.Destination = unixPathClean(value)
mnt.mount.Destination = unixPathClean(value)
case "tmpcopyup", "notmpcopyup":
if mountType != define.TypeTmpfs {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
Expand All @@ -396,23 +410,23 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
return nil, fmt.Errorf("cannot pass 'tmpcopyup' and 'notmpcopyup' mnt.Options more than once: %w", errOptionArg)
}
setTmpcopyup = true
mnt.Options = append(mnt.Options, name)
mnt.mount.Options = append(mnt.mount.Options, name)
case "tmpfs-mode":
if mountType != define.TypeTmpfs {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
}
if !hasValue {
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
}
mnt.Options = append(mnt.Options, fmt.Sprintf("mode=%s", value))
mnt.mount.Options = append(mnt.mount.Options, fmt.Sprintf("mode=%s", value))
case "tmpfs-size":
if mountType != define.TypeTmpfs {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
}
if !hasValue {
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
}
mnt.Options = append(mnt.Options, fmt.Sprintf("size=%s", value))
mnt.mount.Options = append(mnt.mount.Options, fmt.Sprintf("size=%s", value))
case "U", "chown":
if setOwnership {
return nil, fmt.Errorf("cannot pass 'U' or 'chown' option more than once: %w", errOptionArg)
Expand All @@ -422,7 +436,7 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
return nil, err
}
if ok {
mnt.Options = append(mnt.Options, "U")
mnt.mount.Options = append(mnt.mount.Options, "U")
}
setOwnership = true
case "volume-label":
Expand All @@ -434,25 +448,26 @@ func parseMountOptions(mountType string, args []string) (*spec.Mount, error) {
if mountType != define.TypeVolume {
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
}
mnt.Options = append(mnt.Options, arg)
mnt.mount.Options = append(mnt.mount.Options, arg)
default:
return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
}
}
if mountType != "glob" && len(mnt.Destination) == 0 {
if mountType != "glob" && len(mnt.mount.Destination) == 0 {
return nil, errNoDest
}
return &mnt, nil
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)
uMnt, err := parseMountOptions("glob", args)
if err != nil {
return nil, err
}
mnt := uMnt.mount

globs, err := filepath.Glob(mnt.Source)
if err != nil {
Expand Down Expand Up @@ -488,10 +503,11 @@ func getBindMount(args []string) (spec.Mount, error) {
Type: define.TypeBind,
}
var err error
mnt, err := parseMountOptions(newMount.Type, args)
uMnt, err := parseMountOptions(newMount.Type, args)
if err != nil {
return newMount, err
}
mnt := uMnt.mount

if len(mnt.Destination) == 0 {
return newMount, errNoDest
Expand Down Expand Up @@ -519,10 +535,11 @@ func parseMemoryMount(args []string, mountType string) (spec.Mount, error) {
}

var err error
mnt, err := parseMountOptions(newMount.Type, args)
uMnt, err := parseMountOptions(newMount.Type, args)
if err != nil {
return newMount, err
}
mnt := uMnt.mount
if len(mnt.Destination) == 0 {
return newMount, errNoDest
}
Expand Down Expand Up @@ -576,12 +593,14 @@ func getNamedVolume(args []string) (*specgen.NamedVolume, error) {
if err != nil {
return nil, err
}
if len(mnt.Destination) == 0 {
if len(mnt.mount.Destination) == 0 {
return nil, errNoDest
}
newVolume.Options = mnt.Options
newVolume.Name = mnt.Source
newVolume.Dest = mnt.Destination

newVolume.Options = mnt.mount.Options
newVolume.SubPath = mnt.subPath
newVolume.Name = mnt.mount.Source
newVolume.Dest = mnt.mount.Destination
return newVolume, nil
}

Expand Down
20 changes: 20 additions & 0 deletions test/e2e/run_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1069,4 +1069,24 @@ RUN chmod 755 /test1 /test2 /test3`, ALPINE)

mountVolumeAndCheckDirectory(volName, "/test3", "test2", imgName)
})

It("podman run --mount type=volume,subpath=", func() {
volName := "testvol"
mkvol := podmanTest.Podman([]string{"volume", "create", volName})
mkvol.WaitWithDefaultTimeout()
Expect(mkvol).Should(ExitCleanly())

subvol := "/test/test2/"
pathInCtr := "/mnt"
pathToCreate := filepath.Join(pathInCtr, subvol)
popvol := podmanTest.Podman([]string{"run", "-v", fmt.Sprintf("%s:/mnt", volName), ALPINE, "sh", "-c", fmt.Sprintf("mkdir -p %s; touch %s; touch %s", pathToCreate, filepath.Join(pathToCreate, "foo"), filepath.Join(pathToCreate, "bar"))})
popvol.WaitWithDefaultTimeout()
Expect(popvol).Should(ExitCleanly())

checkCtr := podmanTest.Podman([]string{"run", "--mount", fmt.Sprintf("type=volume,source=%s,target=%s,subpath=%s", volName, pathInCtr, subvol), ALPINE, "ls", pathInCtr})
checkCtr.WaitWithDefaultTimeout()
Expect(checkCtr).To(ExitCleanly())
Expect(checkCtr.OutputToString()).To(ContainSubstring("foo"))
Expect(checkCtr.OutputToString()).To(ContainSubstring("bar"))
})
})