| package volume |
| |
| import ( |
| "errors" |
| "fmt" |
| "os" |
| "regexp" |
| "runtime" |
| "strings" |
| |
| "github.com/docker/docker/api/types/mount" |
| "github.com/docker/docker/pkg/stringid" |
| ) |
| |
| type windowsParser struct { |
| } |
| |
| 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 + `))` |
| |
| rxLCOWDestination = `(?P<destination>/(?:[^\\/:*?"<>\r\n]+[/]?)*)` |
| // Destination (aka container path): |
| // - Variation on hostdir but can be a drive followed by colon as well |
| // - If a path, must be absolute. Can include spaces |
| // - Drive cannot be c: (explicitly checked in code, not RegEx) |
| |
| // 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))?` |
| ) |
| |
| type mountValidator func(mnt *mount.Mount) error |
| |
| func windowsSplitRawSpec(raw, destRegex string) ([]string, error) { |
| specExp := regexp.MustCompile(`^` + rxSource + destRegex + rxMode + `$`) |
| match := specExp.FindStringSubmatch(strings.ToLower(raw)) |
| |
| // Must have something back |
| 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 specExp.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"] != "" { |
| volExp := regexp.MustCompile(`^` + rxName + `$`) |
| reservedNameExp := regexp.MustCompile(`^` + rxReservedNames + `$`) |
| |
| if volExp.MatchString(matchgroups["destination"]) { |
| if reservedNameExp.MatchString(matchgroups["destination"]) { |
| return nil, fmt.Errorf("volume name %q cannot be a reserved word for Windows filenames", matchgroups["destination"]) |
| } |
| } else { |
| |
| exists, isDir, _ := currentFileInfoProvider.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 |
| } |
| return rwModes[strings.ToLower(mode)] |
| } |
| func windowsValidateNotRoot(p string) error { |
| p = strings.ToLower(strings.Replace(p, `/`, `\`, -1)) |
| if p == "c:" || p == `c:\` { |
| return fmt.Errorf("destination path cannot be `c:` or `c:\\`: %v", p) |
| } |
| return nil |
| } |
| |
| var windowsSpecificValidators mountValidator = func(mnt *mount.Mount) error { |
| return windowsValidateNotRoot(mnt.Target) |
| } |
| |
| func windowsValidateRegex(p, r string) error { |
| if regexp.MustCompile(`^` + r + `$`).MatchString(strings.ToLower(p)) { |
| return nil |
| } |
| return fmt.Errorf("invalid mount path: '%s'", p) |
| } |
| func windowsValidateAbsolute(p string) error { |
| if err := windowsValidateRegex(p, rxDestination); err != nil { |
| 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 regexp.MustCompile(`^` + rxHostDir + `$`).MatchString(p) { |
| return mount.TypeBind |
| } else { |
| return mount.TypeVolume |
| } |
| } |
| |
| func (p *windowsParser) ReadWrite(mode string) bool { |
| return strings.ToLower(mode) != "ro" |
| } |
| |
| // IsVolumeNameValid checks a volume name in a platform specific manner. |
| func (p *windowsParser) ValidateVolumeName(name string) error { |
| nameExp := regexp.MustCompile(`^` + rxName + `$`) |
| if !nameExp.MatchString(name) { |
| return errors.New("invalid volume name") |
| } |
| nameExp = regexp.MustCompile(`^` + rxReservedNames + `$`) |
| if nameExp.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, rxDestination, windowsSpecificValidators) |
| } |
| |
| 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 |
| } |
| |
| var currentFileInfoProvider fileInfoProvider = defaultFileInfoProvider{} |
| |
| func (p *windowsParser) validateMountConfigReg(mnt *mount.Mount, destRegex string, additionalValidators ...mountValidator) error { |
| |
| for _, v := range additionalValidators { |
| if err := v(mnt); err != nil { |
| return &errMountConfig{mnt, err} |
| } |
| } |
| if len(mnt.Target) == 0 { |
| return &errMountConfig{mnt, errMissingField("Target")} |
| } |
| |
| if err := windowsValidateRegex(mnt.Target, destRegex); 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 := currentFileInfoProvider.fileInfo(mnt.Source) |
| if err != nil { |
| return &errMountConfig{mnt, err} |
| } |
| if !exists { |
| return &errMountConfig{mnt, errBindNotExist} |
| } |
| 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) { |
| return p.parseMountRaw(raw, volumeDriver, rxDestination, true, windowsSpecificValidators) |
| } |
| |
| func (p *windowsParser) parseMountRaw(raw, volumeDriver, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) { |
| arr, err := windowsSplitRawSpec(raw, destRegex) |
| if err != nil { |
| return nil, err |
| } |
| |
| 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.Replace(arr[0], `/`, `\`, -1) |
| spec.Target = arr[1] |
| case 3: |
| // HostSourcePath+DestinationPath+Mode |
| spec.Source = strings.Replace(arr[0], `/`, `\`, -1) |
| spec.Target = arr[1] |
| mode = arr[2] |
| default: |
| return nil, errInvalidSpec(raw) |
| } |
| if convertTargetToBackslash { |
| spec.Target = strings.Replace(spec.Target, `/`, `\`, -1) |
| } |
| |
| 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, destRegex, 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, rxDestination, true, windowsSpecificValidators) |
| } |
| func (p *windowsParser) parseMountSpec(cfg mount.Mount, destRegex string, convertTargetToBackslash bool, additionalValidators ...mountValidator) (*MountPoint, error) { |
| if err := p.validateMountConfigReg(&cfg, destRegex, additionalValidators...); err != nil { |
| return nil, err |
| } |
| mp := &MountPoint{ |
| RW: !cfg.ReadOnly, |
| Destination: cfg.Target, |
| Type: cfg.Type, |
| Spec: cfg, |
| } |
| if convertTargetToBackslash { |
| mp.Destination = strings.Replace(cfg.Target, `/`, `\`, -1) |
| } |
| |
| switch cfg.Type { |
| case mount.TypeVolume: |
| if cfg.Source == "" { |
| mp.Name = stringid.GenerateNonCryptoID() |
| } 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.Replace(cfg.Source, `/`, `\`, -1) |
| case mount.TypeNamedPipe: |
| mp.Source = strings.Replace(cfg.Source, `/`, `\`, -1) |
| } |
| // 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") |
| } |
| |
| specParts := strings.SplitN(spec, ":", 2) |
| id := specParts[0] |
| mode := "rw" |
| |
| if len(specParts) == 2 { |
| mode = specParts[1] |
| 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 mount.Propagation("") |
| } |
| |
| 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") |
| } |