diff --git a/compose/config/config.py b/compose/config/config.py index 870bbad9c..437ed3892 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -651,7 +651,10 @@ def finalize_service(service_config, service_names, version, environment): if 'volumes' in service_dict: service_dict['volumes'] = [ - VolumeSpec.parse(v) for v in service_dict['volumes']] + VolumeSpec.parse( + v, environment.get('COMPOSE_CONVERT_WINDOWS_PATHS') + ) for v in service_dict['volumes'] + ] if 'net' in service_dict: network_mode = service_dict.pop('net') diff --git a/compose/config/types.py b/compose/config/types.py index c450a0f98..4c106747f 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -5,6 +5,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import os +import re from collections import namedtuple import six @@ -14,6 +15,8 @@ from compose.config.errors import ConfigurationError from compose.const import IS_WINDOWS_PLATFORM from compose.utils import splitdrive +win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*') + class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')): @@ -154,7 +157,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): return cls(external, internal, mode) @classmethod - def _parse_win32(cls, volume_config): + def _parse_win32(cls, volume_config, normalize): # relative paths in windows expand to include the drive, eg C:\ # so we join the first 2 parts back together to count as one mode = 'rw' @@ -168,13 +171,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): parts = separate_next_section(volume_config) if len(parts) == 1: - internal = normalize_path_for_engine(os.path.normpath(parts[0])) + internal = parts[0] external = None else: external = parts[0] parts = separate_next_section(parts[1]) - external = normalize_path_for_engine(os.path.normpath(external)) - internal = normalize_path_for_engine(os.path.normpath(parts[0])) + external = os.path.normpath(external) + internal = parts[0] if len(parts) > 1: if ':' in parts[1]: raise ConfigurationError( @@ -183,15 +186,18 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): ) mode = parts[1] + if normalize: + external = normalize_path_for_engine(external) if external else None + return cls(external, internal, mode) @classmethod - def parse(cls, volume_config): + def parse(cls, volume_config, normalize=False): """Parse a volume_config path and split it into external:internal[:mode] parts to be returned as a valid VolumeSpec. """ if IS_WINDOWS_PLATFORM: - return cls._parse_win32(volume_config) + return cls._parse_win32(volume_config, normalize) else: return cls._parse_unix(volume_config) @@ -201,7 +207,14 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): @property def is_named_volume(self): - return self.external and not self.external.startswith(('.', '/', '~')) + res = self.external and not self.external.startswith(('.', '/', '~')) + if not IS_WINDOWS_PLATFORM: + return res + + return ( + res and not self.external.startswith('\\') and + not win32_root_path_pattern.match(self.external) + ) class ServiceLink(namedtuple('_ServiceLink', 'target alias')): diff --git a/compose/network.py b/compose/network.py index 00d68aa4f..e581a4fe6 100644 --- a/compose/network.py +++ b/compose/network.py @@ -26,6 +26,7 @@ class Network(object): self.external_name = external_name self.internal = internal self.enable_ipv6 = enable_ipv6 + self.labels = labels def ensure(self): if self.external_name: diff --git a/tests/unit/config/types_test.py b/tests/unit/config/types_test.py index 8dfa65d52..114273520 100644 --- a/tests/unit/config/types_test.py +++ b/tests/unit/config/types_test.py @@ -63,35 +63,67 @@ class TestVolumeSpec(object): VolumeSpec.parse('one:two:three:four') assert 'has incorrect format' in exc.exconly() - def test_parse_volume_windows_absolute_path(self): - windows_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro" - assert VolumeSpec._parse_win32(windows_path) == ( + def test_parse_volume_windows_absolute_path_normalized(self): + windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" + assert VolumeSpec._parse_win32(windows_path, True) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro" ) - def test_parse_volume_windows_internal_path(self): + def test_parse_volume_windows_absolute_path_native(self): + windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" + assert VolumeSpec._parse_win32(windows_path, False) == ( + "c:\\Users\\me\\Documents\\shiny\\config", + "/opt/shiny/config", + "ro" + ) + + def test_parse_volume_windows_internal_path_normalized(self): windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' - assert VolumeSpec._parse_win32(windows_path) == ( + assert VolumeSpec._parse_win32(windows_path, True) == ( '/c/Users/reimu/scarlet', - '/c/scarlet/app', + 'C:\\scarlet\\app', 'ro' ) - def test_parse_volume_windows_just_drives(self): + def test_parse_volume_windows_internal_path_native(self): + windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' + assert VolumeSpec._parse_win32(windows_path, False) == ( + 'C:\\Users\\reimu\\scarlet', + 'C:\\scarlet\\app', + 'ro' + ) + + def test_parse_volume_windows_just_drives_normalized(self): windows_path = 'E:\\:C:\\:ro' - assert VolumeSpec._parse_win32(windows_path) == ( + assert VolumeSpec._parse_win32(windows_path, True) == ( '/e/', - '/c/', + 'C:\\', 'ro' ) - def test_parse_volume_windows_mixed_notations(self): - windows_path = '/c/Foo:C:\\bar' - assert VolumeSpec._parse_win32(windows_path) == ( + def test_parse_volume_windows_just_drives_native(self): + windows_path = 'E:\\:C:\\:ro' + assert VolumeSpec._parse_win32(windows_path, False) == ( + 'E:\\', + 'C:\\', + 'ro' + ) + + def test_parse_volume_windows_mixed_notations_normalized(self): + windows_path = 'C:\\Foo:/root/foo' + assert VolumeSpec._parse_win32(windows_path, True) == ( '/c/Foo', - '/c/bar', + '/root/foo', + 'rw' + ) + + def test_parse_volume_windows_mixed_notations_native(self): + windows_path = 'C:\\Foo:/root/foo' + assert VolumeSpec._parse_win32(windows_path, False) == ( + 'C:\\Foo', + '/root/foo', 'rw' ) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index a259c476f..1d5aa10fb 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -786,7 +786,7 @@ class ServiceVolumesTest(unittest.TestCase): self.mock_client = mock.create_autospec(docker.Client) def test_build_volume_binding(self): - binding = build_volume_binding(VolumeSpec.parse('/outside:/inside')) + binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True)) assert binding == ('/inside', '/outside:/inside:rw') def test_get_container_data_volumes(self): @@ -845,10 +845,10 @@ class ServiceVolumesTest(unittest.TestCase): def test_merge_volume_bindings(self): options = [ - VolumeSpec.parse('/host/volume:/host/volume:ro'), - VolumeSpec.parse('/host/rw/volume:/host/rw/volume'), - VolumeSpec.parse('/new/volume'), - VolumeSpec.parse('/existing/volume'), + VolumeSpec.parse('/host/volume:/host/volume:ro', True), + VolumeSpec.parse('/host/rw/volume:/host/rw/volume', True), + VolumeSpec.parse('/new/volume', True), + VolumeSpec.parse('/existing/volume', True), ] self.mock_client.inspect_image.return_value = { @@ -882,8 +882,8 @@ class ServiceVolumesTest(unittest.TestCase): 'web', image='busybox', volumes=[ - VolumeSpec.parse('/host/path:/data1'), - VolumeSpec.parse('/host/path:/data2'), + VolumeSpec.parse('/host/path:/data1', True), + VolumeSpec.parse('/host/path:/data2', True), ], client=self.mock_client, ) @@ -1007,7 +1007,7 @@ class ServiceVolumesTest(unittest.TestCase): 'web', client=self.mock_client, image='busybox', - volumes=[VolumeSpec.parse(volume)], + volumes=[VolumeSpec.parse(volume, True)], ).create_container() assert self.mock_client.create_container.call_count == 1