Skip to content

Commit 417d9c2

Browse files
committed
Use individual volumes for recreate instead of volumes_from
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
1 parent 4997fac commit 417d9c2

File tree

5 files changed

+152
-40
lines changed

5 files changed

+152
-40
lines changed

compose/cli/main.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -317,10 +317,7 @@ def run(self, project, options):
317317
}
318318

319319
if options['-e']:
320-
# Merge environment from config with -e command line
321-
container_options['environment'] = dict(
322-
parse_environment(service.options.get('environment')),
323-
**parse_environment(options['-e']))
320+
container_options['environment'] = parse_environment(options['-e'])
324321

325322
if options['--entrypoint']:
326323
container_options['entrypoint'] = options.get('--entrypoint')

compose/container.py

+4
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ def id(self):
4444
def image(self):
4545
return self.dictionary['Image']
4646

47+
@property
48+
def image_config(self):
49+
return self.client.inspect_image(self.image)
50+
4751
@property
4852
def short_id(self):
4953
return self.id[:10]

compose/service.py

+70-16
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
from collections import namedtuple
44
import logging
55
import re
6-
from operator import attrgetter
76
import sys
8-
import six
7+
from operator import attrgetter
98

9+
import six
1010
from docker.errors import APIError
1111
from docker.utils import create_host_config, LogConfig
1212

13-
from .config import DOCKER_CONFIG_KEYS
13+
from .config import DOCKER_CONFIG_KEYS, merge_environment
1414
from .container import Container, get_container_name
1515
from .progress_stream import stream_output, StreamOutputError
1616

