Files
podman/pkg/util/mount_opts.go
Ygal Blum 64ec31ac00 Add nocreate option for named volumes
Add a per-volume 'nocreate' option that prevents automatic creation of
named volumes when they don't exist. When specified, Podman will fail
if the volume is not found instead of creating it automatically.

Usage: -v myvolume:/data:nocreate
       --mount type=volume,src=myvolume,dst=/data,nocreate

See: #27862
Signed-off-by: Ygal Blum <ygal.blum@gmail.com>
2026-02-26 11:14:32 -05:00

242 lines
7.8 KiB
Go

package util
import (
"errors"
"fmt"
"strings"
"github.com/containers/podman/v6/libpod/define"
"github.com/containers/podman/v6/pkg/rootless"
)
var (
// ErrBadMntOption indicates that an invalid mount option was passed.
ErrBadMntOption = errors.New("invalid mount option")
// ErrDupeMntOption indicates that a duplicate mount option was passed.
ErrDupeMntOption = errors.New("duplicate mount option passed")
)
type defaultMountOptions struct {
noexec bool
nosuid bool
nodev bool
}
type getDefaultMountOptionsFn func(path string) (defaultMountOptions, error)
// ProcessOptions parses the options for a bind or tmpfs mount and ensures that
// they are sensible and follow convention. The isTmpfs variable controls
// whether extra, tmpfs-specific options will be allowed.
// The sourcePath variable, if not empty, contains a bind mount source.
// Returns the processed options, a boolean indicating if nocreate was specified, and an error.
func ProcessOptions(options []string, isTmpfs bool, sourcePath string) ([]string, bool, error) {
return processOptionsInternal(options, isTmpfs, sourcePath, getDefaultMountOptions)
}
func processOptionsInternal(options []string, isTmpfs bool, sourcePath string, getDefaultMountOptions getDefaultMountOptionsFn) ([]string, bool, error) {
var foundWrite, foundSize, foundProp, foundMode, foundExec, foundSuid, foundDev, foundCopyUp, foundBind, foundZ, foundU, foundOverlay, foundIdmap, foundCopy, foundNoSwap, foundNoDereference, foundNoCreate bool
recursiveBind := true
newOptions := make([]string, 0, len(options))
for _, opt := range options {
// Some options have parameters - size, mode
key, _, _ := strings.Cut(opt, "=")
// add advanced options such as upperdir=/path and workdir=/path, when overlay is specified
if foundOverlay {
if strings.Contains(opt, "upperdir") {
newOptions = append(newOptions, opt)
continue
}
if strings.Contains(opt, "workdir") {
newOptions = append(newOptions, opt)
continue
}
}
if strings.HasPrefix(key, "subpath") {
newOptions = append(newOptions, opt)
continue
}
if strings.HasPrefix(key, "idmap") {
if foundIdmap {
return nil, false, fmt.Errorf("the 'idmap' option can only be set once: %w", ErrDupeMntOption)
}
foundIdmap = true
newOptions = append(newOptions, opt)
continue
}
switch key {
case "copy", "nocopy":
if foundCopy {
return nil, false, fmt.Errorf("only one of 'nocopy' and 'copy' can be used: %w", ErrDupeMntOption)
}
foundCopy = true
case "O":
foundOverlay = true
case "volume-opt":
// Volume-opt should be relayed and processed by driver.
newOptions = append(newOptions, opt)
case "exec", "noexec":
if foundExec {
return nil, false, fmt.Errorf("only one of 'noexec' and 'exec' can be used: %w", ErrDupeMntOption)
}
foundExec = true
case "suid", "nosuid":
if foundSuid {
return nil, false, fmt.Errorf("only one of 'nosuid' and 'suid' can be used: %w", ErrDupeMntOption)
}
foundSuid = true
case "nodev", "dev":
if foundDev {
return nil, false, fmt.Errorf("only one of 'nodev' and 'dev' can be used: %w", ErrDupeMntOption)
}
foundDev = true
case "rw", "ro":
if foundWrite {
return nil, false, fmt.Errorf("only one of 'rw' and 'ro' can be used: %w", ErrDupeMntOption)
}
foundWrite = true
case "private", "rprivate", "slave", "rslave", "shared", "rshared", "unbindable", "runbindable":
if foundProp {
return nil, false, fmt.Errorf("only one root propagation mode can be used: %w", ErrDupeMntOption)
}
foundProp = true
case "size":
if !isTmpfs {
return nil, false, fmt.Errorf("the 'size' option is only allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if foundSize {
return nil, false, fmt.Errorf("only one tmpfs size can be specified: %w", ErrDupeMntOption)
}
foundSize = true
case "mode":
if !isTmpfs {
return nil, false, fmt.Errorf("the 'mode' option is only allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if foundMode {
return nil, false, fmt.Errorf("only one tmpfs mode can be specified: %w", ErrDupeMntOption)
}
foundMode = true
case "tmpcopyup":
if !isTmpfs {
return nil, false, fmt.Errorf("the 'tmpcopyup' option is only allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if foundCopyUp {
return nil, false, fmt.Errorf("the 'tmpcopyup' or 'notmpcopyup' option can only be set once: %w", ErrDupeMntOption)
}
foundCopyUp = true
case "consistency":
// Often used on MACs and mistakenly on Linux platforms.
// Since Docker ignores this option so shall we.
continue
case "notmpcopyup":
if !isTmpfs {
return nil, false, fmt.Errorf("the 'notmpcopyup' option is only allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if foundCopyUp {
return nil, false, fmt.Errorf("the 'tmpcopyup' or 'notmpcopyup' option can only be set once: %w", ErrDupeMntOption)
}
foundCopyUp = true
// do not propagate notmpcopyup to the OCI runtime
continue
case "noswap":
if !isTmpfs {
return nil, false, fmt.Errorf("the 'noswap' option is only allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if rootless.IsRootless() {
return nil, false, fmt.Errorf("the 'noswap' option is only allowed with rootful tmpfs mounts: %w", ErrBadMntOption)
}
if foundNoSwap {
return nil, false, fmt.Errorf("the 'tmpswap' option can only be set once: %w", ErrDupeMntOption)
}
foundNoSwap = true
newOptions = append(newOptions, opt)
continue
case "no-dereference":
if foundNoDereference {
return nil, false, fmt.Errorf("the 'no-dereference' option can only be set once: %w", ErrDupeMntOption)
}
foundNoDereference = true
case define.TypeBind:
recursiveBind = false
fallthrough
case "rbind":
if isTmpfs {
return nil, false, fmt.Errorf("the 'bind' and 'rbind' options are not allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if foundBind {
return nil, false, fmt.Errorf("only one of 'rbind' and 'bind' can be used: %w", ErrDupeMntOption)
}
foundBind = true
case "z", "Z":
if isTmpfs {
return nil, false, fmt.Errorf("the 'z' and 'Z' options are not allowed with tmpfs mounts: %w", ErrBadMntOption)
}
if foundZ {
return nil, false, fmt.Errorf("only one of 'z' and 'Z' can be used: %w", ErrDupeMntOption)
}
foundZ = true
case "U":
if foundU {
return nil, false, fmt.Errorf("the 'U' option can only be set once: %w", ErrDupeMntOption)
}
foundU = true
case "noatime":
if !isTmpfs {
return nil, false, fmt.Errorf("the 'noatime' option is only allowed with tmpfs mounts: %w", ErrBadMntOption)
}
case "nocreate":
// nocreate is handled separately and not passed to the runtime
foundNoCreate = true
continue
default:
return nil, false, fmt.Errorf("unknown mount option %q: %w", opt, ErrBadMntOption)
}
newOptions = append(newOptions, opt)
}
if !foundProp {
if recursiveBind {
newOptions = append(newOptions, "rprivate")
} else {
newOptions = append(newOptions, "private")
}
}
defaults, err := getDefaultMountOptions(sourcePath)
if err != nil {
return nil, false, err
}
if !foundExec && defaults.noexec {
newOptions = append(newOptions, "noexec")
}
if !foundSuid && defaults.nosuid {
newOptions = append(newOptions, "nosuid")
}
if !foundDev && defaults.nodev {
newOptions = append(newOptions, "nodev")
}
if isTmpfs && !foundCopyUp {
newOptions = append(newOptions, "tmpcopyup")
}
if !isTmpfs && !foundBind {
newOptions = append(newOptions, "rbind")
}
return newOptions, foundNoCreate, nil
}
func ParseDriverOpts(option string) (string, string, error) {
_, val, hasVal := strings.Cut(option, "=")
if !hasVal {
return "", "", fmt.Errorf("cannot parse driver opts: %w", ErrBadMntOption)
}
optKey, optVal, hasOptVal := strings.Cut(val, "=")
if !hasOptVal {
return "", "", fmt.Errorf("cannot parse driver opts: %w", ErrBadMntOption)
}
return optKey, optVal, nil
}