Merge pull request #2126 from mnowster/1188-support-ro-options-in-volumes-from

1188 support ro options in volumes from
This commit is contained in:
Aanand Prasad 2015-10-07 11:57:14 +01:00
commit 627f1acdc4
7 changed files with 105 additions and 34 deletions

View File

@ -17,8 +17,10 @@ from .legacy import check_for_legacy_containers
from .service import ContainerNet from .service import ContainerNet
from .service import ConvergenceStrategy from .service import ConvergenceStrategy
from .service import Net from .service import Net
from .service import parse_volume_from_spec
from .service import Service from .service import Service
from .service import ServiceNet from .service import ServiceNet
from .service import VolumeFromSpec
from .utils import parallel_execute from .utils import parallel_execute
@ -34,12 +36,18 @@ def sort_service_dicts(services):
def get_service_names(links): def get_service_names(links):
return [link.split(':')[0] for link in links] return [link.split(':')[0] for link in links]
def get_service_names_from_volumes_from(volumes_from):
return [
parse_volume_from_spec(volume_from).source
for volume_from in volumes_from
]
def get_service_dependents(service_dict, services): def get_service_dependents(service_dict, services):
name = service_dict['name'] name = service_dict['name']
return [ return [
service for service in services service for service in services
if (name in get_service_names(service.get('links', [])) or if (name in get_service_names(service.get('links', [])) or
name in service.get('volumes_from', []) or name in get_service_names_from_volumes_from(service.get('volumes_from', [])) or
name == get_service_name_from_net(service.get('net'))) name == get_service_name_from_net(service.get('net')))
] ]
@ -176,20 +184,23 @@ class Project(object):
def get_volumes_from(self, service_dict): def get_volumes_from(self, service_dict):
volumes_from = [] volumes_from = []
if 'volumes_from' in service_dict: if 'volumes_from' in service_dict:
for volume_name in service_dict.get('volumes_from', []): for volume_from_config in service_dict.get('volumes_from', []):
volume_from_spec = parse_volume_from_spec(volume_from_config)
# Get service
try: try:
service = self.get_service(volume_name) service_name = self.get_service(volume_from_spec.source)
volumes_from.append(service) volume_from_spec = VolumeFromSpec(service_name, volume_from_spec.mode)
except NoSuchService: except NoSuchService:
try: try:
container = Container.from_id(self.client, volume_name) container_name = Container.from_id(self.client, volume_from_spec.source)
volumes_from.append(container) volume_from_spec = VolumeFromSpec(container_name, volume_from_spec.mode)
except APIError: except APIError:
raise ConfigurationError( raise ConfigurationError(
'Service "%s" mounts volumes from "%s", which is ' 'Service "%s" mounts volumes from "%s", which is '
'not the name of a service or container.' % ( 'not the name of a service or container.' % (
service_dict['name'], service_dict['name'],
volume_name)) volume_from_spec.source))
volumes_from.append(volume_from_spec)
del service_dict['volumes_from'] del service_dict['volumes_from']
return volumes_from return volumes_from

View File

@ -83,6 +83,9 @@ class NoSuchImageError(Exception):
VolumeSpec = namedtuple('VolumeSpec', 'external internal mode') VolumeSpec = namedtuple('VolumeSpec', 'external internal mode')
VolumeFromSpec = namedtuple('VolumeFromSpec', 'source mode')
ServiceName = namedtuple('ServiceName', 'project service number') ServiceName = namedtuple('ServiceName', 'project service number')
@ -520,7 +523,7 @@ class Service(object):
return [(service.name, alias) for service, alias in self.links] return [(service.name, alias) for service, alias in self.links]
def get_volumes_from_names(self): def get_volumes_from_names(self):
return [s.name for s in self.volumes_from if isinstance(s, Service)] return [s.source.name for s in self.volumes_from if isinstance(s.source, Service)]
def get_container_name(self, number, one_off=False): def get_container_name(self, number, one_off=False):
# TODO: Implement issue #652 here # TODO: Implement issue #652 here
@ -560,16 +563,9 @@ class Service(object):
def _get_volumes_from(self): def _get_volumes_from(self):
volumes_from = [] volumes_from = []
for volume_source in self.volumes_from: for volume_from_spec in self.volumes_from:
if isinstance(volume_source, Service): volumes = build_volume_from(volume_from_spec)
containers = volume_source.containers(stopped=True) volumes_from.extend(volumes)
if not containers:
volumes_from.append(volume_source.create_container().id)
else:
volumes_from.extend(map(attrgetter('id'), containers))
elif isinstance(volume_source, Container):
volumes_from.append(volume_source.id)
return volumes_from return volumes_from
@ -989,6 +985,38 @@ def parse_volume_spec(volume_config):
return VolumeSpec(external, internal, mode) return VolumeSpec(external, internal, mode)
def build_volume_from(volume_from_spec):
"""
volume_from can be either a service or a container. We want to return the
container.id and format it into a string complete with the mode.
"""
if isinstance(volume_from_spec.source, Service):
containers = volume_from_spec.source.containers(stopped=True)
if not containers:
return ["{}:{}".format(volume_from_spec.source.create_container().id, volume_from_spec.mode)]
container = containers[0]
return ["{}:{}".format(container.id, volume_from_spec.mode)]
elif isinstance(volume_from_spec.source, Container):
return ["{}:{}".format(volume_from_spec.source.id, volume_from_spec.mode)]
def parse_volume_from_spec(volume_from_config):
parts = volume_from_config.split(':')
if len(parts) > 2:
raise ConfigError("Volume %s has incorrect format, should be "
"external:internal[:mode]" % volume_from_config)
if len(parts) == 1:
source = parts[0]
mode = 'rw'
else:
source, mode = parts
return VolumeFromSpec(source, mode)
# Labels # Labels

