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:
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')

View File

@ -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')):

View File

@ -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:

View File

@ -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'
)

View File

@ -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