소스 검색

Add docker-compose create command.
Closes #1125

Signed-off-by: Stéphane Seguin <[email protected]>

Stéphane Seguin 9 년 전
부모
커밋
3c76d5a467
6개의 변경된 파일179개의 추가작업 그리고 11개의 파일을 삭제
  1. 22 0
      compose/cli/main.py
  2. 15 4
      compose/project.py
  3. 13 7
      compose/service.py
  4. 46 0
      tests/acceptance/cli_test.py
  5. 65 0
      tests/integration/project_test.py
  6. 18 0
      tests/integration/service_test.py

+ 22 - 0
compose/cli/main.py

@@ -130,6 +130,7 @@ class TopLevelCommand(DocoptCommand):
     Commands:
       build              Build or rebuild services
       config             Validate and view the compose file
+      create             Create services
       help               Get help on a command
       kill               Kill containers
       logs               View output from containers
@@ -221,6 +222,27 @@ class TopLevelCommand(DocoptCommand):
             indent=2,
             width=80))
 
+    def create(self, project, options):
+        """
+        Creates containers for a service.
+
+        Usage: create [options] [SERVICE...]
+
+        Options:
+            --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
+        """
+        service_names = options['SERVICE']
+
+        project.create(
+            service_names=service_names,
+            strategy=convergence_strategy_from_opts(options),
+            do_build=not options['--no-build']
+        )
+
     def help(self, project, options):
         """
         Get help on a command.

+ 15 - 4
compose/project.py

@@ -123,6 +123,12 @@ class Project(object):
             [uniques.append(s) for s in services if s not in uniques]
             return uniques
 
+    def get_services_without_duplicate(self, service_names=None, include_deps=False):
+        services = self.get_services(service_names, include_deps)
+        for service in services:
+            service.remove_duplicate_containers()
+        return services
+
     def get_links(self, service_dict):
         links = []
         if 'links' in service_dict:
@@ -215,6 +221,14 @@ class Project(object):
             else:
                 log.info('%s uses an image, skipping' % service.name)
 
+    def create(self, service_names=None, strategy=ConvergenceStrategy.changed, do_build=True):
+        services = self.get_services_without_duplicate(service_names, include_deps=True)
+
+        plans = self._get_convergence_plans(services, strategy)
+
+        for service in services:
+            service.execute_convergence_plan(plans[service.name], do_build, detached=True, start=False)
+
     def up(self,
            service_names=None,
            start_deps=True,
@@ -223,10 +237,7 @@ class Project(object):
            timeout=DEFAULT_TIMEOUT,
            detached=False):
 
-        services = self.get_services(service_names, include_deps=start_deps)
-
-        for service in services:
-            service.remove_duplicate_containers()
+        services = self.get_services_without_duplicate(service_names, include_deps=start_deps)
 
         plans = self._get_convergence_plans(services, strategy)
 

+ 13 - 7
compose/service.py

@@ -328,7 +328,8 @@ class Service(object):
                                  plan,
                                  do_build=True,
                                  timeout=DEFAULT_TIMEOUT,
-                                 detached=False):
+                                 detached=False,
+                                 start=True):
         (action, containers) = plan
         should_attach_logs = not detached
 
@@ -338,7 +339,8 @@ class Service(object):
             if should_attach_logs:
                 container.attach_log_stream()
 
-            container.start()
+            if start:
+                container.start()
 
             return [container]
 
@@ -348,14 +350,16 @@ class Service(object):
                     container,
                     do_build=do_build,
                     timeout=timeout,
-                    attach_logs=should_attach_logs
+                    attach_logs=should_attach_logs,
+                    start_new_container=start
                 )
                 for container in containers
             ]
 
         elif action == 'start':
-            for container in containers:
-                self.start_container_if_stopped(container, attach_logs=should_attach_logs)
+            if start:
+                for container in containers:
+                    self.start_container_if_stopped(container, attach_logs=should_attach_logs)
 
             return containers
 
@@ -373,7 +377,8 @@ class Service(object):
             container,
             do_build=False,
             timeout=DEFAULT_TIMEOUT,
-            attach_logs=False):
+            attach_logs=False,
+            start_new_container=True):
         """Recreate a container.
 
         The original container is renamed to a temporary name so that data
@@ -392,7 +397,8 @@ class Service(object):
         )
         if attach_logs:
             new_container.attach_log_stream()
-        new_container.start()
+        if start_new_container:
+            new_container.start()
         container.remove()
         return new_container
 

+ 46 - 0
tests/acceptance/cli_test.py

@@ -264,6 +264,52 @@ class CLITestCase(DockerClientTestCase):
         ]
         assert not containers
 
