Migrate to unified volume handling code

Unify handling for the --volume, --mount, --volumes-from, --tmpfs
and --init flags into a single file and set of functions. This
will greatly improve readability and maintainability.

Further, properly handle superceding and conflicting mounts. Our
current patchwork has serious issues when mounts conflict, or
when a mount from --volumes-from or an image volume should be
overwritten by a user volume or named volume.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
This commit is contained in:
Matthew Heon 2019-04-15 15:26:29 -04:00
parent 71f65ab07f
commit 9ee50fe2c7
9 changed files with 841 additions and 564 deletions

View file

@ -25,7 +25,6 @@ ${LINTER} \
--deadline=600s --disable-all\
--enable=deadcode\
--enable=errcheck\
--enable=goconst\
--enable=gofmt\
--enable=golint\
--enable=ineffassign\
@ -41,7 +40,6 @@ ${LINTER} \
--exclude='duplicate of.*_test.go.*\(dupl\)$'\
--exclude='cmd\/client\/.*\.go.*\(dupl\)$'\
--exclude='libpod\/.*_easyjson.go:.*'\
--exclude='.* other occurrence\(s\) of "(container|host|tmpfs|unknown)" found in: .*\(goconst\)$'\
--exclude='vendor\/.*'\
--exclude='podman\/.*'\
--exclude='server\/seccomp\/.*\.go.*$'\

View file

