Merge pull request #4026 from shin-/3923-windows-volumes

Do not normalize volume paths on Windows by default
This commit is contained in:
Joffrey F 2016-10-18 14:41:52 -07:00 committed by GitHub
commit 932b3cc10a
5 changed files with 78 additions and 29 deletions

View File

@ -651,7 +651,10 @@ def finalize_service(service_config, service_names, version, environment):
if 'volumes' in service_dict: if 'volumes' in service_dict:
service_dict['volumes'] = [ 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: if 'net' in service_dict:
network_mode = service_dict.pop('net') network_mode = service_dict.pop('net')

View File

@ -5,6 +5,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import re
from collections import namedtuple from collections import namedtuple
import six import six
@ -14,6 +15,8 @@ from compose.config.errors import ConfigurationError
from compose.const import IS_WINDOWS_PLATFORM from compose.const import IS_WINDOWS_PLATFORM
from compose.utils import splitdrive from compose.utils import splitdrive
win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*')
class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')): class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
@ -154,7 +157,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
return cls(external, internal, mode) return cls(external, internal, mode)
@classmethod @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:\ # relative paths in windows expand to include the drive, eg C:\
# so we join the first 2 parts back together to count as one # so we join the first 2 parts back together to count as one
mode = 'rw' mode = 'rw'
@ -168,13 +171,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
parts = separate_next_section(volume_config) parts = separate_next_section(volume_config)
if len(parts) == 1: if len(parts) == 1:
internal = normalize_path_for_engine(os.path.normpath(parts[0])) internal = parts[0]
external = None external = None
else: else:
external = parts[0] external = parts[0]
parts = separate_next_section(parts[1]) parts = separate_next_section(parts[1])
external = normalize_path_for_engine(os.path.normpath(external)) external = os.path.normpath(external)
internal = normalize_path_for_engine(os.path.normpath(parts[0])) internal = parts[0]
if len(parts) > 1: if len(parts) > 1:
if ':' in parts[1]: if ':' in parts[1]:
raise ConfigurationError( raise ConfigurationError(
@ -183,15 +186,18 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
) )
mode = parts[1] mode = parts[1]
if normalize:
external = normalize_path_for_engine(external) if external else None
return cls(external, internal, mode) return cls(external, internal, mode)
@classmethod @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] """Parse a volume_config path and split it into external:internal[:mode]
parts to be returned as a valid VolumeSpec. parts to be returned as a valid VolumeSpec.
""" """
if IS_WINDOWS_PLATFORM: if IS_WINDOWS_PLATFORM:
return cls._parse_win32(volume_config) return cls._parse_win32(volume_config, normalize)
else: else:
return cls._parse_unix(volume_config) return cls._parse_unix(volume_config)
@ -201,7 +207,14 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
@property @property
def is_named_volume(self): 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')): class ServiceLink(namedtuple('_ServiceLink', 'target alias')):

View File

@ -26,6 +26,7 @@ class Network(object):
self.external_name = external_name self.external_name = external_name
self.internal = internal self.internal = internal
self.enable_ipv6 = enable_ipv6 self.enable_ipv6 = enable_ipv6
self.labels = labels
def ensure(self): def ensure(self):
if self.external_name: if self.external_name:

View File

@ -63,35 +63,67 @@ class TestVolumeSpec(object):
VolumeSpec.parse('one:two:three:four') VolumeSpec.parse('one:two:three:four')
assert 'has incorrect format' in exc.exconly() assert 'has incorrect format' in exc.exconly()
def test_parse_volume_windows_absolute_path(self): def test_parse_volume_windows_absolute_path_normalized(self):
windows_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro" windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro"
assert VolumeSpec._parse_win32(windows_path) == ( assert VolumeSpec._parse_win32(windows_path, True) == (
"/c/Users/me/Documents/shiny/config", "/c/Users/me/Documents/shiny/config",
"/opt/shiny/config", "/opt/shiny/config",
"ro" "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' 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/Users/reimu/scarlet',
'/c/scarlet/app', 'C:\\scarlet\\app',
'ro' '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' windows_path = 'E:\\:C:\\:ro'
assert VolumeSpec._parse_win32(windows_path) == ( assert VolumeSpec._parse_win32(windows_path, True) == (
'/e/', '/e/',
'/c/', 'C:\\',
'ro' 'ro'
) )
def test_parse_volume_windows_mixed_notations(self): def test_parse_volume_windows_just_drives_native(self):
windows_path = '/c/Foo:C:\\bar' windows_path = 'E:\\:C:\\:ro'
assert VolumeSpec._parse_win32(windows_path) == ( 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/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' 'rw'
) )

View File

@ -786,7 +786,7 @@ class ServiceVolumesTest(unittest.TestCase):
self.mock_client = mock.create_autospec(docker.Client) self.mock_client = mock.create_autospec(docker.Client)
def test_build_volume_binding(self): 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') assert binding == ('/inside', '/outside:/inside:rw')
def test_get_container_data_volumes(self): def test_get_container_data_volumes(self):
@ -845,10 +845,10 @@ class ServiceVolumesTest(unittest.TestCase):
def test_merge_volume_bindings(self): def test_merge_volume_bindings(self):
options = [ options = [
VolumeSpec.parse('/host/volume:/host/volume:ro'), VolumeSpec.parse('/host/volume:/host/volume:ro', True),
VolumeSpec.parse('/host/rw/volume:/host/rw/volume'), VolumeSpec.parse('/host/rw/volume:/host/rw/volume', True),
VolumeSpec.parse('/new/volume'), VolumeSpec.parse('/new/volume', True),
VolumeSpec.parse('/existing/volume'), VolumeSpec.parse('/existing/volume', True),
] ]
self.mock_client.inspect_image.return_value = { self.mock_client.inspect_image.return_value = {
@ -882,8 +882,8 @@ class ServiceVolumesTest(unittest.TestCase):
'web', 'web',
image='busybox', image='busybox',
volumes=[ volumes=[
VolumeSpec.parse('/host/path:/data1'), VolumeSpec.parse('/host/path:/data1', True),
VolumeSpec.parse('/host/path:/data2'), VolumeSpec.parse('/host/path:/data2', True),
], ],
client=self.mock_client, client=self.mock_client,
) )
@ -1007,7 +1007,7 @@ class ServiceVolumesTest(unittest.TestCase):
'web', 'web',
client=self.mock_client, client=self.mock_client,
image='busybox', image='busybox',
volumes=[VolumeSpec.parse(volume)], volumes=[VolumeSpec.parse(volume, True)],
).create_container() ).create_container()
assert self.mock_client.create_container.call_count == 1 assert self.mock_client.create_container.call_count == 1