From 4f04c03c27433371785cd791f077df4f2e4bcd97 Mon Sep 17 00:00:00 2001 From: Damien Churchill Date: Tue, 11 Jun 2019 13:26:04 +0100 Subject: [PATCH 1/4] lift code from docker/volume/mounts for splitting windows volumes Using the API as provided from the `mounts` package imposes validation on the `src:dest` which shouldn't be performed at this time. To workaround that lift the internal code from that library required to only perform the split. --- drivers/docker/utils.go | 163 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 149 insertions(+), 14 deletions(-) diff --git a/drivers/docker/utils.go b/drivers/docker/utils.go index 9cef58c6ad7d..c1dc4a83f24c 100644 --- a/drivers/docker/utils.go +++ b/drivers/docker/utils.go @@ -2,18 +2,78 @@ package docker import ( "encoding/json" - "errors" "fmt" "os" "os/exec" "path/filepath" + "regexp" "strings" "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" + "github.com/pkg/errors" +) + +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((` + 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((?:\\\\\?\\)?([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(?i)ro|rw))?` ) func parseDockerImage(image string) (repo, tag string) { @@ -222,6 +282,10 @@ func isParentPath(parent, path string) bool { return err == nil && !strings.HasPrefix(rel, "..") } +func errInvalidSpec(spec string) error { + return errors.Errorf("invalid volume specification: '%s'", spec) +} + func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) { if os == "windows" { return parseVolumeSpecWindows(volBind) @@ -229,27 +293,98 @@ func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, return parseVolumeSpecLinux(volBind) } -func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) { - parser := mounts.NewParser("windows") - m, err := parser.ParseMountRaw(volBind, "") +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 { - return "", "", "", err + if !os.IsNotExist(err) { + return false, false, err + } + return false, false, nil } + return true, fi.IsDir(), nil +} - src := m.Source - if src == "" && strings.Contains(volBind, m.Name) { - src = m.Name +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) } - if src == "" { - return "", "", "", errors.New("missing host path") + 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"]) - if m.Destination == "" { - return "", "", "", errors.New("container path is empty") + } + } + } + return split, nil +} + +func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) { + parts, err := windowsSplitRawSpec(volBind, rxDestination) + if err != nil { + return "", "", "", fmt.Errorf("not : format") + } + + if len(parts) < 2 { + return "", "", "", fmt.Errorf("not : format") + } + + hostPath = parts[0] + containerPath = parts[1] + + 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) { From 960f898dff30fb0c819a39bde78846ccd72d2be5 Mon Sep 17 00:00:00 2001 From: Damien Churchill Date: Tue, 11 Jun 2019 16:56:09 +0100 Subject: [PATCH 2/4] drivers/docker: move lifted code out to separate file and link the source & license --- drivers/docker/utils.go | 140 ------------------------ drivers/docker/win32_volume_parse.go | 152 +++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 140 deletions(-) create mode 100644 drivers/docker/win32_volume_parse.go diff --git a/drivers/docker/utils.go b/drivers/docker/utils.go index c1dc4a83f24c..20e03608b9c1 100644 --- a/drivers/docker/utils.go +++ b/drivers/docker/utils.go @@ -6,74 +6,12 @@ import ( "os" "os/exec" "path/filepath" - "regexp" "strings" "github.com/docker/cli/cli/config/configfile" "github.com/docker/distribution/reference" "github.com/docker/docker/registry" docker "github.com/fsouza/go-dockerclient" - "github.com/pkg/errors" -) - -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((` + 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((?:\\\\\?\\)?([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(?i)ro|rw))?` ) func parseDockerImage(image string) (repo, tag string) { @@ -282,10 +220,6 @@ func isParentPath(parent, path string) bool { return err == nil && !strings.HasPrefix(rel, "..") } -func errInvalidSpec(spec string) error { - return errors.Errorf("invalid volume specification: '%s'", spec) -} - func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) { if os == "windows" { return parseVolumeSpecWindows(volBind) @@ -293,80 +227,6 @@ func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, return parseVolumeSpecLinux(volBind) } -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 -} - func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) { parts, err := windowsSplitRawSpec(volBind, rxDestination) if err != nil { diff --git a/drivers/docker/win32_volume_parse.go b/drivers/docker/win32_volume_parse.go new file mode 100644 index 000000000000..132747bee55a --- /dev/null +++ b/drivers/docker/win32_volume_parse.go @@ -0,0 +1,152 @@ +package docker + +import ( + "fmt" + "regexp" + "os" + "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((` + 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((?:\\\\\?\\)?([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(?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 +} + From cb8a5e4caaf8ffb62052bf7d16507bdf58589101 Mon Sep 17 00:00:00 2001 From: Damien Churchill Date: Tue, 11 Jun 2019 17:15:40 +0100 Subject: [PATCH 3/4] run gofmt over the new file --- drivers/docker/win32_volume_parse.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/drivers/docker/win32_volume_parse.go b/drivers/docker/win32_volume_parse.go index 132747bee55a..d86de84544a2 100644 --- a/drivers/docker/win32_volume_parse.go +++ b/drivers/docker/win32_volume_parse.go @@ -1,11 +1,11 @@ package docker import ( - "fmt" - "regexp" - "os" - "strings" - "github.com/pkg/errors" + "fmt" + "github.com/pkg/errors" + "os" + "regexp" + "strings" ) // This code is taken from github.com/docker/volume/mounts/windows_parser.go @@ -71,7 +71,6 @@ const ( rxMode = `(:(?P(?i)ro|rw))?` ) - func errInvalidSpec(spec string) error { return errors.Errorf("invalid volume specification: '%s'", spec) } @@ -149,4 +148,3 @@ func windowsSplitRawSpec(raw, destRegex string) ([]string, error) { } return split, nil } - From 0cce6977d7fd327c80198e290dd1900fb96a857d Mon Sep 17 00:00:00 2001 From: Damien Churchill Date: Tue, 11 Jun 2019 17:30:13 +0100 Subject: [PATCH 4/4] run new file through goimports --- drivers/docker/win32_volume_parse.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/docker/win32_volume_parse.go b/drivers/docker/win32_volume_parse.go index d86de84544a2..8cce6023bb6e 100644 --- a/drivers/docker/win32_volume_parse.go +++ b/drivers/docker/win32_volume_parse.go @@ -2,10 +2,11 @@ package docker import ( "fmt" - "github.com/pkg/errors" "os" "regexp" "strings" + + "github.com/pkg/errors" ) // This code is taken from github.com/docker/volume/mounts/windows_parser.go