4c281fb29a
Formatting the code with https://github.com/mvdan/gofumpt Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
456 lines
14 KiB
Go
456 lines
14 KiB
Go
package mounts // import "github.com/docker/docker/volume/mounts"
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/docker/docker/api/types/mount"
|
|
"github.com/docker/docker/pkg/stringid"
|
|
)
|
|
|
|
// NewWindowsParser creates a parser with Windows semantics.
|
|
func NewWindowsParser() Parser {
|
|
return &windowsParser{
|
|
fi: defaultFileInfoProvider{},
|
|
}
|
|
}
|
|
|
|
type windowsParser struct {
|
|
fi fileInfoProvider
|
|
}
|
|
|
|
const (
|
|
// Spec should be in the format [source:]destination[:mode]
|
|
//
|
|
// Examples: c:\foo bar:d:rw
|
|
// c:\foo:d:\bar
|
|
// myname:d:
|
|
// d:\
|
|
//
|
|
// Explanation of this regex! Thanks @thaJeztah on IRC and gist for help. See
|
|
// https://gist.github.com/thaJeztah/6185659e4978789fb2b2. A good place to
|
|
// test is https://regex-golang.appspot.com/assets/html/index.html
|
|
//
|
|
// Useful link for referencing named capturing groups:
|
|
// http://stackoverflow.com/questions/20750843/using-named-matches-from-go-regex
|
|
//
|
|
// There are three match groups: source, destination and mode.
|
|
//
|
|
|
|
// rxHostDir is the first option of a source
|
|
rxHostDir = `(?:\\\\\?\\)?[a-z]:[\\/](?:[^\\/:*?"<>|\r\n]+[\\/]?)*`
|
|
// rxName is the second option of a source
|
|
rxName = `[^\\/:*?"<>|\r\n]+`
|
|
|
|
// RXReservedNames are reserved names not possible on Windows
|
|
rxReservedNames = `(con|prn|nul|aux|com[1-9]|lpt[1-9])`
|
|
|
|
// rxPipe is a named path pipe (starts with `\\.\pipe\`, possibly with / instead of \)
|
|
rxPipe = `[/\\]{2}.[/\\]pipe[/\\][^:*?"<>|\r\n]+`
|
|
// rxSource is the combined possibilities for a source
|
|
rxSource = `((?P<source>((` + rxHostDir + `)|(` + rxName + `)|(` + rxPipe + `))):)?`
|
|
|
|
// Source. Can be either a host directory, a name, or omitted:
|
|
// HostDir:
|
|
// - Essentially using the folder solution from
|
|
// https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch08s18.html
|
|
// but adding case insensitivity.
|
|
// - Must be an absolute path such as c:\path
|
|
// - Can include spaces such as `c:\program files`
|
|
// - And then followed by a colon which is not in the capture group
|
|
// - And can be optional
|
|
// Name:
|
|
// - Must not contain invalid NTFS filename characters (https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx)
|
|
// - And then followed by a colon which is not in the capture group
|
|
// - And can be optional
|
|
|
|
// rxDestination is the regex expression for the mount destination
|
|
rxDestination = `(?P<destination>((?:\\\\\?\\)?([a-z]):((?:[\\/][^\\/:*?"<>\r\n]+)*[\\/]?))|(` + rxPipe + `))`
|
|
|
|
// rxMode is the regex expression for the mode of the mount
|
|
// Mode (optional):
|
|
// - Hopefully self explanatory in comparison to above regex's.
|
|
// - Colon is not in the capture group
|
|
rxMode = `(:(?P<mode>(?i)ro|rw))?`
|
|
)
|
|
|
|
var (
|
|
volumeNameRegexp = regexp.MustCompile(`^` + rxName + `$`)
|
|
reservedNameRegexp = regexp.MustCompile(`^` + rxReservedNames + `$`)
|
|
hostDirRegexp = regexp.MustCompile(`^` + rxHostDir + `$`)
|
|
mountDestinationRegexp = regexp.MustCompile(`^` + rxDestination + `$`)
|
|
windowsSplitRawSpecRegexp = regexp.MustCompile(`^` + rxSource + rxDestination + rxMode + `$`)
|
|
)
|
|
|
|
type mountValidator func(mnt *mount.Mount) error
|
|
|
|
func (p *windowsParser) splitRawSpec(raw string, splitRegexp *regexp.Regexp) ([]string, error) {
|
|
match := splitRegexp.FindStringSubmatch(strings.ToLower(raw))
|
|
if len(match) == 0 {
|
|
return nil, errInvalidSpec(raw)
|
|
}
|
|
|
|
var split []string
|
|
matchgroups := make(map[string]string)
|
|
// Pull out the sub expressions from the named capture groups
|
|
for i, name := range splitRegexp.SubexpNames() {
|
|
matchgroups[name] = strings.ToLower(match[i])
|
|
}
|
|
if source, exists := matchgroups["source"]; exists {
|
|
if source != "" {
|
|
split = append(split, source)
|
|
}
|
|
}
|
|
if destination, exists := matchgroups["destination"]; exists {
|
|
if destination != "" {
|
|
split = append(split, destination)
|
|
}
|
|
}
|
|
if mode, exists := matchgroups["mode"]; exists {
|
|
if mode != "" {
|
|
split = append(split, mode)
|
|
}
|
|
}
|
|
// Fix #26329. If the destination appears to be a file, and the source is null,
|
|
// it may be because we've fallen through the possible naming regex and hit a
|
|
// situation where the user intention was to map a file into a container through
|
|
// a local volume, but this is not supported by the platform.
|
|
if matchgroups["source"] == "" && matchgroups["destination"] != "" {
|
|
if volumeNameRegexp.MatchString(matchgroups["destination"]) {
|
|
if reservedNameRegexp.MatchString(matchgroups["destination"]) {
|
|
return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"])
|
|
}
|
|
} else {
|
|
exists, isDir, _ := p.fi.fileInfo(matchgroups["destination"])
|
|
if exists && !isDir {
|
|
return nil, fmt.Errorf("file '%s' cannot be mapped. Only directories can be mapped on this platform", matchgroups["destination"])
|
|
}
|
|
}
|
|
}
|
|
return split, nil
|
|
}
|
|
|
|
func windowsValidMountMode(mode string) bool {
|
|
if mode == "" {
|
|
return true
|
|
}
|
|
// TODO should windows mounts produce an error if any mode was provided (they're a no-op on windows)
|
|
return rwModes[strings.ToLower(mode)]
|
|
}
|
|
|
|
func windowsValidateNotRoot(p string) error {
|
|
p = strings.ToLower(strings.ReplaceAll(p, `/`, `\`))
|
|
if p == "c:" || p == `c:\` {
|
|
return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var windowsValidators mountValidator = func(m *mount.Mount) error {
|
|
if err := windowsValidateNotRoot(m.Target); err != nil {
|
|
return err
|
|
}
|
|
if !mountDestinationRegexp.MatchString(strings.ToLower(m.Target)) {
|
|
return fmt.Errorf("invalid mount path: '%s'", m.Target)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func windowsValidateAbsolute(p string) error {
|
|
if !mountDestinationRegexp.MatchString(strings.ToLower(p)) {
|
|
return fmt.Errorf("invalid mount path: '%s' mount path must be absolute", p)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func windowsDetectMountType(p string) mount.Type {
|
|
if strings.HasPrefix(p, `\\.\pipe\`) {
|
|
return mount.TypeNamedPipe
|
|
} else if hostDirRegexp.MatchString(p) {
|
|
return mount.TypeBind
|
|
} else {
|
|
return mount.TypeVolume
|
|
}
|
|
}
|
|
|
|
func (p *windowsParser) ReadWrite(mode string) bool {
|
|
return strings.ToLower(mode) != "ro"
|
|
}
|
|
|
|
// ValidateVolumeName checks a volume name in a platform specific manner.
|
|
func (p *windowsParser) ValidateVolumeName(name string) error {
|
|
if !volumeNameRegexp.MatchString(name) {
|
|
return errors.New("invalid volume name")
|
|
}
|
|
if reservedNameRegexp.MatchString(name) {
|
|
return fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *windowsParser) ValidateMountConfig(mnt *mount.Mount) error {
|
|
return p.validateMountConfigReg(mnt, windowsValidators)
|
|
}
|
|
|
|
type fileInfoProvider interface {
|
|
fileInfo(path string) (exist, isDir bool, err error)
|
|
}
|
|
|
|
type defaultFileInfoProvider struct{}
|
|
|
|
func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return false, false, err
|
|
}
|
|
return false, false, nil
|
|
}
|
|
return true, fi.IsDir(), nil
|
|
}
|
|
|
|
func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, additionalValidators ...mountValidator) error {
|
|
if len(mnt.Target) == 0 {
|
|
return &errMountConfig{mnt, errMissingField("Target")}
|
|
}
|
|
for _, v := range additionalValidators {
|
|
if err := v(mnt); 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 {
|
|
return &errMountConfig{mnt, fmt.Errorf("invalid propagation mode: %s", opts.Propagation)}
|
|
}
|
|
}
|
|
if mnt.VolumeOptions != nil {
|
|
return &errMountConfig{mnt, errExtraField("VolumeOptions")}
|
|
}
|
|
|
|
if err := windowsValidateAbsolute(mnt.Source); err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
|
|
exists, isdir, err := p.fi.fileInfo(mnt.Source)
|
|
if err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
if !exists {
|
|
return &errMountConfig{mnt, errBindSourceDoesNotExist(mnt.Source)}
|
|
}
|
|
if !isdir {
|
|
return &errMountConfig{mnt, fmt.Errorf("source path must be a directory")}
|
|
}
|
|
|
|
case mount.TypeVolume:
|
|
if mnt.BindOptions != nil {
|
|
return &errMountConfig{mnt, errExtraField("BindOptions")}
|
|
}
|
|
|
|
if len(mnt.Source) == 0 && mnt.ReadOnly {
|
|
return &errMountConfig{mnt, fmt.Errorf("must not set ReadOnly mode when using anonymous volumes")}
|
|
}
|
|
|
|
if len(mnt.Source) != 0 {
|
|
if err := p.ValidateVolumeName(mnt.Source); err != nil {
|
|
return &errMountConfig{mnt, err}
|
|
}
|
|
}
|
|
case mount.TypeNamedPipe:
|
|
if len(mnt.Source) == 0 {
|
|
return &errMountConfig{mnt, errMissingField("Source")}
|
|
}
|
|
|
|
if mnt.BindOptions != nil {
|
|
return &errMountConfig{mnt, errExtraField("BindOptions")}
|
|
}
|
|
|
|
if mnt.ReadOnly {
|
|
return &errMountConfig{mnt, errExtraField("ReadOnly")}
|
|
}
|
|
|
|
if windowsDetectMountType(mnt.Source) != mount.TypeNamedPipe {
|
|
return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Source)}
|
|
}
|
|
|
|
if windowsDetectMountType(mnt.Target) != mount.TypeNamedPipe {
|
|
return &errMountConfig{mnt, fmt.Errorf("'%s' is not a valid pipe path", mnt.Target)}
|
|
}
|
|
default:
|
|
return &errMountConfig{mnt, errors.New("mount type unknown")}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *windowsParser) ParseMountRaw(raw, volumeDriver string) (*MountPoint, error) {
|
|
arr, err := p.splitRawSpec(raw, windowsSplitRawSpecRegexp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.parseMount(arr, raw, volumeDriver, true, windowsValidators)
|
|
}
|
|
|
|
func (p *windowsParser) parseMount(arr []string, raw, volumeDriver string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
|
|
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 windowsValidMountMode(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 = strings.ReplaceAll(arr[0], `/`, `\`)
|
|
spec.Target = arr[1]
|
|
case 3:
|
|
// HostSourcePath+DestinationPath+Mode
|
|
spec.Source = strings.ReplaceAll(arr[0], `/`, `\`)
|
|
spec.Target = arr[1]
|
|
mode = arr[2]
|
|
default:
|
|
return nil, errInvalidSpec(raw)
|
|
}
|
|
if convertTargetToBackslash {
|
|
spec.Target = strings.ReplaceAll(spec.Target, `/`, `\`)
|
|
}
|
|
|
|
if !windowsValidMountMode(mode) {
|
|
return nil, errInvalidMode(mode)
|
|
}
|
|
|
|
spec.Type = windowsDetectMountType(spec.Source)
|
|
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
|
|
}
|
|
|
|
mp, err := p.parseMountSpec(spec, convertTargetToBackslash, additionalValidators...)
|
|
if mp != nil {
|
|
mp.Mode = mode
|
|
}
|
|
if err != nil {
|
|
err = fmt.Errorf("%v: %v", errInvalidSpec(raw), err)
|
|
}
|
|
return mp, err
|
|
}
|
|
|
|
func (p *windowsParser) ParseMountSpec(cfg mount.Mount) (*MountPoint, error) {
|
|
return p.parseMountSpec(cfg, true, windowsValidators)
|
|
}
|
|
|
|
func (p *windowsParser) parseMountSpec(cfg mount.Mount, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) {
|
|
if err := p.validateMountConfigReg(&cfg, additionalValidators...); err != nil {
|
|
return nil, err
|
|
}
|
|
mp := &MountPoint{
|
|
RW: !cfg.ReadOnly,
|
|
Destination: cfg.Target,
|
|
Type: cfg.Type,
|
|
Spec: cfg,
|
|
}
|
|
if convertTargetToBackslash {
|
|
mp.Destination = strings.ReplaceAll(cfg.Target, `/`, `\`)
|
|
}
|
|
|
|
switch cfg.Type {
|
|
case mount.TypeVolume:
|
|
if cfg.Source == "" {
|
|
mp.Name = stringid.GenerateRandomID()
|
|
} else {
|
|
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 = strings.ReplaceAll(cfg.Source, `/`, `\`)
|
|
case mount.TypeNamedPipe:
|
|
mp.Source = strings.ReplaceAll(cfg.Source, `/`, `\`)
|
|
}
|
|
// cleanup trailing `\` except for paths like `c:\`
|
|
if len(mp.Source) > 3 && mp.Source[len(mp.Source)-1] == '\\' {
|
|
mp.Source = mp.Source[:len(mp.Source)-1]
|
|
}
|
|
if len(mp.Destination) > 3 && mp.Destination[len(mp.Destination)-1] == '\\' {
|
|
mp.Destination = mp.Destination[:len(mp.Destination)-1]
|
|
}
|
|
return mp, nil
|
|
}
|
|
|
|
func (p *windowsParser) 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 !windowsValidMountMode(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 *windowsParser) DefaultPropagationMode() mount.Propagation {
|
|
return ""
|
|
}
|
|
|
|
func (p *windowsParser) ConvertTmpfsOptions(opt *mount.TmpfsOptions, readOnly bool) (string, error) {
|
|
return "", fmt.Errorf("%s does not support tmpfs", runtime.GOOS)
|
|
}
|
|
|
|
func (p *windowsParser) DefaultCopyMode() bool {
|
|
return false
|
|
}
|
|
|
|
func (p *windowsParser) IsBackwardCompatible(m *MountPoint) bool {
|
|
return false
|
|
}
|
|
|
|
func (p *windowsParser) ValidateTmpfsMountDestination(dest string) error {
|
|
return errors.New("platform does not support tmpfs")
|
|
}
|
|
|
|
func (p *windowsParser) HasResource(m *MountPoint, absolutePath string) bool {
|
|
return false
|
|
}
|