mirror of
https://github.com/containers/podman.git
synced 2025-05-22 01:27:07 +08:00

Add a new option to allow for mounting artifacts in the container, the syntax is added to the existing --mount option: type=artifact,src=$artifactName,dest=/path[,digest=x][,title=x] This works very similar to image mounts. The name is passed down into the container config and then on each start we lookup the artifact and the figure out which blobs to mount. There is no protaction against a user removing the artifact while still being used in a container. When the container is running the bind mounted files will stay there (as the kernel keeps the mounts active even if the bind source was deleted). On the next start it will fail to start as if it does not find the artifact. The good thing is that this technically allows someone to update the artifact with the new file by creating a new artifact with the same name. Signed-off-by: Paul Holzinger <pholzing@redhat.com>
806 lines
24 KiB
Go
806 lines
24 KiB
Go
package specgenutil
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/containers/common/pkg/config"
|
|
"github.com/containers/common/pkg/parse"
|
|
"github.com/containers/podman/v5/libpod/define"
|
|
"github.com/containers/podman/v5/pkg/rootless"
|
|
"github.com/containers/podman/v5/pkg/specgen"
|
|
"github.com/containers/podman/v5/pkg/specgenutilexternal"
|
|
"github.com/containers/podman/v5/pkg/util"
|
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
|
)
|
|
|
|
var (
|
|
errOptionArg = errors.New("must provide an argument for option")
|
|
errNoDest = errors.New("must set volume destination")
|
|
)
|
|
|
|
type containerMountSlice struct {
|
|
mounts []spec.Mount
|
|
volumes []*specgen.NamedVolume
|
|
overlayVolumes []*specgen.OverlayVolume
|
|
imageVolumes []*specgen.ImageVolume
|
|
artifactVolumes []*specgen.ArtifactVolume
|
|
}
|
|
|
|
// containerMountMap contains the container mounts with the destination path as map key
|
|
type containerMountMap struct {
|
|
mounts map[string]spec.Mount
|
|
volumes map[string]*specgen.NamedVolume
|
|
imageVolumes map[string]*specgen.ImageVolume
|
|
artifactVolumes map[string]*specgen.ArtifactVolume
|
|
}
|
|
|
|
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.
|
|
// 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(rtc *config.Config, volumeFlag, mountFlag, tmpfsFlag []string) (*containerMountSlice, error) {
|
|
// Get mounts from the --mounts flag.
|
|
// TODO: The runtime config part of this needs to move into pkg/specgen/generate to avoid querying containers.conf on the client.
|
|
unifiedContainerMounts, err := mounts(mountFlag, rtc.Mounts())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Next --volumes flag.
|
|
volumeMounts, volumeVolumes, overlayVolumes, err := specgen.GenVolumeMounts(volumeFlag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Next --tmpfs flag.
|
|
tmpfsMounts, err := getTmpfsMounts(tmpfsFlag)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Unify mounts from --mount, --volume, --tmpfs.
|
|
// Start with --volume.
|
|
for dest, mount := range volumeMounts {
|
|
if vol, ok := unifiedContainerMounts.mounts[dest]; ok {
|
|
if mount.Source == vol.Source &&
|
|
specgen.StringSlicesEqual(vol.Options, mount.Options) {
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
|
|
}
|
|
unifiedContainerMounts.mounts[dest] = mount
|
|
}
|
|
for dest, volume := range volumeVolumes {
|
|
if vol, ok := unifiedContainerMounts.volumes[dest]; ok {
|
|
if volume.Name == vol.Name &&
|
|
specgen.StringSlicesEqual(vol.Options, volume.Options) {
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
|
|
}
|
|
unifiedContainerMounts.volumes[dest] = volume
|
|
}
|
|
// Now --tmpfs
|
|
for dest, tmpfs := range tmpfsMounts {
|
|
if vol, ok := unifiedContainerMounts.mounts[dest]; ok {
|
|
if vol.Type != define.TypeTmpfs {
|
|
return nil, fmt.Errorf("%v: %w", dest, specgen.ErrDuplicateDest)
|
|
}
|
|
continue
|
|
}
|
|
unifiedContainerMounts.mounts[dest] = tmpfs
|
|
}
|
|
|
|
// 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("%v: %w", dest, specgen.ErrDuplicateDest)
|
|
}
|
|
allMounts[dest] = true
|
|
return nil
|
|
}
|
|
for dest := range unifiedContainerMounts.mounts {
|
|
if err := testAndSet(dest); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for dest := range unifiedContainerMounts.volumes {
|
|
if err := testAndSet(dest); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for dest := range overlayVolumes {
|
|
if err := testAndSet(dest); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for dest := range unifiedContainerMounts.imageVolumes {
|
|
if err := testAndSet(dest); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
for dest := range unifiedContainerMounts.artifactVolumes {
|
|
if err := testAndSet(dest); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Final step: maps to arrays
|
|
finalMounts := make([]spec.Mount, 0, len(unifiedContainerMounts.mounts))
|
|
for _, mount := range unifiedContainerMounts.mounts {
|
|
if mount.Type == define.TypeBind {
|
|
absSrc, err := specgen.ConvertWinMountPath(mount.Source)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting absolute path of %s: %w", mount.Source, err)
|
|
}
|
|
mount.Source = absSrc
|
|
}
|
|
finalMounts = append(finalMounts, mount)
|
|
}
|
|
finalVolumes := make([]*specgen.NamedVolume, 0, len(unifiedContainerMounts.volumes))
|
|
for _, volume := range unifiedContainerMounts.volumes {
|
|
finalVolumes = append(finalVolumes, volume)
|
|
}
|
|
finalOverlayVolume := make([]*specgen.OverlayVolume, 0, len(overlayVolumes))
|
|
for _, volume := range overlayVolumes {
|
|
finalOverlayVolume = append(finalOverlayVolume, volume)
|
|
}
|
|
finalImageVolumes := make([]*specgen.ImageVolume, 0, len(unifiedContainerMounts.imageVolumes))
|
|
for _, volume := range unifiedContainerMounts.imageVolumes {
|
|
finalImageVolumes = append(finalImageVolumes, volume)
|
|
}
|
|
finalArtifactVolumes := make([]*specgen.ArtifactVolume, 0, len(unifiedContainerMounts.artifactVolumes))
|
|
for _, volume := range unifiedContainerMounts.artifactVolumes {
|
|
finalArtifactVolumes = append(finalArtifactVolumes, volume)
|
|
}
|
|
|
|
return &containerMountSlice{
|
|
mounts: finalMounts,
|
|
volumes: finalVolumes,
|
|
overlayVolumes: finalOverlayVolume,
|
|
imageVolumes: finalImageVolumes,
|
|
artifactVolumes: finalArtifactVolumes,
|
|
}, nil
|
|
}
|
|
|
|
// mounts takes user-provided input from the --mount flag as well as mounts
|
|
// specified in containers.conf 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, ...
|
|
// podman run --mount type=artifact,source=$artifact,dest=...
|
|
func mounts(mountFlag []string, configMounts []string) (*containerMountMap, error) {
|
|
finalMounts := make(map[string]spec.Mount)
|
|
finalNamedVolumes := make(map[string]*specgen.NamedVolume)
|
|
finalImageVolumes := make(map[string]*specgen.ImageVolume)
|
|
finalArtifactVolumes := make(map[string]*specgen.ArtifactVolume)
|
|
parseMounts := func(mounts []string, ignoreDup bool) error {
|
|
for _, mount := range mounts {
|
|
// TODO: Docker defaults to "volume" if no mount type is specified.
|
|
mountType, tokens, err := specgenutilexternal.FindMountType(mount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch mountType {
|
|
case define.TypeBind:
|
|
mount, err := getBindMount(tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
case "glob":
|
|
mounts, err := getGlobMounts(tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, mount := range mounts {
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
}
|
|
case define.TypeTmpfs, define.TypeRamfs:
|
|
mount, err := parseMemoryMount(tokens, mountType)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
case define.TypeDevpts:
|
|
mount, err := getDevptsMount(tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", mount.Destination, specgen.ErrDuplicateDest)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
case "image":
|
|
volume, err := getImageVolume(tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := finalImageVolumes[volume.Destination]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", volume.Destination, specgen.ErrDuplicateDest)
|
|
}
|
|
finalImageVolumes[volume.Destination] = volume
|
|
case "artifact":
|
|
volume, err := getArtifactVolume(tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := finalArtifactVolumes[volume.Destination]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", volume.Destination, specgen.ErrDuplicateDest)
|
|
}
|
|
finalArtifactVolumes[volume.Destination] = volume
|
|
case "volume":
|
|
volume, err := getNamedVolume(tokens)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := finalNamedVolumes[volume.Dest]; ok {
|
|
if ignoreDup {
|
|
continue
|
|
}
|
|
return fmt.Errorf("%v: %w", volume.Dest, specgen.ErrDuplicateDest)
|
|
}
|
|
finalNamedVolumes[volume.Dest] = volume
|
|
default:
|
|
return fmt.Errorf("invalid filesystem type %q", mountType)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Parse mounts passed in from the user
|
|
if err := parseMounts(mountFlag, false); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If user specified a mount flag that conflicts with a containers.conf flag, then ignore
|
|
// the duplicate. This means that the parsing of the containers.conf configMounts should always
|
|
// happen second.
|
|
if err := parseMounts(configMounts, true); err != nil {
|
|
return nil, fmt.Errorf("parsing containers.conf mounts: %w", err)
|
|
}
|
|
|
|
return &containerMountMap{
|
|
mounts: finalMounts,
|
|
volumes: finalNamedVolumes,
|
|
imageVolumes: finalImageVolumes,
|
|
artifactVolumes: finalArtifactVolumes,
|
|
}, nil
|
|
}
|
|
|
|
func parseMountOptions(mountType string, args []string) (*universalMount, error) {
|
|
var setTmpcopyup, setRORW, setSuid, setDev, setExec, setRelabel, setOwnership, setSwap bool
|
|
|
|
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.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)
|
|
}
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
switch value {
|
|
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "unbindable", "runbindable":
|
|
// Do nothing, sane value
|
|
default:
|
|
return nil, fmt.Errorf("invalid value %q", arg)
|
|
}
|
|
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.mount.Options = append(mnt.mount.Options, fmt.Sprintf("idmap=%s", value))
|
|
} else {
|
|
mnt.mount.Options = append(mnt.mount.Options, "idmap")
|
|
}
|
|
case "readonly", "ro", "rw":
|
|
if setRORW {
|
|
return nil, fmt.Errorf("cannot pass 'readonly', 'ro', or 'rw' mnt.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 name == "readonly" {
|
|
name = "ro"
|
|
}
|
|
if hasValue {
|
|
switch strings.ToLower(value) {
|
|
case "true":
|
|
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.mount.Options = append(mnt.mount.Options, "ro")
|
|
}
|
|
}
|
|
} else {
|
|
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.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.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.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)
|
|
}
|
|
if rootless.IsRootless() {
|
|
return nil, fmt.Errorf("the 'noswap' option is only allowed with rootful tmpfs mounts: %w", errOptionArg)
|
|
}
|
|
setSwap = true
|
|
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)
|
|
}
|
|
setRelabel = true
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%s mount option must be 'private' or 'shared': %w", name, util.ErrBadMntOption)
|
|
}
|
|
switch value {
|
|
case "private":
|
|
mnt.mount.Options = append(mnt.mount.Options, "Z")
|
|
case "shared":
|
|
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.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.mount.Source != "" {
|
|
return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
|
|
}
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
if len(value) == 0 {
|
|
return nil, fmt.Errorf("host directory cannot be empty: %w", errOptionArg)
|
|
}
|
|
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.mount.Destination != "" {
|
|
return nil, fmt.Errorf("cannot pass %q option more than once: %w", name, errOptionArg)
|
|
}
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(value); err != nil {
|
|
return nil, err
|
|
}
|
|
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)
|
|
}
|
|
if setTmpcopyup {
|
|
return nil, fmt.Errorf("cannot pass 'tmpcopyup' and 'notmpcopyup' mnt.Options more than once: %w", errOptionArg)
|
|
}
|
|
setTmpcopyup = true
|
|
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.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.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)
|
|
}
|
|
ok, err := validChownFlag(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ok {
|
|
mnt.mount.Options = append(mnt.mount.Options, "U")
|
|
}
|
|
setOwnership = true
|
|
case "volume-label":
|
|
if mountType != define.TypeVolume {
|
|
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
|
|
}
|
|
return nil, fmt.Errorf("the --volume-label option is not presently implemented")
|
|
case "volume-opt":
|
|
if mountType != define.TypeVolume {
|
|
return nil, fmt.Errorf("%q option not supported for %q mount types", name, mountType)
|
|
}
|
|
mnt.mount.Options = append(mnt.mount.Options, arg)
|
|
default:
|
|
return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
|
|
}
|
|
}
|
|
if mountType != "glob" && len(mnt.mount.Destination) == 0 {
|
|
return nil, errNoDest
|
|
}
|
|
return mnt, nil
|
|
}
|
|
|
|
// Parse glob mounts entry from the --mount flag.
|
|
func getGlobMounts(args []string) ([]spec.Mount, error) {
|
|
mounts := []spec.Mount{}
|
|
|
|
uMnt, err := parseMountOptions("glob", args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mnt := uMnt.mount
|
|
|
|
globs, err := filepath.Glob(mnt.Source)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(globs) == 0 {
|
|
return nil, fmt.Errorf("no file paths matching glob %q", mnt.Source)
|
|
}
|
|
|
|
options, err := parse.ValidateVolumeOpts(mnt.Options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, src := range globs {
|
|
var newMount spec.Mount
|
|
newMount.Type = define.TypeBind
|
|
newMount.Options = options
|
|
newMount.Source = src
|
|
if len(mnt.Destination) == 0 {
|
|
newMount.Destination = src
|
|
} else {
|
|
newMount.Destination = filepath.Join(mnt.Destination, filepath.Base(src))
|
|
}
|
|
mounts = append(mounts, newMount)
|
|
}
|
|
|
|
return mounts, 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 err error
|
|
uMnt, err := parseMountOptions(newMount.Type, args)
|
|
if err != nil {
|
|
return newMount, err
|
|
}
|
|
mnt := uMnt.mount
|
|
|
|
if len(mnt.Destination) == 0 {
|
|
return newMount, errNoDest
|
|
}
|
|
|
|
if len(mnt.Source) == 0 {
|
|
mnt.Source = mnt.Destination
|
|
}
|
|
|
|
options, err := parse.ValidateVolumeOpts(mnt.Options)
|
|
if err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Source = mnt.Source
|
|
newMount.Destination = mnt.Destination
|
|
newMount.Options = options
|
|
return newMount, nil
|
|
}
|
|
|
|
// Parse a single tmpfs/ramfs mount entry from the --mount flag
|
|
func parseMemoryMount(args []string, mountType string) (spec.Mount, error) {
|
|
newMount := spec.Mount{
|
|
Type: mountType,
|
|
Source: mountType,
|
|
}
|
|
|
|
var err error
|
|
uMnt, err := parseMountOptions(newMount.Type, args)
|
|
if err != nil {
|
|
return newMount, err
|
|
}
|
|
mnt := uMnt.mount
|
|
if len(mnt.Destination) == 0 {
|
|
return newMount, errNoDest
|
|
}
|
|
newMount.Destination = mnt.Destination
|
|
newMount.Options = mnt.Options
|
|
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 _, arg := range args {
|
|
name, value, hasValue := strings.Cut(arg, "=")
|
|
switch name {
|
|
case "uid", "gid", "mode", "ptmxmode", "newinstance", "max":
|
|
newMount.Options = append(newMount.Options, arg)
|
|
case "target", "dst", "destination":
|
|
if !hasValue {
|
|
return newMount, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(value); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Destination = unixPathClean(value)
|
|
setDest = true
|
|
default:
|
|
return newMount, fmt.Errorf("%s: %w", name, 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)
|
|
|
|
mnt, err := parseMountOptions(define.TypeVolume, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(mnt.mount.Destination) == 0 {
|
|
return nil, errNoDest
|
|
}
|
|
|
|
newVolume.Options = mnt.mount.Options
|
|
newVolume.SubPath = mnt.subPath
|
|
newVolume.Name = mnt.mount.Source
|
|
newVolume.Dest = mnt.mount.Destination
|
|
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 _, arg := range args {
|
|
name, value, hasValue := strings.Cut(arg, "=")
|
|
switch name {
|
|
case "src", "source":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
newVolume.Source = value
|
|
case "target", "dst", "destination":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(value); err != nil {
|
|
return nil, err
|
|
}
|
|
newVolume.Destination = unixPathClean(value)
|
|
case "rw", "readwrite":
|
|
switch value {
|
|
case "true":
|
|
newVolume.ReadWrite = true
|
|
case "false":
|
|
// Nothing to do. RO is default.
|
|
default:
|
|
return nil, fmt.Errorf("invalid rw value %q: %w", value, util.ErrBadMntOption)
|
|
}
|
|
case "subpath":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
if !filepath.IsAbs(value) {
|
|
return nil, fmt.Errorf("volume subpath %q must be an absolute path", value)
|
|
}
|
|
newVolume.SubPath = value
|
|
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", name, 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
|
|
}
|
|
|
|
// Parse the arguments into an artifact volume. An artifact volume creates mounts
|
|
// based on an existing artifact in the store.
|
|
func getArtifactVolume(args []string) (*specgen.ArtifactVolume, error) {
|
|
newVolume := new(specgen.ArtifactVolume)
|
|
|
|
for _, arg := range args {
|
|
name, value, hasValue := strings.Cut(arg, "=")
|
|
switch name {
|
|
case "src", "source":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
newVolume.Source = value
|
|
case "target", "dst", "destination":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(value); err != nil {
|
|
return nil, err
|
|
}
|
|
newVolume.Destination = unixPathClean(value)
|
|
case "title":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
newVolume.Title = value
|
|
|
|
case "digest":
|
|
if !hasValue {
|
|
return nil, fmt.Errorf("%v: %w", name, errOptionArg)
|
|
}
|
|
newVolume.Digest = value
|
|
default:
|
|
return nil, fmt.Errorf("%s: %w", name, util.ErrBadMntOption)
|
|
}
|
|
}
|
|
|
|
if len(newVolume.Source)*len(newVolume.Destination) == 0 {
|
|
return nil, errors.New("must set source and destination for artifact 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 vol, ok := m[destPath]; ok {
|
|
if specgen.StringSlicesEqual(vol.Options, options) {
|
|
continue
|
|
}
|
|
return nil, fmt.Errorf("%v: %w", destPath, specgen.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(value string) (bool, error) {
|
|
// U=[true|false]
|
|
switch {
|
|
case strings.EqualFold(value, "true"), value == "":
|
|
return true, nil
|
|
case strings.EqualFold(value, "false"):
|
|
return false, nil
|
|
default:
|
|
return false, fmt.Errorf("'U' or 'chown' must be set to true or false, instead received %q: %w", value, errOptionArg)
|
|
}
|
|
}
|
|
|
|
// Use path instead of filepath to preserve Unix style paths on Windows
|
|
func unixPathClean(p string) string {
|
|
return path.Clean(p)
|
|
}
|