@ -205,7 +205,8 @@ func playKubeYAMLCmd(c *cliconfig.KubePlayValues, ctx context.Context, runtime *
return pod, errors.Errorf("Directories are the only supported HostPath type")
}
}
if err := shared.ValidateVolumeHostDir(hostPath.Path); err != nil {
if err := createconfig.ValidateVolumeHostDir(hostPath.Path); err != nil {
return pod, errors.Wrapf(err, "Error in parsing HostPath in YAML")
}
volumes[volume.Name] = hostPath.Path
@ -351,7 +352,7 @@ func kubeContainerToCreateConfig(ctx context.Context, containerYAML v1.Container
if !exists {
return nil, errors.Errorf("Volume mount %s specified for container but not configured in volumes", volume.Name)
}
if err := shared.ValidateVolumeCtrDir(volume.MountPath); err != nil {
if err := createconfig.ValidateVolumeCtrDir(volume.MountPath); err != nil {
return nil, errors.Wrapf(err, "error in parsing MountPath")
}
containerConfig.Volumes = append(containerConfig.Volumes, fmt.Sprintf("%s:%s", host_path, volume.MountPath))

View file

@ -25,7 +25,6 @@ import (
"github.com/docker/go-connections/nat"
"github.com/docker/go-units"
"github.com/google/shlex"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/selinux/go-selinux/label"
"github.com/opentracing/opentracing-go"
"github.com/pkg/errors"
@ -341,18 +340,6 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod.
}
blkioWeight = uint16(u)
}
var mountList []spec.Mount
if mountList, err = parseMounts(c.StringArray("mount")); err != nil {
return nil, err
}
if err = parseVolumes(c.StringArray("volume")); err != nil {
return nil, err
}
if err = parseVolumesFrom(c.StringSlice("volumes-from")); err != nil {
return nil, err
}
tty := c.Bool("tty")
@ -636,6 +623,8 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod.
HTTPProxy: c.Bool("http-proxy"),
NoHosts: c.Bool("no-hosts"),
IDMappings: idmappings,
Init: c.Bool("init"),
InitPath: c.String("init-path"),
Image: imageName,
ImageID: imageID,
Interactive: c.Bool("interactive"),
@ -696,26 +685,13 @@ func ParseCreateOpts(ctx context.Context, c *GenericCLIResults, runtime *libpod.
Tty: tty,
User: user,
UsernsMode: usernsMode,
Mounts: mountList,
MountsFlag: c.StringArray("mount"),
Volumes: c.StringArray("volume"),
WorkDir: workDir,
Rootfs: rootfs,
VolumesFrom: c.StringSlice("volumes-from"),
Syslog: c.Bool("syslog"),
}
if c.Bool("init") {
initPath := c.String("init-path")
if initPath == "" {
rtc, err := runtime.GetConfig()
if err != nil {
return nil, err
}
initPath = rtc.InitPath
}
if err := config.AddContainerInitBinary(initPath); err != nil {
return nil, err
}
}
if config.Privileged {
config.LabelOpts = label.DisableSecOpt()

View file

@ -2,15 +2,11 @@ package shared
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/libpod/cmd/podman/shared/parse"
cc "github.com/containers/libpod/pkg/spec"
"github.com/containers/libpod/pkg/sysinfo"
"github.com/docker/go-units"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -78,186 +74,6 @@ func addWarning(warnings []string, msg string) []string {
return append(warnings, msg)
}
// Format supported.
// podman run --mount type=bind,src=/etc/resolv.conf,target=/etc/resolv.conf ...
// podman run --mount type=tmpfs,target=/dev/shm ..
func parseMounts(mounts []string) ([]spec.Mount, error) {
// 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.
var mountList []spec.Mount
errInvalidSyntax := errors.Errorf("incorrect mount format: should be --mount type=<bind|tmpfs>,[src=<host-dir>,]target=<ctr-dir>[,options]")
for _, mount := range mounts {
var tokenCount int
var mountInfo spec.Mount
arr := strings.SplitN(mount, ",", 2)
if len(arr) < 2 {
return nil, errors.Wrapf(errInvalidSyntax, "%q", mount)
}
kv := strings.Split(arr[0], "=")
if kv[0] != "type" {
return nil, errors.Wrapf(errInvalidSyntax, "%q", mount)
}
switch kv[1] {
case "bind":
mountInfo.Type = string(cc.TypeBind)
case "tmpfs":
mountInfo.Type = string(cc.TypeTmpfs)
mountInfo.Source = string(cc.TypeTmpfs)
mountInfo.Options = append(mountInfo.Options, []string{"rprivate", "noexec", "nosuid", "nodev", "size=65536k"}...)
default:
return nil, errors.Errorf("invalid filesystem type %q", kv[1])
}
tokens := strings.Split(arr[1], ",")
for i, val := range tokens {
if i == (tokenCount - 1) {
//Parse tokens before options.
break
}
kv := strings.Split(val, "=")
switch kv[0] {
case "ro", "nosuid", "nodev", "noexec":
mountInfo.Options = append(mountInfo.Options, kv[0])
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z":
if mountInfo.Type != "bind" {
return nil, errors.Errorf("%s can only be used with bind mounts", kv[0])
}
mountInfo.Options = append(mountInfo.Options, kv[0])
case "tmpfs-mode":
if mountInfo.Type != "tmpfs" {
return nil, errors.Errorf("%s can only be used with tmpfs mounts", kv[0])
}
mountInfo.Options = append(mountInfo.Options, fmt.Sprintf("mode=%s", kv[1]))
case "tmpfs-size":
if mountInfo.Type != "tmpfs" {
return nil, errors.Errorf("%s can only be used with tmpfs mounts", kv[0])
}
shmSize, err := units.FromHumanSize(kv[1])
if err != nil {
return nil, errors.Wrapf(err, "unable to translate tmpfs-size")
}
mountInfo.Options = append(mountInfo.Options, fmt.Sprintf("size=%d", shmSize))
case "bind-propagation":
if mountInfo.Type != "bind" {
return nil, errors.Errorf("%s can only be used with bind mounts", kv[0])
}
mountInfo.Options = append(mountInfo.Options, kv[1])
case "src", "source":
if mountInfo.Type == "tmpfs" {
return nil, errors.Errorf("cannot use src= on a tmpfs file system")
}
if err := ValidateVolumeHostDir(kv[1]); err != nil {
return nil, err
}
mountInfo.Source = kv[1]
case "target", "dst", "destination":
if err := ValidateVolumeCtrDir(kv[1]); err != nil {
return nil, err
}
mountInfo.Destination = kv[1]
default:
return nil, errors.Errorf("incorrect mount option : %s", kv[0])
}
}
mountList = append(mountList, mountInfo)
}
return mountList, nil
}
func parseVolumes(volumes []string) error {
for _, volume := range volumes {
arr := strings.SplitN(volume, ":", 3)
if len(arr) < 2 {
return errors.Errorf("incorrect volume format %q, should be host-dir:ctr-dir[:option]", volume)
}
if err := ValidateVolumeHostDir(arr[0]); err != nil {
return err
}
if err := ValidateVolumeCtrDir(arr[1]); err != nil {
return err
}
if len(arr) > 2 {
if err := validateVolumeOpts(arr[2]); err != nil {
return err
}
}
}
return nil
}
func parseVolumesFrom(volumesFrom []string) error {
for _, vol := range volumesFrom {
arr := strings.SplitN(vol, ":", 2)
if len(arr) == 2 {
if strings.Contains(arr[1], "Z") || strings.Contains(arr[1], "private") || strings.Contains(arr[1], "slave") || strings.Contains(arr[1], "shared") {
return errors.Errorf("invalid options %q, can only specify 'ro', 'rw', and 'z", arr[1])
}
if err := validateVolumeOpts(arr[1]); err != nil {
return err
}
}
}
return nil
}
// ValidateVolumeHostDir ...
func ValidateVolumeHostDir(hostDir string) error {
if len(hostDir) == 0 {
return errors.Errorf("host directory cannot be empty")
}
if filepath.IsAbs(hostDir) {
if _, err := os.Stat(hostDir); err != nil {
return errors.Wrapf(err, "error checking path %q", hostDir)
}
}
// If hostDir is not an absolute path, that means the user wants to create a
// named volume. This will be done later on in the code.
return nil
}
// ValidateVolumeCtrDir ...
func ValidateVolumeCtrDir(ctrDir string) error {
if len(ctrDir) == 0 {
return errors.Errorf("container directory cannot be empty")
}
if !filepath.IsAbs(ctrDir) {
return errors.Errorf("invalid container path, must be an absolute path %q", ctrDir)
}
return nil
}
func validateVolumeOpts(option string) error {
var foundRootPropagation, foundRWRO, foundLabelChange int
options := strings.Split(option, ",")
for _, opt := range options {
switch opt {
case "rw", "ro":
foundRWRO++
if foundRWRO > 1 {
return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option)
}
case "z", "Z":
foundLabelChange++
if foundLabelChange > 1 {
return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option)
}
case "private", "rprivate", "shared", "rshared", "slave", "rslave":
foundRootPropagation++
if foundRootPropagation > 1 {
return errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private' or '[r]slave' option", option)
}
default:
return errors.Errorf("invalid option type %q", option)
}
}
return nil
}
func verifyContainerResources(config *cc.CreateConfig, update bool) ([]string, error) {
warnings := []string{}
sysInfo := sysinfo.New(true)

View file

@ -4,6 +4,7 @@ import (
"github.com/containers/libpod/libpod"
spec "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// MakeContainerConfig generates all configuration necessary to start a
@ -15,15 +16,27 @@ func (config *CreateConfig) MakeContainerConfig(runtime *libpod.Runtime, pod *li
return nil, nil, errors.Wrapf(libpod.ErrInvalidArg, "pod was given but no pod is specified")
}
runtimeSpec, namedVolumes, err := config.createConfigToOCISpec(runtime)
// Parse volumes flag into OCI spec mounts and libpod Named Volumes.
// If there is an identical mount in the OCI spec, we will replace it
// with a mount generated here.
mounts, namedVolumes, err := config.parseVolumes(runtime)
if err != nil {
return nil, nil, err
}
options, err := config.getContainerCreateOptions(runtime, pod, namedVolumes)
logrus.Debugf("got mounts as %v", mounts)
runtimeSpec, err := config.createConfigToOCISpec(runtime, mounts)
if err != nil {
return nil, nil, err
}
options, err := config.getContainerCreateOptions(runtime, pod, mounts, namedVolumes)
if err != nil {
return nil, nil, err
}
logrus.Debugf("created OCI spec and options for new container")
return runtimeSpec, options, nil
}

View file

@ -1,7 +1,6 @@
package createconfig
import (
"fmt"
"net"
"os"
"strconv"
@ -12,7 +11,6 @@ import (
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/namespaces"
"github.com/containers/storage"
"github.com/containers/storage/pkg/stringid"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/docker/go-connections/nat"
spec "github.com/opencontainers/runtime-spec/specs-go"
@ -26,12 +24,6 @@ import (
const (
bps = iota
iops
// TypeBind is the type for mounting host dir
TypeBind = "bind"
// TypeVolume is the type for remote storage volumes
// TypeVolume = "volume" // re-enable upon use
// TypeTmpfs is the type for mounting tmpfs
TypeTmpfs = "tmpfs"
)
// CreateResourceConfig represents resource elements in CreateConfig
@ -87,6 +79,8 @@ type CreateConfig struct {
HostAdd []string //add-host
Hostname string //hostname
HTTPProxy bool
Init bool // init
InitPath string //init-path
Image string
ImageID string
BuiltinImgVolumes map[string]struct{} // volumes defined in the image config
@ -125,14 +119,16 @@ type CreateConfig struct {
UsernsMode namespaces.UsernsMode //userns
User string //user
UtsMode namespaces.UTSMode //uts
Mounts []spec.Mount //mounts
Volumes []string //volume
Mounts []spec.Mount
MountsFlag []string // mounts
NamedVolumes []*libpod.ContainerNamedVolume
Volumes []string //volume
VolumesFrom []string
WorkDir string //workdir
LabelOpts []string //SecurityOpts
NoNewPrivs bool //SecurityOpts
ApparmorProfile string //SecurityOpts
SeccompProfilePath string //SecurityOpts
WorkDir string //workdir
LabelOpts []string //SecurityOpts
NoNewPrivs bool //SecurityOpts
ApparmorProfile string //SecurityOpts
SeccompProfilePath string //SecurityOpts
SecurityOpts []string
Rootfs string
Syslog bool // Whether to enable syslog on exit commands
@ -146,222 +142,6 @@ func (c *CreateConfig) CreateBlockIO() (*spec.LinuxBlockIO, error) {
return c.createBlockIO()
}
// 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 (c *CreateConfig) AddContainerInitBinary(path string) error {
if path == "" {
return fmt.Errorf("please specify a path to the container-init binary")
}
if !c.PidMode.IsPrivate() {
return fmt.Errorf("cannot add init binary as PID 1 (PID namespace isn't private)")
}
if c.Systemd {
return fmt.Errorf("cannot use container-init binary with systemd")
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return errors.Wrap(err, "container-init binary not found on the host")
}
c.Command = append([]string{"/dev/init", "--"}, c.Command...)
c.Mounts = append(c.Mounts, spec.Mount{
Destination: "/dev/init",
Type: TypeBind,
Source: path,
Options: []string{TypeBind, "ro"},
})
return nil
}
func processOptions(options []string) []string {
var (
foundrw, foundro bool
rootProp string
)
options = append(options, "rbind")
for _, opt := range options {
switch opt {
case "rw":
foundrw = true
case "ro":
foundro = true
case "private", "rprivate", "slave", "rslave", "shared", "rshared":
rootProp = opt
}
}
if !foundrw && !foundro {
options = append(options, "rw")
}
if rootProp == "" {
options = append(options, "rprivate")
}
return options
}
func (c *CreateConfig) initFSMounts() []spec.Mount {
var mounts []spec.Mount
for _, m := range c.Mounts {
m.Options = processOptions(m.Options)
if m.Type == "tmpfs" {
m.Options = append(m.Options, "tmpcopyup")
} else {
mounts = append(mounts, m)
}
}
return mounts
}
// GetVolumeMounts takes user provided input for bind mounts and creates Mount structs
func (c *CreateConfig) GetVolumeMounts(specMounts []spec.Mount) ([]spec.Mount, error) {
m := []spec.Mount{}
for _, i := range c.Volumes {
var options []string
spliti := strings.Split(i, ":")
if len(spliti) > 2 {
options = strings.Split(spliti[2], ",")
}
m = append(m, spec.Mount{
Destination: spliti[1],
Type: string(TypeBind),
Source: spliti[0],
Options: processOptions(options),
})
logrus.Debugf("User mount %s:%s options %v", spliti[0], spliti[1], options)
}
if c.ImageVolumeType == "ignore" {
return m, nil
}
for vol := range c.BuiltinImgVolumes {
if libpod.MountExists(specMounts, vol) || libpod.MountExists(m, vol) {
continue
}
mount := spec.Mount{
Destination: vol,
Type: c.ImageVolumeType,
Options: []string{"rprivate", "rw", "nodev"},
}
if c.ImageVolumeType == "tmpfs" {
mount.Source = "tmpfs"
mount.Options = append(mount.Options, "tmpcopyup")
} else {
// TODO: Move support for this and tmpfs into libpod
// Should tmpfs also be handled as named volumes? Wouldn't be hard
// This will cause a new local Volume to be created on your system
mount.Source = stringid.GenerateNonCryptoID()
mount.Options = append(mount.Options, TypeBind)
}
m = append(m, mount)
}
return m, nil
}
// GetVolumesFrom reads the create-config artifact of the container to get volumes from
// and adds it to c.Volumes of the current container.
func (c *CreateConfig) GetVolumesFrom(runtime *libpod.Runtime) error {
if os.Geteuid() != 0 {
return nil
}
for _, vol := range c.VolumesFrom {
options := ""
splitVol := strings.SplitN(vol, ":", 2)
if len(splitVol) == 2 {
options = splitVol[1]
}
ctr, err := runtime.LookupContainer(splitVol[0])
if err != nil {
return errors.Wrapf(err, "error looking up container %q", 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 errors.Errorf("error retrieving container %s spec", ctr.ID())
}
for _, mnt := range spec.Mounts {
if mnt.Type != TypeBind {
continue
}
if _, exists := userVolumes[mnt.Destination]; exists {
userVolumes[mnt.Destination] = true
localOptions := options
if localOptions == "" {
localOptions = strings.Join(mnt.Options, ",")
}
c.Volumes = append(c.Volumes, fmt.Sprintf("%s:%s:%s", mnt.Source, mnt.Destination, localOptions))
}
}
// 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
}
localOptions := options
if localOptions == "" {
localOptions = strings.Join(namedVol.Options, ",")
}
c.Volumes = append(c.Volumes, fmt.Sprintf("%s:%s:%s", namedVol.Name, namedVol.Dest, localOptions))
}
// 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 nil
}
//GetTmpfsMounts takes user provided input for Tmpfs mounts and creates Mount structs
func (c *CreateConfig) GetTmpfsMounts() []spec.Mount {
var m []spec.Mount
for _, i := range c.Tmpfs {
// Default options if nothing passed
options := []string{"rprivate", "rw", "noexec", "nosuid", "nodev", "size=65536k"}
spliti := strings.Split(i, ":")
destPath := spliti[0]
if len(spliti) > 1 {
options = strings.Split(spliti[1], ",")
}
m = append(m, spec.Mount{
Destination: destPath,
Type: string(TypeTmpfs),
Options: options,
Source: string(TypeTmpfs),
})
}
return m
}
func (c *CreateConfig) createExitCommand(runtime *libpod.Runtime) ([]string, error) {
config, err := runtime.GetConfig()
if err != nil {
@ -395,7 +175,7 @@ func (c *CreateConfig) createExitCommand(runtime *libpod.Runtime) ([]string, err
}
// GetContainerCreateOptions takes a CreateConfig and returns a slice of CtrCreateOptions
func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *libpod.Pod, namedVolumes []*libpod.ContainerNamedVolume) ([]libpod.CtrCreateOption, error) {
func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *libpod.Pod, mounts []spec.Mount, namedVolumes []*libpod.ContainerNamedVolume) ([]libpod.CtrCreateOption, error) {
var options []libpod.CtrCreateOption
var portBindings []ocicni.PortMapping
var err error
@ -422,22 +202,18 @@ func (c *CreateConfig) getContainerCreateOptions(runtime *libpod.Runtime, pod *l
}
}
if len(c.Volumes) != 0 {
// Volumes consist of multiple, comma-delineated fields
// The image spec only includes one part of that, so drop the
// others, if they are included
volumes := make([]string, 0, len(c.Volumes))
for _, vol := range c.Volumes {
// We always want the volume destination
splitVol := strings.SplitN(vol, ":", 3)
if len(splitVol) > 1 {
volumes = append(volumes, splitVol[1])
} else {
volumes = append(volumes, splitVol[0])
}
if len(mounts) != 0 || len(namedVolumes) != 0 {
destinations := []string{}
// Take all mount and named volume destinations.
for _, mount := range mounts {
destinations = append(destinations, mount.Destination)
}
for _, volume := range namedVolumes {
destinations = append(destinations, volume.Dest)
}
options = append(options, libpod.WithUserVolumes(volumes))
options = append(options, libpod.WithUserVolumes(destinations))
}
if len(namedVolumes) != 0 {

View file

@ -2,7 +2,6 @@ package createconfig
import (
"os"
"path"
"path/filepath"
"strings"
@ -21,61 +20,6 @@ import (
const cpuPeriod = 100000
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
}
// Split named volumes from normal volumes
func splitNamedVolumes(mounts []spec.Mount) ([]spec.Mount, []*libpod.ContainerNamedVolume) {
newMounts := make([]spec.Mount, 0)
namedVolumes := make([]*libpod.ContainerNamedVolume, 0)
for _, mount := range mounts {
// If it's not a named volume, append unconditionally
if mount.Type != TypeBind {
newMounts = append(newMounts, mount)
continue
}
// Volumes that are not named volumes must be an absolute or
// relative path.
// Volume names may not begin with a non-alphanumeric character
// so the HasPrefix() check is safe here.
if strings.HasPrefix(mount.Source, "/") || strings.HasPrefix(mount.Source, ".") {
newMounts = append(newMounts, mount)
} else {
namedVolume := new(libpod.ContainerNamedVolume)
namedVolume.Name = mount.Source
namedVolume.Dest = mount.Destination
namedVolume.Options = mount.Options
namedVolumes = append(namedVolumes, namedVolume)
}
}
return newMounts, namedVolumes
}
func getAvailableGids() (int64, error) {
idMap, err := user.ParseIDMapFile("/proc/self/gid_map")
if err != nil {
@ -89,11 +33,11 @@ func getAvailableGids() (int64, error) {
}
// CreateConfigToOCISpec parses information needed to create a container into an OCI runtime spec
func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spec.Spec, []*libpod.ContainerNamedVolume, error) { //nolint
func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime, userMounts []spec.Mount) (*spec.Spec, error) {
cgroupPerm := "ro"
g, err := generate.New("linux")
if err != nil {
return nil, nil, err
return nil, err
}
// Remove the default /dev/shm mount to ensure we overwrite it
g.RemoveMount("/dev/shm")
@ -139,7 +83,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
if isRootless {
nGids, err := getAvailableGids()
if err != nil {
return nil, nil, err
return nil, err
}
if nGids < 5 {
// If we have no GID mappings, the gid=5 default option would fail, so drop it.
@ -214,7 +158,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
if hostname == "" && (config.NetMode.IsHost() || config.UtsMode.IsHost()) {
hostname, err = os.Hostname()
if err != nil {
return nil, nil, errors.Wrap(err, "unable to retrieve hostname")
return nil, errors.Wrap(err, "unable to retrieve hostname")
}
}
g.RemoveHostname()
@ -304,13 +248,13 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
// already adding them all.
if !rootless.IsRootless() {
if err := config.AddPrivilegedDevices(&g); err != nil {
return nil, nil, err
return nil, err
}
}
} else {
for _, devicePath := range config.Devices {
if err := devicesFromPath(&g, devicePath); err != nil {
return nil, nil, err
return nil, err
}
}
}
@ -340,7 +284,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
spliti := strings.SplitN(i, ":", 2)
if len(spliti) > 1 {
if _, _, err := mount.ParseTmpfsOptions(spliti[1]); err != nil {
return nil, nil, err
return nil, err
}
options = strings.Split(spliti[1], ",")
}
@ -384,32 +328,33 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
g.AddMount(tmpfsMnt)
}
}
for name, val := range config.Env {
g.AddProcessEnv(name, val)
}
if err := addRlimits(config, &g); err != nil {
return nil, nil, err
return nil, err
}
if err := addPidNS(config, &g); err != nil {
return nil, nil, err
return nil, err
}
if err := addUserNS(config, &g); err != nil {
return nil, nil, err
return nil, err
}
if err := addNetNS(config, &g); err != nil {
return nil, nil, err
return nil, err
}
if err := addUTSNS(config, &g); err != nil {
return nil, nil, err
return nil, err
}
if err := addIpcNS(config, &g); err != nil {
return nil, nil, err
return nil, err
}
configSpec := g.Config
@ -417,7 +362,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
// NOTE: Must happen before SECCOMP
if !config.Privileged {
if err := setupCapabilities(config, configSpec); err != nil {
return nil, nil, err
return nil, err
}
} else {
g.SetupPrivileged(true)
@ -428,7 +373,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
if config.SeccompProfilePath != "unconfined" {
seccompConfig, err := getSeccompConfig(config, configSpec)
if err != nil {
return nil, nil, err
return nil, err
}
configSpec.Linux.Seccomp = seccompConfig
}
@ -439,27 +384,14 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
}
// BIND MOUNTS
if err := config.GetVolumesFrom(runtime); err != nil {
return nil, nil, errors.Wrap(err, "error getting volume mounts from --volumes-from flag")
}
volumeMounts, err := config.GetVolumeMounts(configSpec.Mounts)
if err != nil {
return nil, nil, errors.Wrapf(err, "error getting volume mounts")
}
configSpec.Mounts = supercedeUserMounts(volumeMounts, configSpec.Mounts)
//--mount
configSpec.Mounts = supercedeUserMounts(config.initFSMounts(), configSpec.Mounts)
// Split normal mounts and named volumes
newMounts, namedVolumes := splitNamedVolumes(configSpec.Mounts)
configSpec.Mounts = newMounts
configSpec.Mounts = supercedeUserMounts(userMounts, configSpec.Mounts)
// Process mounts to ensure correct options
configSpec.Mounts = initFSMounts(configSpec.Mounts)
// BLOCK IO
blkio, err := config.CreateBlockIO()
if err != nil {
return nil, nil, errors.Wrapf(err, "error creating block io")
return nil, errors.Wrapf(err, "error creating block io")
}
if blkio != nil {
configSpec.Linux.Resources.BlockIO = blkio
@ -468,7 +400,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
if rootless.IsRootless() {
if addedResources {
return nil, nil, errors.New("invalid configuration, cannot set resources with rootless containers")
return nil, errors.New("invalid configuration, cannot set resources with rootless containers")
}
configSpec.Linux.Resources = &spec.LinuxResources{}
}
@ -476,7 +408,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
// Make sure that the bind mounts keep options like nosuid, noexec, nodev.
mounts, err := pmount.GetMounts()
if err != nil {
return nil, nil, err
return nil, err
}
for i := range configSpec.Mounts {
m := &configSpec.Mounts[i]
@ -492,7 +424,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
}
mount, err := findMount(m.Source, mounts)
if err != nil {
return nil, nil, err
return nil, err
}
if mount == nil {
continue
@ -510,7 +442,7 @@ func (config *CreateConfig) createConfigToOCISpec(runtime *libpod.Runtime) (*spe
}
}
return configSpec, namedVolumes, nil
return configSpec, nil
}
func findMount(target string, mounts []*pmount.Info) (*pmount.Info, error) {

View file

@ -8,22 +8,22 @@ import (
"github.com/stretchr/testify/assert"
)
func TestCreateConfig_GetVolumeMounts(t *testing.T) {
func TestGetVolumeMountsOneVolume(t *testing.T) {
data := spec.Mount{
Destination: "/foobar",
Type: "bind",
Source: "foobar",
Source: "/foobar",
Options: []string{"ro", "rbind", "rprivate"},
}
config := CreateConfig{
Volumes: []string{"foobar:/foobar:ro"},
Volumes: []string{"/foobar:/foobar:ro"},
}
specMount, err := config.GetVolumeMounts([]spec.Mount{})
specMount, _, err := config.getVolumeMounts()
assert.NoError(t, err)
assert.True(t, reflect.DeepEqual(data, specMount[0]))
assert.True(t, reflect.DeepEqual(data, specMount[data.Destination]))
}
func TestCreateConfig_GetTmpfsMounts(t *testing.T) {
func TestGetTmpfsMounts(t *testing.T) {
data := spec.Mount{
Destination: "/homer",
Type: "tmpfs",
@ -33,7 +33,7 @@ func TestCreateConfig_GetTmpfsMounts(t *testing.T) {
config := CreateConfig{
Tmpfs: []string{"/homer:rw,size=787448k,mode=1777"},
}
tmpfsMount := config.GetTmpfsMounts()
assert.True(t, reflect.DeepEqual(data, tmpfsMount[0]))
tmpfsMount, err := config.getTmpfsMounts()
assert.NoError(t, err)
assert.True(t, reflect.DeepEqual(data, tmpfsMount[data.Destination]))
}

765
pkg/spec/storage.go Normal file
View file

@ -0,0 +1,765 @@
package createconfig
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/containers/libpod/libpod"
"github.com/containers/storage/pkg/stringid"
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")
badOptionError = errors.Errorf("invalid mount option")
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: Tmpfs options - we should probably check user-given ones, provide sane
// defaults even if the user provides a few...
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.
// 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
}
// 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
}
// 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
}
// TODO: Allow an explicit list of named volumes and spec mounts in the
// createconfig. Podman doesn't need them, but other libpod consumers
// might want them.
// Final step: maps to arrays
finalMounts := make([]spec.Mount, 0, len(baseMounts))
for _, mount := range baseMounts {
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.
func (config *CreateConfig) getVolumesFrom(runtime *libpod.Runtime) (map[string]spec.Mount, map[string]*libpod.ContainerNamedVolume, error) {
// TODO: This can probably be disabled now
if os.Geteuid() != 0 {
return nil, nil, nil
}
// Both of these are maps of mount destination to mount type.
// We ensure that each destination is only mounted to once in this way.
// We tiebreak only within each category; we'll let libpod tiebreak
// between mounts and named volumes.
// 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?
finalMounts := make(map[string]spec.Mount)
finalNamedVolumes := make(map[string]*libpod.ContainerNamedVolume)
for _, vol := range config.VolumesFrom {
options := []string{}
// TODO: now that container names may contain colons this is no
// longer safe.
// I don't know if there's much we can do here...
splitVol := strings.SplitN(vol, ":", 2)
if len(splitVol) == 2 {
if strings.Contains(splitVol[1], "Z") ||
strings.Contains(splitVol[1], "private") ||
strings.Contains(splitVol[1], "slave") ||
strings.Contains(splitVol[1], "shared") {
return nil, nil, errors.Errorf("invalid options %q, can only specify 'ro', 'rw', and 'z", splitVol[1])
}
if err := ValidateVolumeOpts(splitVol[1]); err != nil {
return nil, nil, err
}
options = strings.Split(splitVol[1], ",")
}
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 fylesystem 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,
}
setSource := false
setDest := false
for _, val := range args {
kv := strings.Split(val, "=")
switch kv[0] {
case "ro", "nosuid", "nodev", "noexec":
// TODO: detect duplication of these options
newMount.Options = append(newMount.Options, kv[0])
case "shared", "rshared", "private", "rprivate", "slave", "rslave", "Z", "z":
// TODO: detect duplicate mount propagation options
newMount.Options = append(newMount.Options, kv[0])
case "bind-propagation":
// TODO: this should conflict with the mount propagation
// options above.
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 := 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 := ValidateVolumeCtrDir(kv[1]); err != nil {
return newMount, err
}
newMount.Destination = kv[1]
setDest = true
default:
return newMount, errors.Wrapf(badOptionError, kv[0])
}
}
if !setDest {
return newMount, noDestError
}
if !setSource {
newMount.Source = newMount.Destination
}
// Process options
newMount.Options = processOptions(newMount.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,
}
setDest := false
for _, val := range args {
kv := strings.Split(val, "=")
switch kv[0] {
case "ro", "nosuid", "nodev", "noexec":
// TODO: detect duplication of these options
newMount.Options = append(newMount.Options, kv[0])
case "tmpfs-mode":
// TODO: detect duplicate modes
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":
// TODO: detect duplicate sizes
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 := ValidateVolumeCtrDir(kv[1]); err != nil {
return newMount, err
}
newMount.Destination = kv[1]
setDest = true
default:
return newMount, errors.Wrapf(badOptionError, 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)
setSource := false
setDest := false
for _, val := range args {
kv := strings.Split(val, "=")
switch kv[0] {
case "ro", "nosuid", "nodev", "noexec":
// TODO: detect duplication of these options
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 := ValidateVolumeCtrDir(kv[1]); err != nil {
return nil, err
}
newVolume.Dest = kv[1]
setDest = true
default:
return nil, errors.Wrapf(badOptionError, kv[0])
}
}
if !setSource {
return nil, errors.Errorf("must set source volume")
}
if !setDest {
return nil, noDestError
}
return newVolume, nil
}
// ValidateVolumeHostDir validates a volume mount's source directory
func ValidateVolumeHostDir(hostDir string) error {
if len(hostDir) == 0 {
return errors.Errorf("host directory cannot be empty")
}
if filepath.IsAbs(hostDir) {
if _, err := os.Stat(hostDir); err != nil {
return errors.Wrapf(err, "error checking path %q", hostDir)
}
}
// If hostDir is not an absolute path, that means the user wants to create a
// named volume. This will be done later on in the code.
return nil
}
// ValidateVolumeCtrDir validates a volume mount's destination directory.
func ValidateVolumeCtrDir(ctrDir string) error {
if len(ctrDir) == 0 {
return errors.Errorf("container directory cannot be empty")
}
if !filepath.IsAbs(ctrDir) {
return errors.Errorf("invalid container path %q, must be an absolute path", ctrDir)
}
return nil
}
// ValidateVolumeOpts validates a volume's options
func ValidateVolumeOpts(option string) error {
var foundRootPropagation, foundRWRO, foundLabelChange int
options := strings.Split(option, ",")
for _, opt := range options {
switch opt {
case "rw", "ro":
foundRWRO++
if foundRWRO > 1 {
return errors.Errorf("invalid options %q, can only specify 1 'rw' or 'ro' option", option)
}
case "z", "Z":
foundLabelChange++
if foundLabelChange > 1 {
return errors.Errorf("invalid options %q, can only specify 1 'z' or 'Z' option", option)
}
case "private", "rprivate", "shared", "rshared", "slave", "rslave":
foundRootPropagation++
if foundRootPropagation > 1 {
return errors.Errorf("invalid options %q, can only specify 1 '[r]shared', '[r]private' or '[r]slave' option", option)
}
default:
return errors.Errorf("invalid option type %q", option)
}
}
return nil
}
// GetVolumeMounts takes user provided input for bind mounts and creates Mount structs
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
)
splitVol := strings.Split(vol, ":")
if len(splitVol) > 3 {
return nil, nil, errors.Wrapf(volumeFormatErr, vol)
}
src = splitVol[0]
if len(splitVol) == 1 {
dest = src
} else if len(splitVol) > 1 {
dest = splitVol[1]
}
if len(splitVol) > 2 {
if err := ValidateVolumeOpts(splitVol[2]); err != nil {
return nil, nil, err
}
options = strings.Split(splitVol[2], ",")
}
if err := ValidateVolumeHostDir(src); err != nil {
return nil, nil, err
}
if err := ValidateVolumeCtrDir(dest); err != nil {
return nil, nil, err
}
if strings.HasPrefix(src, "/") || strings.HasPrefix(src, ".") {
// This is not a named volume
newMount := spec.Mount{
Destination: dest,
Type: string(TypeBind),
Source: src,
Options: processOptions(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 = dest
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 {
if config.ImageVolumeType == "tmpfs" {
// Tmpfs image volumes are handled as mounts
mount := spec.Mount{
Destination: vol,
Source: TypeTmpfs,
Type: TypeTmpfs,
Options: []string{"rprivate", "rw", "nodev"},
}
mounts[vol] = mount
} else {
namedVolume := new(libpod.ContainerNamedVolume)
namedVolume.Name = stringid.GenerateNonCryptoID()
namedVolume.Options = []string{"rprivate", "rw", "nodev"}
namedVolume.Dest = vol
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
options := []string{"rprivate", "rw", "noexec", "nosuid", "nodev", "size=65536k"}
spliti := strings.Split(i, ":")
destPath := spliti[0]
if len(spliti) > 1 {
options = strings.Split(spliti[1], ",")
}
if _, ok := m[destPath]; ok {
return nil, errors.Wrapf(errDuplicateDest, destPath)
}
// TODO: Do we want to set some default options if the user
// passed options?
// TODO: should we validate the options passed in?
mount := spec.Mount{
Destination: 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
}
// Handle options for volume mounts
func processOptions(options []string) []string {
var (
foundrw, foundro bool
rootProp string
)
options = append(options, "rbind")
for _, opt := range options {
switch opt {
case "rw":
foundrw = true
case "ro":
foundro = true
case "private", "rprivate", "slave", "rslave", "shared", "rshared":
rootProp = opt
}
}
if !foundrw && !foundro {
options = append(options, "rw")
}
if rootProp == "" {
options = append(options, "rprivate")
}
return options
}
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 {
var mounts []spec.Mount
for _, m := range inputMounts {
m.Options = processOptions(m.Options)
if m.Type == "tmpfs" {
m.Options = append(m.Options, "tmpcopyup")
} else {
mounts = append(mounts, m)
}
}
return mounts
}