Files
podman/pkg/specgenutil/volumes.go
Paul Holzinger 9e94dc53b2 add new artifact mount type
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>
2025-03-12 19:42:14 +01:00

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)
}