Browse Source

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 <[email protected]>
Mazz Mosley 10 years ago
parent
commit
f4cd5b1d45
5 changed files with 51 additions and 5 deletions
  1. 6 5
      compose/config/config.py
  2. 1 0
      compose/const.py
  3. 14 0
      compose/service.py
  4. 15 0
      tests/unit/config/config_test.py
  5. 15 0
      tests/unit/service_test.py

+ 6 - 5
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):

+ 1 - 0
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'

+ 14 - 0
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)

+ 15 - 0
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):

+ 15 - 0
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'))