Skip to content

Commit

Permalink
Detect Windows absolute paths on non-Windows CLI
Browse files Browse the repository at this point in the history
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;
https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/src/path/filepath/path_windows.go#L12-L65

Signed-off-by: Sebastiaan van Stijn <[email protected]>
  • Loading branch information
thaJeztah committed Jul 9, 2019
1 parent 906eefc commit ea9fac0
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 6 deletions.
13 changes: 7 additions & 6 deletions cli/compose/loader/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,12 +479,13 @@ func resolveVolumePaths(volumes []types.ServiceVolumeConfig, workingDir string,
}

filePath := expandUser(volume.Source, lookupEnv)
// Check for a Unix absolute path first, to handle a Windows client
// with a Unix daemon. This handles a Windows client connecting to a
// Unix daemon. Note that this is not required for Docker for Windows
// when specifying a local Windows path, because Docker for Windows
// translates the Windows path into a valid path within the VM.
if !path.IsAbs(filePath) {
// Check if source is an absolute path (either Unix or Windows), to
// handle a Windows client with a Unix daemon or vice-versa.
//
// Note that this is not required for Docker for Windows when specifying
// a local Windows path, because Docker for Windows translates the Windows
// path into a valid path within the VM.
if !path.IsAbs(filePath) && !isAbs(filePath) {
filePath = absPath(workingDir, filePath)
}
volume.Source = filePath
Expand Down
78 changes: 78 additions & 0 deletions cli/compose/loader/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,84 @@ services:
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: "C-drive lowercase",
yaml: `
version: '3.3'
services:
windows:
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
volumes:
- type: bind
source: c:\
target: c:\data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `c:\`, Target: `c:\data`},
},
{
doc: "C-drive uppercase",
yaml: `
version: '3.3'
services:
windows:
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
volumes:
- type: bind
source: C:\
target: C:\data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `C:\`, Target: `C:\data`},
},
{
doc: "C-drive subdirectory",
yaml: `
version: '3.3'
services:
windows:
image: mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
volumes:
- type: bind
source: C:\some-dir
target: C:\data
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `C:\some-dir`, Target: `C:\data`},
},
{
doc: "forward-slashes",
yaml: `
version: '3.3'
services:
app:
image: app:latest
volumes:
- type: bind
source: /z/hostfolder
target: /c/containerfolder
`,
expected: types.ServiceVolumeConfig{Type: "bind", Source: `/z/hostfolder`, Target: `/c/containerfolder`},
},
}

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) {
config, err := loadYAML(`
version: "3.5"
Expand Down
69 changes: 69 additions & 0 deletions cli/compose/loader/windows_path.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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.

// This file contains utilities to check for Windows absolute paths on Linux.
// The code in this file was copied from the Golang filepath package
// https://github.com/golang/go/blob/1d0e94b1e13d5e8a323a63cd1cc1ef95290c9c36/src/path/filepath/path_windows.go#L12-L65

import "path"

func isSlash(c uint8) bool {
return c == '\\' || c == '/'
}

// isAbs reports whether the path is a Windows absolute path.
func isAbs(filePath string) (b bool) {
if path.IsAbs(filePath) {
return true
}
l := volumeNameLen(filePath)
if l == 0 {
return false
}
filePath = filePath[l:]
if filePath == "" {
return false
}
return isSlash(filePath[0])
}

// volumeNameLen returns length of the leading volume name on Windows.
// It returns 0 elsewhere.
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 comments on commit ea9fac0

Please sign in to comment.