Detect Windows absolute paths on non-Windows CLI

When deploying a stack using a relative path as bind-mount
source in the compose file, the CLI converts the relative
path to an absolute path, relative to the location of the
docker-compose file.

This causes a problem when deploying a stack that uses
an absolute Windows path, because a non-Windows client will
fail to detect that the path (e.g. `C:\somedir`) is an absolute
path (and not a relative directory named `C:\`).

The existing code did already take Windows clients deploying
a Linux stack into account (by checking if the path had a leading
slash). This patch adds the reverse, and adds detection for Windows
absolute paths on non-Windows clients.

The code used to detect Windows absolute paths is copied from the
Golang filepath package;
1d0e94b1e1/src/path/filepath/path_windows.go (L12-L65)

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit d6dd08d568)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2019-07-09 22:13:19 +02:00
parent 2027d17a9d
commit 87e400e44e
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
4 changed files with 212 additions and 6 deletions

View File

@ -479,12 +479,13 @@ func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string,
} }
filePath := expandUser(volume.Source, lookupEnv) filePath := expandUser(volume.Source, lookupEnv)
// Check for a Unix absolute path first, to handle a Windows client // Check if source is an absolute path (either Unix or Windows), to
// with a Unix daemon. This handles a Windows client connecting to a // handle a Windows client with a Unix daemon or vice-versa.
// Unix daemon. Note that this is not required for Docker for Windows //
// when specifying a local Windows path, because Docker for Windows // Note that this is not required for Docker for Windows when specifying
// translates the Windows path into a valid path within the VM. // a local Windows path, because Docker for Windows translates the Windows
if !path.IsAbs(filePath) { // path into a valid path within the VM.
if !path.IsAbs(filePath) && !isAbs(filePath) {
filePath = absPath(workingDir, filePath) filePath = absPath(workingDir, filePath)
} }
volume.Source = filePath volume.Source = filePath

View File

@ -978,6 +978,84 @@ services:
assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`) assert.Error(t, err, `invalid mount config for type "bind": field Source must not be empty`)
} }
func TestLoadBindMountSourceIsWindowsAbsolute(t *testing.T) {
tests := []struct {
doc string
yaml string
expected types.ServiceVolumeConfig
}{
{
doc: "Z-drive lowercase",
yaml: `
version: '3.3'
services:
windows:
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
volumes:
- type: bind
source: z:\
target: c:\data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `z:\`, Target: `c:\data`},
},
{
doc: "Z-drive uppercase",
yaml: `
version: '3.3'
services:
windows:
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
volumes:
- type: bind
source: Z:\
target: C:\data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `Z:\`, Target: `C:\data`},
},
{
doc: "Z-drive subdirectory",
yaml: `
version: '3.3'
services:
windows:
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
volumes:
- type: bind
source: Z:\some-dir
target: C:\data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `Z:\some-dir`, Target: `C:\data`},
},
{
doc: "forward-slashes",
yaml: `
version: '3.3'
services:
app:
image: app:latest
volumes:
- type: bind
source: /z/some-dir
target: /c/data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `/z/some-dir`, Target: `/c/data`},
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
config, err := loadYAML(tc.yaml)
assert.NilError(t, err)
assert.Check(t, is.Len(config.Services[0].Volumes, 1))
assert.Check(t, is.DeepEqual(tc.expected, config.Services[0].Volumes[0]))
})
}
}
func TestLoadBindMountWithSource(t *testing.T) { func TestLoadBindMountWithSource(t *testing.T) {
config, err := loadYAML(` config, err := loadYAML(`
version: "3.5" version: "3.5"

View File

@ -0,0 +1,66 @@
package loader
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// https://github.com/golang/go/blob/master/LICENSE
// This file contains utilities to check for Windows absolute paths on Linux.
// The code in this file was largely copied from the Golang filepath package
// https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/src/path/filepath/path_windows.go#L12-L65
func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}
// isAbs reports whether the path is a Windows absolute path.
func isAbs(path string) (b bool) {
l := volumeNameLen(path)
if l == 0 {
return false
}
path = path[l:]
if path == "" {
return false
}
return isSlash(path[0])
}
// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
// nolint: gocyclo
func volumeNameLen(path string) int {
if len(path) < 2 {
return 0
}
// with drive letter
c := path[0]
if path[1] == ':' && ('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
return 2
}
// is it UNC? https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
if l := len(path); l >= 5 && isSlash(path[0]) && isSlash(path[1]) &&
!isSlash(path[2]) && path[2] != '.' {
// first, leading `\\` and next shouldn't be `\`. its server name.
for n := 3; n < l-1; n++ {
// second, next '\' shouldn't be repeated.
if isSlash(path[n]) {
n++
// third, following something characters. its share name.
if !isSlash(path[n]) {
if path[n] == '.' {
break
}
for ; n < l; n++ {
if isSlash(path[n]) {
break
}
}
return n
}
break
}
}
}
return 0
}

View File

@ -0,0 +1,61 @@
package loader
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// https://github.com/golang/go/blob/master/LICENSE
// The code in this file was copied from the Golang filepath package with some
// small modifications to run it on non-Windows platforms.
// https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/src/path/filepath/path_test.go#L711-L763
import "testing"
type IsAbsTest struct {
path string
isAbs bool
}
var isabstests = []IsAbsTest{
{"", false},
{"/", true},
{"/usr/bin/gcc", true},
{"..", false},
{"/a/../bb", true},
{".", false},
{"./", false},
{"lala", false},
}
var winisabstests = []IsAbsTest{
{`C:\`, true},
{`c\`, false},
{`c::`, false},
{`c:`, false},
{`/`, false},
{`\`, false},
{`\Windows`, false},
{`c:a\b`, false},
{`c:\a\b`, true},
{`c:/a/b`, true},
{`\\host\share\foo`, true},
{`//host/share/foo/bar`, true},
}
func TestIsAbs(t *testing.T) {
tests := append(isabstests, winisabstests...)
// All non-windows tests should fail, because they have no volume letter.
for _, test := range isabstests {
tests = append(tests, IsAbsTest{test.path, false})
}
// All non-windows test should work as intended if prefixed with volume letter.
for _, test := range isabstests {
tests = append(tests, IsAbsTest{"c:" + test.path, test.isAbs})
}
for _, test := range winisabstests {
if r := isAbs(test.path); r != test.isAbs {
t.Errorf("IsAbs(%q) = %v, want %v", test.path, r, test.isAbs)
}
}
}