Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions compose/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions compose/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import sys

DEFAULT_TIMEOUT = 10
HTTP_TIMEOUT = int(os.environ.get('COMPOSE_HTTP_TIMEOUT', os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)))
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'
LABEL_SERVICE = 'com.docker.compose.service'
LABEL_VERSION = 'com.docker.compose.version'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
HTTP_TIMEOUT = int(os.environ.get('COMPOSE_HTTP_TIMEOUT', os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)))
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
47 changes: 41 additions & 6 deletions compose/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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
Expand Down Expand Up @@ -936,20 +937,54 @@ def build_volume_binding(volume_spec):
return volume_spec.internal, "{}:{}:{}".format(*volume_spec)


def normalize_paths_for_engine(external_path, internal_path):
"""
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/
"""
if IS_WINDOWS_PLATFORM:
if external_path:
drive, tail = os.path.splitdrive(external_path)

if drive:
reformatted_drive = "/{}".format(drive.replace(":", ""))
external_path = reformatted_drive + tail

external_path = "/".join(external_path.split("\\"))

return external_path, "/".join(internal_path.split("\\"))
else:
return external_path, internal_path


def parse_volume_spec(volume_config):
parts = volume_config.split(':')
"""
Parse a volume_config path and split it into external:internal[:mode]
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:
raise ConfigError("Volume %s has incorrect format, should be "
"external:internal[:mode]" % volume_config)

if len(parts) == 1:
external = None
internal = os.path.normpath(parts[0])
external, internal = normalize_paths_for_engine(None, os.path.normpath(parts[0]))
else:
external = os.path.normpath(parts[0])
internal = os.path.normpath(parts[1])
external, internal = normalize_paths_for_engine(os.path.normpath(parts[0]), os.path.normpath(parts[1]))

mode = parts[2] if len(parts) == 3 else 'rw'
mode = 'rw'
if len(parts) == 3:
mode = parts[2]

return VolumeSpec(external, internal, mode)

Expand Down
19 changes: 17 additions & 2 deletions tests/unit/config/config_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ def test_no_binding(self):
d = make_service_dict('foo', {'build': '.', 'volumes': ['/data']}, working_dir='.')
self.assertEqual(d['volumes'], ['/data'])

@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
@mock.patch.dict(os.environ)
def test_volume_binding_with_environment_variable(self):
os.environ['VOLUME_PATH'] = '/host/path'
Expand All @@ -433,7 +432,7 @@ def test_volume_binding_with_environment_variable(self):
)[0]
self.assertEqual(d['volumes'], ['/host/path:/container/path'])

@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
@pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='posix paths')
@mock.patch.dict(os.environ)
def test_volume_binding_with_home(self):
os.environ['HOME'] = '/home/user'
Expand Down Expand Up @@ -464,6 +463,7 @@ def test_relative_path_does_expand_posix(self):
self.assertEqual(d['volumes'], ['/home/me/otherproject:/data'])

@pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='windows paths')
@pytest.mark.skipif(IS_WINDOWS_PLATFORM, reason='waiting for this to be resolved: https://github.com/docker/compose/issues/2128')
def test_relative_path_does_expand_windows(self):
d = make_service_dict('foo', {'build': '.', 'volumes': ['./data:/data']}, working_dir='C:\\Users\\me\\myproject')
self.assertEqual(d['volumes'], ['C:\\Users\\me\\myproject\\data:/data'])
Expand Down Expand Up @@ -1124,6 +1124,21 @@ def test_expand_path_with_tilde(self):
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):
Expand Down
16 changes: 15 additions & 1 deletion tests/unit/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,6 @@ def mock_get_image(images):
raise NoSuchImageError()


@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
class ServiceVolumesTest(unittest.TestCase):

def setUp(self):
Expand All @@ -466,6 +465,21 @@ def test_parse_volume_spec_too_many_parts(self):
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_absolute_path(self):
windows_absolute_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro"

spec = parse_volume_spec(windows_absolute_path)

self.assertEqual(
spec,
(
"/c/Users/me/Documents/shiny/config",
"/opt/shiny/config",
"ro"
)
)

def test_build_volume_binding(self):
binding = build_volume_binding(parse_volume_spec('/outside:/inside'))
self.assertEqual(binding, ('/inside', '/outside:/inside:rw'))
Expand Down