From ced94a35045f0fef9dfa707769539c3c3692e385 Mon Sep 17 00:00:00 2001
From: Aanand Prasad <aanand.prasad@gmail.com>
Date: Wed, 15 Jul 2015 16:19:39 +0100
Subject: [PATCH] Make smart recreate the default

Add --force-recreate flag to enable the old default behaviour of
recreating everything.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
---
 compose/cli/main.py                  | 35 +++++++++++++++++-----------
 compose/project.py                   | 15 +++++++-----
 compose/service.py                   | 23 ++++++++++--------
 docs/cli.md                          | 17 +++++++++-----
 tests/integration/cli_test.py        | 11 ++++++---
 tests/integration/project_test.py    |  4 ++--
 tests/integration/resilience_test.py |  6 ++---
 tests/integration/state_test.py      | 19 +++++++--------
 8 files changed, 77 insertions(+), 53 deletions(-)

diff --git a/compose/cli/main.py b/compose/cli/main.py
index bfe389432..58b225307 100644
--- a/compose/cli/main.py
+++ b/compose/cli/main.py
@@ -429,17 +429,22 @@ class TopLevelCommand(Command):
 
     def up(self, project, options):
         """
-        Build, (re)create, start and attach to containers for a service.
+        Builds, (re)creates, starts, and attaches to containers for a service.
 
-        By default, `docker-compose up` will aggregate the output of each container, and
-        when it exits, all containers will be stopped. If you run `docker-compose up -d`,
-        it'll start the containers in the background and leave them running.
+        Unless they are already running, this command also starts any linked services.
 
-        If there are existing containers for a service, `docker-compose up` will stop
-        and recreate them (preserving mounted volumes with volumes-from),
-        so that changes in `docker-compose.yml` are picked up. If you do not want existing
-        containers to be recreated, `docker-compose up --no-recreate` will re-use existing
-        containers.
+        The `docker-compose up` command aggregates the output of each container. When
+        the command exits, all containers are stopped. Running `docker-compose up -d`
+        starts the containers in the background and leaves them running.
+
+        If there are existing containers for a service, and the service's configuration
+        or image was changed after the container's creation, `docker-compose up` picks
+        up the changes by stopping and recreating the containers (preserving mounted
+        volumes). To prevent Compose from picking up changes, use the `--no-recreate`
+        flag.
+
+        If you want to force Compose to stop and recreate all containers, use the
+        `--force-recreate` flag.
 
         Usage: up [options] [SERVICE...]
 
@@ -450,9 +455,10 @@ class TopLevelCommand(Command):
                                    print new container names.
             --no-color             Produce monochrome output.
             --no-deps              Don't start linked services.
-            --x-smart-recreate     Only recreate containers whose configuration or
-                                   image needs to be updated. (EXPERIMENTAL)
+            --force-recreate       Recreate containers even if their configuration and
+                                   image haven't changed. Incompatible with --no-recreate.
             --no-recreate          If containers already exist, don't recreate them.
+                                   Incompatible with --force-recreate.
             --no-build             Don't build an image, even if it's missing
             -t, --timeout TIMEOUT  Use this timeout in seconds for container shutdown
                                    when attached or when containers are already
@@ -465,15 +471,18 @@ class TopLevelCommand(Command):
 
         start_deps = not options['--no-deps']
         allow_recreate = not options['--no-recreate']
-        smart_recreate = options['--x-smart-recreate']
+        force_recreate = options['--force-recreate']
         service_names = options['SERVICE']
         timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
 
+        if force_recreate and not allow_recreate:
+            raise UserError("--force-recreate and --no-recreate cannot be combined.")
+
         to_attach = project.up(
             service_names=service_names,
             start_deps=start_deps,
             allow_recreate=allow_recreate,
-            smart_recreate=smart_recreate,
+            force_recreate=force_recreate,
             insecure_registry=insecure_registry,
             do_build=not options['--no-build'],
             timeout=timeout
diff --git a/compose/project.py b/compose/project.py
index 7928316a6..541ff3ff8 100644
--- a/compose/project.py
+++ b/compose/project.py
@@ -223,11 +223,14 @@ class Project(object):
            service_names=None,
            start_deps=True,
            allow_recreate=True,
-           smart_recreate=False,
+           force_recreate=False,
            insecure_registry=False,
            do_build=True,
            timeout=DEFAULT_TIMEOUT):
 
+        if force_recreate and not allow_recreate:
+            raise ValueError("force_recreate and allow_recreate are in conflict")
+
         services = self.get_services(service_names, include_deps=start_deps)
 
         for service in services:
@@ -236,7 +239,7 @@ class Project(object):
         plans = self._get_convergence_plans(
             services,
             allow_recreate=allow_recreate,
-            smart_recreate=smart_recreate,
+            force_recreate=force_recreate,
         )
 
         return [
@@ -253,7 +256,7 @@ class Project(object):
     def _get_convergence_plans(self,
                                services,
                                allow_recreate=True,
-                               smart_recreate=False):
+                               force_recreate=False):
 
         plans = {}
 
@@ -265,19 +268,19 @@ class Project(object):
                 and plans[name].action == 'recreate'
             ]
 
-            if updated_dependencies:
+            if updated_dependencies and allow_recreate:
                 log.debug(
                     '%s has upstream changes (%s)',
                     service.name, ", ".join(updated_dependencies),
                 )
                 plan = service.convergence_plan(
                     allow_recreate=allow_recreate,
-                    smart_recreate=False,
+                    force_recreate=True,
                 )
             else:
                 plan = service.convergence_plan(
                     allow_recreate=allow_recreate,
-                    smart_recreate=smart_recreate,
+                    force_recreate=force_recreate,
                 )
 
             plans[service.name] = plan
diff --git a/compose/service.py b/compose/service.py
index d10d563e4..a488b2c68 100644
--- a/compose/service.py
+++ b/compose/service.py
@@ -264,25 +264,28 @@ class Service(object):
 
     def convergence_plan(self,
                          allow_recreate=True,
-                         smart_recreate=False):
+                         force_recreate=False):
+
+        if force_recreate and not allow_recreate:
+            raise ValueError("force_recreate and allow_recreate are in conflict")
 
         containers = self.containers(stopped=True)
 
         if not containers:
             return ConvergencePlan('create', [])
 
-        if smart_recreate and not self._containers_have_diverged(containers):
-            stopped = [c for c in containers if not c.is_running]
-
-            if stopped:
-                return ConvergencePlan('start', stopped)
-
-            return ConvergencePlan('noop', containers)
-
         if not allow_recreate:
             return ConvergencePlan('start', containers)
 
-        return ConvergencePlan('recreate', containers)
+        if force_recreate or self._containers_have_diverged(containers):
+            return ConvergencePlan('recreate', containers)
+
+        stopped = [c for c in containers if not c.is_running]
+
+        if stopped:
+            return ConvergencePlan('start', stopped)
+
+        return ConvergencePlan('noop', containers)
 
     def _containers_have_diverged(self, containers):
         config_hash = None
diff --git a/docs/cli.md b/docs/cli.md
index 6b04f21d8..43cf61c52 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -127,15 +127,20 @@ Stops running containers without removing them. They can be started again with
 
 Builds, (re)creates, starts, and attaches to containers for a service.
 
-Linked services will be started, unless they are already running.
+Unless they are already running, this command also starts any linked services.
 
-By default, `docker-compose up` will aggregate the output of each container and,
-when it exits, all containers will be stopped. Running `docker-compose up -d`,
-will start the containers in the background and leave them running.
+The `docker-compose up` command aggregates the output of each container. When
+the command exits, all containers are stopped. Running `docker-compose up -d`
+starts the containers in the background and leaves them running.
 
-By default, if there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `docker-compose.yml` are picked up. If you do not want containers stopped and recreated, use `docker-compose up --no-recreate`. This will still start any stopped containers, if needed.
+If there are existing containers for a service, and the service's configuration
+or image was changed after the container's creation, `docker-compose up` picks
+up the changes by stopping and recreating the containers (preserving mounted
+volumes). To prevent Compose from picking up changes, use the `--no-recreate`
+flag.
 
-[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
+If you want to force Compose to stop and recreate all containers, use the
+`--force-recreate` flag.
 
 ## Options
 
diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py
index c35978152..f3b3b9f5f 100644
--- a/tests/integration/cli_test.py
+++ b/tests/integration/cli_test.py
@@ -9,6 +9,7 @@ from mock import patch
 
 from .testcases import DockerClientTestCase
 from compose.cli.main import TopLevelCommand
+from compose.cli.errors import UserError
 from compose.project import NoSuchService
 
 
@@ -136,21 +137,21 @@ class CLITestCase(DockerClientTestCase):
         self.assertEqual(len(db.containers()), 0)
         self.assertEqual(len(console.containers()), 0)
 
-    def test_up_with_recreate(self):
+    def test_up_with_force_recreate(self):
         self.command.dispatch(['up', '-d'], None)
         service = self.project.get_service('simple')
         self.assertEqual(len(service.containers()), 1)
 
         old_ids = [c.id for c in service.containers()]
 
-        self.command.dispatch(['up', '-d'], None)
+        self.command.dispatch(['up', '-d', '--force-recreate'], None)
         self.assertEqual(len(service.containers()), 1)
 
         new_ids = [c.id for c in service.containers()]
 
         self.assertNotEqual(old_ids, new_ids)
 
-    def test_up_with_keep_old(self):
+    def test_up_with_no_recreate(self):
         self.command.dispatch(['up', '-d'], None)
         service = self.project.get_service('simple')
         self.assertEqual(len(service.containers()), 1)
@@ -164,6 +165,10 @@ class CLITestCase(DockerClientTestCase):
 
         self.assertEqual(old_ids, new_ids)
 
+    def test_up_with_force_recreate_and_no_recreate(self):
+        with self.assertRaises(UserError):
+            self.command.dispatch(['up', '-d', '--force-recreate', '--no-recreate'], None)
+
     def test_up_with_timeout(self):
         self.command.dispatch(['up', '-d', '-t', '1'], None)
         service = self.project.get_service('simple')
diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py
index 195940423..9788c1861 100644
--- a/tests/integration/project_test.py
+++ b/tests/integration/project_test.py
@@ -197,7 +197,7 @@ class ProjectTest(DockerClientTestCase):
         self.assertEqual(len(db.containers()), 1)
         self.assertEqual(len(web.containers()), 1)
 
-    def test_project_up_recreates_containers(self):
+    def test_recreate_preserves_volumes(self):
         web = self.create_service('web')
         db = self.create_service('db', volumes=['/etc'])
         project = Project('composetest', [web, db], self.client)
@@ -209,7 +209,7 @@ class ProjectTest(DockerClientTestCase):
         old_db_id = project.containers()[0].id
         db_volume_path = project.containers()[0].get('Volumes./etc')
 
-        project.up()
+        project.up(force_recreate=True)
         self.assertEqual(len(project.containers()), 2)
 
         db_container = [c for c in project.containers() if 'db' in c.name][0]
diff --git a/tests/integration/resilience_test.py b/tests/integration/resilience_test.py
index 392490902..e0c76f299 100644
--- a/tests/integration/resilience_test.py
+++ b/tests/integration/resilience_test.py
@@ -17,14 +17,14 @@ class ResilienceTest(DockerClientTestCase):
         self.host_path = container.get('Volumes')['/var/db']
 
     def test_successful_recreate(self):
-        self.project.up()
+        self.project.up(force_recreate=True)
         container = self.db.containers()[0]
         self.assertEqual(container.get('Volumes')['/var/db'], self.host_path)
 
     def test_create_failure(self):
         with mock.patch('compose.service.Service.create_container', crash):
             with self.assertRaises(Crash):
-                self.project.up()
+                self.project.up(force_recreate=True)
 
         self.project.up()
         container = self.db.containers()[0]
@@ -33,7 +33,7 @@ class ResilienceTest(DockerClientTestCase):
     def test_start_failure(self):
         with mock.patch('compose.service.Service.start_container', crash):
             with self.assertRaises(Crash):
-                self.project.up()
+                self.project.up(force_recreate=True)
 
         self.project.up()
         container = self.db.containers()[0]
diff --git a/tests/integration/state_test.py b/tests/integration/state_test.py
index ce7c0d81b..63027586e 100644
--- a/tests/integration/state_test.py
+++ b/tests/integration/state_test.py
@@ -12,7 +12,6 @@ from .testcases import DockerClientTestCase
 
 class ProjectTestCase(DockerClientTestCase):
     def run_up(self, cfg, **kwargs):
-        kwargs.setdefault('smart_recreate', True)
         kwargs.setdefault('timeout', 1)
 
         project = self.make_project(cfg)
@@ -155,7 +154,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
 
 def converge(service,
              allow_recreate=True,
-             smart_recreate=False,
+             force_recreate=False,
              insecure_registry=False,
              do_build=True):
     """
