dotcloud/docker

View on GitHub
volume/mounts/linux_parser.go

Summary

Maintainability
C
1 day
Test Coverage
package mounts // import "github.com/docker/docker/volume/mounts"

import (
    "errors"
    "fmt"
    "path"
    "path/filepath"
    "strings"

    "github.com/docker/docker/api/types/mount"
    "github.com/docker/docker/volume"
)

// NewLinuxParser creates a parser with Linux semantics.
func NewLinuxParser() Parser {
    return &linuxParser{
        fi: defaultFileInfoProvider{},
    }
}

type linuxParser struct {
    fi fileInfoProvider
}

func linuxValidateNotRoot(p string) error {
    p = path.Clean(strings.ReplaceAll(p, `\`, `/`))
    if p == "/" {
        return ErrVolumeTargetIsRoot
    }
    return nil
}

func linuxValidateAbsolute(p string) error {
    p = strings.ReplaceAll(p, `\`, `/`)
    if path.IsAbs(p) {
        return nil
    }
    return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
}

func (p *linuxParser) ValidateMountConfig(mnt *mount.Mount) error {
    // there was something looking like a bug in existing codebase:
    // - validateMountConfig on linux was called with options skipping bind source existence when calling ParseMountRaw
    // - but not when calling ParseMountSpec directly... nor when the unit test called it directly
    return p.validateMountConfigImpl(mnt, true)
}

func (p *linuxParser) validateMountConfigImpl(mnt *mount.Mount, validateBindSourceExists bool) error {
    if len(mnt.Target) == 0 {
        return &errMountConfig{mnt, errMissingField("Target")}
    }

    if err := linuxValidateNotRoot(mnt.Target); err != nil {
        return &errMountConfig{mnt, err}
    }

    if err := linuxValidateAbsolute(mnt.Target); err != nil {
        return &errMountConfig{mnt, err}
    }

    switch mnt.Type {
    case mount.TypeBind:
        if len(mnt.Source) == 0 {
            return &errMountConfig{mnt, errMissingField("Source")}
        }
        // Don't error out just because the propagation mode is not supported on the platform
        if opts := mnt.BindOptions; opts != nil {
            if len(opts.Propagation) > 0 && len(linuxPropagationModes) > 0 {
                if _, ok := linuxPropagationModes[opts.Propagation]; !ok {
                    return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
                }
            }
        }
        if mnt.VolumeOptions != nil {
            return &errMountConfig{mnt, errExtraField("VolumeOptions")}
        }

        if err := linuxValidateAbsolute(mnt.Source); err != nil {
            return &errMountConfig{mnt, err}
        }

        if validateBindSourceExists {
            exists, _, err := p.fi.fileInfo(mnt.Source)
            if err != nil {
                return &errMountConfig{mnt, err}
            }

            createMountpoint := mnt.BindOptions != nil && mnt.BindOptions.CreateMountpoint
            if !exists && !createMountpoint {
                return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
            }
        }

    case mount.TypeVolume:
        if mnt.BindOptions != nil {
            return &errMountConfig{mnt, errExtraField("BindOptions")}
        }
        anonymousVolume := len(mnt.Source) == 0

        if mnt.VolumeOptions != nil && mnt.VolumeOptions.Subpath != "" {
            if anonymousVolume {
                return &errMountConfig{mnt, errAnonymousVolumeWithSubpath}
            }

            if !filepath.IsLocal(mnt.VolumeOptions.Subpath) {
                return &errMountConfig{mnt, errInvalidSubpath}
            }
        }
        if mnt.ReadOnly && anonymousVolume {
            return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
        }
    case mount.TypeTmpfs:
        if mnt.BindOptions != nil {
            return &errMountConfig{mnt, errExtraField("BindOptions")}
        }
        if len(mnt.Source) != 0 {
            return &errMountConfig{mnt, errExtraField("Source")}
        }
        if _, err := p.ConvertTmpfsOptions(mnt.TmpfsOptions, mnt.ReadOnly); err != nil {
            return &errMountConfig{mnt, err}
        }
    default:
        return &errMountConfig{mnt, errors.New("mount type unknown")}
    }
    return nil
}

// label modes
var linuxLabelModes = map[string]bool{
    "Z": true,
    "z": true,
}

// consistency modes
var linuxConsistencyModes = map[mount.Consistency]bool{
    mount.ConsistencyFull:      true,
    mount.ConsistencyCached:    true,
    mount.ConsistencyDelegated: true,
}

var linuxPropagationModes = map[mount.Propagation]bool{
    mount.PropagationPrivate:  true,
    mount.PropagationRPrivate: true,
    mount.PropagationSlave:    true,
    mount.PropagationRSlave:   true,
    mount.PropagationShared:   true,
    mount.PropagationRShared:  true,
}

const linuxDefaultPropagationMode = mount.PropagationRPrivate

func linuxGetPropagation(mode string) mount.Propagation {
    for _, o := range strings.Split(mode, ",") {
        prop := mount.Propagation(o)
        if linuxPropagationModes[prop] {
            return prop
        }
    }
    return linuxDefaultPropagationMode
}

func linuxHasPropagation(mode string) bool {
    for _, o := range strings.Split(mode, ",") {
        if linuxPropagationModes[mount.Propagation(o)] {
            return true
        }
    }
    return false
}

func linuxValidMountMode(mode string) bool {
    if mode == "" {
        return true
    }

    rwModeCount := 0
    labelModeCount := 0
    propagationModeCount := 0
    copyModeCount := 0
    consistencyModeCount := 0

    for _, o := range strings.Split(mode, ",") {
        switch {
        case rwModes[o]:
            rwModeCount++
        case linuxLabelModes[o]:
            labelModeCount++
        case linuxPropagationModes[mount.Propagation(o)]:
            propagationModeCount++
        case copyModeExists(o):
            copyModeCount++
        case linuxConsistencyModes[mount.Consistency(o)]:
            consistencyModeCount++
        default:
            return false
        }
    }

    // Only one string for each mode is allowed.
    if rwModeCount > 1 || labelModeCount > 1 || propagationModeCount > 1 || copyModeCount > 1 || consistencyModeCount > 1 {
        return false
    }
    return true
}

var validTmpfsOptions = map[string]bool{
    "exec":   true,
    "noexec": true,
}

func validateTmpfsOptions(rawOptions [][]string) ([]string, error) {
    var options []string
    for _, opt := range rawOptions {
        if len(opt) < 1 || len(opt) > 2 {
            return nil, errors.New("invalid option array length")
        }
        if _, ok := validTmpfsOptions[opt[0]]; !ok {
            return nil, errors.New("invalid option: " + opt[0])
        }

        if len(opt) == 1 {
            options = append(options, opt[0])
        } else {
            options = append(options, fmt.Sprintf("%s=%s", opt[0], opt[1]))
        }
    }
    return options, nil
}

func (p *linuxParser) ReadWrite(mode string) bool {
    if !linuxValidMountMode(mode) {
        return false
    }

    for _, o := range strings.Split(mode, ",") {
        if o == "ro" {
            return false
        }
    }
    return true
}

func (p *linuxParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
    arr := strings.SplitN(raw, ":", 4)
    if arr[0] == "" {
        return nil, errInvalidSpec(raw)
    }

    var spec mount.Mount
    var mode string
    switch len(arr) {
    case 1:
        // Just a destination path in the container
        spec.Target = arr[0]
    case 2:
        if linuxValidMountMode(arr[1]) {
            // Destination + Mode is not a valid volume - volumes
            // cannot include a mode. e.g. /foo:rw
            return nil, errInvalidSpec(raw)
        }
        // Host Source Path or Name + Destination
        spec.Source = arr[0]
        spec.Target = arr[1]
    case 3:
        // HostSourcePath+DestinationPath+Mode
        spec.Source = arr[0]
        spec.Target = arr[1]
        mode = arr[2]
    default:
        return nil, errInvalidSpec(raw)
    }

    if !linuxValidMountMode(mode) {
        return nil, errInvalidMode(mode)
    }

    if path.IsAbs(spec.Source) {
        spec.Type = mount.TypeBind
    } else {
        spec.Type = mount.TypeVolume
    }

    spec.ReadOnly = !p.ReadWrite(mode)

    // cannot assume that if a volume driver is passed in that we should set it
    if volumeDriver != "" && spec.Type == mount.TypeVolume {
        spec.VolumeOptions = &mount.VolumeOptions{
            DriverConfig: &mount.Driver{Name: volumeDriver},
        }
    }

    if copyData, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
        if spec.VolumeOptions == nil {
            spec.VolumeOptions = &mount.VolumeOptions{}
        }
        spec.VolumeOptions.NoCopy = !copyData
    }
    if linuxHasPropagation(mode) {
        spec.BindOptions = &mount.BindOptions{
            Propagation: linuxGetPropagation(mode),
        }
    }

    mp, err := p.parseMountSpec(spec, false)
    if mp != nil {
        mp.Mode = mode
    }
    if err != nil {
        err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
    }
    return mp, err
}

func (p *linuxParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
    return p.parseMountSpec(cfg, true)
}

func (p *linuxParser) parseMountSpec(cfg mount.Mount, validateBindSourceExists bool) (*MountPoint, error) {
    if err := p.validateMountConfigImpl(&cfg, validateBindSourceExists); err != nil {
        return nil, err
    }
    mp := &MountPoint{
        RW:          !cfg.ReadOnly,
        Destination: path.Clean(filepath.ToSlash(cfg.Target)),
        Type:        cfg.Type,
        Spec:        cfg,
    }

    switch cfg.Type {
    case mount.TypeVolume:
        if cfg.Source != "" {
            // non-anonymous volume
            mp.Name = cfg.Source
        }
        mp.CopyData = p.DefaultCopyMode()

        if cfg.VolumeOptions != nil {
            if cfg.VolumeOptions.DriverConfig != nil {
                mp.Driver = cfg.VolumeOptions.DriverConfig.Name
            }
            if cfg.VolumeOptions.NoCopy {
                mp.CopyData = false
            }
        }
    case mount.TypeBind:
        mp.Source = path.Clean(filepath.ToSlash(cfg.Source))
        if cfg.BindOptions != nil && len(cfg.BindOptions.Propagation) > 0 {
            mp.Propagation = cfg.BindOptions.Propagation
        } else {
            // If user did not specify a propagation mode, get
            // default propagation mode.
            mp.Propagation = linuxDefaultPropagationMode
        }
    case mount.TypeTmpfs:
        // NOP
    }
    return mp, nil
}

func (p *linuxParser) ParseVolumesFrom(spec string) (string, string, error) {
    if len(spec) == 0 {
        return "", "", fmt.Errorf("volumes-from specification cannot be an empty string")
    }

    id, mode, _ := strings.Cut(spec, ":")
    if mode == "" {
        return id, "rw", nil
    }
    if !linuxValidMountMode(mode) {
        return "", "", errInvalidMode(mode)
    }
    // For now don't allow propagation properties while importing
    // volumes from data container. These volumes will inherit
    // the same propagation property as of the original volume
    // in data container. This probably can be relaxed in future.
    if linuxHasPropagation(mode) {
        return "", "", errInvalidMode(mode)
    }
    // Do not allow copy modes on volumes-from
    if _, isSet := getCopyMode(mode, p.DefaultCopyMode()); isSet {
        return "", "", errInvalidMode(mode)
    }
    return id, mode, nil
}

func (p *linuxParser) DefaultPropagationMode() mount.Propagation {
    return linuxDefaultPropagationMode
}

func (p *linuxParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
    var rawOpts []string
    if readOnly {
        rawOpts = append(rawOpts, "ro")
    }

    if opt != nil && opt.Mode != 0 {
        rawOpts = append(rawOpts, fmt.Sprintf("mode=%o", opt.Mode))
    }

    if opt != nil && opt.SizeBytes != 0 {
        // calculate suffix here, making this linux specific, but that is
        // okay, since API is that way anyways.

        // we do this by finding the suffix that divides evenly into the
        // value, returning the value itself, with no suffix, if it fails.
        //
        // For the most part, we don't enforce any semantic to this values.
        // The operating system will usually align this and enforce minimum
        // and maximums.
        var (
            size   = opt.SizeBytes
            suffix string
        )
        for _, r := range []struct {
            suffix  string
            divisor int64
        }{
            {"g", 1 << 30},
            {"m", 1 << 20},
            {"k", 1 << 10},
        } {
            if size%r.divisor == 0 {
                size = size / r.divisor
                suffix = r.suffix
                break
            }
        }

        rawOpts = append(rawOpts, fmt.Sprintf("size=%d%s", size, suffix))
    }

    if opt != nil && len(opt.Options) > 0 {
        tmpfsOpts, err := validateTmpfsOptions(opt.Options)
        if err != nil {
            return "", err
        }
        rawOpts = append(rawOpts, tmpfsOpts...)
    }

    return strings.Join(rawOpts, ","), nil
}

func (p *linuxParser) DefaultCopyMode() bool {
    return true
}

func (p *linuxParser) ValidateVolumeName(name string) error {
    return nil
}

func (p *linuxParser) IsBackwardCompatible(m *MountPoint) bool {
    return len(m.Source) > 0 || m.Driver == volume.DefaultDriverName
}

func (p *linuxParser) ValidateTmpfsMountDestination(dest string) error {
    if err := linuxValidateNotRoot(dest); err != nil {
        return err
    }
    return linuxValidateAbsolute(dest)
}