diff --git a/compose/config/config.py b/compose/config/config.py index 225919415..8cbaae272 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -1030,7 +1030,13 @@ def resolve_volume_paths(working_dir, service_dict): def resolve_volume_path(working_dir, volume): - container_path, host_path = split_path_mapping(volume) + if isinstance(volume, dict): + host_path = volume.get('source') + container_path = volume.get('target') + if host_path and volume.get('read_only'): + container_path += ':ro' + else: + container_path, host_path = split_path_mapping(volume) if host_path is not None: if host_path.startswith('.'): @@ -1112,6 +1118,8 @@ def split_path_mapping(volume_path): path. Using splitdrive so windows absolute paths won't cause issues with splitting on ':'. """ + if isinstance(volume_path, dict): + return (volume_path.get('target'), volume_path) drive, volume_config = splitdrive(volume_path) if ':' in volume_config: @@ -1123,7 +1131,9 @@ def split_path_mapping(volume_path): def join_path_mapping(pair): (container, host) = pair - if host is None: + if isinstance(host, dict): + return host + elif host is None: return container else: return ":".join((host, container)) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 6a498e250..14e6f7336 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -321,7 +321,7 @@ class CLITestCase(DockerClientTestCase): result = self.dispatch(['config']) assert yaml.load(result.stdout) == { - 'version': '3.0', + 'version': '3.2', 'networks': {}, 'volumes': { 'foobar': { @@ -371,6 +371,11 @@ class CLITestCase(DockerClientTestCase): 'timeout': '1s', 'retries': 5, }, + 'volumes': [ + '/host/path:/container/path:ro', + 'foobar:/container/volumepath:rw', + '/anonymous' + ], 'stop_grace_period': '20s', }, diff --git a/tests/fixtures/v3-full/docker-compose.yml b/tests/fixtures/v3-full/docker-compose.yml index a1661ab93..27f3c6e04 100644 --- a/tests/fixtures/v3-full/docker-compose.yml +++ b/tests/fixtures/v3-full/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3" +version: "3.2" services: web: image: busybox @@ -34,6 +34,17 @@ services: timeout: 1s retries: 5 + volumes: + - source: /host/path + target: /container/path + type: bind + read_only: true + - source: foobar + type: volume + target: /container/volumepath + - type: volume + target: /anonymous + stop_grace_period: 20s volumes: foobar: diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index c86485d7b..c9eb3796e 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -29,6 +29,7 @@ from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_1 as V2_1 from compose.const import COMPOSEFILE_V3_0 as V3_0 from compose.const import COMPOSEFILE_V3_1 as V3_1 +from compose.const import COMPOSEFILE_V3_2 as V3_2 from compose.const import IS_WINDOWS_PLATFORM from compose.utils import nanoseconds_from_time_seconds from tests import mock @@ -964,6 +965,44 @@ class ConfigTest(unittest.TestCase): ] assert service_sort(service_dicts) == service_sort(expected) + @mock.patch.dict(os.environ) + def test_load_with_multiple_files_v3_2(self): + os.environ['COMPOSE_CONVERT_WINDOWS_PATHS'] = 'true' + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': '3.2', + 'services': { + 'web': { + 'image': 'example/web', + 'volumes': [ + {'source': '/a', 'target': '/b', 'type': 'bind'}, + {'source': 'vol', 'target': '/x', 'type': 'volume', 'read_only': True} + ] + } + }, + 'volumes': {'vol': {}} + } + ) + + override_file = config.ConfigFile( + 'override.yaml', + { + 'version': '3.2', + 'services': { + 'web': { + 'volumes': ['/c:/b', '/anonymous'] + } + } + } + ) + details = config.ConfigDetails('.', [base_file, override_file]) + service_dicts = config.load(details).services + svc_volumes = map(lambda v: v.repr(), service_dicts[0]['volumes']) + assert sorted(svc_volumes) == sorted( + ['/anonymous', '/c:/b:rw', 'vol:/x:ro'] + ) + def test_undeclared_volume_v2(self): base_file = config.ConfigFile( 'base.yaml', @@ -1544,6 +1583,29 @@ class ConfigTest(unittest.TestCase): 'ports': types.ServicePort.parse('5432') } + def test_merge_service_dicts_heterogeneous_volumes(self): + base = { + 'volumes': ['/a:/b', '/x:/z'], + } + + override = { + 'image': 'alpine:edge', + 'volumes': [ + {'source': '/e', 'target': '/b', 'type': 'bind'}, + {'source': '/c', 'target': '/d', 'type': 'bind'} + ] + } + + actual = config.merge_service_dicts_from_files( + base, override, V3_2 + ) + + assert actual['volumes'] == [ + {'source': '/e', 'target': '/b', 'type': 'bind'}, + {'source': '/c', 'target': '/d', 'type': 'bind'}, + '/x:/z' + ] + def test_merge_logging_v1(self): base = { 'image': 'alpine:edge',