From 3c76d5a46770b68910223456226a5353ce2f51c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Seguin?= Date: Mon, 14 Dec 2015 22:46:13 +0100 Subject: [PATCH] Add docker-compose create command. Closes #1125 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Stéphane Seguin --- compose/cli/main.py | 22 +++++++++++ compose/project.py | 19 +++++++-- compose/service.py | 20 ++++++---- tests/acceptance/cli_test.py | 46 ++++++++++++++++++++++ tests/integration/project_test.py | 65 +++++++++++++++++++++++++++++++ tests/integration/service_test.py | 18 +++++++++ 6 files changed, 179 insertions(+), 11 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index f30ea3340..a98795e30 100644 --- a/compose/cli/main.py +++ b/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. diff --git a/compose/project.py b/compose/project.py index 84413174e..d40468562 100644 --- a/compose/project.py +++ b/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) diff --git a/compose/service.py b/compose/service.py index 0387b6e99..791e57aef 100644 --- a/compose/service.py +++ b/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 diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 666196293..032b507d7 100644 --- a/tests/acceptance/cli_test.py +++ b/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') diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 443ff9783..229f653a4 100644 --- a/tests/integration/project_test.py +++ b/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')]) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 5a809423a..84ef696e7 100644 --- a/tests/integration/service_test.py +++ b/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'})