View File

@ -346,11 +346,13 @@ should always begin with `.` or `..`.
### volumes_from ### volumes_from
Mount all of the volumes from another service or container. Mount all of the volumes from another service or container, optionally
specifying read-only access(``ro``) or read-write(``rw``).
volumes_from: volumes_from:
- service_name - service_name
- container_name - container_name
- service_name:rw
### cpu\_shares, cpuset, domainname, entrypoint, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, privileged, read\_only, restart, stdin\_open, tty, user, volume\_driver, working\_dir ### cpu\_shares, cpuset, domainname, entrypoint, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, privileged, read\_only, restart, stdin\_open, tty, user, volume\_driver, working\_dir

View File

@ -6,6 +6,7 @@ from compose.const import LABEL_PROJECT
from compose.container import Container from compose.container import Container
from compose.project import Project from compose.project import Project
from compose.service import ConvergenceStrategy from compose.service import ConvergenceStrategy
from compose.service import VolumeFromSpec
def build_service_dicts(service_config): def build_service_dicts(service_config):
@ -72,7 +73,7 @@ class ProjectTest(DockerClientTestCase):
) )
db = project.get_service('db') db = project.get_service('db')
data = project.get_service('data') data = project.get_service('data')
self.assertEqual(db.volumes_from, [data]) self.assertEqual(db.volumes_from, [VolumeFromSpec(data, 'rw')])
def test_volumes_from_container(self): def test_volumes_from_container(self):
data_container = Container.create( data_container = Container.create(
@ -93,7 +94,7 @@ class ProjectTest(DockerClientTestCase):
client=self.client, client=self.client,
) )
db = project.get_service('db') db = project.get_service('db')
self.assertEqual(db.volumes_from, [data_container]) self.assertEqual(db._get_volumes_from(), [data_container.id + ':rw'])
def test_net_from_service(self): def test_net_from_service(self):
project = Project.from_dicts( project = Project.from_dicts(

View File

@ -25,6 +25,7 @@ from compose.service import ConfigError
from compose.service import ConvergencePlan from compose.service import ConvergencePlan
from compose.service import Net from compose.service import Net
from compose.service import Service from compose.service import Service
from compose.service import VolumeFromSpec
def create_and_start_container(service, **override_options): def create_and_start_container(service, **override_options):
@ -272,12 +273,18 @@ class ServiceTest(DockerClientTestCase):
command=["top"], command=["top"],
labels={LABEL_PROJECT: 'composetest'}, labels={LABEL_PROJECT: 'composetest'},
) )
host_service = self.create_service('host', volumes_from=[volume_service, volume_container_2]) host_service = self.create_service(
'host',
volumes_from=[
VolumeFromSpec(volume_service, 'rw'),
VolumeFromSpec(volume_container_2, 'rw')
]
)
host_container = host_service.create_container() host_container = host_service.create_container()
host_service.start_container(host_container) host_service.start_container(host_container)
self.assertIn(volume_container_1.id, self.assertIn(volume_container_1.id + ':rw',
host_container.get('HostConfig.VolumesFrom')) host_container.get('HostConfig.VolumesFrom'))
self.assertIn(volume_container_2.id, self.assertIn(volume_container_2.id + ':rw',
host_container.get('HostConfig.VolumesFrom')) host_container.get('HostConfig.VolumesFrom'))
def test_execute_convergence_plan_recreate(self): def test_execute_convergence_plan_recreate(self):

View File

