123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- 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")
- }
|