Skip to content

Commit 7eb476e

Browse files
committed
Resolves #447, fix volume logic for recreate container
Signed-off-by: Daniel Nephin <dnephin@gmail.com>
1 parent 26f45ef commit 7eb476e

4 files changed

Lines changed: 115 additions & 28 deletions

File tree

fig/project.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,8 @@ def up(self,
181181

182182
for container in create_func(
183183
insecure_registry=insecure_registry,
184-
detach=detach,
185-
do_build=do_build):
184+
detach=detach,
185+
do_build=do_build):
186186
running_containers.append(container)
187187

188188
return running_containers

fig/service.py

Lines changed: 53 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,23 +280,29 @@ def recreate_container(self, container, **override_options):
280280
else:
281281
raise
282282

283+
intermediate_options = dict(self.options, **override_options)
283284
intermediate_container = Container.create(
284285
self.client,
285286
image=container.image,
286287
entrypoint=['/bin/echo'],
287288
command=[],
288289
detach=True,
289290
)
290-
intermediate_container.start(volumes_from=container.id)
291+
intermediate_container.start(
292+
binds=get_container_data_volumes(
293+
container, intermediate_options.get('volumes')))
291294
intermediate_container.wait()
292295
container.remove()
293296

297+
# TODO: volumes are being passed to both start and create, this is
298+
# probably unnecessary
294299
options = dict(override_options)
295300
new_container = self.create_container(do_build=False, **options)
296-
self.start_container(new_container, intermediate_container=intermediate_container)
301+
self.start_container(
302+
new_container,
303+
intermediate_container=intermediate_container)
297304

298305
intermediate_container.remove()
299-
300306
return new_container
301307

