mirror of
https://github.com/docker/compose.git
synced 2025-07-21 12:44:54 +02:00
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:
parent
883227c4d8
commit
a66bf72199
@ -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('/')
|
||||||
|
)
|
||||||
|
@ -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:
|
||||||
|
@ -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))
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user