@@ -183,10 +183,10 @@ def create_container(self,
183183
Create a container for this service. If the image doesn't exist, attempt to pull
184184
it.
185185
"""
186-
override_options['volumes_from'] = self._get_volumes_from(previous_container)
187186
container_options = self._get_container_create_options(
188187
override_options,
189188
one_off=one_off,
189+
previous_container=previous_container,
190190
)
191191

192192
if (do_build and
@@ -247,6 +247,12 @@ def recreate_container(self, container, **override_options):
247247
self.client.rename(
248248
container.id,
249249
'%s_%s' % (container.short_id, container.name))
250+
251+
override_options = dict(
252+
override_options,
253+
environment=merge_environment(
254+
override_options.get('environment'),
255+
{'affinity:container': '=' + container.id}))
250256
new_container = self.create_container(
251257
do_build=False,
252258
previous_container=container,
@@ -326,7 +332,7 @@ def _get_links(self, link_to_self):
326332
links.append((external_link, link_name))
327333
return links
328334

329-
def _get_volumes_from(self, previous_container=None):
335+
def _get_volumes_from(self):
330336
volumes_from = []
331337
for volume_source in self.volumes_from:
332338
if isinstance(volume_source, Service):
@@ -339,9 +345,6 @@ def _get_volumes_from(self, previous_container=None):
339345
elif isinstance(volume_source, Container):
340346
volumes_from.append(volume_source.id)
341347

342-
if previous_container:
343-
volumes_from.append(previous_container.id)
344-
345348
return volumes_from
346349

347350
def _get_net(self):
@@ -363,7 +366,11 @@ def _get_net(self):
363366

364367
return net
365368

366-
def _get_container_create_options(self, override_options, one_off=False):
369+
def _get_container_create_options(
370+
self,
371+
override_options,
372+
one_off=False,
373+
previous_container=None):
367374
container_options = dict(
368375
(k, self.options[k])
369376
for k in DOCKER_CONFIG_KEYS if k in self.options)
@@ -396,11 +403,19 @@ def _get_container_create_options(self, override_options, one_off=False):
396403
ports.append(port)
397404
container_options['ports'] = ports
398405

406+
override_options['binds'] = merge_volume_bindings(
407+
container_options.get('volumes') or [],
408+
previous_container)
409+
399410
if 'volumes' in container_options:
400411
container_options['volumes'] = dict(
401412
(parse_volume_spec(v).internal, {})
402413
for v in container_options['volumes'])
403414

415+
container_options['environment'] = merge_environment(
416+
self.options.get('environment'),
417+
override_options.get('environment'))
418+
404419
if self.can_be_built():
405420
container_options['image'] = self.full_name
406421

@@ -418,11 +433,6 @@ def _get_container_host_config(self, override_options, one_off=False):
418433
options = dict(self.options, **override_options)
419434
port_bindings = build_port_bindings(options.get('ports') or [])
420435

421-
volume_bindings = dict(
422-
build_volume_binding(parse_volume_spec(volume))
423-
for volume in options.get('volumes') or []
424-
if ':' in volume)
425-
426436
privileged = options.get('privileged', False)
427437
cap_add = options.get('cap_add', None)
428438
cap_drop = options.get('cap_drop', None)
@@ -447,8 +457,8 @@ def _get_container_host_config(self, override_options, one_off=False):
447457
return create_host_config(
448458
links=self._get_links(link_to_self=one_off),
449459
port_bindings=port_bindings,
450-
binds=volume_bindings,
451-
volumes_from=options.get('volumes_from'),
460+
binds=options.get('binds'),
461+
volumes_from=self._get_volumes_from(),
452462
privileged=privileged,
453463
network_mode=self._get_net(),
454464
devices=devices,
@@ -531,6 +541,50 @@ def pull(self, insecure_registry=False):
531541
stream_output(output, sys.stdout)
532542

533543

544+
def get_container_data_volumes(container, volumes_option):
545+
"""Find the container data volumes that are in `volumes_option`, and return
546+
a mapping of volume bindings for those volumes.
547+
"""
548+
volumes = []
549+
550+
volumes_option = volumes_option or []
551+
container_volumes = container.get('Volumes') or {}
552+
image_volumes = container.image_config['ContainerConfig'].get('Volumes') or {}
553+
554+
for volume in set(volumes_option + image_volumes.keys()):
555+
volume = parse_volume_spec(volume)
556+
# No need to preserve host volumes
557+
if volume.external:
558+
continue
559+
560+
volume_path = container_volumes.get(volume.internal)
561+
# New volume, doesn't exist in the old container
562+
if not volume_path:
563+
continue
564+
565+
# Copy existing volume from old container
566+
volume = volume._replace(external=volume_path)
567+
volumes.append(build_volume_binding(volume))
568+
569+
return dict(volumes)
570+
571+
572+
def merge_volume_bindings(volumes_option, previous_container):
573+
"""Return a list of volume bindings for a container. Container data volumes
574+
are replaced by those from the previous container.
575+
"""
576+
volume_bindings = dict(
577+
build_volume_binding(parse_volume_spec(volume))
578+
for volume in volumes_option or []
579+
if ':' in volume)
580+
581+
if previous_container:
582+
volume_bindings.update(
583+
get_container_data_volumes(previous_container, volumes_option))
584+
585+
return volume_bindings
586+
587+
534588
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
535589

536590

tests/integration/service_test.py

+14-13
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def test_create_container_with_unspecified_volume(self):
107107
service = self.create_service('db', volumes=['/var/db'])
108108
container = service.create_container()
109109
service.start_container(container)
110-
self.assertIn('/var/db', container.inspect()['Volumes'])
110+
self.assertIn('/var/db', container.get('Volumes'))
111111

112112
def test_create_container_with_cpu_shares(self):
113113
service = self.create_service('db', cpu_shares=73)
@@ -239,24 +239,27 @@ def test_recreate_containers(self):
239239
command=['300']
240240
)
241241
old_container = service.create_container()
242-
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['sleep'])
243-
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['300'])
244-
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
242+
self.assertEqual(old_container.get('Config.Entrypoint'), ['sleep'])
243+
self.assertEqual(old_container.get('Config.Cmd'), ['300'])
244+
self.assertIn('FOO=1', old_container.get('Config.Env'))
245245
self.assertEqual(old_container.name, 'composetest_db_1')
246246
service.start_container(old_container)
247-
volume_path = old_container.inspect()['Volumes']['/etc']
247+
old_container.inspect() # reload volume data
248+
volume_path = old_container.get('Volumes')['/etc']
248249

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

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

254-
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep'])
255-
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300'])
256-
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
255+
self.assertEqual(new_container.get('Config.Entrypoint'), ['sleep'])
256+
self.assertEqual(new_container.get('Config.Cmd'), ['300'])
257+
self.assertIn('FOO=2', new_container.get('Config.Env'))
257258
self.assertEqual(new_container.name, 'composetest_db_1')
258-
self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path)
259-
self.assertIn(old_container.id, new_container.dictionary['HostConfig']['VolumesFrom'])
259+
self.assertEqual(new_container.get('Volumes')['/etc'], volume_path)
260+
self.assertIn(
261+
'affinity:container==%s' % old_container.id,
262+
new_container.get('Config.Env'))
260263

261264
self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
262265
self.assertNotEqual(old_container.id, new_container.id)
@@ -289,9 +292,7 @@ def test_recreate_containers_with_image_declared_volume(self):
289292
self.assertEqual(old_container.get('Volumes').keys(), ['/data'])
290293
volume_path = old_container.get('Volumes')['/data']
291294

292-
service.recreate_containers()
293-
new_container = service.containers()[0]
294-
service.start_container(new_container)
295+
new_container = service.recreate_containers()[0]
295296
self.assertEqual(new_container.get('Volumes').keys(), ['/data'])
296297
self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
297298

tests/unit/service_test.py

+63-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
ConfigError,
1515
build_port_bindings,
1616
build_volume_binding,
17+
get_container_data_volumes,
1718
get_container_name,
19+
merge_volume_bindings,
1820
parse_repository_tag,
1921
parse_volume_spec,
2022
split_port,
@@ -86,13 +88,6 @@ def test_get_volumes_from_container(self):
8688

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

89-
def test_get_volumes_from_previous_container(self):
90-
container_id = 'aabbccddee'
91-
service = Service('test', image='foo')
92-
container = mock.Mock(id=container_id, spec=Container, image='foo')
93-
94-
self.assertEqual(service._get_volumes_from(container), [container_id])
95-
9691
def test_get_volumes_from_service_container_exists(self):
9792
container_ids = ['aabbccddee', '12345']
9893
from_service = mock.create_autospec(Service)
@@ -320,6 +315,9 @@ def test_create_container_no_build(self):
320315

321316
class ServiceVolumesTest(unittest.TestCase):
322317

318+
def setUp(self):
319+
self.mock_client = mock.create_autospec(docker.Client)
320+
323321
def test_parse_volume_spec_only_one_path(self):
324322
spec = parse_volume_spec('/the/volume')
325323
self.assertEqual(spec, (None, '/the/volume', 'rw'))
@@ -345,3 +343,61 @@ def test_build_volume_binding(self):
345343
self.assertEqual(
346344
binding,
347345
('/outside', dict(bind='/inside', ro=False)))
346+
347+
def test_get_container_data_volumes(self):
348+
options = [
349+
'/host/volume:/host/volume:ro',
350+
'/new/volume',
351+
'/existing/volume',
352+
]
353+
354+
self.mock_client.inspect_image.return_value = {
355+
'ContainerConfig': {
356+
'Volumes': {
357+
'/mnt/image/data': {},
358+
}
359+
}
360+
}
361+
container = Container(self.mock_client, {
362+
'Image': 'ababab',
363+
'Volumes': {
364+
'/host/volume': '/host/volume',
365+
'/existing/volume': '/var/lib/docker/aaaaaaaa',
366+
'/removed/volume': '/var/lib/docker/bbbbbbbb',
367+
'/mnt/image/data': '/var/lib/docker/cccccccc',
368+
},
369+
}, has_been_inspected=True)
370+
371+
expected = {
372+
'/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False},
373+
'/var/lib/docker/cccccccc': {'bind': '/mnt/image/data', 'ro': False},
374+
}
375+
376+
binds = get_container_data_volumes(container, options)
377+
self.assertEqual(binds, expected)
378+
379+
def test_merge_volume_bindings(self):
380+
options = [
381+
'/host/volume:/host/volume:ro',
382+
'/host/rw/volume:/host/rw/volume',
383+
'/new/volume',
384+
'/existing/volume',
385+
]
386+
387+
self.mock_client.inspect_image.return_value = {
388+
'ContainerConfig': {'Volumes': {}}
389+
}
390+
391+
intermediate_container = Container(self.mock_client, {
392+
'Image': 'ababab',
393+
'Volumes': {'/existing/volume': '/var/lib/docker/aaaaaaaa'},
394+
}, has_been_inspected=True)
395+
396+
expected = {
397+
'/host/volume': {'bind': '/host/volume', 'ro': True},
398+
'/host/rw/volume': {'bind': '/host/rw/volume', 'ro': False},
399+
'/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False},
400+
}
401+
402+
binds = merge_volume_bindings(options, intermediate_container)
403+
self.assertEqual(binds, expected)

0 commit comments

Comments
 (0)