mirror of https://github.com/docker/compose.git
Merge pull request #3922 from shin-/3897-volumespec_parse_win32
Improve volumespec parsing on windows platforms
This commit is contained in:
commit
7687412e03
|
@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import ntpath
|
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
|
@ -16,6 +15,7 @@ from cached_property import cached_property
|
||||||
from ..const import COMPOSEFILE_V1 as V1
|
from ..const import COMPOSEFILE_V1 as V1
|
||||||
from ..const import COMPOSEFILE_V2_0 as V2_0
|
from ..const import COMPOSEFILE_V2_0 as V2_0
|
||||||
from ..utils import build_string_dict
|
from ..utils import build_string_dict
|
||||||
|
from ..utils import splitdrive
|
||||||
from .environment import env_vars_from_file
|
from .environment import env_vars_from_file
|
||||||
from .environment import Environment
|
from .environment import Environment
|
||||||
from .environment import split_env
|
from .environment import split_env
|
||||||
|
@ -943,13 +943,7 @@ def split_path_mapping(volume_path):
|
||||||
path. Using splitdrive so windows absolute paths won't cause issues with
|
path. Using splitdrive so windows absolute paths won't cause issues with
|
||||||
splitting on ':'.
|
splitting on ':'.
|
||||||
"""
|
"""
|
||||||
# splitdrive is very naive, so handle special cases where we can be sure
|
drive, volume_config = splitdrive(volume_path)
|
||||||
# the first character is not a drive.
|
|
||||||
if (volume_path.startswith('.') or volume_path.startswith('~') or
|
|
||||||
volume_path.startswith('/')):
|
|
||||||
drive, volume_config = '', volume_path
|
|
||||||
else:
|
|
||||||
drive, volume_config = ntpath.splitdrive(volume_path)
|
|
||||||
|
|
||||||
if ':' in volume_config:
|
if ':' in volume_config:
|
||||||
(host, container) = volume_config.split(':', 1)
|
(host, container) = volume_config.split(':', 1)
|
||||||
|
|
|
@ -12,6 +12,7 @@ import six
|
||||||
from compose.config.config import V1
|
from compose.config.config import V1
|
||||||
from compose.config.errors import ConfigurationError
|
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
|
||||||
|
|
||||||
|
|
||||||
class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
|
class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')):
|
||||||
|
@ -114,41 +115,23 @@ def parse_extra_hosts(extra_hosts_config):
|
||||||
return extra_hosts_dict
|
return extra_hosts_dict
|
||||||
|
|
||||||
|
|
||||||
def normalize_paths_for_engine(external_path, internal_path):
|
def normalize_path_for_engine(path):
|
||||||
"""Windows paths, c:\my\path\shiny, need to be changed to be compatible with
|
"""Windows paths, c:\my\path\shiny, need to be changed to be compatible with
|
||||||
the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
|
the Engine. Volume paths are expected to be linux style /c/my/path/shiny/
|
||||||
"""
|
"""
|
||||||
if not IS_WINDOWS_PLATFORM:
|
drive, tail = splitdrive(path)
|
||||||
return external_path, internal_path
|
|
||||||
|
|
||||||
if external_path:
|
if drive:
|
||||||
drive, tail = os.path.splitdrive(external_path)
|
path = '/' + drive.lower().rstrip(':') + tail
|
||||||
|
|
||||||
if drive:
|
return path.replace('\\', '/')
|
||||||
external_path = '/' + drive.lower().rstrip(':') + tail
|
|
||||||
|
|
||||||
external_path = external_path.replace('\\', '/')
|
|
||||||
|
|
||||||
return external_path, internal_path.replace('\\', '/')
|
|
||||||
|
|
||||||
|
|
||||||
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, volume_config):
|
def _parse_unix(cls, volume_config):
|
||||||
"""Parse a volume_config path and split it into external:internal[:mode]
|
parts = volume_config.split(':')
|
||||||
parts to be returned as a valid VolumeSpec.
|
|
||||||
"""
|
|
||||||
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, tail = os.path.splitdrive(volume_config)
|
|
||||||
parts = tail.split(":")
|
|
||||||
|
|
||||||
if drive:
|
|
||||||
parts[0] = drive + parts[0]
|
|
||||||
else:
|
|
||||||
parts = volume_config.split(':')
|
|
||||||
|
|
||||||
if len(parts) > 3:
|
if len(parts) > 3:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
|
@ -156,13 +139,11 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
||||||
"external:internal[:mode]" % volume_config)
|
"external:internal[:mode]" % volume_config)
|
||||||
|
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
external, internal = normalize_paths_for_engine(
|
external = None
|
||||||
None,
|
internal = os.path.normpath(parts[0])
|
||||||
os.path.normpath(parts[0]))
|
|
||||||
else:
|
else:
|
||||||
external, internal = normalize_paths_for_engine(
|
external = os.path.normpath(parts[0])
|
||||||
os.path.normpath(parts[0]),
|
internal = os.path.normpath(parts[1])
|
||||||
os.path.normpath(parts[1]))
|
|
||||||
|
|
||||||
mode = 'rw'
|
mode = 'rw'
|
||||||
if len(parts) == 3:
|
if len(parts) == 3:
|
||||||
|
@ -170,6 +151,48 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
||||||
|
|
||||||
return cls(external, internal, mode)
|
return cls(external, internal, mode)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_win32(cls, volume_config):
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
def separate_next_section(volume_config):
|
||||||
|
drive, tail = splitdrive(volume_config)
|
||||||
|
parts = tail.split(':', 1)
|
||||||
|
if drive:
|
||||||
|
parts[0] = drive + parts[0]
|
||||||
|
return parts
|
||||||
|
|
||||||
|
parts = separate_next_section(volume_config)
|
||||||
|
if len(parts) == 1:
|
||||||
|
internal = normalize_path_for_engine(os.path.normpath(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]))
|
||||||
|
if len(parts) > 1:
|
||||||
|
if ':' in parts[1]:
|
||||||
|
raise ConfigurationError(
|
||||||
|
"Volume %s has incorrect format, should be "
|
||||||
|
"external:internal[:mode]" % volume_config
|
||||||
|
)
|
||||||
|
mode = parts[1]
|
||||||
|
|
||||||
|
return cls(external, internal, mode)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, volume_config):
|
||||||
|
"""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)
|
||||||
|
else:
|
||||||
|
return cls._parse_unix(volume_config)
|
||||||
|
|
||||||
def repr(self):
|
def repr(self):
|
||||||
external = self.external + ':' if self.external else ''
|
external = self.external + ':' if self.external else ''
|
||||||
return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self)
|
return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import hashlib
|
||||||
import json
|
import json
|
||||||
import json.decoder
|
import json.decoder
|
||||||
import logging
|
import logging
|
||||||
|
import ntpath
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
@ -108,3 +109,11 @@ def microseconds_from_time_nano(time_nano):
|
||||||
|
|
||||||
def build_string_dict(source_dict):
|
def build_string_dict(source_dict):
|
||||||
return dict((k, str(v if v is not None else '')) for k, v in source_dict.items())
|
return dict((k, str(v if v is not None else '')) for k, v in source_dict.items())
|
||||||
|
|
||||||
|
|
||||||
|
def splitdrive(path):
|
||||||
|
if len(path) == 0:
|
||||||
|
return ('', '')
|
||||||
|
if path[0] in ['.', '\\', '/', '~']:
|
||||||
|
return ('', path)
|
||||||
|
return ntpath.splitdrive(path)
|
||||||
|
|
|
@ -9,7 +9,6 @@ from compose.config.errors import ConfigurationError
|
||||||
from compose.config.types import parse_extra_hosts
|
from compose.config.types import parse_extra_hosts
|
||||||
from compose.config.types import VolumeFromSpec
|
from compose.config.types import VolumeFromSpec
|
||||||
from compose.config.types import VolumeSpec
|
from compose.config.types import VolumeSpec
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
|
||||||
|
|
||||||
|
|
||||||
def test_parse_extra_hosts_list():
|
def test_parse_extra_hosts_list():
|
||||||
|
@ -64,15 +63,38 @@ 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()
|
||||||
|
|
||||||
@pytest.mark.xfail((not IS_WINDOWS_PLATFORM), reason='does not have a drive')
|
|
||||||
def test_parse_volume_windows_absolute_path(self):
|
def test_parse_volume_windows_absolute_path(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(windows_path) == (
|
assert VolumeSpec._parse_win32(windows_path) == (
|
||||||
"/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):
|
||||||
|
windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro'
|
||||||
|
assert VolumeSpec._parse_win32(windows_path) == (
|
||||||
|
'/c/Users/reimu/scarlet',
|
||||||
|
'/c/scarlet/app',
|
||||||
|
'ro'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_volume_windows_just_drives(self):
|
||||||
|
windows_path = 'E:\\:C:\\:ro'
|
||||||
|
assert VolumeSpec._parse_win32(windows_path) == (
|
||||||
|
'/e/',
|
||||||
|
'/c/',
|
||||||
|
'ro'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse_volume_windows_mixed_notations(self):
|
||||||
|
windows_path = '/c/Foo:C:\\bar'
|
||||||
|
assert VolumeSpec._parse_win32(windows_path) == (
|
||||||
|
'/c/Foo',
|
||||||
|
'/c/bar',
|
||||||
|
'rw'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestVolumesFromSpec(object):
|
class TestVolumesFromSpec(object):
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue