Skip to content

Commit

Permalink
Use individual volumes for recreate instead of volumes_from
Browse files Browse the repository at this point in the history
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
  • Loading branch information
dnephin committed May 10, 2015
1 parent 1748b0f commit b8cec3f
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 35 deletions.
4 changes: 4 additions & 0 deletions compose/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ def id(self):
def image(self):
return self.dictionary['Image']

@property
def image_config(self):
return self.client.inspect_image(self.image)

@property
def short_id(self):
return self.id[:10]
Expand Down
74 changes: 59 additions & 15 deletions compose/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
from collections import namedtuple
import logging
import re
from operator import attrgetter
import sys
import six
from operator import attrgetter

import six
from docker.errors import APIError
from docker.utils import create_host_config, LogConfig

Expand Down Expand Up @@ -182,10 +182,10 @@ def create_container(self,
Create a container for this service. If the image doesn't exist, attempt to pull
it.
"""
override_options['volumes_from'] = self._get_volumes_from(previous_container)
container_options = self._get_container_create_options(
override_options,
one_off=one_off,
previous_container=previous_container,
)

if (do_build and
Expand Down Expand Up @@ -325,7 +325,7 @@ def _get_links(self, link_to_self):
links.append((external_link, link_name))
return links

def _get_volumes_from(self, previous_container=None):
def _get_volumes_from(self):
volumes_from = []
for volume_source in self.volumes_from:
if isinstance(volume_source, Service):
Expand All @@ -338,9 +338,6 @@ def _get_volumes_from(self, previous_container=None):
elif isinstance(volume_source, Container):
volumes_from.append(volume_source.id)

if previous_container:
volumes_from.append(previous_container.id)

return volumes_from

def _get_net(self):
Expand All @@ -362,7 +359,11 @@ def _get_net(self):

return net

def _get_container_create_options(self, override_options, one_off=False):
def _get_container_create_options(
self,
override_options,
one_off=False,
previous_container=None):
container_options = dict(
(k, self.options[k])
for k in DOCKER_CONFIG_KEYS if k in self.options)
Expand Down Expand Up @@ -395,6 +396,10 @@ def _get_container_create_options(self, override_options, one_off=False):
ports.append(port)
container_options['ports'] = ports

override_options['binds'] = merge_volume_bindings(
container_options.get('volumes') or [],
previous_container)

if 'volumes' in container_options:
container_options['volumes'] = dict(
(parse_volume_spec(v).internal, {})
Expand All @@ -417,11 +422,6 @@ def _get_container_host_config(self, override_options, one_off=False):
options = dict(self.options, **override_options)
port_bindings = build_port_bindings(options.get('ports') or [])

volume_bindings = dict(
build_volume_binding(parse_volume_spec(volume))
for volume in options.get('volumes') or []
if ':' in volume)

privileged = options.get('privileged', False)
cap_add = options.get('cap_add', None)
cap_drop = options.get('cap_drop', None)
Expand All @@ -444,8 +444,8 @@ def _get_container_host_config(self, override_options, one_off=False):
return create_host_config(
links=self._get_links(link_to_self=one_off),
port_bindings=port_bindings,
binds=volume_bindings,
volumes_from=options.get('volumes_from'),
binds=options.get('binds'),
volumes_from=self._get_volumes_from(),
privileged=privileged,
network_mode=self._get_net(),
dns=dns,
Expand Down Expand Up @@ -527,6 +527,50 @@ def pull(self, insecure_registry=False):
stream_output(output, sys.stdout)


def get_container_data_volumes(container, volumes_option):
"""Find the container data volumes that are in `volumes_option`, and return
a mapping of volume bindings for those volumes.
"""
volumes = []

volumes_option = volumes_option or []
container_volumes = container.get('Volumes') or {}
image_volumes = container.image_config['ContainerConfig'].get('Volumes') or {}

for volume in set(volumes_option + image_volumes.keys()):
volume = parse_volume_spec(volume)
# No need to preserve host volumes
if volume.external:
continue

volume_path = container_volumes.get(volume.internal)
# New volume, doesn't exist in the old container
if not volume_path:
continue

# Copy existing volume from old container
volume = volume._replace(external=volume_path)
volumes.append(build_volume_binding(volume))

return dict(volumes)


def merge_volume_bindings(volumes_option, previous_container):
"""Return a list of volume bindings for a container. Container data volumes
are replaced by those from the previous container.
"""
volume_bindings = dict(
build_volume_binding(parse_volume_spec(volume))
for volume in volumes_option or []
if ':' in volume)

if previous_container:
volume_bindings.update(
get_container_data_volumes(previous_container, volumes_option))

return volume_bindings


NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')


Expand Down
24 changes: 11 additions & 13 deletions tests/integration/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def test_create_container_with_unspecified_volume(self):
service = self.create_service('db', volumes=['/var/db'])
container = service.create_container()
service.start_container(container)
self.assertIn('/var/db', container.inspect()['Volumes'])
self.assertIn('/var/db', container.get('Volumes'))

def test_create_container_with_cpu_shares(self):
service = self.create_service('db', cpu_shares=73)
Expand Down Expand Up @@ -239,24 +239,24 @@ def test_recreate_containers(self):
command=['300']
)
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['sleep'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['300'])
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
self.assertEqual(old_container.get('Config.Entrypoint'), ['sleep'])
self.assertEqual(old_container.get('Config.Cmd'), ['300'])
self.assertIn('FOO=1', old_container.get('Config.Env'))
self.assertEqual(old_container.name, 'composetest_db_1')
service.start_container(old_container)
volume_path = old_container.inspect()['Volumes']['/etc']
old_container.inspect() # reload volume data
volume_path = old_container.get('Volumes')['/etc']

num_containers_before = len(self.client.containers(all=True))

service.options['environment']['FOO'] = '2'
new_container, = service.recreate_containers()

self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep'])
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300'])
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
self.assertEqual(new_container.get('Config.Entrypoint'), ['sleep'])
self.assertEqual(new_container.get('Config.Cmd'), ['300'])
self.assertIn('FOO=2', new_container.get('Config.Env'))
self.assertEqual(new_container.name, 'composetest_db_1')
self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path)
self.assertIn(old_container.id, new_container.dictionary['HostConfig']['VolumesFrom'])
self.assertEqual(new_container.get('Volumes')['/etc'], volume_path)

self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
self.assertNotEqual(old_container.id, new_container.id)
Expand Down Expand Up @@ -289,9 +289,7 @@ def test_recreate_containers_with_image_declared_volume(self):
self.assertEqual(old_container.get('Volumes').keys(), ['/data'])
volume_path = old_container.get('Volumes')['/data']

service.recreate_containers()
new_container = service.containers()[0]
service.start_container(new_container)
new_container = service.recreate_containers()[0]
self.assertEqual(new_container.get('Volumes').keys(), ['/data'])
self.assertEqual(new_container.get('Volumes')['/data'], volume_path)

Expand Down
70 changes: 63 additions & 7 deletions tests/unit/service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
ConfigError,
build_port_bindings,
build_volume_binding,
get_container_data_volumes,
get_container_name,
merge_volume_bindings,
parse_repository_tag,
parse_volume_spec,
split_port,
Expand Down Expand Up @@ -86,13 +88,6 @@ def test_get_volumes_from_container(self):

self.assertEqual(service._get_volumes_from(), [container_id])

def test_get_volumes_from_previous_container(self):
container_id = 'aabbccddee'
service = Service('test', image='foo')
container = mock.Mock(id=container_id, spec=Container, image='foo')

self.assertEqual(service._get_volumes_from(container), [container_id])

def test_get_volumes_from_service_container_exists(self):
container_ids = ['aabbccddee', '12345']
from_service = mock.create_autospec(Service)
Expand Down Expand Up @@ -320,6 +315,9 @@ def test_create_container_no_build(self):

class ServiceVolumesTest(unittest.TestCase):

def setUp(self):
self.mock_client = mock.create_autospec(docker.Client)

def test_parse_volume_spec_only_one_path(self):
spec = parse_volume_spec('/the/volume')
self.assertEqual(spec, (None, '/the/volume', 'rw'))
Expand All @@ -345,3 +343,61 @@ def test_build_volume_binding(self):
self.assertEqual(
binding,
('/outside', dict(bind='/inside', ro=False)))

def test_get_container_data_volumes(self):
options = [
'/host/volume:/host/volume:ro',
'/new/volume',
'/existing/volume',
]

self.mock_client.inspect_image.return_value = {
'ContainerConfig': {
'Volumes': {
'/mnt/image/data': {},
}
}
}
container = Container(self.mock_client, {
'Image': 'ababab',
'Volumes': {
'/host/volume': '/host/volume',
'/existing/volume': '/var/lib/docker/aaaaaaaa',
'/removed/volume': '/var/lib/docker/bbbbbbbb',
'/mnt/image/data': '/var/lib/docker/cccccccc',
},
}, has_been_inspected=True)

expected = {
'/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False},
'/var/lib/docker/cccccccc': {'bind': '/mnt/image/data', 'ro': False},
}

binds = get_container_data_volumes(container, options)
self.assertEqual(binds, expected)

def test_merge_volume_bindings(self):
options = [
'/host/volume:/host/volume:ro',
'/host/rw/volume:/host/rw/volume',
'/new/volume',
'/existing/volume',
]

self.mock_client.inspect_image.return_value = {
'ContainerConfig': {'Volumes': {}}
}

intermediate_container = Container(self.mock_client, {
'Image': 'ababab',
'Volumes': {'/existing/volume': '/var/lib/docker/aaaaaaaa'},
}, has_been_inspected=True)

expected = {
'/host/volume': {'bind': '/host/volume', 'ro': True},
'/host/rw/volume': {'bind': '/host/rw/volume', 'ro': False},
'/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False},
}

binds = merge_volume_bindings(options, intermediate_container)
self.assertEqual(binds, expected)

0 comments on commit b8cec3f

Please sign in to comment.