Files
podman/pkg/specgenutil/volumes.go
Sascha Grunert a46f798831 pkg: switch to golang native error wrapping
We now use the golang error wrapping format specifier `%w` instead of
the deprecated github.com/pkg/errors package.

[NO NEW TESTS NEEDED]

Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
2022-07-08 08:54:47 +02:00

708 lines
21 KiB
Go

package specgenutil
import (
"encoding/csv"
"errors"
"fmt"
"path"
"strings"
"github.com/containers/common/pkg/parse"
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/pkg/specgen"
"github.com/containers/podman/v4/pkg/util"
spec "github.com/opencontainers/runtime-spec/specs-go"
)
var (
errDuplicateDest = errors.New("duplicate mount destination")
errOptionArg = errors.New("must provide an argument for option")
errNoDest = errors.New("must set volume destination")
errInvalidSyntax = errors.New("incorrect mount format: should be --mount type=<bind|tmpfs|volume>,[src=<host-dir|volume-name>,]target=<ctr-dir>[,options]")
)
// 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.
// Does not handle image volumes, init, and --volumes-from flags.
// Can also add tmpfs mounts from read-only tmpfs.
// TODO: handle options parsing/processing via containers/storage/pkg/mount
func parseVolumes(volumeFlag, mountFlag, tmpfsFlag []string, addReadOnlyTmpfs bool) ([]spec.Mount, []*specgen.NamedVolume, []*specgen.OverlayVolume, []*specgen.ImageVolume, error) {
// Get mounts from the --mounts flag.
unifiedMounts, unifiedVolumes, unifiedImageVolumes, err := Mounts(mountFlag)
if err != nil {
return nil, nil, nil, nil, err
}
// Next --volumes flag.
volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag)
if err != nil {
return nil, nil, nil, nil, err
}
// Next --tmpfs flag.
tmpfsMounts, err := getTmpfsMounts(tmpfsFlag)
if err != nil {
return nil, nil, nil, nil, err
}
// Unify mounts from --mount, --volume, --tmpfs.
// Start with --volume.
for dest, mount := range volumeMounts {
if _, ok := unifiedMounts[dest]; ok {
return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
}
unifiedMounts[dest] = mount
}
for dest, volume := range volumeVolumes {
if _, ok := unifiedVolumes[dest]; ok {
return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
}
unifiedVolumes[dest] = volume
}
// Now --tmpfs
for dest, tmpfs := range tmpfsMounts {
if _, ok := unifiedMounts[dest]; ok {
return nil, nil, nil, nil, fmt.Errorf("%v: %w", dest, errDuplicateDest)
}
unifiedMounts[dest] = tmpfs
}
// If requested, add tmpfs filesystems for read-only containers.
if addReadOnlyTmpfs {
readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"}
options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"}
for _, dest := range readonlyTmpfs {
if _, ok := unifiedMounts[dest]; ok {
continue
}
if _, ok := unifiedVolumes[dest]; ok {
continue
}
unifiedMounts[dest] = spec.Mount{
Destination: dest,
Type: define.TypeTmpfs,
Source: "tmpfs",
Options: options,
}
}
}
// Check for conflicts between named volumes, overlay & image volumes,
// and mounts
allMounts := make(map[string]bool)
testAndSet := func(dest string) error {
if _, ok := allMounts[dest]; ok {
return fmt.Errorf("conflict at mount destination %v: %w", dest, errDuplicateDest)
}
allMounts[dest] = true
return nil
}
for dest := range unifiedMounts {
if err := testAndSet(dest); err != nil {
return nil, nil, nil, nil, err
}
}
for dest := range unifiedVolumes {
if err := testAndSet(dest); err != nil {
return nil, nil, nil, nil, err
}
}
for dest := range overlayVolumes {
if err := testAndSet(dest); err != nil {
return nil, nil, nil, nil, err
}
}
for dest := range unifiedImageVolumes {
if err := testAndSet(dest); err != nil {
return nil, nil, nil, nil, err
}
}
// Final step: maps to arrays
finalMounts := make([]spec.Mount, 0, len(unifiedMounts))
for _, mount := range unifiedMounts {
if mount.Type == define.TypeBind {
absSrc, err := specgen.ConvertWinMountPath(mount.Source)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("error getting absolute path of %s: %w", mount.Source, err)
}
mount.Source = absSrc
}
finalMounts = append(finalMounts, mount)
}
finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedVolumes))
for _, volume := range unifiedVolumes {
finalVolumes = append(finalVolumes, volume)
}
finalOverlayVolume := make([]*specgen.OverlayVolume, 0)
for _, volume := range overlayVolumes {
finalOverlayVolume = append(finalOverlayVolume, volume)
}
finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedImageVolumes))
for _, volume := range unifiedImageVolumes {
finalImageVolumes = append(finalImageVolumes, volume)
}
return finalMounts, finalVolumes, finalOverlayVolume, finalImageVolumes, nil
}
// findMountType parses the input and extracts the type of the mount type and
// the remaining non-type tokens.
func findMountType(input string) (mountType string, tokens []string, err error) {
// Split by comma, iterate over the slice and look for
// "type=$mountType". Everything else is appended to tokens.
found := false
csvReader := csv.NewReader(strings.NewReader(input))
records, err := csvReader.ReadAll()
if err != nil {
return "", nil, err
}
if len(records) != 1 {
return "", nil, errInvalidSyntax
}
for _, s := range records[0] {
kv := strings.Split(s, "=")
if found || !(len(kv) == 2 && kv[0] == "type") {
tokens = append(tokens, s)
continue
}
mountType = kv[1]
found = true
}
if !found {
err = errInvalidSyntax
}
return
}
// Mounts takes user-provided input from the --mount flag and creates OCI
// spec mounts and Libpod named volumes.
// podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
// podman run --mount type=tmpfs,target=/dev/shm ...
// podman run --mount type=volume,source=test-volume, ...
func Mounts(mountFlag []string) (map[string]spec.Mount, map[string]*specgen.NamedVolume, map[string]*specgen.ImageVolume, error) {
finalMounts := make(map[string]spec.Mount)
finalNamedVolumes := make(map[string]*specgen.NamedVolume)
finalImageVolumes := make(map[string]*specgen.ImageVolume)
for _, mount := range mountFlag {
// TODO: Docker defaults to "volume" if no mount type is specified.
mountType, tokens, err := findMountType(mount)
if err != nil {
return nil, nil, nil, err
}
switch mountType {
case define.TypeBind:
mount, err := getBindMount(tokens)
if err != nil {
return nil, nil, nil, err
}
if _, ok := finalMounts[mount.Destination]; ok {
return nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
}
finalMounts[mount.Destination] = mount
case define.TypeTmpfs:
mount, err := getTmpfsMount(tokens)
if err != nil {
return nil, nil, nil, err
}
if _, ok := finalMounts[mount.Destination]; ok {
return nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
}
finalMounts[mount.Destination] = mount
case define.TypeDevpts:
mount, err := getDevptsMount(tokens)
if err != nil {
return nil, nil, nil, err
}
if _, ok := finalMounts[mount.Destination]; ok {
return nil, nil, nil, fmt.Errorf("%v: %w", mount.Destination, errDuplicateDest)
}
finalMounts[mount.Destination] = mount
case "image":
volume, err := getImageVolume(tokens)
if err != nil {
return nil, nil, nil, err
}
if _, ok := finalImageVolumes[volume.Destination]; ok {
return nil, nil, nil, fmt.Errorf("%v: %w", volume.Destination, errDuplicateDest)
}
finalImageVolumes[volume.Destination] = volume
case "volume":
volume, err := getNamedVolume(tokens)
if err != nil {
return nil, nil, nil, err
}
if _, ok := finalNamedVolumes[volume.Dest]; ok {
return nil, nil, nil, fmt.Errorf("%v: %w", volume.Dest, errDuplicateDest)
}
finalNamedVolumes[volume.Dest] = volume
default:
return nil, nil, nil, fmt.Errorf("invalid filesystem type %q", mountType)
}
}
return finalMounts, finalNamedVolumes, finalImageVolumes, nil
}
// Parse a single bind mount entry from the --mount flag.
func getBindMount(args []string) (spec.Mount, error) {
newMount := spec.Mount{
Type: define.TypeBind,
}
var setSource, setDest, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership bool
for _, val := range args {
kv := strings.SplitN(val, "=", 2)
switch kv[0] {
case "bind-nonrecursive":
newMount.Options = append(newMount.Options, "bind")
case "readonly", "ro", "rw":
if setRORW {
return newMount, fmt.Errorf("cannot pass 'readonly', 'ro', or 'rw' options more than once: %w", errOptionArg)
}
setRORW = true
// Can be formatted as one of:
// readonly
// readonly=[true|false]
// ro
// ro=[true|false]
// rw
// rw=[true|false]
if kv[0] == "readonly" {
kv[0] = "ro"
}
switch len(kv) {
case 1:
newMount.Options = append(newMount.Options, kv[0])
case 2:
switch strings.ToLower(kv[1]) {
case "true":
newMount.Options = append(newMount.Options, kv[0])
case "false":
// Set the opposite only for rw
// ro's opposite is the default
if kv[0] == "rw" {
newMount.Options = append(newMount.Options, "ro")
}
default:
return newMount, fmt.Errorf("'readonly', 'ro', or 'rw' must be set to true or false, instead received %q: %w", kv[1], errOptionArg)
}
default:
return newMount, fmt.Errorf("badly formatted option %q: %w", val, errOptionArg)
}
case "nosuid", "suid":
if setSuid {
return newMount, fmt.Errorf("cannot pass 'nosuid' and 'suid' options more than once: %w", errOptionArg)
}
setSuid = true
newMount.Options = append(newMount.Options, kv[0])
case "nodev", "dev":
if setDev {
return newMount, fmt.Errorf("cannot pass 'nodev' and 'dev' options more than once: %w", errOptionArg)
}
setDev = true
newMount.Options = append(newMount.Options, kv[0])
case "noexec", "exec":
if setExec {
return newMount, fmt.Errorf("cannot pass 'noexec' and 'exec' options more than once: %w", errOptionArg)
}
setExec = true
newMount.Options = append(newMount.Options, kv[0])
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "unbindable", "runbindable", "Z", "z":
newMount.Options = append(newMount.Options, kv[0])
case "bind-propagation":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
newMount.Options = append(newMount.Options, kv[1])
case "src", "source":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
if len(kv[1]) == 0 {
return newMount, fmt.Errorf("host directory cannot be empty: %w", errOptionArg)
}
newMount.Source = kv[1]
setSource = true
case "target", "dst", "destination":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
return newMount, err
}
newMount.Destination = unixPathClean(kv[1])
setDest = true
case "relabel":
if setRelabel {
return newMount, fmt.Errorf("cannot pass 'relabel' option more than once: %w", errOptionArg)
}
setRelabel = true
if len(kv) != 2 {
return newMount, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", kv[0], util.ErrBadMntOption)
}
switch kv[1] {
case "private":
newMount.Options = append(newMount.Options, "Z")
case "shared":
newMount.Options = append(newMount.Options, "z")
default:
return newMount, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", kv[0], util.ErrBadMntOption)
}
case "U", "chown":
if setOwnership {
return newMount, fmt.Errorf("cannot pass 'U' or 'chown' option more than once: %w", errOptionArg)
}
ok, err := validChownFlag(val)
if err != nil {
return newMount, err
}
if ok {
newMount.Options = append(newMount.Options, "U")
}
setOwnership = true
case "idmap":
if len(kv) > 1 {
newMount.Options = append(newMount.Options, fmt.Sprintf("idmap=%s", kv[1]))
} else {
newMount.Options = append(newMount.Options, "idmap")
}
case "consistency":
// Often used on MACs and mistakenly on Linux platforms.
// Since Docker ignores this option so shall we.
continue
default:
return newMount, fmt.Errorf("%s: %w", kv[0], util.ErrBadMntOption)
}
}
if !setDest {
return newMount, errNoDest
}
if !setSource {
newMount.Source = newMount.Destination
}
options, err := parse.ValidateVolumeOpts(newMount.Options)
if err != nil {
return newMount, err
}
newMount.Options = options
return newMount, nil
}
// Parse a single tmpfs mount entry from the --mount flag
func getTmpfsMount(args []string) (spec.Mount, error) {
newMount := spec.Mount{
Type: define.TypeTmpfs,
Source: define.TypeTmpfs,
}
var setDest, setRORW, setSuid, setDev, setExec, setTmpcopyup, setOwnership bool
for _, val := range args {
kv := strings.SplitN(val, "=", 2)
switch kv[0] {
case "tmpcopyup", "notmpcopyup":
if setTmpcopyup {
return newMount, fmt.Errorf("cannot pass 'tmpcopyup' and 'notmpcopyup' options more than once: %w", errOptionArg)
}
setTmpcopyup = true
newMount.Options = append(newMount.Options, kv[0])
case "ro", "rw":
if setRORW {
return newMount, fmt.Errorf("cannot pass 'ro' and 'rw' options more than once: %w", errOptionArg)
}
setRORW = true
newMount.Options = append(newMount.Options, kv[0])
case "nosuid", "suid":
if setSuid {
return newMount, fmt.Errorf("cannot pass 'nosuid' and 'suid' options more than once: %w", errOptionArg)
}
setSuid = true
newMount.Options = append(newMount.Options, kv[0])
case "nodev", "dev":
if setDev {
return newMount, fmt.Errorf("cannot pass 'nodev' and 'dev' options more than once: %w", errOptionArg)
}
setDev = true
newMount.Options = append(newMount.Options, kv[0])
case "noexec", "exec":
if setExec {
return newMount, fmt.Errorf("cannot pass 'noexec' and 'exec' options more than once: %w", errOptionArg)
}
setExec = true
newMount.Options = append(newMount.Options, kv[0])
case "tmpfs-mode":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1]))
case "tmpfs-size":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1]))
case "src", "source":
return newMount, fmt.Errorf("source is not supported with tmpfs mounts")
case "target", "dst", "destination":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
return newMount, err
}
newMount.Destination = unixPathClean(kv[1])
setDest = true
case "U", "chown":
if setOwnership {
return newMount, fmt.Errorf("cannot pass 'U' or 'chown' option more than once: %w", errOptionArg)
}
ok, err := validChownFlag(val)
if err != nil {
return newMount, err
}
if ok {
newMount.Options = append(newMount.Options, "U")
}
setOwnership = true
case "consistency":
// Often used on MACs and mistakenly on Linux platforms.
// Since Docker ignores this option so shall we.
continue
default:
return newMount, fmt.Errorf("%s: %w", kv[0], util.ErrBadMntOption)
}
}
if !setDest {
return newMount, errNoDest
}
return newMount, nil
}
// Parse a single devpts mount entry from the --mount flag
func getDevptsMount(args []string) (spec.Mount, error) {
newMount := spec.Mount{
Type: define.TypeDevpts,
Source: define.TypeDevpts,
}
var setDest bool
for _, val := range args {
kv := strings.SplitN(val, "=", 2)
switch kv[0] {
case "uid", "gid", "mode", "ptxmode", "newinstance", "max":
newMount.Options = append(newMount.Options, val)
case "target", "dst", "destination":
if len(kv) == 1 {
return newMount, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
return newMount, err
}
newMount.Destination = unixPathClean(kv[1])
setDest = true
default:
return newMount, fmt.Errorf("%s: %w", kv[0], util.ErrBadMntOption)
}
}
if !setDest {
return newMount, errNoDest
}
return newMount, nil
}
// Parse a single volume mount entry from the --mount flag.
// Note that the volume-label option for named volumes is currently NOT supported.
// TODO: add support for --volume-label
func getNamedVolume(args []string) (*specgen.NamedVolume, error) {
newVolume := new(specgen.NamedVolume)
var setDest, setRORW, setSuid, setDev, setExec, setOwnership bool
for _, val := range args {
kv := strings.SplitN(val, "=", 2)
switch kv[0] {
case "volume-opt":
newVolume.Options = append(newVolume.Options, val)
case "ro", "rw":
if setRORW {
return nil, fmt.Errorf("cannot pass 'ro' and 'rw' options more than once: %w", errOptionArg)
}
setRORW = true
newVolume.Options = append(newVolume.Options, kv[0])
case "nosuid", "suid":
if setSuid {
return nil, fmt.Errorf("cannot pass 'nosuid' and 'suid' options more than once: %w", errOptionArg)
}
setSuid = true
newVolume.Options = append(newVolume.Options, kv[0])
case "nodev", "dev":
if setDev {
return nil, fmt.Errorf("cannot pass 'nodev' and 'dev' options more than once: %w", errOptionArg)
}
setDev = true
newVolume.Options = append(newVolume.Options, kv[0])
case "noexec", "exec":
if setExec {
return nil, fmt.Errorf("cannot pass 'noexec' and 'exec' options more than once: %w", errOptionArg)
}
setExec = true
newVolume.Options = append(newVolume.Options, kv[0])
case "volume-label":
return nil, fmt.Errorf("the --volume-label option is not presently implemented")
case "src", "source":
if len(kv) == 1 {
return nil, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
newVolume.Name = kv[1]
case "target", "dst", "destination":
if len(kv) == 1 {
return nil, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
return nil, err
}
newVolume.Dest = unixPathClean(kv[1])
setDest = true
case "U", "chown":
if setOwnership {
return newVolume, fmt.Errorf("cannot pass 'U' or 'chown' option more than once: %w", errOptionArg)
}
ok, err := validChownFlag(val)
if err != nil {
return newVolume, err
}
if ok {
newVolume.Options = append(newVolume.Options, "U")
}
setOwnership = true
case "consistency":
// Often used on MACs and mistakenly on Linux platforms.
// Since Docker ignores this option so shall we.
continue
default:
return nil, fmt.Errorf("%s: %w", kv[0], util.ErrBadMntOption)
}
}
if !setDest {
return nil, errNoDest
}
return newVolume, nil
}
// Parse the arguments into an image volume. An image volume is a volume based
// on a container image. The container image is first mounted on the host and
// is then bind-mounted into the container. An ImageVolume is always mounted
// read-only.
func getImageVolume(args []string) (*specgen.ImageVolume, error) {
newVolume := new(specgen.ImageVolume)
for _, val := range args {
kv := strings.SplitN(val, "=", 2)
switch kv[0] {
case "src", "source":
if len(kv) == 1 {
return nil, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
newVolume.Source = kv[1]
case "target", "dst", "destination":
if len(kv) == 1 {
return nil, fmt.Errorf("%v: %w", kv[0], errOptionArg)
}
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
return nil, err
}
newVolume.Destination = unixPathClean(kv[1])
case "rw", "readwrite":
switch kv[1] {
case "true":
newVolume.ReadWrite = true
case "false":
// Nothing to do. RO is default.
default:
return nil, fmt.Errorf("invalid rw value %q: %w", kv[1], util.ErrBadMntOption)
}
case "consistency":
// Often used on MACs and mistakenly on Linux platforms.
// Since Docker ignores this option so shall we.
continue
default:
return nil, fmt.Errorf("%s: %w", kv[0], util.ErrBadMntOption)
}
}
if len(newVolume.Source)*len(newVolume.Destination) == 0 {
return nil, errors.New("must set source and destination for image volume")
}
return newVolume, nil
}
// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts
func getTmpfsMounts(tmpfsFlag []string) (map[string]spec.Mount, error) {
m := make(map[string]spec.Mount)
for _, i := range tmpfsFlag {
// Default options if nothing passed
var options []string
spliti := strings.Split(i, ":")
destPath := spliti[0]
if err := parse.ValidateVolumeCtrDir(spliti[0]); err != nil {
return nil, err
}
if len(spliti) > 1 {
options = strings.Split(spliti[1], ",")
}
if _, ok := m[destPath]; ok {
return nil, fmt.Errorf("%v: %w", destPath, errDuplicateDest)
}
mount := spec.Mount{
Destination: unixPathClean(destPath),
Type: define.TypeTmpfs,
Options: options,
Source: define.TypeTmpfs,
}
m[destPath] = mount
}
return m, nil
}
// validChownFlag ensures that the U or chown flag is correctly used
func validChownFlag(flag string) (bool, error) {
kv := strings.SplitN(flag, "=", 2)
switch len(kv) {
case 1:
case 2:
// U=[true|false]
switch strings.ToLower(kv[1]) {
case "true":
case "false":
return false, nil
default:
return false, fmt.Errorf("'U' or 'chown' must be set to true or false, instead received %q: %w", kv[1], errOptionArg)
}
default:
return false, fmt.Errorf("badly formatted option %q: %w", flag, errOptionArg)
}
return true, nil
}
// Use path instead of filepath to preserve Unix style paths on Windows
func unixPathClean(p string) string {
return path.Clean(p)
}