302308
def start_container_if_stopped(self, container, **options):
@@ -309,12 +315,6 @@ def start_container_if_stopped(self, container, **options):
309315
def start_container(self, container, intermediate_container=None, **override_options):
310316
options = dict(self.options, **override_options)
311317
port_bindings = build_port_bindings(options.get('ports') or [])
312-
313-
volume_bindings = dict(
314-
build_volume_binding(parse_volume_spec(volume))
315-
for volume in options.get('volumes') or []
316-
if ':' in volume)
317-
318318
privileged = options.get('privileged', False)
319319
net = options.get('net', 'bridge')
320320
dns = options.get('dns', None)
@@ -323,12 +323,14 @@ def start_container(self, container, intermediate_container=None, **override_opt
323323
cap_drop = options.get('cap_drop', None)
324324

325325
restart = parse_restart_spec(options.get('restart', None))
326+
binds = get_volume_bindings(
327+
options.get('volumes'), intermediate_container)
326328

327329
container.start(
328330
links=self._get_links(link_to_self=options.get('one_off', False)),
329331
port_bindings=port_bindings,
330-
binds=volume_bindings,
331-
volumes_from=self._get_volumes_from(intermediate_container),
332+
binds=binds,
333+
volumes_from=self._get_volumes_from(),
332334
privileged=privileged,
333335
network_mode=net,
334336
dns=dns,
@@ -390,7 +392,7 @@ def _get_links(self, link_to_self):
390392
links.append((external_link, link_name))
391393
return links
392394

393-
def _get_volumes_from(self, intermediate_container=None):
395+
def _get_volumes_from(self):
394396
volumes_from = []
395397
for volume_source in self.volumes_from:
396398
if isinstance(volume_source, Service):
@@ -404,9 +406,6 @@ def _get_volumes_from(self, intermediate_container=None):
404406
elif isinstance(volume_source, Container):
405407
volumes_from.append(volume_source.id)
406408

407-
if intermediate_container:
408-
volumes_from.append(intermediate_container.id)
409-
410409
return volumes_from
411410

412411
def _get_container_create_options(self, override_options, one_off=False):
@@ -521,6 +520,45 @@ def pull(self, insecure_registry=False):
521520
)
522521

523522

523+
def get_container_data_volumes(container, volumes_option):
524+
"""Find the container data volumes that are in `volumes_option`, and return
525+
a mapping of volume bindings for those volumes.
526+
"""
527+
volumes = []
528+
for volume in volumes_option or []:
529+
volume = parse_volume_spec(volume)
530+
# No need to preserve host volumes
531+
if volume.external:
532+
continue
533+
534+
volume_path = (container.get('Volumes') or {}).get(volume.internal)
535+
# New volume, doesn't exist in the old container
536+
if not volume_path:
537+
continue
538+
539+
# Copy existing volume from old container
540+
volume = volume._replace(external=volume_path)
541+
volumes.append(build_volume_binding(volume))
542+
543+
return dict(volumes)
544+
545+
546+
def get_volume_bindings(volumes_option, intermediate_container):
547+
"""Return a list of volume bindings for a container. Container data volume
548+
bindings are replaced by those in the intermediate container.
549+
"""
550+
volume_bindings = dict(
551+
build_volume_binding(parse_volume_spec(volume))
552+
for volume in volumes_option or []
553+
if ':' in volume)
554+
555+
if intermediate_container:
556+
volume_bindings.update(
557+
get_container_data_volumes(intermediate_container, volumes_option))
558+
559+
return volume_bindings
560+
561+
524562
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
525563

526564

tests/integration/service_test.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def test_create_container_with_unspecified_volume(self):
9898
service = self.create_service('db', volumes=['/var/db'])
9999
container = service.create_container()
100100
service.start_container(container)
101-
self.assertIn('/var/db', container.inspect()['Volumes'])
101+
self.assertIn('/var/db', container.get('Volumes'))
102102

103103
def test_create_container_with_cpu_shares(self):
104104
service = self.create_service('db', cpu_shares=73)
@@ -179,6 +179,16 @@ def test_recreate_containers_when_containers_are_stopped(self):
179179
service.recreate_containers()
180180
self.assertEqual(len(service.containers(stopped=True)), 1)
181181

182+
def test_recreate_containers_with_volume_changes(self):
183+
service = self.create_service('withvolumes', volumes=['/etc'])
184+
old_container = create_and_start_container(service)
185+
self.assertEqual(old_container.get('Volumes').keys(), ['/etc'])
186+
187+
service = self.create_service('withvolumes')
188+
container, = service.recreate_containers()
189+
service.start_container(container)
190+
self.assertEqual(container.get('Volumes'), {})
191+
182192
def test_start_container_passes_through_options(self):
183193
db = self.create_service('db')
184194
create_and_start_container(db, environment={'FOO': 'BAR'})

tests/unit/service_test.py

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
from fig import Service
1212
from fig.container import Container
1313
from fig.service import (
14+
APIError,
1415
ConfigError,
15-
split_port,
1616
build_port_bindings,
17-
parse_volume_spec,
1817
build_volume_binding,
19-
APIError,
18+
get_container_data_volumes,
19+
get_volume_bindings,
2020
parse_repository_tag,
21+
parse_volume_spec,
22+
split_port,
2123
)
2224

2325

@@ -57,13 +59,6 @@ def test_get_volumes_from_container(self):
5759

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

60-
def test_get_volumes_from_intermediate_container(self):
61-
container_id = 'aabbccddee'
62-
service = Service('test')
63-
container = mock.Mock(id=container_id, spec=Container)
64-
65-
self.assertEqual(service._get_volumes_from(container), [container_id])
66-
6762
def test_get_volumes_from_service_container_exists(self):
6863
container_ids = ['aabbccddee', '12345']
6964
from_service = mock.create_autospec(Service)
@@ -288,6 +283,50 @@ def test_building_volume_binding_with_home(self):
288283
binding,
289284
('/home/user', dict(bind='/home/user', ro=False)))
290285

286+
def test_get_container_data_volumes(self):
287+
options = [
288+
'/host/volume:/host/volume:ro',
289+
'/new/volume',
290+
'/existing/volume',
291+
]
292+
293+
container = Container(None, {
294+
'Volumes': {
295+
'/host/volume': '/host/volume',
296+
'/existing/volume': '/var/lib/docker/aaaaaaaa',
297+
'/removed/volume': '/var/lib/docker/bbbbbbbb',
298+
},
299+
}, has_been_inspected=True)
300+
301+
expected = {
302+
'/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False},
303+
}
304+
305+
binds = get_container_data_volumes(container, options)
306+
self.assertEqual(binds, expected)
307+
308+
def test_get_volume_bindings(self):
309+
options = [
310+
'/host/volume:/host/volume:ro',
311+
'/host/rw/volume:/host/rw/volume',
312+
'/new/volume',
313+
'/existing/volume',
314+
]
315+
316+
intermediate_container = Container(None, {
317+
'Volumes': {'/existing/volume': '/var/lib/docker/aaaaaaaa'},
318+
}, has_been_inspected=True)
319+
320+
expected = {
321+
'/host/volume': {'bind': '/host/volume', 'ro': True},
322+
'/host/rw/volume': {'bind': '/host/rw/volume', 'ro': False},
323+
'/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False},
324+
}
325+
326+
binds = get_volume_bindings(options, intermediate_container)
327+
self.assertEqual(binds, expected)
328+
329+
291330
class ServiceEnvironmentTest(unittest.TestCase):
292331

293332
def setUp(self):

0 commit comments

Comments
 (0)