mirror of https://github.com/docker/cli.git
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>
This commit is contained in:
parent
1161a9c417
commit
d6dd08d568
|
@ -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
|
||||||
|
|
|
@ -985,6 +985,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"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue