diff --git a/compose/config/types.py b/compose/config/types.py index b872cba91..2bb2519d1 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -163,3 +163,9 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): def repr(self): external = self.external + ':' if self.external else '' return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self) + + @property + def is_named_volume(self): + return self.external and not ( + self.external.startswith('.') or self.external.startswith('/') + ) diff --git a/compose/project.py b/compose/project.py index 0fea875a6..bbc61a5bb 100644 --- a/compose/project.py +++ b/compose/project.py @@ -74,6 +74,17 @@ class Project(object): if 'default' not in network_config: all_networks.append(project.default_network) + if config_data.volumes: + for vol_name, data in config_data.volumes.items(): + project.volumes.append( + Volume( + client=client, project=name, name=vol_name, + driver=data.get('driver'), + driver_opts=data.get('driver_opts'), + external_name=data.get('external_name') + ) + ) + for service_dict in config_data.services: if use_networking: networks = get_networks(service_dict, all_networks) @@ -85,6 +96,9 @@ class Project(object): links = project.get_links(service_dict) volumes_from = get_volumes_from(project, service_dict) + if config_data.version == 2: + match_named_volumes(service_dict, project.volumes) + project.services.append( Service( client=client, @@ -94,23 +108,13 @@ class Project(object): links=links, net=net, volumes_from=volumes_from, - **service_dict)) + **service_dict) + ) project.networks += custom_networks if 'default' not in network_config and project.uses_default_network(): project.networks.append(project.default_network) - if config_data.volumes: - for vol_name, data in config_data.volumes.items(): - project.volumes.append( - Volume( - client=client, project=name, name=vol_name, - driver=data.get('driver'), - driver_opts=data.get('driver_opts'), - external_name=data.get('external_name') - ) - ) - return project @property @@ -472,6 +476,23 @@ def get_networks(service_dict, network_definitions): return networks +def match_named_volumes(service_dict, project_volumes): + for volume_spec in service_dict.get('volumes', []): + if volume_spec.is_named_volume: + declared_volume = next( + (v for v in project_volumes if v.name == volume_spec.external), + None + ) + if not declared_volume: + raise ConfigurationError( + 'Named volume "{0}" is used in service "{1}" but no' + ' declaration was found in the volumes section.'.format( + volume_spec.repr(), service_dict.get('name') + ) + ) + volume_spec._replace(external=declared_volume.full_name) + + def get_volumes_from(project, service_dict): volumes_from = service_dict.pop('volumes_from', None) if not volumes_from: diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index d3fbb71eb..aa0dd33f0 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -813,3 +813,39 @@ class ProjectTest(DockerClientTestCase): assert 'Volume {0} declared as external'.format( vol_name ) in str(e.exception) + + @v2_only() + def test_project_up_named_volumes_in_binds(self): + vol_name = '{0:x}'.format(random.getrandbits(32)) + full_vol_name = 'composetest_{0}'.format(vol_name) + + base_file = config.ConfigFile( + 'base.yml', + { + 'version': 2, + 'services': { + 'simple': { + 'image': 'busybox:latest', + 'command': 'top', + 'volumes': ['{0}:/data'.format(vol_name)] + }, + }, + 'volumes': { + vol_name: {'driver': 'local'} + } + + }) + config_details = config.ConfigDetails('.', [base_file]) + config_data = config.load(config_details) + project = Project.from_config( + name='composetest', config_data=config_data, client=self.client + ) + service = project.services[0] + self.assertEqual(service.name, 'simple') + volumes = service.options.get('volumes') + self.assertEqual(len(volumes), 1) + self.assertEqual(volumes[0].external, full_vol_name) + project.up() + engine_volumes = self.client.volumes() + self.assertIsNone(next(v for v in engine_volumes if v['Name'] == vol_name)) + self.assertIsNotNone(next(v for v in engine_volumes if v['Name'] == full_vol_name)) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index ffd4455f3..f587d6970 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -7,8 +7,10 @@ import docker from .. import mock from .. import unittest +from compose.config import ConfigurationError from compose.config.config import Config from compose.config.types import VolumeFromSpec +from compose.config.types import VolumeSpec from compose.const import LABEL_SERVICE from compose.container import Container from compose.project import Project @@ -476,3 +478,44 @@ class ProjectTest(unittest.TestCase): ), ) self.assertEqual([c.id for c in project.containers()], ['1']) + + def test_undeclared_volume_v2(self): + config = Config( + version=2, + services=[ + { + 'name': 'web', + 'image': 'busybox:latest', + 'volumes': [VolumeSpec.parse('data0028:/data:ro')], + }, + ], + networks=None, + volumes=None, + ) + with self.assertRaises(ConfigurationError): + Project.from_config('composetest', config, None) + + config = Config( + version=2, + services=[ + { + 'name': 'web', + 'image': 'busybox:latest', + 'volumes': [VolumeSpec.parse('./data0028:/data:ro')], + }, + ], networks=None, volumes=None, + ) + Project.from_config('composetest', config, None) + + def test_undeclared_volume_v1(self): + config = Config( + version=1, + services=[ + { + 'name': 'web', + 'image': 'busybox:latest', + 'volumes': [VolumeSpec.parse('data0028:/data:ro')], + }, + ], networks=None, volumes=None, + ) + Project.from_config('composetest', config, None)