From b07091ac5f5ed8657909a30d26fff680fb765e73 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 22 Jan 2018 15:59:52 -0800 Subject: [PATCH] Add up flag `--renew-anon-volumes` (shorthand -V) to avoid reusing the previous container's data Signed-off-by: Joffrey F --- compose/cli/main.py | 15 ++++-- compose/project.py | 6 ++- compose/service.py | 15 +++--- tests/integration/service_test.py | 77 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index de4fbd4306..380257dbfe 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -936,7 +936,7 @@ def up(self, options): --always-recreate-deps Recreate dependent containers. Incompatible with --no-recreate. --no-recreate If containers already exist, don't recreate - them. Incompatible with --force-recreate. + them. Incompatible with --force-recreate and -V. --no-build Don't build an image, even if it's missing. --no-start Don't start the services after creating them. --build Build images before starting containers. @@ -945,8 +945,10 @@ def up(self, options): -t, --timeout TIMEOUT Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10) - --remove-orphans Remove containers for services not - defined in the Compose file + -V, --renew-anon-volumes Recreate anonymous volumes instead of retrieving + data from the previous containers. + --remove-orphans Remove containers for services not defined + in the Compose file. --exit-code-from SERVICE Return the exit code of the selected service container. Implies --abort-on-container-exit. --scale SERVICE=NUM Scale SERVICE to NUM instances. Overrides the @@ -992,6 +994,7 @@ def up(rebuild): start=not no_start, always_recreate_deps=always_recreate_deps, reset_container_image=rebuild, + renew_anonymous_volumes=options.get('--renew-anon-volumes') ) try: @@ -1083,10 +1086,14 @@ def compute_exit_code(exit_value_from, attached_containers, cascade_starter, all def convergence_strategy_from_opts(options): no_recreate = options['--no-recreate'] force_recreate = options['--force-recreate'] + renew_anonymous_volumes = options.get('--renew-anon-volumes') if force_recreate and no_recreate: raise UserError("--force-recreate and --no-recreate cannot be combined.") - if force_recreate: + if no_recreate and renew_anonymous_volumes: + raise UserError('--no-recreate and --renew-anon-volumes cannot be combined.') + + if force_recreate or renew_anonymous_volumes: return ConvergenceStrategy.always if no_recreate: diff --git a/compose/project.py b/compose/project.py index 6af4cd94a6..1880f39ad0 100644 --- a/compose/project.py +++ b/compose/project.py @@ -445,7 +445,8 @@ def up(self, rescale=True, start=True, always_recreate_deps=False, - reset_container_image=False): + reset_container_image=False, + renew_anonymous_volumes=False): self.initialize() if not ignore_orphans: @@ -474,7 +475,8 @@ def do(service): rescale=rescale, start=start, project_services=scaled_services, - reset_container_image=reset_container_image + reset_container_image=reset_container_image, + renew_anonymous_volumes=renew_anonymous_volumes, ) def get_deps(service): diff --git a/compose/service.py b/compose/service.py index 4e23635118..0e147194f7 100644 --- a/compose/service.py +++ b/compose/service.py @@ -409,7 +409,8 @@ def create_and_start(service, n): return containers - def _execute_convergence_recreate(self, containers, scale, timeout, detached, start): + def _execute_convergence_recreate(self, containers, scale, timeout, detached, start, + renew_anonymous_volumes): if scale is not None and len(containers) > scale: self._downscale(containers[scale:], timeout) containers = containers[:scale] @@ -417,7 +418,7 @@ def _execute_convergence_recreate(self, containers, scale, timeout, detached, st def recreate(container): return self.recreate_container( container, timeout=timeout, attach_logs=not detached, - start_new_container=start + start_new_container=start, renew_anonymous_volumes=renew_anonymous_volumes ) containers, errors = parallel_execute( containers, @@ -470,7 +471,7 @@ def stop_and_remove(container): def execute_convergence_plan(self, plan, timeout=None, detached=False, start=True, scale_override=None, rescale=True, project_services=None, - reset_container_image=False): + reset_container_image=False, renew_anonymous_volumes=False): (action, containers) = plan scale = scale_override if scale_override is not None else self.scale_num containers = sorted(containers, key=attrgetter('number')) @@ -495,7 +496,8 @@ def execute_convergence_plan(self, plan, timeout=None, detached=False, for c in containers: c.reset_image(img_id) return self._execute_convergence_recreate( - containers, scale, timeout, detached, start + containers, scale, timeout, detached, start, + renew_anonymous_volumes, ) if action == 'start': @@ -515,7 +517,8 @@ def execute_convergence_plan(self, plan, timeout=None, detached=False, raise Exception("Invalid action: {}".format(action)) - def recreate_container(self, container, timeout=None, attach_logs=False, start_new_container=True): + def recreate_container(self, container, timeout=None, attach_logs=False, start_new_container=True, + renew_anonymous_volumes=False): """Recreate a container. The original container is renamed to a temporary name so that data @@ -526,7 +529,7 @@ def recreate_container(self, container, timeout=None, attach_logs=False, start_n container.stop(timeout=self.stop_timeout(timeout)) container.rename_to_tmp_name() new_container = self.create_container( - previous_container=container, + previous_container=container if not renew_anonymous_volumes else None, number=container.labels.get(LABEL_CONTAINER_NUMBER), quiet=True, ) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index a6efc24a99..e00ae433d3 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -589,6 +589,25 @@ def test_execute_convergence_plan_with_image_declared_volume(self): assert [mount['Destination'] for mount in new_container.get('Mounts')] == ['/data'] assert new_container.get_mount('/data')['Source'] == volume_path + def test_execute_convergence_plan_with_image_declared_volume_renew(self): + service = Service( + project='composetest', + name='db', + client=self.client, + build={'context': 'tests/fixtures/dockerfile-with-volume'}, + ) + + old_container = create_and_start_container(service) + assert [mount['Destination'] for mount in old_container.get('Mounts')] == ['/data'] + volume_path = old_container.get_mount('/data')['Source'] + + new_container, = service.execute_convergence_plan( + ConvergencePlan('recreate', [old_container]), renew_anonymous_volumes=True + ) + + assert [mount['Destination'] for mount in new_container.get('Mounts')] == ['/data'] + assert new_container.get_mount('/data')['Source'] != volume_path + def test_execute_convergence_plan_when_image_volume_masks_config(self): service = self.create_service( 'db', @@ -637,6 +656,64 @@ def test_execute_convergence_plan_when_host_volume_is_removed(self): ) assert new_container.get_mount('/data')['Source'] != host_path + def test_execute_convergence_plan_anonymous_volume_renew(self): + service = self.create_service( + 'db', + image='busybox', + volumes=[VolumeSpec(None, '/data', 'rw')]) + + old_container = create_and_start_container(service) + assert ( + [mount['Destination'] for mount in old_container.get('Mounts')] == + ['/data'] + ) + volume_path = old_container.get_mount('/data')['Source'] + + new_container, = service.execute_convergence_plan( + ConvergencePlan('recreate', [old_container]), + renew_anonymous_volumes=True + ) + + assert ( + [mount['Destination'] for mount in new_container.get('Mounts')] == + ['/data'] + ) + assert new_container.get_mount('/data')['Source'] != volume_path + + def test_execute_convergence_plan_anonymous_volume_recreate_then_renew(self): + service = self.create_service( + 'db', + image='busybox', + volumes=[VolumeSpec(None, '/data', 'rw')]) + + old_container = create_and_start_container(service) + assert ( + [mount['Destination'] for mount in old_container.get('Mounts')] == + ['/data'] + ) + volume_path = old_container.get_mount('/data')['Source'] + + mid_container, = service.execute_convergence_plan( + ConvergencePlan('recreate', [old_container]), + ) + + assert ( + [mount['Destination'] for mount in mid_container.get('Mounts')] == + ['/data'] + ) + assert mid_container.get_mount('/data')['Source'] == volume_path + + new_container, = service.execute_convergence_plan( + ConvergencePlan('recreate', [mid_container]), + renew_anonymous_volumes=True + ) + + assert ( + [mount['Destination'] for mount in new_container.get('Mounts')] == + ['/data'] + ) + assert new_container.get_mount('/data')['Source'] != volume_path + def test_execute_convergence_plan_without_start(self): service = self.create_service( 'db',