@ -168,7 +168,7 @@ class ProjectTest(unittest.TestCase):
'volumes_from': ['aaa'] 'volumes_from': ['aaa']
} }
], self.mock_client) ], self.mock_client)
self.assertEqual(project.get_service('test')._get_volumes_from(), [container_id]) self.assertEqual(project.get_service('test')._get_volumes_from(), [container_id + ":rw"])
def test_use_volumes_from_service_no_container(self): def test_use_volumes_from_service_no_container(self):
container_name = 'test_vol_1' container_name = 'test_vol_1'
@ -191,7 +191,7 @@ class ProjectTest(unittest.TestCase):
'volumes_from': ['vol'] 'volumes_from': ['vol']
} }
], self.mock_client) ], self.mock_client)
self.assertEqual(project.get_service('test')._get_volumes_from(), [container_name]) self.assertEqual(project.get_service('test')._get_volumes_from(), [container_name + ":rw"])
@mock.patch.object(Service, 'containers') @mock.patch.object(Service, 'containers')
def test_use_volumes_from_service_container(self, mock_return): def test_use_volumes_from_service_container(self, mock_return):
@ -211,7 +211,7 @@ class ProjectTest(unittest.TestCase):
'volumes_from': ['vol'] 'volumes_from': ['vol']
} }
], None) ], None)
self.assertEqual(project.get_service('test')._get_volumes_from(), container_ids) self.assertEqual(project.get_service('test')._get_volumes_from(), [container_ids[0] + ':rw'])
def test_net_unset(self): def test_net_unset(self):
project = Project.from_dicts('test', [ project = Project.from_dicts('test', [

View File

@ -24,6 +24,7 @@ from compose.service import parse_repository_tag
from compose.service import parse_volume_spec from compose.service import parse_volume_spec
from compose.service import Service from compose.service import Service
from compose.service import ServiceNet from compose.service import ServiceNet
from compose.service import VolumeFromSpec
class ServiceTest(unittest.TestCase): class ServiceTest(unittest.TestCase):
@ -75,9 +76,18 @@ class ServiceTest(unittest.TestCase):
service = Service( service = Service(
'test', 'test',
image='foo', image='foo',
volumes_from=[mock.Mock(id=container_id, spec=Container)]) volumes_from=[VolumeFromSpec(mock.Mock(id=container_id, spec=Container), 'rw')])
self.assertEqual(service._get_volumes_from(), [container_id]) self.assertEqual(service._get_volumes_from(), [container_id + ':rw'])
def test_get_volumes_from_container_read_only(self):
container_id = 'aabbccddee'
service = Service(
'test',
image='foo',
volumes_from=[VolumeFromSpec(mock.Mock(id=container_id, spec=Container), 'ro')])
self.assertEqual(service._get_volumes_from(), [container_id + ':ro'])
def test_get_volumes_from_service_container_exists(self): def test_get_volumes_from_service_container_exists(self):
container_ids = ['aabbccddee', '12345'] container_ids = ['aabbccddee', '12345']
@ -86,9 +96,21 @@ class ServiceTest(unittest.TestCase):
mock.Mock(id=container_id, spec=Container) mock.Mock(id=container_id, spec=Container)
for container_id in container_ids for container_id in container_ids
] ]
service = Service('test', volumes_from=[from_service], image='foo') service = Service('test', volumes_from=[VolumeFromSpec(from_service, 'rw')], image='foo')
self.assertEqual(service._get_volumes_from(), container_ids) self.assertEqual(service._get_volumes_from(), [container_ids[0] + ":rw"])
def test_get_volumes_from_service_container_exists_with_flags(self):
for mode in ['ro', 'rw', 'z', 'rw,z', 'z,rw']:
container_ids = ['aabbccddee:' + mode, '12345:' + mode]
from_service = mock.create_autospec(Service)
from_service.containers.return_value = [
mock.Mock(id=container_id.split(':')[0], spec=Container)
for container_id in container_ids
]
service = Service('test', volumes_from=[VolumeFromSpec(from_service, mode)], image='foo')
self.assertEqual(service._get_volumes_from(), [container_ids[0]])
def test_get_volumes_from_service_no_container(self): def test_get_volumes_from_service_no_container(self):
container_id = 'abababab' container_id = 'abababab'
@ -97,9 +119,9 @@ class ServiceTest(unittest.TestCase):
from_service.create_container.return_value = mock.Mock( from_service.create_container.return_value = mock.Mock(
id=container_id, id=container_id,
spec=Container) spec=Container)
service = Service('test', image='foo', volumes_from=[from_service]) service = Service('test', image='foo', volumes_from=[VolumeFromSpec(from_service, 'rw')])
self.assertEqual(service._get_volumes_from(), [container_id]) self.assertEqual(service._get_volumes_from(), [container_id + ':rw'])
from_service.create_container.assert_called_once_with() from_service.create_container.assert_called_once_with()
def test_split_domainname_none(self): def test_split_domainname_none(self):
@ -357,7 +379,7 @@ class ServiceTest(unittest.TestCase):
client=self.mock_client, client=self.mock_client,
net=ServiceNet(Service('other')), net=ServiceNet(Service('other')),
links=[(Service('one'), 'one')], links=[(Service('one'), 'one')],
volumes_from=[Service('two')]) volumes_from=[VolumeFromSpec(Service('two'), 'rw')])
config_dict = service.config_dict() config_dict = service.config_dict()
expected = { expected = {