Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lift code from docker/volume/mounts for splitting windows volumes #5811

Merged
merged 4 commits into from
Jun 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions drivers/docker/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package docker

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
Expand All @@ -12,7 +11,6 @@ import (
"github.com/docker/cli/cli/config/configfile"
"github.com/docker/distribution/reference"
"github.com/docker/docker/registry"
"github.com/docker/docker/volume/mounts"
docker "github.com/fsouza/go-dockerclient"
)

Expand Down Expand Up @@ -230,26 +228,23 @@ func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string,
}

func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) {
parser := mounts.NewParser("windows")
m, err := parser.ParseMountRaw(volBind, "")
parts, err := windowsSplitRawSpec(volBind, rxDestination)
if err != nil {
return "", "", "", err
return "", "", "", fmt.Errorf("not <src>:<destination> format")
}

src := m.Source
if src == "" && strings.Contains(volBind, m.Name) {
src = m.Name
if len(parts) < 2 {
return "", "", "", fmt.Errorf("not <src>:<destination> format")
}

if src == "" {
return "", "", "", errors.New("missing host path")
}
hostPath = parts[0]
containerPath = parts[1]

if m.Destination == "" {
return "", "", "", errors.New("container path is empty")
if len(parts) > 2 {
mode = parts[2]
}

return src, m.Destination, m.Mode, nil
return
}

func parseVolumeSpecLinux(volBind string) (hostPath string, containerPath string, mode string, err error) {
Expand Down
151 changes: 151 additions & 0 deletions drivers/docker/win32_volume_parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package docker

import (
"fmt"
"os"
"regexp"
"strings"

"github.com/pkg/errors"
)

// This code is taken from github.com/docker/volume/mounts/windows_parser.go
// See https://github.com/moby/moby/blob/master/LICENSE for the license, Apache License 2.0 at this time.

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 + `))`

// 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))?`
)

func errInvalidSpec(spec string) error {
return errors.Errorf("invalid volume specification: '%s'", spec)
}

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 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
}