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.
This commit is contained in:
parent
456c4c8bd2
commit
70daca3395
|
@ -2,18 +2,78 @@ package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/config/configfile"
|
"github.com/docker/cli/cli/config/configfile"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/docker/registry"
|
"github.com/docker/docker/registry"
|
||||||
"github.com/docker/docker/volume/mounts"
|
|
||||||
docker "github.com/fsouza/go-dockerclient"
|
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<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 parseDockerImage(image string) (repo, tag string) {
|
func parseDockerImage(image string) (repo, tag string) {
|
||||||
|
@ -222,6 +282,10 @@ func isParentPath(parent, path string) bool {
|
||||||
return err == nil && !strings.HasPrefix(rel, "..")
|
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) {
|
func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string, mode string, err error) {
|
||||||
if os == "windows" {
|
if os == "windows" {
|
||||||
return parseVolumeSpecWindows(volBind)
|
return parseVolumeSpecWindows(volBind)
|
||||||
|
@ -229,27 +293,98 @@ func parseVolumeSpec(volBind, os string) (hostPath string, containerPath string,
|
||||||
return parseVolumeSpecLinux(volBind)
|
return parseVolumeSpecLinux(volBind)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseVolumeSpecWindows(volBind string) (hostPath string, containerPath string, mode string, err error) {
|
type fileInfoProvider interface {
|
||||||
parser := mounts.NewParser("windows")
|
fileInfo(path string) (exist, isDir bool, err error)
|
||||||
m, err := parser.ParseMountRaw(volBind, "")
|
}
|
||||||
|
|
||||||
|
type defaultFileInfoProvider struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (defaultFileInfoProvider) fileInfo(path string) (exist, isDir bool, err error) {
|
||||||
|
fi, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", err
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
src := m.Source
|
var split []string
|
||||||
if src == "" && strings.Contains(volBind, m.Name) {
|
matchgroups := make(map[string]string)
|
||||||
src = m.Name
|
// 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 {
|
||||||
|
return "", "", "", fmt.Errorf("not <src>:<destination> format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if src == "" {
|
if len(parts) < 2 {
|
||||||
return "", "", "", errors.New("missing host path")
|
return "", "", "", fmt.Errorf("not <src>:<destination> format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.Destination == "" {
|
hostPath = parts[0]
|
||||||
return "", "", "", errors.New("container path is empty")
|
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) {
|
func parseVolumeSpecLinux(volBind string) (hostPath string, containerPath string, mode string, err error) {
|
||||||
|
|
Loading…
Reference in a new issue