+    def test_create(self):
+        self.dispatch(['create'])
+        service = self.project.get_service('simple')
+        another = self.project.get_service('another')
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(another.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+        self.assertEqual(len(another.containers(stopped=True)), 1)
+
+    def test_create_with_force_recreate(self):
+        self.dispatch(['create'], None)
+        service = self.project.get_service('simple')
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
+        old_ids = [c.id for c in service.containers(stopped=True)]
+
+        self.dispatch(['create', '--force-recreate'], None)
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
+        new_ids = [c.id for c in service.containers(stopped=True)]
+
+        self.assertNotEqual(old_ids, new_ids)
+
+    def test_create_with_no_recreate(self):
+        self.dispatch(['create'], None)
+        service = self.project.get_service('simple')
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
+        old_ids = [c.id for c in service.containers(stopped=True)]
+
+        self.dispatch(['create', '--no-recreate'], None)
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
+        new_ids = [c.id for c in service.containers(stopped=True)]
+
+        self.assertEqual(old_ids, new_ids)
+
+    def test_create_with_force_recreate_and_no_recreate(self):
+        self.dispatch(
+            ['create', '--force-recreate', '--no-recreate'],
+            returncode=1)
+
     def test_up_detached(self):
         self.dispatch(['up', '-d'])
         service = self.project.get_service('simple')

+ 65 - 0
tests/integration/project_test.py

@@ -213,6 +213,71 @@ class ProjectTest(DockerClientTestCase):
         project.remove_stopped()
         self.assertEqual(len(project.containers(stopped=True)), 0)
 
+    def test_create(self):
+        web = self.create_service('web')
+        db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
+        project = Project('composetest', [web, db], self.client)
+
+        project.create(['db'])
+        self.assertEqual(len(project.containers()), 0)
+        self.assertEqual(len(project.containers(stopped=True)), 1)
+        self.assertEqual(len(db.containers()), 0)
+        self.assertEqual(len(db.containers(stopped=True)), 1)
+        self.assertEqual(len(web.containers(stopped=True)), 0)
+
+    def test_create_twice(self):
+        web = self.create_service('web')
+        db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
+        project = Project('composetest', [web, db], self.client)
+
+        project.create(['db', 'web'])
+        project.create(['db', 'web'])
+        self.assertEqual(len(project.containers()), 0)
+        self.assertEqual(len(project.containers(stopped=True)), 2)
+        self.assertEqual(len(db.containers()), 0)
+        self.assertEqual(len(db.containers(stopped=True)), 1)
+        self.assertEqual(len(web.containers()), 0)
+        self.assertEqual(len(web.containers(stopped=True)), 1)
+
+    def test_create_with_links(self):
+        db = self.create_service('db')
+        web = self.create_service('web', links=[(db, 'db')])
+        project = Project('composetest', [db, web], self.client)
+
+        project.create(['web'])
+        self.assertEqual(len(project.containers()), 0)
+        self.assertEqual(len(project.containers(stopped=True)), 2)
+        self.assertEqual(len(db.containers()), 0)
+        self.assertEqual(len(db.containers(stopped=True)), 1)
+        self.assertEqual(len(web.containers()), 0)
+        self.assertEqual(len(web.containers(stopped=True)), 1)
+
+    def test_create_strategy_always(self):
+        db = self.create_service('db')
+        project = Project('composetest', [db], self.client)
+        project.create(['db'])
+        old_id = project.containers(stopped=True)[0].id
+
+        project.create(['db'], strategy=ConvergenceStrategy.always)
+        self.assertEqual(len(project.containers()), 0)
+        self.assertEqual(len(project.containers(stopped=True)), 1)
+
+        db_container = project.containers(stopped=True)[0]
+        self.assertNotEqual(db_container.id, old_id)
+
+    def test_create_strategy_never(self):
+        db = self.create_service('db')
+        project = Project('composetest', [db], self.client)
+        project.create(['db'])
+        old_id = project.containers(stopped=True)[0].id
+
+        project.create(['db'], strategy=ConvergenceStrategy.never)
+        self.assertEqual(len(project.containers()), 0)
+        self.assertEqual(len(project.containers(stopped=True)), 1)
+
+        db_container = project.containers(stopped=True)[0]
+        self.assertEqual(db_container.id, old_id)
+
     def test_project_up(self):
         web = self.create_service('web')
         db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])

+ 18 - 0
tests/integration/service_test.py

@@ -333,6 +333,24 @@ class ServiceTest(DockerClientTestCase):
         self.assertEqual(list(new_container.get('Volumes')), ['/data'])
         self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
 
+    def test_execute_convergence_plan_without_start(self):
+        service = self.create_service(
+            'db',
+            build='tests/fixtures/dockerfile-with-volume'
+        )
+
+        containers = service.execute_convergence_plan(ConvergencePlan('create', []), start=False)
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
+        containers = service.execute_convergence_plan(ConvergencePlan('recreate', containers), start=False)
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
+        service.execute_convergence_plan(ConvergencePlan('start', containers), start=False)
+        self.assertEqual(len(service.containers()), 0)
+        self.assertEqual(len(service.containers(stopped=True)), 1)
+
     def test_start_container_passes_through_options(self):
         db = self.create_service('db')
         create_and_start_container(db, environment={'FOO': 'BAR'})