Skip to content

Commit 55095ef

Browse files
committed
Merge pull request #711 from dnephin/fix_volumes_on_recreate
Fix volumes on recreate
2 parents 72095f5 + 7eb476e commit 55095ef

4 files changed

Lines changed: 142 additions & 64 deletions

File tree

fig/project.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -176,18 +176,14 @@ def up(self,
176176
do_build=True):
177177
running_containers = []
178178
for service in self.get_services(service_names, include_links=start_links):
179-
if recreate:
180-
for (_, container) in service.recreate_containers(
181-
insecure_registry=insecure_registry,
182-
detach=detach,
183-
do_build=do_build):
184-
running_containers.append(container)
185-
else:
186-
for container in service.start_or_create_containers(
187-
insecure_registry=insecure_registry,
188-
detach=detach,
189-
do_build=do_build):
190-
running_containers.append(container)
179+
create_func = (service.recreate_containers if recreate
180+
else service.start_or_create_containers)
181+
182+
for container in create_func(
183+
insecure_registry=insecure_registry,
184+
detach=detach,
185+
do_build=do_build):
186+
running_containers.append(container)
191187

192188
return running_containers
193189

fig/service.py

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,9 @@ def create_container(self,
242242

243243
def recreate_containers(self, insecure_registry=False, do_build=True, **override_options):
244244
"""
245-
If a container for this service doesn't exist, create and start one. If there are
246-
any, stop them, create+start new ones, and remove the old containers.
245+
If a container for this service doesn't exist, create and start one. If
246+
there are any, stop them, create+start new ones, and remove the old
247+
containers.
247248
"""
248249
containers = self.containers(stopped=True)
249250
if not containers:
@@ -253,21 +254,22 @@ def recreate_containers(self, insecure_registry=False, do_build=True, **override
253254
do_build=do_build,
254255
**override_options)
255256
self.start_container(container)
256-
return [(None, container)]
257+
return [container]
257258
else:
258-
tuples = []
259-
260-
for c in containers:
261-
log.info("Recreating %s..." % c.name)
262-
tuples.append(self.recreate_container(c, insecure_registry=insecure_registry, **override_options))
263-
264-
return tuples
259+
return [
260+
self.recreate_container(
261+
container,
262+
insecure_registry=insecure_registry,
263+
**override_options)
264+
for container in containers
265+
]
265266

266267
def recreate_container(self, container, **override_options):
267268
"""Recreate a container. An intermediate container is created so that
268269
the new container has the same name, while still supporting
269270
`volumes-from` the original container.
270271
"""
272+
log.info("Recreating %s..." % container.name)
271273
try:
272274
container.stop()
273275
except APIError as e:
@@ -278,24 +280,30 @@ def recreate_container(self, container, **override_options):
278280
else:
279281
raise
280282

283+
intermediate_options = dict(self.options, **override_options)
281284
intermediate_container = Container.create(
282285
self.client,
283286
image=container.image,
284287
entrypoint=['/bin/echo'],
285288
command=[],
286289
detach=True,
287290
)
288-
intermediate_container.start(volumes_from=container.id)
291+
intermediate_container.start(
292+
binds=get_container_data_volumes(
293+
container, intermediate_options.get('volumes')))
289294
intermediate_container.wait()
290295
container.remove()
291296

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

296305
intermediate_container.remove()
297-
298-
return (intermediate_container, new_container)
306+
return new_container
299307

300308
def start_container_if_stopped(self, container, **options):
301309
if container.is_running:
@@ -307,12 +315,6 @@ def start_container_if_stopped(self, container, **options):
307315
def start_container(self, container, intermediate_container=None, **override_options):
308316
options = dict(self.options, **override_options)
309317
port_bindings = build_port_bindings(options.get('ports') or [])
310-
311-
volume_bindings = dict(
312-
build_volume_binding(parse_volume_spec(volume))
313-
for volume in options.get('volumes') or []
314-
if ':' in volume)
315-
316318
privileged = options.get('privileged', False)
317319
net = options.get('net', 'bridge')
318320
dns = options.get('dns', None)
@@ -321,12 +323,14 @@ def start_container(self, container, intermediate_container=None, **override_opt
321323
cap_drop = options.get('cap_drop', None)
322324

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

325329
container.start(
326330
links=self._get_links(link_to_self=options.get('one_off', False)),
327331
port_bindings=port_bindings,
328-
binds=volume_bindings,
329-
volumes_from=self._get_volumes_from(intermediate_container),
332+
binds=binds,
333+
volumes_from=self._get_volumes_from(),
330334
privileged=privileged,
331335
network_mode=net,
332336
dns=dns,
@@ -388,7 +392,7 @@ def _get_links(self, link_to_self):
388392
links.append((external_link, link_name))
389393
return links
390394

391-
def _get_volumes_from(self, intermediate_container=None):
395+
def _get_volumes_from(self):
392396
volumes_from = []
393397
for volume_source in self.volumes_from:
394398
if isinstance(volume_source, Service):
@@ -402,9 +406,6 @@ def _get_volumes_from(self, intermediate_container=None):
402406
elif isinstance(volume_source, Container):
403407
volumes_from.append(volume_source.id)
404408

405-
if intermediate_container:
406-
volumes_from.append(intermediate_container.id)
407-
408409
return volumes_from
409410

410411
def _get_container_create_options(self, override_options, one_off=False):
@@ -519,6 +520,45 @@ def pull(self, insecure_registry=False):
519520
)
520521

521522

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+
522562
NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$')
523563

524564

tests/integration/service_test.py

Lines changed: 19 additions & 16 deletions
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)
@@ -148,30 +148,23 @@ def test_recreate_containers(self):
148148
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
149149
self.assertEqual(old_container.name, 'figtest_db_1')
150150
service.start_container(old_container)
151-
volume_path = old_container.inspect()['Volumes']['/etc']
151+
volume_path = old_container.get('Volumes')['/etc']
152152

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

155155
service.options['environment']['FOO'] = '2'
156-
tuples = service.recreate_containers()
157-
self.assertEqual(len(tuples), 1)
156+
containers = service.recreate_containers()
157+
self.assertEqual(len(containers), 1)
158158

159-
intermediate_container = tuples[0][0]
160-
new_container = tuples[0][1]
161-
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['/bin/echo'])
162-
163-
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep'])
164-
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300'])
165-
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
159+
new_container = containers[0]
160+
self.assertEqual(new_container.get('Config.Entrypoint'), ['sleep'])
161+
self.assertEqual(new_container.get('Config.Cmd'), ['300'])
162+
self.assertIn('FOO=2', new_container.get('Config.Env'))
166163
self.assertEqual(new_container.name, 'figtest_db_1')
167-
self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path)
168-
self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom'])
164+
self.assertEqual(new_container.get('Volumes')['/etc'], volume_path)
169165

170166
self.assertEqual(len(self.client.containers(all=True)), num_containers_before)
171167
self.assertNotEqual(old_container.id, new_container.id)
172-
self.assertRaises(APIError,
173-
self.client.inspect_container,
174-
intermediate_container.id)
175168

176169
def test_recreate_containers_when_containers_are_stopped(self):
177170
service = self.create_service(
@@ -186,6 +179,16 @@ def test_recreate_containers_when_containers_are_stopped(self):
186179
service.recreate_containers()
187180
self.assertEqual(len(service.containers(stopped=True)), 1)
188181

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+
189192
def test_start_container_passes_through_options(self):
190193
db = self.create_service('db')
191194
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)