فهرست منبع

Do not normalize volume paths on Windows by default
Add environment variable to enable normalization if needed.
Do not normalize internal paths

Signed-off-by: Joffrey F <[email protected]>

Joffrey F 9 سال پیش
والد
کامیت
efb09af271
4فایلهای تغییر یافته به همراه77 افزوده شده و 29 حذف شده
  1. 4 1
      compose/config/config.py
  2. 20 7
      compose/config/types.py
  3. 45 13
      tests/unit/config/types_test.py
  4. 8 8
      tests/unit/service_test.py

+ 4 - 1
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')

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

+ 45 - 13
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_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(self):
+    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'
         )
 

+ 8 - 8
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