@@ -164,7 +163,7 @@ def converge(service,
     """
     plan = service.convergence_plan(
         allow_recreate=allow_recreate,
-        smart_recreate=smart_recreate,
+        force_recreate=force_recreate,
     )
 
     return service.execute_convergence_plan(
@@ -180,7 +179,7 @@ class ServiceStateTest(DockerClientTestCase):
 
     def test_trigger_create(self):
         web = self.create_service('web')
-        self.assertEqual(('create', []), web.convergence_plan(smart_recreate=True))
+        self.assertEqual(('create', []), web.convergence_plan())
 
     def test_trigger_noop(self):
         web = self.create_service('web')
@@ -188,7 +187,7 @@ class ServiceStateTest(DockerClientTestCase):
         web.start()
 
         web = self.create_service('web')
-        self.assertEqual(('noop', [container]), web.convergence_plan(smart_recreate=True))
+        self.assertEqual(('noop', [container]), web.convergence_plan())
 
     def test_trigger_start(self):
         options = dict(command=["top"])
@@ -205,7 +204,7 @@ class ServiceStateTest(DockerClientTestCase):
         web = self.create_service('web', **options)
         self.assertEqual(
             ('start', containers[0:1]),
-            web.convergence_plan(smart_recreate=True),
+            web.convergence_plan(),
         )
 
     def test_trigger_recreate_with_config_change(self):
@@ -213,14 +212,14 @@ class ServiceStateTest(DockerClientTestCase):
         container = web.create_container()
 
         web = self.create_service('web', command=["top", "-d", "1"])
-        self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
+        self.assertEqual(('recreate', [container]), web.convergence_plan())
 
     def test_trigger_recreate_with_nonexistent_image_tag(self):
         web = self.create_service('web', image="busybox:latest")
         container = web.create_container()
 
         web = self.create_service('web', image="nonexistent-image")
-        self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
+        self.assertEqual(('recreate', [container]), web.convergence_plan())
 
     def test_trigger_recreate_with_image_change(self):
         repo = 'composetest_myimage'
@@ -240,7 +239,7 @@ class ServiceStateTest(DockerClientTestCase):
             self.client.remove_container(c)
 
             web = self.create_service('web', image=image)
-            self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
+            self.assertEqual(('recreate', [container]), web.convergence_plan())
 
         finally:
             self.client.remove_image(image)
@@ -263,7 +262,7 @@ class ServiceStateTest(DockerClientTestCase):
             web.build()
 
             web = self.create_service('web', build=context)
-            self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
+            self.assertEqual(('recreate', [container]), web.convergence_plan())
         finally:
             shutil.rmtree(context)