From f4cd5b1d45f0eee1a731af1664c75d27e9b4aa18 Mon Sep 17 00:00:00 2001 From: Mazz Mosley Date: Tue, 22 Sep 2015 16:13:42 +0100 Subject: [PATCH] Handle windows volume paths When a relative path is expanded and we're on a windows platform, it expands to include the drive, eg C:\ , which was causing a ConfigError as we split on ":" in parse_volume_spec and that was giving too many parts. Use os.path.splitdrive instead of manually calculating the drive. This should help us deal with windows drives as part of the volume path better than us doing it manually. Signed-off-by: Mazz Mosley --- compose/config/config.py | 11 ++++++----- compose/const.py | 1 + compose/service.py | 14 ++++++++++++++ tests/unit/config/config_test.py | 15 +++++++++++++++ tests/unit/service_test.py | 15 +++++++++++++++ 5 files changed, 51 insertions(+), 5 deletions(-) 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'))