Match named volumes in service definitions with declared volumes

Raise ConfigurationError for undeclared named volumes
Test new behavior

Signed-off-by: Joffrey F <joffrey@docker.com>
This commit is contained in:
Joffrey F 2016-01-21 18:03:58 -08:00 committed by Aanand Prasad
parent 883227c4d8
commit a66bf72199
4 changed files with 118 additions and 12 deletions

View File

@ -163,3 +163,9 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
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)
@property
def is_named_volume(self):
return self.external and not (
self.external.startswith('.') or self.external.startswith('/')
)

View File

@ -74,6 +74,17 @@ class Project(object):
if 'default' not in network_config: if 'default' not in network_config:
all_networks.append(project.default_network) 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: for service_dict in config_data.services:
if use_networking: if use_networking:
networks = get_networks(service_dict, all_networks) networks = get_networks(service_dict, all_networks)
@ -85,6 +96,9 @@ class Project(object):
links = project.get_links(service_dict) links = project.get_links(service_dict)
volumes_from = get_volumes_from(project, 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( project.services.append(
Service( Service(
client=client, client=client,
@ -94,23 +108,13 @@ class Project(object):
links=links, links=links,
net=net, net=net,
volumes_from=volumes_from, volumes_from=volumes_from,
**service_dict)) **service_dict)
)
project.networks += custom_networks project.networks += custom_networks
if 'default' not in network_config and project.uses_default_network(): if 'default' not in network_config and project.uses_default_network():
project.networks.append(project.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 return project
@property @property
@ -472,6 +476,23 @@ def get_networks(service_dict, network_definitions):
return networks 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): def get_volumes_from(project, service_dict):
volumes_from = service_dict.pop('volumes_from', None) volumes_from = service_dict.pop('volumes_from', None)
if not volumes_from: if not volumes_from:

View File

@ -813,3 +813,39 @@ class ProjectTest(DockerClientTestCase):
assert 'Volume {0} declared as external'.format( assert 'Volume {0} declared as external'.format(
vol_name vol_name
) in str(e.exception) ) 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))

View File

@ -7,8 +7,10 @@ import docker
from .. import mock from .. import mock
from .. import unittest from .. import unittest
from compose.config import ConfigurationError
from compose.config.config import Config from compose.config.config import Config
from compose.config.types import VolumeFromSpec from compose.config.types import VolumeFromSpec
from compose.config.types import VolumeSpec
from compose.const import LABEL_SERVICE from compose.const import LABEL_SERVICE
from compose.container import Container from compose.container import Container
from compose.project import Project from compose.project import Project
@ -476,3 +478,44 @@ class ProjectTest(unittest.TestCase):
), ),
) )
self.assertEqual([c.id for c in project.containers()], ['1']) 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)