mirror of
https://github.com/containers/podman.git
synced 2025-05-21 00:56:36 +08:00

Add a new `no-dereference` mount option supported by crun 1.11+ to re-create/copy a symlink if it's the source of a mount. By default the kernel will resolve the symlink on the host and mount the target. As reported in #20098, there are use cases where the symlink structure must be preserved by all means. Fixes: #20098 Fixes: issues.redhat.com/browse/RUN-1935 Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
package parse
|
|
|
|
// this package contains functions that parse and validate
|
|
// user input and is shared either amongst container engine subcommands
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
finalOpts := make([]string, 0, len(options))
|
|
for _, opt := range options {
|
|
// support advanced options like upperdir=/path, workdir=/path
|
|
if strings.Contains(opt, "upperdir") {
|
|
foundUpperDir++
|
|
if foundUpperDir > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 upperdir per overlay", strings.Join(options, ", "))
|
|
}
|
|
finalOpts = append(finalOpts, opt)
|
|
continue
|
|
}
|
|
if strings.Contains(opt, "workdir") {
|
|
foundWorkDir++
|
|
if foundWorkDir > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 workdir per overlay", strings.Join(options, ", "))
|
|
}
|
|
finalOpts = append(finalOpts, opt)
|
|
continue
|
|
}
|
|
if strings.HasPrefix(opt, "idmap") {
|
|
finalOpts = append(finalOpts, opt)
|
|
continue
|
|
}
|
|
|
|
switch opt {
|
|
case "noexec", "exec":
|
|
foundExec++
|
|
if foundExec > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'noexec' or 'exec' option", strings.Join(options, ", "))
|
|
}
|
|
case "nodev", "dev":
|
|
foundDev++
|
|
if foundDev > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'nodev' or 'dev' option", strings.Join(options, ", "))
|
|
}
|
|
case "nosuid", "suid":
|
|
foundSuid++
|
|
if foundSuid > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'nosuid' or 'suid' option", strings.Join(options, ", "))
|
|
}
|
|
case "rw", "ro":
|
|
foundRWRO++
|
|
if foundRWRO > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", strings.Join(options, ", "))
|
|
}
|
|
case "z", "Z", "O":
|
|
foundLabelChange++
|
|
if foundLabelChange > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'z', 'Z', or 'O' option", strings.Join(options, ", "))
|
|
}
|
|
case "U":
|
|
foundChown++
|
|
if foundChown > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'U' option", strings.Join(options, ", "))
|
|
}
|
|
case "private", "rprivate", "shared", "rshared", "slave", "rslave", "unbindable", "runbindable":
|
|
foundRootPropagation++
|
|
if foundRootPropagation > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private', '[r]slave' or '[r]unbindable' option", strings.Join(options, ", "))
|
|
}
|
|
case "bind", "rbind":
|
|
bindType++
|
|
if bindType > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 '[r]bind' option", strings.Join(options, ", "))
|
|
}
|
|
case "cached", "delegated":
|
|
// The discarded ops are OS X specific volume options
|
|
// introduced in a recent Docker version.
|
|
// They have no meaning on Linux, so here we silently
|
|
// drop them. This matches Docker's behavior (the options
|
|
// are intended to be always safe to use, even not on OS
|
|
// X).
|
|
continue
|
|
case "copy", "nocopy":
|
|
foundCopy++
|
|
if foundCopy > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'copy' or 'nocopy' option", strings.Join(options, ", "))
|
|
}
|
|
case "no-dereference":
|
|
foundCopySymlink++
|
|
if foundCopySymlink > 1 {
|
|
return nil, fmt.Errorf("invalid options %q, can only specify 1 'no-dereference' option", strings.Join(options, ", "))
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("invalid option type %q", opt)
|
|
}
|
|
finalOpts = append(finalOpts, opt)
|
|
}
|
|
return finalOpts, nil
|
|
}
|
|
|
|
// Device parses device mapping string to a src, dest & permissions string
|
|
// Valid values for device looklike:
|
|
//
|
|
// '/dev/sdc"
|
|
// '/dev/sdc:/dev/xvdc"
|
|
// '/dev/sdc:/dev/xvdc:rwm"
|
|
// '/dev/sdc:rm"
|
|
func Device(device string) (src, dest, permissions string, err error) {
|
|
permissions = "rwm"
|
|
arr := strings.Split(device, ":")
|
|
switch len(arr) {
|
|
case 3:
|
|
if !isValidDeviceMode(arr[2]) {
|
|
return "", "", "", fmt.Errorf("invalid device mode: %s", arr[2])
|
|
}
|
|
permissions = arr[2]
|
|
fallthrough
|
|
case 2:
|
|
if isValidDeviceMode(arr[1]) {
|
|
permissions = arr[1]
|
|
} else {
|
|
if arr[1] == "" || arr[1][0] != '/' {
|
|
return "", "", "", fmt.Errorf("invalid device mode: %s", arr[1])
|
|
}
|
|
dest = arr[1]
|
|
}
|
|
fallthrough
|
|
case 1:
|
|
if len(arr[0]) > 0 {
|
|
src = arr[0]
|
|
break
|
|
}
|
|
fallthrough
|
|
default:
|
|
return "", "", "", fmt.Errorf("invalid device specification: %s", device)
|
|
}
|
|
|
|
if dest == "" {
|
|
dest = src
|
|
}
|
|
return src, dest, permissions, nil
|
|
}
|
|
|
|
// isValidDeviceMode checks if the mode for device is valid or not.
|
|
// isValid mode is a composition of r (read), w (write), and m (mknod).
|
|
func isValidDeviceMode(mode string) bool {
|
|
legalDeviceMode := map[rune]bool{
|
|
'r': true,
|
|
'w': true,
|
|
'm': true,
|
|
}
|
|
if mode == "" {
|
|
return false
|
|
}
|
|
for _, c := range mode {
|
|
if !legalDeviceMode[c] {
|
|
return false
|
|
}
|
|
legalDeviceMode[c] = false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// ValidateVolumeHostDir validates a volume mount's source directory
|
|
func ValidateVolumeHostDir(hostDir string) error {
|
|
if hostDir == "" {
|
|
return errors.New("host directory cannot be empty")
|
|
}
|
|
if filepath.IsAbs(hostDir) {
|
|
if _, err := os.Stat(hostDir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// If hostDir is not an absolute path, that means the user wants to create a
|
|
// named volume. This will be done later on in the code.
|
|
return nil
|
|
}
|
|
|
|
// ValidateVolumeCtrDir validates a volume mount's destination directory.
|
|
func ValidateVolumeCtrDir(ctrDir string) error {
|
|
if ctrDir == "" {
|
|
return errors.New("container directory cannot be empty")
|
|
}
|
|
if !path.IsAbs(ctrDir) {
|
|
return fmt.Errorf("invalid container path %q, must be an absolute path", ctrDir)
|
|
}
|
|
return nil
|
|
}
|