diff --git a/compose/config/config.py b/compose/config/config.py index 0444ba3a..9e9cb857 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -526,12 +526,13 @@ def path_mappings_from_dict(d): return [join_path_mapping(v) for v in d.items()] -def split_path_mapping(string): - if ':' in string: - (host, container) = string.split(':', 1) - return (container, host) +def split_path_mapping(volume_path): + drive, volume_config = os.path.splitdrive(volume_path) + if ':' in volume_config: + (host, container) = volume_config.split(':', 1) + return (container, drive + host) else: - return (string, None) + return (volume_path, None) def join_path_mapping(pair): diff --git a/compose/const.py b/compose/const.py index b43e655b..b04b7e7e 100644 --- a/compose/const.py +++ b/compose/const.py @@ -2,6 +2,7 @@ import os import sys DEFAULT_TIMEOUT = 10 +IS_WINDOWS_PLATFORM = (sys.platform == "win32") LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_ONE_OFF = 'com.docker.compose.oneoff' LABEL_PROJECT = 'com.docker.compose.project' diff --git a/compose/service.py b/compose/service.py index 960d3936..4df10fbb 100644 --- a/compose/service.py +++ b/compose/service.py @@ -20,6 +20,7 @@ from .config import DOCKER_CONFIG_KEYS from .config import merge_environment from .config.validation import VALID_NAME_CHARS from .const import DEFAULT_TIMEOUT +from .const import IS_WINDOWS_PLATFORM from .const import LABEL_CONFIG_HASH from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_ONE_OFF @@ -937,7 +938,20 @@ def build_volume_binding(volume_spec): def parse_volume_spec(volume_config): + """ + A volume_config string, which is a path, split it into external:internal[:mode] + parts to be returned as a valid VolumeSpec tuple. + """ parts = volume_config.split(':') + + if IS_WINDOWS_PLATFORM: + # relative paths in windows expand to include the drive, eg C:\ + # so we join the first 2 parts back together to count as one + drive, volume_path = os.path.splitdrive(volume_config) + windows_parts = volume_path.split(":") + windows_parts[0] = os.path.join(drive, windows_parts[0]) + parts = windows_parts + if len(parts) > 3: raise ConfigError("Volume %s has incorrect format, should be " "external:internal[:mode]" % volume_config) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 3269cdff..cf299738 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1124,6 +1124,21 @@ class ExpandPathTest(unittest.TestCase): self.assertEqual(result, user_path + 'otherdir/somefile') +class VolumePathTest(unittest.TestCase): + + @pytest.mark.xfail((not IS_WINDOWS_PLATFORM), reason='does not have a drive') + def test_split_path_mapping_with_windows_path(self): + windows_volume_path = "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config:/opt/connect/config:ro" + expected_mapping = ( + "/opt/connect/config:ro", + "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config" + ) + + mapping = config.split_path_mapping(windows_volume_path) + + self.assertEqual(mapping, expected_mapping) + + @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') class BuildPathTest(unittest.TestCase): def setUp(self): diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index a1c195ac..b0cee1ee 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -466,6 +466,21 @@ class ServiceVolumesTest(unittest.TestCase): with self.assertRaises(ConfigError): parse_volume_spec('one:two:three:four') + @pytest.mark.xfail((not IS_WINDOWS_PLATFORM), reason='does not have a drive') + def test_parse_volume_windows_relative_path(self): + windows_relative_path = "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config:\\opt\\connect\\config:ro" + + spec = parse_volume_spec(windows_relative_path) + + self.assertEqual( + spec, + ( + "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config", + "\\opt\\connect\\config", + "ro" + ) + ) + def test_build_volume_binding(self): binding = build_volume_binding(parse_volume_spec('/outside:/inside')) self.assertEqual(binding, ('/inside', '/outside:/inside:rw'))