mirror of
https://github.com/containers/podman.git
synced 2025-06-02 02:26:52 +08:00

Previously, when `podman run` encountered a volume mount without separate source and destination (e.g. `-v /run`) we would assume that both were the same - a bind mount of `/run` on the host to `/run` in the container. However, this does not match Docker's behavior - in Docker, this makes an anonymous named volume that will be mounted at `/run`. We already have (more limited) support for these anonymous volumes in the form of image volumes. Extend this support to allow it to be used with user-created volumes coming in from the `-v` flag. This change also affects how named volumes created by the container but given names are treated by `podman run --rm` and `podman rm -v`. Previously, they would be removed with the container in these cases, but this did not match Docker's behaviour. Docker only removed anonymous volumes. With this patch we move to that model as well; `podman run -v testvol:/test` will not have `testvol` survive the container being removed by `podman rm -v`. The sum total of these changes let us turn on volume removal in `--rm` by default. Fixes: #4276 Signed-off-by: Matthew Heon <matthew.heon@pm.me>
922 lines
28 KiB
Go
922 lines
28 KiB
Go
package createconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/containers/buildah/pkg/parse"
|
|
"github.com/containers/libpod/libpod"
|
|
"github.com/containers/libpod/pkg/util"
|
|
pmount "github.com/containers/storage/pkg/mount"
|
|
spec "github.com/opencontainers/runtime-spec/specs-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
// TypeBind is the type for mounting host dir
|
|
TypeBind = "bind"
|
|
// TypeVolume is the type for named volumes
|
|
TypeVolume = "volume"
|
|
// TypeTmpfs is the type for mounting tmpfs
|
|
TypeTmpfs = "tmpfs"
|
|
)
|
|
|
|
var (
|
|
errDuplicateDest = errors.Errorf("duplicate mount destination")
|
|
optionArgError = errors.Errorf("must provide an argument for option")
|
|
noDestError = errors.Errorf("must set volume destination")
|
|
)
|
|
|
|
// Parse all volume-related options in the create config into a set of mounts
|
|
// and named volumes to add to the container.
|
|
// Handles --volumes-from, --volumes, --tmpfs, --init, and --init-path flags.
|
|
// TODO: Named volume options - should we default to rprivate? It bakes into a
|
|
// bind mount under the hood...
|
|
// TODO: handle options parsing/processing via containers/storage/pkg/mount
|
|
func (config *CreateConfig) parseVolumes(runtime *libpod.Runtime) ([]spec.Mount, []*libpod.ContainerNamedVolume, error) {
|
|
// Add image volumes.
|
|
baseMounts, baseVolumes, err := config.getImageVolumes()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Add --volumes-from.
|
|
// Overrides image volumes unconditionally.
|
|
vFromMounts, vFromVolumes, err := config.getVolumesFrom(runtime)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
for dest, mount := range vFromMounts {
|
|
baseMounts[dest] = mount
|
|
}
|
|
for dest, volume := range vFromVolumes {
|
|
baseVolumes[dest] = volume
|
|
}
|
|
|
|
// Next mounts from the --mounts flag.
|
|
// Do not override yet.
|
|
unifiedMounts, unifiedVolumes, err := config.getMounts()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Next --volumes flag.
|
|
// Do not override yet.
|
|
volumeMounts, volumeVolumes, err := config.getVolumeMounts()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Next --tmpfs flag.
|
|
// Do not override yet.
|
|
tmpfsMounts, err := config.getTmpfsMounts()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Unify mounts from --mount, --volume, --tmpfs.
|
|
// Also add mounts + volumes directly from createconfig.
|
|
// Start with --volume.
|
|
for dest, mount := range volumeMounts {
|
|
if _, ok := unifiedMounts[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, dest)
|
|
}
|
|
unifiedMounts[dest] = mount
|
|
}
|
|
for dest, volume := range volumeVolumes {
|
|
if _, ok := unifiedVolumes[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, dest)
|
|
}
|
|
unifiedVolumes[dest] = volume
|
|
}
|
|
// Now --tmpfs
|
|
for dest, tmpfs := range tmpfsMounts {
|
|
if _, ok := unifiedMounts[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, dest)
|
|
}
|
|
unifiedMounts[dest] = tmpfs
|
|
}
|
|
// Now spec mounts and volumes
|
|
for _, mount := range config.Mounts {
|
|
dest := mount.Destination
|
|
if _, ok := unifiedMounts[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, dest)
|
|
}
|
|
unifiedMounts[dest] = mount
|
|
}
|
|
for _, volume := range config.NamedVolumes {
|
|
dest := volume.Dest
|
|
if _, ok := unifiedVolumes[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, dest)
|
|
}
|
|
unifiedVolumes[dest] = volume
|
|
}
|
|
|
|
// If requested, add container init binary
|
|
if config.Init {
|
|
initPath := config.InitPath
|
|
if initPath == "" {
|
|
rtc, err := runtime.GetConfig()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
initPath = rtc.InitPath
|
|
}
|
|
initMount, err := config.addContainerInitBinary(initPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if _, ok := unifiedMounts[initMount.Destination]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, "conflict with mount added by --init to %q", initMount.Destination)
|
|
}
|
|
unifiedMounts[initMount.Destination] = initMount
|
|
}
|
|
|
|
// Before superceding, we need to find volume mounts which conflict with
|
|
// named volumes, and vice versa.
|
|
// We'll delete the conflicts here as we supercede.
|
|
for dest := range unifiedMounts {
|
|
if _, ok := baseVolumes[dest]; ok {
|
|
delete(baseVolumes, dest)
|
|
}
|
|
}
|
|
for dest := range unifiedVolumes {
|
|
if _, ok := baseMounts[dest]; ok {
|
|
delete(baseMounts, dest)
|
|
}
|
|
}
|
|
|
|
// Supercede volumes-from/image volumes with unified volumes from above.
|
|
// This is an unconditional replacement.
|
|
for dest, mount := range unifiedMounts {
|
|
baseMounts[dest] = mount
|
|
}
|
|
for dest, volume := range unifiedVolumes {
|
|
baseVolumes[dest] = volume
|
|
}
|
|
|
|
// If requested, add tmpfs filesystems for read-only containers.
|
|
if config.ReadOnlyRootfs && config.ReadOnlyTmpfs {
|
|
readonlyTmpfs := []string{"/tmp", "/var/tmp", "/run"}
|
|
options := []string{"rw", "rprivate", "nosuid", "nodev", "tmpcopyup"}
|
|
for _, dest := range readonlyTmpfs {
|
|
if _, ok := baseMounts[dest]; ok {
|
|
continue
|
|
}
|
|
if _, ok := baseVolumes[dest]; ok {
|
|
continue
|
|
}
|
|
localOpts := options
|
|
if dest == "/run" {
|
|
localOpts = append(localOpts, "noexec", "size=65536k")
|
|
} else {
|
|
localOpts = append(localOpts, "exec")
|
|
}
|
|
baseMounts[dest] = spec.Mount{
|
|
Destination: dest,
|
|
Type: "tmpfs",
|
|
Source: "tmpfs",
|
|
Options: localOpts,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for conflicts between named volumes and mounts
|
|
for dest := range baseMounts {
|
|
if _, ok := baseVolumes[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest)
|
|
}
|
|
}
|
|
for dest := range baseVolumes {
|
|
if _, ok := baseMounts[dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, "conflict at mount destination %v", dest)
|
|
}
|
|
}
|
|
|
|
// Final step: maps to arrays
|
|
finalMounts := make([]spec.Mount, 0, len(baseMounts))
|
|
for _, mount := range baseMounts {
|
|
if mount.Type == TypeBind {
|
|
absSrc, err := filepath.Abs(mount.Source)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "error getting absolute path of %s", mount.Source)
|
|
}
|
|
mount.Source = absSrc
|
|
}
|
|
finalMounts = append(finalMounts, mount)
|
|
}
|
|
finalVolumes := make([]*libpod.ContainerNamedVolume, 0, len(baseVolumes))
|
|
for _, volume := range baseVolumes {
|
|
finalVolumes = append(finalVolumes, volume)
|
|
}
|
|
|
|
return finalMounts, finalVolumes, nil
|
|
}
|
|
|
|
// Parse volumes from - a set of containers whose volumes we will mount in.
|
|
// Grab the containers, retrieve any user-created spec mounts and all named
|
|
// volumes, and return a list of them.
|
|
// Conflicts are resolved simply - the last container specified wins.
|
|
// Container names may be suffixed by mount options after a colon.
|
|
// TODO: We should clean these paths if possible
|
|
func (config *CreateConfig) getVolumesFrom(runtime *libpod.Runtime) (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) {
|
|
// Both of these are maps of mount destination to mount type.
|
|
// We ensure that each destination is only mounted to once in this way.
|
|
finalMounts := make(map[string]spec.Mount)
|
|
finalNamedVolumes := make(map[string]*libpod.ContainerNamedVolume)
|
|
|
|
for _, vol := range config.VolumesFrom {
|
|
var (
|
|
options = []string{}
|
|
err error
|
|
splitVol = strings.SplitN(vol, ":", 2)
|
|
)
|
|
if len(splitVol) == 2 {
|
|
splitOpts := strings.Split(splitVol[1], ",")
|
|
for _, checkOpt := range splitOpts {
|
|
switch checkOpt {
|
|
case "z", "ro", "rw":
|
|
// Do nothing, these are valid options
|
|
default:
|
|
return nil, nil, errors.Errorf("invalid options %q, can only specify 'ro', 'rw', and 'z'", splitVol[1])
|
|
}
|
|
}
|
|
|
|
if options, err = parse.ValidateVolumeOpts(splitOpts); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
ctr, err := runtime.LookupContainer(splitVol[0])
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "error looking up container %q for volumes-from", splitVol[0])
|
|
}
|
|
|
|
logrus.Debugf("Adding volumes from container %s", ctr.ID())
|
|
|
|
// Look up the container's user volumes. This gets us the
|
|
// destinations of all mounts the user added to the container.
|
|
userVolumesArr := ctr.UserVolumes()
|
|
|
|
// We're going to need to access them a lot, so convert to a map
|
|
// to reduce looping.
|
|
// We'll also use the map to indicate if we missed any volumes along the way.
|
|
userVolumes := make(map[string]bool)
|
|
for _, dest := range userVolumesArr {
|
|
userVolumes[dest] = false
|
|
}
|
|
|
|
// Now we get the container's spec and loop through its volumes
|
|
// and append them in if we can find them.
|
|
spec := ctr.Spec()
|
|
if spec == nil {
|
|
return nil, nil, errors.Errorf("error retrieving container %s spec for volumes-from", ctr.ID())
|
|
}
|
|
for _, mnt := range spec.Mounts {
|
|
if mnt.Type != TypeBind {
|
|
continue
|
|
}
|
|
if _, exists := userVolumes[mnt.Destination]; exists {
|
|
userVolumes[mnt.Destination] = true
|
|
|
|
if len(options) != 0 {
|
|
mnt.Options = options
|
|
}
|
|
|
|
if _, ok := finalMounts[mnt.Destination]; ok {
|
|
logrus.Debugf("Overriding mount to %s with new mount from container %s", mnt.Destination, ctr.ID())
|
|
}
|
|
finalMounts[mnt.Destination] = mnt
|
|
}
|
|
}
|
|
|
|
// We're done with the spec mounts. Add named volumes.
|
|
// Add these unconditionally - none of them are automatically
|
|
// part of the container, as some spec mounts are.
|
|
namedVolumes := ctr.NamedVolumes()
|
|
for _, namedVol := range namedVolumes {
|
|
if _, exists := userVolumes[namedVol.Dest]; exists {
|
|
userVolumes[namedVol.Dest] = true
|
|
}
|
|
|
|
if len(options) != 0 {
|
|
namedVol.Options = options
|
|
}
|
|
|
|
if _, ok := finalMounts[namedVol.Dest]; ok {
|
|
logrus.Debugf("Overriding named volume mount to %s with new named volume from container %s", namedVol.Dest, ctr.ID())
|
|
}
|
|
finalNamedVolumes[namedVol.Dest] = namedVol
|
|
}
|
|
|
|
// Check if we missed any volumes
|
|
for volDest, found := range userVolumes {
|
|
if !found {
|
|
logrus.Warnf("Unable to match volume %s from container %s for volumes-from", volDest, ctr.ID())
|
|
}
|
|
}
|
|
}
|
|
|
|
return finalMounts, finalNamedVolumes, nil
|
|
}
|
|
|
|
// getMounts 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 (config *CreateConfig) getMounts() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) {
|
|
finalMounts := make(map[string]spec.Mount)
|
|
finalNamedVolumes := make(map[string]*libpod.ContainerNamedVolume)
|
|
|
|
errInvalidSyntax := errors.Errorf("incorrect mount format: should be --mount type=<bind|tmpfs|volume>,[src=<host-dir|volume-name>,]target=<ctr-dir>[,options]")
|
|
|
|
// TODO(vrothberg): the manual parsing can be replaced with a regular expression
|
|
// to allow a more robust parsing of the mount format and to give
|
|
// precise errors regarding supported format versus suppored options.
|
|
for _, mount := range config.MountsFlag {
|
|
arr := strings.SplitN(mount, ",", 2)
|
|
if len(arr) < 2 {
|
|
return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount)
|
|
}
|
|
kv := strings.Split(arr[0], "=")
|
|
// TODO: type is not explicitly required in Docker.
|
|
// If not specified, it defaults to "volume".
|
|
if len(kv) != 2 || kv[0] != "type" {
|
|
return nil, nil, errors.Wrapf(errInvalidSyntax, "%q", mount)
|
|
}
|
|
|
|
tokens := strings.Split(arr[1], ",")
|
|
switch kv[1] {
|
|
case TypeBind:
|
|
mount, err := getBindMount(tokens)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
case TypeTmpfs:
|
|
mount, err := getTmpfsMount(tokens)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if _, ok := finalMounts[mount.Destination]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, mount.Destination)
|
|
}
|
|
finalMounts[mount.Destination] = mount
|
|
case "volume":
|
|
volume, err := getNamedVolume(tokens)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if _, ok := finalNamedVolumes[volume.Dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, volume.Dest)
|
|
}
|
|
finalNamedVolumes[volume.Dest] = volume
|
|
default:
|
|
return nil, nil, errors.Errorf("invalid filesystem type %q", kv[1])
|
|
}
|
|
}
|
|
|
|
return finalMounts, finalNamedVolumes, nil
|
|
}
|
|
|
|
// Parse a single bind mount entry from the --mount flag.
|
|
func getBindMount(args []string) (spec.Mount, error) {
|
|
newMount := spec.Mount{
|
|
Type: TypeBind,
|
|
}
|
|
|
|
var setSource, setDest, setRORW, setSuid, setDev, setExec, setRelabel bool
|
|
|
|
for _, val := range args {
|
|
kv := strings.Split(val, "=")
|
|
switch kv[0] {
|
|
case "bind-nonrecursive":
|
|
newMount.Options = append(newMount.Options, "bind")
|
|
case "ro", "rw":
|
|
if setRORW {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' or 'rw' options more than once")
|
|
}
|
|
setRORW = true
|
|
// Can be formatted as one of:
|
|
// ro
|
|
// ro=[true|false]
|
|
// rw
|
|
// rw=[true|false]
|
|
if len(kv) == 1 {
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
} else if len(kv) == 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, errors.Wrapf(optionArgError, "%s must be set to true or false, instead received %q", kv[0], kv[1])
|
|
}
|
|
} else {
|
|
return newMount, errors.Wrapf(optionArgError, "badly formatted option %q", val)
|
|
}
|
|
case "nosuid", "suid":
|
|
if setSuid {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once")
|
|
}
|
|
setSuid = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "nodev", "dev":
|
|
if setDev {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once")
|
|
}
|
|
setDev = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "noexec", "exec":
|
|
if setExec {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once")
|
|
}
|
|
setExec = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z":
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "bind-propagation":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newMount.Options = append(newMount.Options, kv[1])
|
|
case "src", "source":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := parse.ValidateVolumeHostDir(kv[1]); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Source = kv[1]
|
|
setSource = true
|
|
case "target", "dst", "destination":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Destination = filepath.Clean(kv[1])
|
|
setDest = true
|
|
case "relabel":
|
|
if setRelabel {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'relabel' option more than once")
|
|
}
|
|
setRelabel = true
|
|
if len(kv) != 2 {
|
|
return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0])
|
|
}
|
|
switch kv[1] {
|
|
case "private":
|
|
newMount.Options = append(newMount.Options, "z")
|
|
case "shared":
|
|
newMount.Options = append(newMount.Options, "Z")
|
|
default:
|
|
return newMount, errors.Wrapf(util.ErrBadMntOption, "%s mount option must be 'private' or 'shared'", kv[0])
|
|
}
|
|
default:
|
|
return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0])
|
|
}
|
|
}
|
|
|
|
if !setDest {
|
|
return newMount, noDestError
|
|
}
|
|
|
|
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: TypeTmpfs,
|
|
Source: TypeTmpfs,
|
|
}
|
|
|
|
var setDest, setRORW, setSuid, setDev, setExec bool
|
|
|
|
for _, val := range args {
|
|
kv := strings.Split(val, "=")
|
|
switch kv[0] {
|
|
case "ro", "rw":
|
|
if setRORW {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once")
|
|
}
|
|
setRORW = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "nosuid", "suid":
|
|
if setSuid {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once")
|
|
}
|
|
setSuid = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "nodev", "dev":
|
|
if setDev {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once")
|
|
}
|
|
setDev = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "noexec", "exec":
|
|
if setExec {
|
|
return newMount, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once")
|
|
}
|
|
setExec = true
|
|
newMount.Options = append(newMount.Options, kv[0])
|
|
case "tmpfs-mode":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newMount.Options = append(newMount.Options, fmt.Sprintf("mode=%s", kv[1]))
|
|
case "tmpfs-size":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newMount.Options = append(newMount.Options, fmt.Sprintf("size=%s", kv[1]))
|
|
case "src", "source":
|
|
return newMount, errors.Errorf("source is not supported with tmpfs mounts")
|
|
case "target", "dst", "destination":
|
|
if len(kv) == 1 {
|
|
return newMount, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
|
|
return newMount, err
|
|
}
|
|
newMount.Destination = filepath.Clean(kv[1])
|
|
setDest = true
|
|
default:
|
|
return newMount, errors.Wrapf(util.ErrBadMntOption, kv[0])
|
|
}
|
|
}
|
|
|
|
if !setDest {
|
|
return newMount, noDestError
|
|
}
|
|
|
|
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) (*libpod.ContainerNamedVolume, error) {
|
|
newVolume := new(libpod.ContainerNamedVolume)
|
|
|
|
var setSource, setDest, setRORW, setSuid, setDev, setExec bool
|
|
|
|
for _, val := range args {
|
|
kv := strings.Split(val, "=")
|
|
switch kv[0] {
|
|
case "ro", "rw":
|
|
if setRORW {
|
|
return nil, errors.Wrapf(optionArgError, "cannot pass 'ro' and 'rw' options more than once")
|
|
}
|
|
setRORW = true
|
|
newVolume.Options = append(newVolume.Options, kv[0])
|
|
case "nosuid", "suid":
|
|
if setSuid {
|
|
return nil, errors.Wrapf(optionArgError, "cannot pass 'nosuid' and 'suid' options more than once")
|
|
}
|
|
setSuid = true
|
|
newVolume.Options = append(newVolume.Options, kv[0])
|
|
case "nodev", "dev":
|
|
if setDev {
|
|
return nil, errors.Wrapf(optionArgError, "cannot pass 'nodev' and 'dev' options more than once")
|
|
}
|
|
setDev = true
|
|
newVolume.Options = append(newVolume.Options, kv[0])
|
|
case "noexec", "exec":
|
|
if setExec {
|
|
return nil, errors.Wrapf(optionArgError, "cannot pass 'noexec' and 'exec' options more than once")
|
|
}
|
|
setExec = true
|
|
newVolume.Options = append(newVolume.Options, kv[0])
|
|
case "volume-label":
|
|
return nil, errors.Errorf("the --volume-label option is not presently implemented")
|
|
case "src", "source":
|
|
if len(kv) == 1 {
|
|
return nil, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
newVolume.Name = kv[1]
|
|
setSource = true
|
|
case "target", "dst", "destination":
|
|
if len(kv) == 1 {
|
|
return nil, errors.Wrapf(optionArgError, kv[0])
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(kv[1]); err != nil {
|
|
return nil, err
|
|
}
|
|
newVolume.Dest = filepath.Clean(kv[1])
|
|
setDest = true
|
|
default:
|
|
return nil, errors.Wrapf(util.ErrBadMntOption, kv[0])
|
|
}
|
|
}
|
|
|
|
if !setSource {
|
|
return nil, errors.Errorf("must set source volume")
|
|
}
|
|
if !setDest {
|
|
return nil, noDestError
|
|
}
|
|
|
|
return newVolume, nil
|
|
}
|
|
|
|
func (config *CreateConfig) getVolumeMounts() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) {
|
|
mounts := make(map[string]spec.Mount)
|
|
volumes := make(map[string]*libpod.ContainerNamedVolume)
|
|
|
|
volumeFormatErr := errors.Errorf("incorrect volume format, should be [host-dir:]ctr-dir[:option]")
|
|
|
|
for _, vol := range config.Volumes {
|
|
var (
|
|
options []string
|
|
src string
|
|
dest string
|
|
err error
|
|
)
|
|
|
|
splitVol := strings.Split(vol, ":")
|
|
if len(splitVol) > 3 {
|
|
return nil, nil, errors.Wrapf(volumeFormatErr, vol)
|
|
}
|
|
|
|
src = splitVol[0]
|
|
if len(splitVol) == 1 {
|
|
// This is an anonymous named volume. Only thing given
|
|
// is destination.
|
|
// Name/source will be blank, and populated by libpod.
|
|
src = ""
|
|
dest = splitVol[0]
|
|
} else if len(splitVol) > 1 {
|
|
dest = splitVol[1]
|
|
}
|
|
if len(splitVol) > 2 {
|
|
if options, err = parse.ValidateVolumeOpts(strings.Split(splitVol[2], ",")); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
// Do not check source dir for anonymous volumes
|
|
if len(splitVol) > 1 {
|
|
if err := parse.ValidateVolumeHostDir(src); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
if err := parse.ValidateVolumeCtrDir(dest); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
cleanDest := filepath.Clean(dest)
|
|
|
|
if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") {
|
|
// This is not a named volume
|
|
newMount := spec.Mount{
|
|
Destination: cleanDest,
|
|
Type: string(TypeBind),
|
|
Source: src,
|
|
Options: options,
|
|
}
|
|
if _, ok := mounts[newMount.Destination]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, newMount.Destination)
|
|
}
|
|
mounts[newMount.Destination] = newMount
|
|
} else {
|
|
// This is a named volume
|
|
newNamedVol := new(libpod.ContainerNamedVolume)
|
|
newNamedVol.Name = src
|
|
newNamedVol.Dest = cleanDest
|
|
newNamedVol.Options = options
|
|
|
|
if _, ok := volumes[newNamedVol.Dest]; ok {
|
|
return nil, nil, errors.Wrapf(errDuplicateDest, newNamedVol.Dest)
|
|
}
|
|
volumes[newNamedVol.Dest] = newNamedVol
|
|
}
|
|
|
|
logrus.Debugf("User mount %s:%s options %v", src, dest, options)
|
|
}
|
|
|
|
return mounts, volumes, nil
|
|
}
|
|
|
|
// Get mounts for container's image volumes
|
|
func (config *CreateConfig) getImageVolumes() (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) {
|
|
mounts := make(map[string]spec.Mount)
|
|
volumes := make(map[string]*libpod.ContainerNamedVolume)
|
|
|
|
if config.ImageVolumeType == "ignore" {
|
|
return mounts, volumes, nil
|
|
}
|
|
|
|
for vol := range config.BuiltinImgVolumes {
|
|
cleanDest := filepath.Clean(vol)
|
|
if config.ImageVolumeType == "tmpfs" {
|
|
// Tmpfs image volumes are handled as mounts
|
|
mount := spec.Mount{
|
|
Destination: cleanDest,
|
|
Source: TypeTmpfs,
|
|
Type: TypeTmpfs,
|
|
Options: []string{"rprivate", "rw", "nodev"},
|
|
}
|
|
mounts[vol] = mount
|
|
} else {
|
|
// Anonymous volumes have no name.
|
|
namedVolume := new(libpod.ContainerNamedVolume)
|
|
namedVolume.Options = []string{"rprivate", "rw", "nodev"}
|
|
namedVolume.Dest = cleanDest
|
|
volumes[vol] = namedVolume
|
|
}
|
|
}
|
|
|
|
return mounts, volumes, nil
|
|
}
|
|
|
|
// GetTmpfsMounts creates spec.Mount structs for user-requested tmpfs mounts
|
|
func (config *CreateConfig) getTmpfsMounts() (map[string]spec.Mount, error) {
|
|
m := make(map[string]spec.Mount)
|
|
for _, i := range config.Tmpfs {
|
|
// 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, errors.Wrapf(errDuplicateDest, destPath)
|
|
}
|
|
|
|
mount := spec.Mount{
|
|
Destination: filepath.Clean(destPath),
|
|
Type: string(TypeTmpfs),
|
|
Options: options,
|
|
Source: string(TypeTmpfs),
|
|
}
|
|
m[destPath] = mount
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// AddContainerInitBinary adds the init binary specified by path iff the
|
|
// container will run in a private PID namespace that is not shared with the
|
|
// host or another pre-existing container, where an init-like process is
|
|
// already running.
|
|
//
|
|
// Note that AddContainerInitBinary prepends "/dev/init" "--" to the command
|
|
// to execute the bind-mounted binary as PID 1.
|
|
func (config *CreateConfig) addContainerInitBinary(path string) (spec.Mount, error) {
|
|
mount := spec.Mount{
|
|
Destination: "/dev/init",
|
|
Type: TypeBind,
|
|
Source: path,
|
|
Options: []string{TypeBind, "ro"},
|
|
}
|
|
|
|
if path == "" {
|
|
return mount, fmt.Errorf("please specify a path to the container-init binary")
|
|
}
|
|
if !config.PidMode.IsPrivate() {
|
|
return mount, fmt.Errorf("cannot add init binary as PID 1 (PID namespace isn't private)")
|
|
}
|
|
if config.Systemd {
|
|
return mount, fmt.Errorf("cannot use container-init binary with systemd")
|
|
}
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
return mount, errors.Wrap(err, "container-init binary not found on the host")
|
|
}
|
|
config.Command = append([]string{"/dev/init", "--"}, config.Command...)
|
|
return mount, nil
|
|
}
|
|
|
|
// Supercede existing mounts in the spec with new, user-specified mounts.
|
|
// TODO: Should we unmount subtree mounts? E.g., if /tmp/ is mounted by
|
|
// one mount, and we already have /tmp/a and /tmp/b, should we remove
|
|
// the /tmp/a and /tmp/b mounts in favor of the more general /tmp?
|
|
func supercedeUserMounts(mounts []spec.Mount, configMount []spec.Mount) []spec.Mount {
|
|
if len(mounts) > 0 {
|
|
// If we have overlappings mounts, remove them from the spec in favor of
|
|
// the user-added volume mounts
|
|
destinations := make(map[string]bool)
|
|
for _, mount := range mounts {
|
|
destinations[path.Clean(mount.Destination)] = true
|
|
}
|
|
// Copy all mounts from spec to defaultMounts, except for
|
|
// - mounts overridden by a user supplied mount;
|
|
// - all mounts under /dev if a user supplied /dev is present;
|
|
mountDev := destinations["/dev"]
|
|
for _, mount := range configMount {
|
|
if _, ok := destinations[path.Clean(mount.Destination)]; !ok {
|
|
if mountDev && strings.HasPrefix(mount.Destination, "/dev/") {
|
|
// filter out everything under /dev if /dev is user-mounted
|
|
continue
|
|
}
|
|
|
|
logrus.Debugf("Adding mount %s", mount.Destination)
|
|
mounts = append(mounts, mount)
|
|
}
|
|
}
|
|
return mounts
|
|
}
|
|
return configMount
|
|
}
|
|
|
|
// Ensure mount options on all mounts are correct
|
|
func initFSMounts(inputMounts []spec.Mount) ([]spec.Mount, error) {
|
|
// We need to look up mounts so we can figure out the proper mount flags
|
|
// to apply.
|
|
systemMounts, err := pmount.GetMounts()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error retrieving system mounts to look up mount options")
|
|
}
|
|
|
|
// TODO: We probably don't need to re-build the mounts array
|
|
var mounts []spec.Mount
|
|
for _, m := range inputMounts {
|
|
if m.Type == TypeBind {
|
|
baseMnt, err := findMount(m.Destination, systemMounts)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "error looking up mountpoint for mount %s", m.Destination)
|
|
}
|
|
var noexec, nosuid, nodev bool
|
|
for _, baseOpt := range strings.Split(baseMnt.Opts, ",") {
|
|
switch baseOpt {
|
|
case "noexec":
|
|
noexec = true
|
|
case "nosuid":
|
|
nosuid = true
|
|
case "nodev":
|
|
nodev = true
|
|
}
|
|
}
|
|
|
|
defaultMountOpts := new(util.DefaultMountOptions)
|
|
defaultMountOpts.Noexec = noexec
|
|
defaultMountOpts.Nosuid = nosuid
|
|
defaultMountOpts.Nodev = nodev
|
|
|
|
opts, err := util.ProcessOptions(m.Options, false, defaultMountOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.Options = opts
|
|
}
|
|
if m.Type == TypeTmpfs && filepath.Clean(m.Destination) != "/dev" {
|
|
opts, err := util.ProcessOptions(m.Options, true, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.Options = opts
|
|
}
|
|
|
|
mounts = append(mounts, m)
|
|
}
|
|
return mounts, nil
|
|
}
|
|
|
|
// TODO: We could make this a bit faster by building a tree of the mountpoints
|
|
// and traversing it to identify the correct mount.
|
|
func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) {
|
|
var err error
|
|
target, err = filepath.Abs(target)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "cannot resolve %s", target)
|
|
}
|
|
var bestSoFar *pmount.Info
|
|
for _, i := range mounts {
|
|
if bestSoFar != nil && len(bestSoFar.Mountpoint) > len(i.Mountpoint) {
|
|
// Won't be better than what we have already found
|
|
continue
|
|
}
|
|
if strings.HasPrefix(target, i.Mountpoint) {
|
|
bestSoFar = i
|
|
}
|
|
}
|
|
return bestSoFar, nil
|
|
}
|