diff --git a/volume/mounts/lcow_parser_test.go b/volume/mounts/lcow_parser_test.go new file mode 100644 index 0000000000..8e5e2261df --- /dev/null +++ b/volume/mounts/lcow_parser_test.go @@ -0,0 +1,171 @@ +package mounts // import "github.com/docker/docker/volume/mounts" + +import ( + "strings" + "testing" + + "github.com/docker/docker/api/types/mount" +) + +func TestLCOWParseMountRaw(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + currentFileInfoProvider = mockFiProvider{} + + valid := []string{ + `/foo`, + `/foo/`, + `/foo bar`, + `c:\:/foo`, + `c:\windows\:/foo`, + `c:\windows:/s p a c e`, + `c:\windows:/s p a c e:RW`, + `c:\program files:/s p a c e i n h o s t d i r`, + `0123456789name:/foo`, + `MiXeDcAsEnAmE:/foo`, + `name:/foo`, + `name:/foo:rW`, + `name:/foo:RW`, + `name:/foo:RO`, + `c:/:/forward/slashes/are/good/too`, + `c:/:/including with/spaces:ro`, + `/Program Files (x86)`, // With capitals and brackets + } + + invalid := map[string]string{ + ``: "invalid volume specification: ", + `.`: "invalid volume specification: ", + `c:`: "invalid volume specification: ", + `c:\`: "invalid volume specification: ", + `../`: "invalid volume specification: ", + `c:\:../`: "invalid volume specification: ", + `c:\:/foo:xyzzy`: "invalid volume specification: ", + `/`: "destination can't be '/'", + `/..`: "destination can't be '/'", + `c:\notexist:/foo`: `source path does not exist: c:\notexist`, + `c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`, + `name<:/foo`: `invalid volume specification`, + `name>:/foo`: `invalid volume specification`, + `name::/foo`: `invalid volume specification`, + `name":/foo`: `invalid volume specification`, + `name\:/foo`: `invalid volume specification`, + `name*:/foo`: `invalid volume specification`, + `name|:/foo`: `invalid volume specification`, + `name?:/foo`: `invalid volume specification`, + `name/:/foo`: `invalid volume specification`, + `/foo:rw`: `invalid volume specification`, + `/foo:ro`: `invalid volume specification`, + `con:/foo`: `cannot be a reserved word for Windows filenames`, + `PRN:/foo`: `cannot be a reserved word for Windows filenames`, + `aUx:/foo`: `cannot be a reserved word for Windows filenames`, + `nul:/foo`: `cannot be a reserved word for Windows filenames`, + `com1:/foo`: `cannot be a reserved word for Windows filenames`, + `com2:/foo`: `cannot be a reserved word for Windows filenames`, + `com3:/foo`: `cannot be a reserved word for Windows filenames`, + `com4:/foo`: `cannot be a reserved word for Windows filenames`, + `com5:/foo`: `cannot be a reserved word for Windows filenames`, + `com6:/foo`: `cannot be a reserved word for Windows filenames`, + `com7:/foo`: `cannot be a reserved word for Windows filenames`, + `com8:/foo`: `cannot be a reserved word for Windows filenames`, + `com9:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt1:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt2:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt3:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt4:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt5:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt6:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt7:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt8:/foo`: `cannot be a reserved word for Windows filenames`, + `lpt9:/foo`: `cannot be a reserved word for Windows filenames`, + `\\.\pipe\foo:/foo`: `Linux containers on Windows do not support named pipe mounts`, + } + + parser := &lcowParser{} + + for _, path := range valid { + if _, err := parser.ParseMountRaw(path, "local"); err != nil { + t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if mp, err := parser.ParseMountRaw(path, "local"); err == nil { + t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + } + } + } +} + +func TestLCOWParseMountRawSplit(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + currentFileInfoProvider = mockFiProvider{} + + cases := []struct { + bind string + driver string + expType mount.Type + expDest string + expSource string + expName string + expDriver string + expRW bool + fail bool + }{ + {`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false}, + {`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false}, + {`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false}, + {`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true}, + {`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false}, + {`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false}, + {`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false}, + {`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, + {`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, + {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true}, + {`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, + {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, + } + + parser := &lcowParser{} + for i, c := range cases { + t.Logf("case %d", i) + m, err := parser.ParseMountRaw(c.bind, c.driver) + if c.fail { + if err == nil { + t.Errorf("Expected error, was nil, for spec %s\n", c.bind) + } + continue + } + + if m == nil || err != nil { + t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err) + continue + } + + if m.Destination != c.expDest { + t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) + } + + if m.Source != c.expSource { + t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind) + } + + if m.Name != c.expName { + t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind) + } + + if m.Driver != c.expDriver { + t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind) + } + + if m.RW != c.expRW { + t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind) + } + if m.Type != c.expType { + t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind) + } + } +} diff --git a/volume/mounts/linux_parser_test.go b/volume/mounts/linux_parser_test.go index ad56a502f6..00c8cff47a 100644 --- a/volume/mounts/linux_parser_test.go +++ b/volume/mounts/linux_parser_test.go @@ -1,12 +1,206 @@ package mounts // import "github.com/docker/docker/volume/mounts" import ( + "fmt" "strings" "testing" "github.com/docker/docker/api/types/mount" + "gotest.tools/v3/assert" ) +func TestLinuxParseMountRaw(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + currentFileInfoProvider = mockFiProvider{} + + valid := []string{ + "/home", + "/home:/home", + "/home:/something/else", + "/with space", + "/home:/with space", + "relative:/absolute-path", + "hostPath:/containerPath:ro", + "/hostPath:/containerPath:rw", + "/rw:/ro", + "/hostPath:/containerPath:shared", + "/hostPath:/containerPath:rshared", + "/hostPath:/containerPath:slave", + "/hostPath:/containerPath:rslave", + "/hostPath:/containerPath:private", + "/hostPath:/containerPath:rprivate", + "/hostPath:/containerPath:ro,shared", + "/hostPath:/containerPath:ro,slave", + "/hostPath:/containerPath:ro,private", + "/hostPath:/containerPath:ro,z,shared", + "/hostPath:/containerPath:ro,Z,slave", + "/hostPath:/containerPath:Z,ro,slave", + "/hostPath:/containerPath:slave,Z,ro", + "/hostPath:/containerPath:Z,slave,ro", + "/hostPath:/containerPath:slave,ro,Z", + "/hostPath:/containerPath:rslave,ro,Z", + "/hostPath:/containerPath:ro,rshared,Z", + "/hostPath:/containerPath:ro,Z,rprivate", + } + + invalid := map[string]string{ + "": "invalid volume specification", + "./": "mount path must be absolute", + "../": "mount path must be absolute", + "/:../": "mount path must be absolute", + "/:path": "mount path must be absolute", + ":": "invalid volume specification", + "/tmp:": "invalid volume specification", + ":test": "invalid volume specification", + ":/test": "invalid volume specification", + "tmp:": "invalid volume specification", + ":test:": "invalid volume specification", + "::": "invalid volume specification", + ":::": "invalid volume specification", + "/tmp:::": "invalid volume specification", + ":/tmp::": "invalid volume specification", + "/path:rw": "invalid volume specification", + "/path:ro": "invalid volume specification", + "/rw:rw": "invalid volume specification", + "path:ro": "invalid volume specification", + "/path:/path:sw": `invalid mode`, + "/path:/path:rwz": `invalid mode`, + "/path:/path:ro,rshared,rslave": `invalid mode`, + "/path:/path:ro,z,rshared,rslave": `invalid mode`, + "/path:shared": "invalid volume specification", + "/path:slave": "invalid volume specification", + "/path:private": "invalid volume specification", + "name:/absolute-path:shared": "invalid volume specification", + "name:/absolute-path:rshared": "invalid volume specification", + "name:/absolute-path:slave": "invalid volume specification", + "name:/absolute-path:rslave": "invalid volume specification", + "name:/absolute-path:private": "invalid volume specification", + "name:/absolute-path:rprivate": "invalid volume specification", + } + + parser := &linuxParser{} + + for _, path := range valid { + if _, err := parser.ParseMountRaw(path, "local"); err != nil { + t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if mp, err := parser.ParseMountRaw(path, "local"); err == nil { + t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + } + } + } +} + +func TestLinuxParseMountRawSplit(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + currentFileInfoProvider = mockFiProvider{} + + cases := []struct { + bind string + driver string + expType mount.Type + expDest string + expSource string + expName string + expDriver string + expRW bool + fail bool + }{ + {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false}, + {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false}, + {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false}, + {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true}, + {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false}, + {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false}, + {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false}, + {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false}, + {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true}, + } + + parser := &linuxParser{} + for i, c := range cases { + t.Logf("case %d", i) + m, err := parser.ParseMountRaw(c.bind, c.driver) + if c.fail { + if err == nil { + t.Errorf("Expected error, was nil, for spec %s\n", c.bind) + } + continue + } + + if m == nil || err != nil { + t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err) + continue + } + + if m.Destination != c.expDest { + t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) + } + + if m.Source != c.expSource { + t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind) + } + + if m.Name != c.expName { + t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind) + } + + if m.Driver != c.expDriver { + t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind) + } + + if m.RW != c.expRW { + t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind) + } + if m.Type != c.expType { + t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind) + } + } +} + +// TestLinuxParseMountSpecBindWithFileinfoError makes sure that the parser returns +// the error produced by the fileinfo provider. +// +// Some extra context for the future in case of changes and possible wtf are we +// testing this for: +// +// Currently this "fileInfoProvider" returns (bool, bool, error) +// The 1st bool is "does this path exist" +// The 2nd bool is "is this path a dir" +// Then of course the error is an error. +// +// The issue is the parser was ignoring the error and only looking at the +// "does this path exist" boolean, which is always false if there is an error. +// Then the error returned to the caller was a (slightly, maybe) friendlier +// error string than what comes from `os.Stat` +// So ...the caller was always getting an error saying the path doesn't exist +// even if it does exist but got some other error (like a permission error). +// This is confusing to users. +func TestLinuxParseMountSpecBindWithFileinfoError(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + + testErr := fmt.Errorf("some crazy error") + currentFileInfoProvider = &mockFiProviderWithError{err: testErr} + + parser := &linuxParser{} + + _, err := parser.ParseMountSpec(mount.Mount{ + Type: mount.TypeBind, + Source: `/bananas`, + Target: `/bananas`, + }) + assert.ErrorContains(t, err, testErr.Error()) +} + func TestConvertTmpfsOptions(t *testing.T) { type testCase struct { opt mount.TmpfsOptions diff --git a/volume/mounts/parser_test.go b/volume/mounts/parser_test.go index 029da80857..1e0117dae6 100644 --- a/volume/mounts/parser_test.go +++ b/volume/mounts/parser_test.go @@ -1,22 +1,13 @@ package mounts // import "github.com/docker/docker/volume/mounts" import ( - "errors" "io/ioutil" "os" - "runtime" - "strings" "testing" "github.com/docker/docker/api/types/mount" - "gotest.tools/v3/assert" ) -type parseMountRawTestSet struct { - valid []string - invalid map[string]string -} - type mockFiProvider struct{} func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) { @@ -41,363 +32,25 @@ func (mockFiProvider) fileInfo(path string) (exists, isDir bool, err error) { return false, false, nil } -func TestParseMountRaw(t *testing.T) { +// always returns the configured error +// this is used to test error handling +type mockFiProviderWithError struct{ err error } - previousProvider := currentFileInfoProvider - defer func() { currentFileInfoProvider = previousProvider }() - currentFileInfoProvider = mockFiProvider{} - windowsSet := parseMountRawTestSet{ - valid: []string{ - `d:\`, - `d:`, - `d:\path`, - `d:\path with space`, - `c:\:d:\`, - `c:\windows\:d:`, - `c:\windows:d:\s p a c e`, - `c:\windows:d:\s p a c e:RW`, - `c:\program files:d:\s p a c e i n h o s t d i r`, - `0123456789name:d:`, - `MiXeDcAsEnAmE:d:`, - `name:D:`, - `name:D::rW`, - `name:D::RW`, - `name:D::RO`, - `c:/:d:/forward/slashes/are/good/too`, - `c:/:d:/including with/spaces:ro`, - `c:\Windows`, // With capital - `c:\Program Files (x86)`, // With capitals and brackets - `\\?\c:\windows\:d:`, // Long path handling (source) - `c:\windows\:\\?\d:\`, // Long path handling (target) - `\\.\pipe\foo:\\.\pipe\foo`, // named pipe - `//./pipe/foo://./pipe/foo`, // named pipe forward slashes - }, - invalid: map[string]string{ - ``: "invalid volume specification: ", - `.`: "invalid volume specification: ", - `..\`: "invalid volume specification: ", - `c:\:..\`: "invalid volume specification: ", - `c:\:d:\:xyzzy`: "invalid volume specification: ", - `c:`: "cannot be `c:`", - `c:\`: "cannot be `c:`", - `c:\notexist:d:`: `source path does not exist: c:\notexist`, - `c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`, - `name<:d:`: `invalid volume specification`, - `name>:d:`: `invalid volume specification`, - `name::d:`: `invalid volume specification`, - `name":d:`: `invalid volume specification`, - `name\:d:`: `invalid volume specification`, - `name*:d:`: `invalid volume specification`, - `name|:d:`: `invalid volume specification`, - `name?:d:`: `invalid volume specification`, - `name/:d:`: `invalid volume specification`, - `d:\pathandmode:rw`: `invalid volume specification`, - `d:\pathandmode:ro`: `invalid volume specification`, - `con:d:`: `cannot be a reserved word for Windows filenames`, - `PRN:d:`: `cannot be a reserved word for Windows filenames`, - `aUx:d:`: `cannot be a reserved word for Windows filenames`, - `nul:d:`: `cannot be a reserved word for Windows filenames`, - `com1:d:`: `cannot be a reserved word for Windows filenames`, - `com2:d:`: `cannot be a reserved word for Windows filenames`, - `com3:d:`: `cannot be a reserved word for Windows filenames`, - `com4:d:`: `cannot be a reserved word for Windows filenames`, - `com5:d:`: `cannot be a reserved word for Windows filenames`, - `com6:d:`: `cannot be a reserved word for Windows filenames`, - `com7:d:`: `cannot be a reserved word for Windows filenames`, - `com8:d:`: `cannot be a reserved word for Windows filenames`, - `com9:d:`: `cannot be a reserved word for Windows filenames`, - `lpt1:d:`: `cannot be a reserved word for Windows filenames`, - `lpt2:d:`: `cannot be a reserved word for Windows filenames`, - `lpt3:d:`: `cannot be a reserved word for Windows filenames`, - `lpt4:d:`: `cannot be a reserved word for Windows filenames`, - `lpt5:d:`: `cannot be a reserved word for Windows filenames`, - `lpt6:d:`: `cannot be a reserved word for Windows filenames`, - `lpt7:d:`: `cannot be a reserved word for Windows filenames`, - `lpt8:d:`: `cannot be a reserved word for Windows filenames`, - `lpt9:d:`: `cannot be a reserved word for Windows filenames`, - `c:\windows\system32\ntdll.dll`: `Only directories can be mapped on this platform`, - `\\.\pipe\foo:c:\pipe`: `'c:\pipe' is not a valid pipe path`, - }, - } - lcowSet := parseMountRawTestSet{ - valid: []string{ - `/foo`, - `/foo/`, - `/foo bar`, - `c:\:/foo`, - `c:\windows\:/foo`, - `c:\windows:/s p a c e`, - `c:\windows:/s p a c e:RW`, - `c:\program files:/s p a c e i n h o s t d i r`, - `0123456789name:/foo`, - `MiXeDcAsEnAmE:/foo`, - `name:/foo`, - `name:/foo:rW`, - `name:/foo:RW`, - `name:/foo:RO`, - `c:/:/forward/slashes/are/good/too`, - `c:/:/including with/spaces:ro`, - `/Program Files (x86)`, // With capitals and brackets - }, - invalid: map[string]string{ - ``: "invalid volume specification: ", - `.`: "invalid volume specification: ", - `c:`: "invalid volume specification: ", - `c:\`: "invalid volume specification: ", - `../`: "invalid volume specification: ", - `c:\:../`: "invalid volume specification: ", - `c:\:/foo:xyzzy`: "invalid volume specification: ", - `/`: "destination can't be '/'", - `/..`: "destination can't be '/'", - `c:\notexist:/foo`: `source path does not exist: c:\notexist`, - `c:\windows\system32\ntdll.dll:/foo`: `source path must be a directory`, - `name<:/foo`: `invalid volume specification`, - `name>:/foo`: `invalid volume specification`, - `name::/foo`: `invalid volume specification`, - `name":/foo`: `invalid volume specification`, - `name\:/foo`: `invalid volume specification`, - `name*:/foo`: `invalid volume specification`, - `name|:/foo`: `invalid volume specification`, - `name?:/foo`: `invalid volume specification`, - `name/:/foo`: `invalid volume specification`, - `/foo:rw`: `invalid volume specification`, - `/foo:ro`: `invalid volume specification`, - `con:/foo`: `cannot be a reserved word for Windows filenames`, - `PRN:/foo`: `cannot be a reserved word for Windows filenames`, - `aUx:/foo`: `cannot be a reserved word for Windows filenames`, - `nul:/foo`: `cannot be a reserved word for Windows filenames`, - `com1:/foo`: `cannot be a reserved word for Windows filenames`, - `com2:/foo`: `cannot be a reserved word for Windows filenames`, - `com3:/foo`: `cannot be a reserved word for Windows filenames`, - `com4:/foo`: `cannot be a reserved word for Windows filenames`, - `com5:/foo`: `cannot be a reserved word for Windows filenames`, - `com6:/foo`: `cannot be a reserved word for Windows filenames`, - `com7:/foo`: `cannot be a reserved word for Windows filenames`, - `com8:/foo`: `cannot be a reserved word for Windows filenames`, - `com9:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt1:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt2:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt3:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt4:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt5:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt6:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt7:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt8:/foo`: `cannot be a reserved word for Windows filenames`, - `lpt9:/foo`: `cannot be a reserved word for Windows filenames`, - `\\.\pipe\foo:/foo`: `Linux containers on Windows do not support named pipe mounts`, - }, - } - linuxSet := parseMountRawTestSet{ - valid: []string{ - "/home", - "/home:/home", - "/home:/something/else", - "/with space", - "/home:/with space", - "relative:/absolute-path", - "hostPath:/containerPath:ro", - "/hostPath:/containerPath:rw", - "/rw:/ro", - "/hostPath:/containerPath:shared", - "/hostPath:/containerPath:rshared", - "/hostPath:/containerPath:slave", - "/hostPath:/containerPath:rslave", - "/hostPath:/containerPath:private", - "/hostPath:/containerPath:rprivate", - "/hostPath:/containerPath:ro,shared", - "/hostPath:/containerPath:ro,slave", - "/hostPath:/containerPath:ro,private", - "/hostPath:/containerPath:ro,z,shared", - "/hostPath:/containerPath:ro,Z,slave", - "/hostPath:/containerPath:Z,ro,slave", - "/hostPath:/containerPath:slave,Z,ro", - "/hostPath:/containerPath:Z,slave,ro", - "/hostPath:/containerPath:slave,ro,Z", - "/hostPath:/containerPath:rslave,ro,Z", - "/hostPath:/containerPath:ro,rshared,Z", - "/hostPath:/containerPath:ro,Z,rprivate", - }, - invalid: map[string]string{ - "": "invalid volume specification", - "./": "mount path must be absolute", - "../": "mount path must be absolute", - "/:../": "mount path must be absolute", - "/:path": "mount path must be absolute", - ":": "invalid volume specification", - "/tmp:": "invalid volume specification", - ":test": "invalid volume specification", - ":/test": "invalid volume specification", - "tmp:": "invalid volume specification", - ":test:": "invalid volume specification", - "::": "invalid volume specification", - ":::": "invalid volume specification", - "/tmp:::": "invalid volume specification", - ":/tmp::": "invalid volume specification", - "/path:rw": "invalid volume specification", - "/path:ro": "invalid volume specification", - "/rw:rw": "invalid volume specification", - "path:ro": "invalid volume specification", - "/path:/path:sw": `invalid mode`, - "/path:/path:rwz": `invalid mode`, - "/path:/path:ro,rshared,rslave": `invalid mode`, - "/path:/path:ro,z,rshared,rslave": `invalid mode`, - "/path:shared": "invalid volume specification", - "/path:slave": "invalid volume specification", - "/path:private": "invalid volume specification", - "name:/absolute-path:shared": "invalid volume specification", - "name:/absolute-path:rshared": "invalid volume specification", - "name:/absolute-path:slave": "invalid volume specification", - "name:/absolute-path:rslave": "invalid volume specification", - "name:/absolute-path:private": "invalid volume specification", - "name:/absolute-path:rprivate": "invalid volume specification", - }, - } - - linParser := &linuxParser{} - winParser := &windowsParser{} - lcowParser := &lcowParser{} - tester := func(parser Parser, set parseMountRawTestSet) { - for _, path := range set.valid { - if _, err := parser.ParseMountRaw(path, "local"); err != nil { - t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err) - } - } - - for path, expectedError := range set.invalid { - if mp, err := parser.ParseMountRaw(path, "local"); err == nil { - t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) - } else { - if !strings.Contains(err.Error(), expectedError) { - t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) - } - } - } - } - - tester(linParser, linuxSet) - tester(winParser, windowsSet) - tester(lcowParser, lcowSet) -} - -// testParseMountRaw is a structure used by TestParseMountRawSplit for -// specifying test cases for the ParseMountRaw() function. -type testParseMountRaw struct { - bind string - driver string - expType mount.Type - expDest string - expSource string - expName string - expDriver string - expRW bool - fail bool -} - -func TestParseMountRawSplit(t *testing.T) { - previousProvider := currentFileInfoProvider - defer func() { currentFileInfoProvider = previousProvider }() - currentFileInfoProvider = mockFiProvider{} - windowsCases := []testParseMountRaw{ - {`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false}, - {`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, - {`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false}, - {`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, - {`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true}, - {`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, - {`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, - {`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false}, - {`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, - {`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, - {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false}, - {`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, - {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, - } - lcowCases := []testParseMountRaw{ - {`c:\:/foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false}, - {`c:\:/foo:ro`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, false}, - {`c:\:/foo:rw`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", true, false}, - {`c:\:/foo:foo`, "local", mount.TypeBind, `/foo`, `c:\`, ``, "", false, true}, - {`name:/foo:rw`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false}, - {`name:/foo`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", true, false}, - {`name:/foo:ro`, "local", mount.TypeVolume, `/foo`, ``, `name`, "local", false, false}, - {`name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, - {`driver/name:/`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, - {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, true}, - {`\\.\pipe\foo:/data`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, - {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, - } - linuxCases := []testParseMountRaw{ - {"/tmp:/tmp1", "", mount.TypeBind, "/tmp1", "/tmp", "", "", true, false}, - {"/tmp:/tmp2:ro", "", mount.TypeBind, "/tmp2", "/tmp", "", "", false, false}, - {"/tmp:/tmp3:rw", "", mount.TypeBind, "/tmp3", "/tmp", "", "", true, false}, - {"/tmp:/tmp4:foo", "", mount.TypeBind, "", "", "", "", false, true}, - {"name:/named1", "", mount.TypeVolume, "/named1", "", "name", "", true, false}, - {"name:/named2", "external", mount.TypeVolume, "/named2", "", "name", "external", true, false}, - {"name:/named3:ro", "local", mount.TypeVolume, "/named3", "", "name", "local", false, false}, - {"local/name:/tmp:rw", "", mount.TypeVolume, "/tmp", "", "local/name", "", true, false}, - {"/tmp:tmp", "", mount.TypeBind, "", "", "", "", true, true}, - } - linParser := &linuxParser{} - winParser := &windowsParser{} - lcowParser := &lcowParser{} - tester := func(parser Parser, cases []testParseMountRaw) { - for i, c := range cases { - t.Logf("case %d", i) - m, err := parser.ParseMountRaw(c.bind, c.driver) - if c.fail { - if err == nil { - t.Errorf("Expected error, was nil, for spec %s\n", c.bind) - } - continue - } - - if m == nil || err != nil { - t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err) - continue - } - - if m.Destination != c.expDest { - t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) - } - - if m.Source != c.expSource { - t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind) - } - - if m.Name != c.expName { - t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind) - } - - if m.Driver != c.expDriver { - t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind) - } - - if m.RW != c.expRW { - t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind) - } - if m.Type != c.expType { - t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind) - } - } - } - - tester(linParser, linuxCases) - tester(winParser, windowsCases) - tester(lcowParser, lcowCases) +func (m mockFiProviderWithError) fileInfo(path string) (bool, bool, error) { + return false, false, m.err } func TestParseMountSpec(t *testing.T) { - type c struct { - input mount.Mount - expected MountPoint - } testDir, err := ioutil.TempDir("", "test-mount-config") if err != nil { t.Fatal(err) } defer os.RemoveAll(testDir) parser := NewParser() - cases := []c{ + cases := []struct { + input mount.Mount + expected MountPoint + }{ {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}}, {mount.Mount{Type: mount.TypeBind, Source: testDir, Target: testDestinationPath}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, RW: true, Propagation: parser.DefaultPropagationMode()}}, {mount.Mount{Type: mount.TypeBind, Source: testDir + string(os.PathSeparator), Target: testDestinationPath, ReadOnly: true}, MountPoint{Type: mount.TypeBind, Source: testDir, Destination: testDestinationPath, Propagation: parser.DefaultPropagationMode()}}, @@ -437,48 +90,3 @@ func TestParseMountSpec(t *testing.T) { } } - -// always returns the configured error -// this is used to test error handling -type mockFiProviderWithError struct{ err error } - -func (m mockFiProviderWithError) fileInfo(path string) (bool, bool, error) { - return false, false, m.err -} - -// TestParseMountSpecBindWithFileinfoError makes sure that the parser returns -// the error produced by the fileinfo provider. -// -// Some extra context for the future in case of changes and possible wtf are we -// testing this for: -// -// Currently this "fileInfoProvider" returns (bool, bool, error) -// The 1st bool is "does this path exist" -// The 2nd bool is "is this path a dir" -// Then of course the error is an error. -// -// The issue is the parser was ignoring the error and only looking at the -// "does this path exist" boolean, which is always false if there is an error. -// Then the error returned to the caller was a (slightly, maybe) friendlier -// error string than what comes from `os.Stat` -// So ...the caller was always getting an error saying the path doesn't exist -// even if it does exist but got some other error (like a permission error). -// This is confusing to users. -func TestParseMountSpecBindWithFileinfoError(t *testing.T) { - previousProvider := currentFileInfoProvider - defer func() { currentFileInfoProvider = previousProvider }() - - testErr := errors.New("some crazy error") - currentFileInfoProvider = &mockFiProviderWithError{err: testErr} - - p := "/bananas" - if runtime.GOOS == "windows" { - p = `c:\bananas` - } - m := mount.Mount{Type: mount.TypeBind, Source: p, Target: p} - - parser := NewParser() - - _, err := parser.ParseMountSpec(m) - assert.ErrorContains(t, err, "some crazy error") -} diff --git a/volume/mounts/windows_parser_test.go b/volume/mounts/windows_parser_test.go new file mode 100644 index 0000000000..82fce8d0f9 --- /dev/null +++ b/volume/mounts/windows_parser_test.go @@ -0,0 +1,214 @@ +package mounts // import "github.com/docker/docker/volume/mounts" + +import ( + "fmt" + "strings" + "testing" + + "github.com/docker/docker/api/types/mount" + "gotest.tools/v3/assert" +) + +func TestWindowsParseMountRaw(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + currentFileInfoProvider = mockFiProvider{} + + valid := []string{ + `d:\`, + `d:`, + `d:\path`, + `d:\path with space`, + `c:\:d:\`, + `c:\windows\:d:`, + `c:\windows:d:\s p a c e`, + `c:\windows:d:\s p a c e:RW`, + `c:\program files:d:\s p a c e i n h o s t d i r`, + `0123456789name:d:`, + `MiXeDcAsEnAmE:d:`, + `name:D:`, + `name:D::rW`, + `name:D::RW`, + `name:D::RO`, + `c:/:d:/forward/slashes/are/good/too`, + `c:/:d:/including with/spaces:ro`, + `c:\Windows`, // With capital + `c:\Program Files (x86)`, // With capitals and brackets + `\\?\c:\windows\:d:`, // Long path handling (source) + `c:\windows\:\\?\d:\`, // Long path handling (target) + `\\.\pipe\foo:\\.\pipe\foo`, // named pipe + `//./pipe/foo://./pipe/foo`, // named pipe forward slashes + } + + invalid := map[string]string{ + ``: "invalid volume specification: ", + `.`: "invalid volume specification: ", + `..\`: "invalid volume specification: ", + `c:\:..\`: "invalid volume specification: ", + `c:\:d:\:xyzzy`: "invalid volume specification: ", + `c:`: "cannot be `c:`", + `c:\`: "cannot be `c:`", + `c:\notexist:d:`: `source path does not exist: c:\notexist`, + `c:\windows\system32\ntdll.dll:d:`: `source path must be a directory`, + `name<:d:`: `invalid volume specification`, + `name>:d:`: `invalid volume specification`, + `name::d:`: `invalid volume specification`, + `name":d:`: `invalid volume specification`, + `name\:d:`: `invalid volume specification`, + `name*:d:`: `invalid volume specification`, + `name|:d:`: `invalid volume specification`, + `name?:d:`: `invalid volume specification`, + `name/:d:`: `invalid volume specification`, + `d:\pathandmode:rw`: `invalid volume specification`, + `d:\pathandmode:ro`: `invalid volume specification`, + `con:d:`: `cannot be a reserved word for Windows filenames`, + `PRN:d:`: `cannot be a reserved word for Windows filenames`, + `aUx:d:`: `cannot be a reserved word for Windows filenames`, + `nul:d:`: `cannot be a reserved word for Windows filenames`, + `com1:d:`: `cannot be a reserved word for Windows filenames`, + `com2:d:`: `cannot be a reserved word for Windows filenames`, + `com3:d:`: `cannot be a reserved word for Windows filenames`, + `com4:d:`: `cannot be a reserved word for Windows filenames`, + `com5:d:`: `cannot be a reserved word for Windows filenames`, + `com6:d:`: `cannot be a reserved word for Windows filenames`, + `com7:d:`: `cannot be a reserved word for Windows filenames`, + `com8:d:`: `cannot be a reserved word for Windows filenames`, + `com9:d:`: `cannot be a reserved word for Windows filenames`, + `lpt1:d:`: `cannot be a reserved word for Windows filenames`, + `lpt2:d:`: `cannot be a reserved word for Windows filenames`, + `lpt3:d:`: `cannot be a reserved word for Windows filenames`, + `lpt4:d:`: `cannot be a reserved word for Windows filenames`, + `lpt5:d:`: `cannot be a reserved word for Windows filenames`, + `lpt6:d:`: `cannot be a reserved word for Windows filenames`, + `lpt7:d:`: `cannot be a reserved word for Windows filenames`, + `lpt8:d:`: `cannot be a reserved word for Windows filenames`, + `lpt9:d:`: `cannot be a reserved word for Windows filenames`, + `c:\windows\system32\ntdll.dll`: `Only directories can be mapped on this platform`, + `\\.\pipe\foo:c:\pipe`: `'c:\pipe' is not a valid pipe path`, + } + + parser := &windowsParser{} + + for _, path := range valid { + if _, err := parser.ParseMountRaw(path, "local"); err != nil { + t.Errorf("ParseMountRaw(`%q`) should succeed: error %q", path, err) + } + } + + for path, expectedError := range invalid { + if mp, err := parser.ParseMountRaw(path, "local"); err == nil { + t.Errorf("ParseMountRaw(`%q`) should have failed validation. Err '%v' - MP: %v", path, err, mp) + } else { + if !strings.Contains(err.Error(), expectedError) { + t.Errorf("ParseMountRaw(`%q`) error should contain %q, got %v", path, expectedError, err.Error()) + } + } + } +} + +func TestWindowsParseMountRawSplit(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + currentFileInfoProvider = mockFiProvider{} + + cases := []struct { + bind string + driver string + expType mount.Type + expDest string + expSource string + expName string + expDriver string + expRW bool + fail bool + }{ + {`c:\:d:`, "local", mount.TypeBind, `d:`, `c:\`, ``, "", true, false}, + {`c:\:d:\`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, + {`c:\:d:\:ro`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, false}, + {`c:\:d:\:rw`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", true, false}, + {`c:\:d:\:foo`, "local", mount.TypeBind, `d:\`, `c:\`, ``, "", false, true}, + {`name:d::rw`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, + {`name:d:`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", true, false}, + {`name:d::ro`, "local", mount.TypeVolume, `d:`, ``, `name`, "local", false, false}, + {`name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, + {`driver/name:c:`, "", mount.TypeVolume, ``, ``, ``, "", true, true}, + {`\\.\pipe\foo:\\.\pipe\bar`, "local", mount.TypeNamedPipe, `\\.\pipe\bar`, `\\.\pipe\foo`, "", "", true, false}, + {`\\.\pipe\foo:c:\foo\bar`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, + {`c:\foo\bar:\\.\pipe\foo`, "local", mount.TypeNamedPipe, ``, ``, "", "", true, true}, + } + + parser := &windowsParser{} + for i, c := range cases { + t.Logf("case %d", i) + m, err := parser.ParseMountRaw(c.bind, c.driver) + if c.fail { + if err == nil { + t.Errorf("Expected error, was nil, for spec %s\n", c.bind) + } + continue + } + + if m == nil || err != nil { + t.Errorf("ParseMountRaw failed for spec '%s', driver '%s', error '%v'", c.bind, c.driver, err) + continue + } + + if m.Destination != c.expDest { + t.Errorf("Expected destination '%s, was %s', for spec '%s'", c.expDest, m.Destination, c.bind) + } + + if m.Source != c.expSource { + t.Errorf("Expected source '%s', was '%s', for spec '%s'", c.expSource, m.Source, c.bind) + } + + if m.Name != c.expName { + t.Errorf("Expected name '%s', was '%s' for spec '%s'", c.expName, m.Name, c.bind) + } + + if m.Driver != c.expDriver { + t.Errorf("Expected driver '%s', was '%s', for spec '%s'", c.expDriver, m.Driver, c.bind) + } + + if m.RW != c.expRW { + t.Errorf("Expected RW '%v', was '%v' for spec '%s'", c.expRW, m.RW, c.bind) + } + if m.Type != c.expType { + t.Fatalf("Expected type '%s', was '%s', for spec '%s'", c.expType, m.Type, c.bind) + } + } +} + +// TestWindowsParseMountSpecBindWithFileinfoError makes sure that the parser returns +// the error produced by the fileinfo provider. +// +// Some extra context for the future in case of changes and possible wtf are we +// testing this for: +// +// Currently this "fileInfoProvider" returns (bool, bool, error) +// The 1st bool is "does this path exist" +// The 2nd bool is "is this path a dir" +// Then of course the error is an error. +// +// The issue is the parser was ignoring the error and only looking at the +// "does this path exist" boolean, which is always false if there is an error. +// Then the error returned to the caller was a (slightly, maybe) friendlier +// error string than what comes from `os.Stat` +// So ...the caller was always getting an error saying the path doesn't exist +// even if it does exist but got some other error (like a permission error). +// This is confusing to users. +func TestWindowsParseMountSpecBindWithFileinfoError(t *testing.T) { + previousProvider := currentFileInfoProvider + defer func() { currentFileInfoProvider = previousProvider }() + + testErr := fmt.Errorf("some crazy error") + currentFileInfoProvider = &mockFiProviderWithError{err: testErr} + + parser := &windowsParser{} + + _, err := parser.ParseMountSpec(mount.Mount{ + Type: mount.TypeBind, + Source: `c:\bananas`, + Target: `c:\bananas`, + }) + assert.ErrorContains(t, err, testErr.Error()) +}