Ver código fonte

Merge pull request #246 from d11wtq/feature/auto_start

Feature: `fig up` and `fig run` now start linked containers (closes #31).
Ben Firshman 11 anos atrás
pai
commit
95aa61cfe5

+ 8 - 2
docs/cli.md

@@ -45,7 +45,7 @@ For example:
 
     $ fig run web python manage.py shell
 
-Note that this will not start any services that the command's service links to. So if, for example, your one-off command talks to your database, you will need to run `fig up -d db` first.
+By default, linked services will be started, unless they are already running.
 
 One-off commands are started in new containers with the same config as a normal container for that service, so volumes, links, etc will all be created as expected. The only thing different to a normal container is the command will be overridden with the one specified and no ports will be created in case they collide.
 
@@ -53,6 +53,10 @@ Links are also created between one-off commands and the other containers for tha
 
     $ fig run db /bin/sh -c "psql -h \$DB_1_PORT_5432_TCP_ADDR -U docker"
 
+If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag:
+
+    $ fig run --no-deps web python manage.py shell
+
 ## scale
 
 Set number of containers to run for a service.
@@ -74,8 +78,10 @@ Stop running containers without removing them. They can be started again with `f
 
 Build, (re)create, start and attach to containers for a service.
 
+Linked services will be started, unless they are already running.
+
 By default, `fig up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `fig up -d`, it'll start the containers in the background and leave them running.
 
-If there are existing containers for a service, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up.
+By default if there are existing containers for a service, `fig up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `fig.yml` are picked up. If you do no want containers to be stopped and recreated, use `fig up --no-recreate`. This will still start any stopped containers, if needed.
 
 [volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/

+ 35 - 14
fig/cli/main.py

@@ -202,21 +202,30 @@ class TopLevelCommand(Command):
 
             $ fig run web python manage.py shell
 
-        Note that this will not start any services that the command's service
-        links to. So if, for example, your one-off command talks to your
-        database, you will need to run `fig up -d db` first.
+        By default, linked services will be started, unless they are already
+        running. If you do not want to start linked services, use
+        `fig run --no-deps SERVICE COMMAND [ARGS...]`.
 
         Usage: run [options] SERVICE COMMAND [ARGS...]
 
         Options:
-            -d    Detached mode: Run container in the background, print new
-                  container name
-            -T    Disable pseudo-tty allocation. By default `fig run`
-                  allocates a TTY.
-            --rm  Remove container after run. Ignored in detached mode.
+            -d         Detached mode: Run container in the background, print
+                       new container name.
+            -T         Disable pseudo-tty allocation. By default `fig run`
+                       allocates a TTY.
+            --rm       Remove container after run. Ignored in detached mode.
+            --no-deps  Don't start linked services.
         """
+
         service = self.project.get_service(options['SERVICE'])
 
+        if not options['--no-deps']:
+            self.project.up(
+                service_names=service.get_linked_names(),
+                start_links=True,
+                recreate=False
+            )
+
         tty = True
         if options['-d'] or options['-T'] or not sys.stdin.isatty():
             tty = False
@@ -293,17 +302,29 @@ class TopLevelCommand(Command):
 
         If there are existing containers for a service, `fig up` will stop
         and recreate them (preserving mounted volumes with volumes-from),
-        so that changes in `fig.yml` are picked up.
+        so that changes in `fig.yml` are picked up. If you do not want existing
+        containers to be recreated, `fig up --no-recreate` will re-use existing
+        containers.
 
         Usage: up [options] [SERVICE...]
 
         Options:
-            -d    Detached mode: Run containers in the background, print new
-                  container names
+            -d             Detached mode: Run containers in the background,
+                           print new container names.
+            --no-deps      Don't start linked services.
+            --no-recreate  If containers already exist, don't recreate them.
         """
         detached = options['-d']
 
-        to_attach = self.project.up(service_names=options['SERVICE'])
+        start_links = not options['--no-deps']
+        recreate = not options['--no-recreate']
+        service_names = options['SERVICE']
+
+        to_attach = self.project.up(
+            service_names=service_names,
+            start_links=start_links,
+            recreate=recreate
+        )
 
         if not detached:
             print("Attaching to", list_containers(to_attach))
@@ -313,12 +334,12 @@ class TopLevelCommand(Command):
                 log_printer.run()
             finally:
                 def handler(signal, frame):
-                    self.project.kill(service_names=options['SERVICE'])
+                    self.project.kill(service_names=service_names)
                     sys.exit(0)
                 signal.signal(signal.SIGINT, handler)
 
                 print("Gracefully stopping... (press Ctrl+C again to force)")
-                self.project.stop(service_names=options['SERVICE'])
+                self.project.stop(service_names=service_names)
 
     def _attach_to_container(self, container_id, raw=False):
         socket_in = self.client.attach_socket(container_id, params={'stdin': 1, 'stream': 1})

+ 46 - 14
fig/project.py

@@ -64,6 +64,7 @@ class Project(object):
                         raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name))
 
                 del service_dict['links']
+
             project.services.append(Service(client=client, project=name, links=links, **service_dict))
         return project
 
@@ -88,22 +89,35 @@ class Project(object):
 
         raise NoSuchService(name)
 
-    def get_services(self, service_names=None):
+    def get_services(self, service_names=None, include_links=False):
         """
         Returns a list of this project's services filtered
-        by the provided list of names, or all services if
-        service_names is None or [].
+        by the provided list of names, or all services if service_names is None
+        or [].
+
+        If include_links is specified, returns a list including the links for
+        service_names, in order of dependency.
 
-        Preserves the original order of self.services.
+        Preserves the original order of self.services where possible,
+        reordering as needed to resolve links.
 
-        Raises NoSuchService if any of the named services
-        do not exist.
+        Raises NoSuchService if any of the named services do not exist.
         """
         if service_names is None or len(service_names) == 0:
-            return self.services
+            return self.get_services(
+                service_names=[s.name for s in self.services],
+                include_links=include_links
+            )
         else:
             unsorted = [self.get_service(name) for name in service_names]
-            return [s for s in self.services if s in unsorted]
+            services = [s for s in self.services if s in unsorted]
+
+            if include_links:
+                services = reduce(self._inject_links, services, [])
+
+            uniques = []
+            [uniques.append(s) for s in services if s not in uniques]
+            return uniques
 
     def start(self, service_names=None, **options):
         for service in self.get_services(service_names):
@@ -124,14 +138,18 @@ class Project(object):
             else:
                 log.info('%s uses an image, skipping' % service.name)
 
-    def up(self, service_names=None):
-        new_containers = []
+    def up(self, service_names=None, start_links=True, recreate=True):
+        running_containers = []
 
-        for service in self.get_services(service_names):
-            for (_, new) in service.recreate_containers():
-                new_containers.append(new)
+        for service in self.get_services(service_names, include_links=start_links):
+            if recreate:
+                for (_, container) in service.recreate_containers():
+                    running_containers.append(container)
+            else:
+                for container in service.start_or_create_containers():
+                    running_containers.append(container)
 
-        return new_containers
+        return running_containers
 
     def remove_stopped(self, service_names=None, **options):
         for service in self.get_services(service_names):
@@ -144,6 +162,20 @@ class Project(object):
                 l.append(container)
         return l
 
+    def _inject_links(self, acc, service):
+        linked_names = service.get_linked_names()
+
+        if len(linked_names) > 0:
+            linked_services = self.get_services(
+                service_names=linked_names,
+                include_links=True
+            )
+        else:
+            linked_services = []
+
+        linked_services.append(service)
+        return acc + linked_services
+
 
 class NoSuchService(Exception):
     def __init__(self, name):

+ 21 - 3
fig/service.py

@@ -75,9 +75,7 @@ class Service(object):
 
     def start(self, **options):
         for c in self.containers(stopped=True):
-            if not c.is_running:
-                log.info("Starting %s..." % c.name)
-                self.start_container(c, **options)
+            self.start_container_if_stopped(c, **options)
 
     def stop(self, **options):
         for c in self.containers():
@@ -200,6 +198,13 @@ class Service(object):
 
         return (intermediate_container, new_container)
 
+    def start_container_if_stopped(self, container, **options):
+        if container.is_running:
+            return container
+        else:
+            log.info("Starting %s..." % container.name)
+            return self.start_container(container, **options)
+
     def start_container(self, container=None, volumes_from=None, **override_options):
         if container is None:
             container = self.create_container(**override_options)
@@ -243,6 +248,19 @@ class Service(object):
         )
         return container
 
+    def start_or_create_containers(self):
+        containers = self.containers(stopped=True)
+
+        if len(containers) == 0:
+            log.info("Creating %s..." % self.next_container_name())
+            new_container = self.create_container()
+            return [self.start_container(new_container)]
+        else:
+            return [self.start_container_if_stopped(c) for c in containers]
+
+    def get_linked_names(self):
+        return [s.name for (s, _) in self.links]
+
     def next_container_name(self, one_off=False):
         bits = [self.project, self.name]
         if one_off:

+ 11 - 0
tests/fixtures/links-figfile/fig.yml

@@ -0,0 +1,11 @@
+db:
+  image: busybox:latest
+  command: /bin/sleep 300
+web:
+  image: busybox:latest
+  command: /bin/sleep 300
+  links:
+    - db:db
+console:
+  image: busybox:latest
+  command: /bin/sleep 300

+ 98 - 1
tests/integration/cli_test.py

@@ -1,17 +1,20 @@
-from __future__ import unicode_literals
 from __future__ import absolute_import
 from .testcases import DockerClientTestCase
 from mock import patch
 from fig.cli.main import TopLevelCommand
 from fig.packages.six import StringIO
+import sys
 
 class CLITestCase(DockerClientTestCase):
     def setUp(self):
         super(CLITestCase, self).setUp()
+        self.old_sys_exit = sys.exit
+        sys.exit = lambda code=0: None
         self.command = TopLevelCommand()
         self.command.base_dir = 'tests/fixtures/simple-figfile'
 
     def tearDown(self):
+        sys.exit = self.old_sys_exit
         self.command.project.kill()
         self.command.project.remove_stopped()
 
@@ -43,6 +46,100 @@ class CLITestCase(DockerClientTestCase):
         self.assertNotIn('fig_another_1', output)
         self.assertIn('fig_yetanother_1', output)
 
+    def test_up(self):
+        self.command.dispatch(['up', '-d'], None)
+        service = self.command.project.get_service('simple')
+        another = self.command.project.get_service('another')
+        self.assertEqual(len(service.containers()), 1)
+        self.assertEqual(len(another.containers()), 1)
+
+    def test_up_with_links(self):
+        self.command.base_dir = 'tests/fixtures/links-figfile'
+        self.command.dispatch(['up', '-d', 'web'], None)
+        web = self.command.project.get_service('web')
+        db = self.command.project.get_service('db')
+        console = self.command.project.get_service('console')
+        self.assertEqual(len(web.containers()), 1)
+        self.assertEqual(len(db.containers()), 1)
+        self.assertEqual(len(console.containers()), 0)
+
+    def test_up_with_no_deps(self):
+        self.command.base_dir = 'tests/fixtures/links-figfile'
+        self.command.dispatch(['up', '-d', '--no-deps', 'web'], None)
+        web = self.command.project.get_service('web')
+        db = self.command.project.get_service('db')
+        console = self.command.project.get_service('console')
+        self.assertEqual(len(web.containers()), 1)
+        self.assertEqual(len(db.containers()), 0)
+        self.assertEqual(len(console.containers()), 0)
+
+    def test_up_with_recreate(self):
+        self.command.dispatch(['up', '-d'], None)
+        service = self.command.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.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):
+        self.command.dispatch(['up', '-d'], None)
+        service = self.command.project.get_service('simple')
+        self.assertEqual(len(service.containers()), 1)
+
+        old_ids = [c.id for c in service.containers()]
+
+        self.command.dispatch(['up', '-d', '--no-recreate'], None)
+        self.assertEqual(len(service.containers()), 1)
+
+        new_ids = [c.id for c in service.containers()]
+
+        self.assertEqual(old_ids, new_ids)
+
+
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_run_with_links(self, mock_stdout):
+        mock_stdout.fileno = lambda: 1
+
+        self.command.base_dir = 'tests/fixtures/links-figfile'
+        self.command.dispatch(['run', 'web', '/bin/true'], None)
+        db = self.command.project.get_service('db')
+        console = self.command.project.get_service('console')
+        self.assertEqual(len(db.containers()), 1)
+        self.assertEqual(len(console.containers()), 0)
+
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_run_with_no_deps(self, mock_stdout):
+        mock_stdout.fileno = lambda: 1
+
+        self.command.base_dir = 'tests/fixtures/links-figfile'
+        self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
+        db = self.command.project.get_service('db')
+        self.assertEqual(len(db.containers()), 0)
+
+    @patch('sys.stdout', new_callable=StringIO)
+    def test_run_does_not_recreate_linked_containers(self, mock_stdout):
+        mock_stdout.fileno = lambda: 1
+
+        self.command.base_dir = 'tests/fixtures/links-figfile'
+        self.command.dispatch(['up', '-d', 'db'], None)
+        db = self.command.project.get_service('db')
+        self.assertEqual(len(db.containers()), 1)
+
+        old_ids = [c.id for c in db.containers()]
+
+        self.command.dispatch(['run', 'web', '/bin/true'], None)
+        self.assertEqual(len(db.containers()), 1)
+
+        new_ids = [c.id for c in db.containers()]
+
+        self.assertEqual(old_ids, new_ids)
+
     def test_rm(self):
         service = self.command.project.get_service('simple')
         service.create_container()

+ 116 - 0
tests/integration/project_test.py

@@ -44,6 +44,21 @@ class ProjectTest(DockerClientTestCase):
         project.start()
         self.assertEqual(len(project.containers()), 0)
 
+        project.up(['db'])
+        self.assertEqual(len(project.containers()), 1)
+        self.assertEqual(len(db.containers()), 1)
+        self.assertEqual(len(web.containers()), 0)
+
+        project.kill()
+        project.remove_stopped()
+
+    def test_project_up_recreates_containers(self):
+        web = self.create_service('web')
+        db = self.create_service('db', volumes=['/var/db'])
+        project = Project('figtest', [web, db], self.client)
+        project.start()
+        self.assertEqual(len(project.containers()), 0)
+
         project.up(['db'])
         self.assertEqual(len(project.containers()), 1)
         old_db_id = project.containers()[0].id
@@ -59,6 +74,107 @@ class ProjectTest(DockerClientTestCase):
         project.kill()
         project.remove_stopped()
 
+    def test_project_up_with_no_recreate_running(self):
+        web = self.create_service('web')
+        db = self.create_service('db', volumes=['/var/db'])
+        project = Project('figtest', [web, db], self.client)
+        project.start()
+        self.assertEqual(len(project.containers()), 0)
+
+        project.up(['db'])
+        self.assertEqual(len(project.containers()), 1)
+        old_db_id = project.containers()[0].id
+        db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db']
+
+        project.up(recreate=False)
+        self.assertEqual(len(project.containers()), 2)
+
+        db_container = [c for c in project.containers() if 'db' in c.name][0]
+        self.assertEqual(c.id, old_db_id)
+        self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
+
+        project.kill()
+        project.remove_stopped()
+
+    def test_project_up_with_no_recreate_stopped(self):
+        web = self.create_service('web')
+        db = self.create_service('db', volumes=['/var/db'])
+        project = Project('figtest', [web, db], self.client)
+        project.start()
+        self.assertEqual(len(project.containers()), 0)
+
+        project.up(['db'])
+        project.stop()
+
+        old_containers = project.containers(stopped=True)
+
+        self.assertEqual(len(old_containers), 1)
+        old_db_id = old_containers[0].id
+        db_volume_path = old_containers[0].inspect()['Volumes']['/var/db']
+
+        project.up(recreate=False)
+
+        new_containers = project.containers(stopped=True)
+        self.assertEqual(len(new_containers), 2)
+
+        db_container = [c for c in new_containers if 'db' in c.name][0]
+        self.assertEqual(c.id, old_db_id)
+        self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path)
+
+        project.kill()
+        project.remove_stopped()
+
+    def test_project_up_without_all_services(self):
+        console = self.create_service('console')
+        db = self.create_service('db')
+        project = Project('figtest', [console, db], self.client)
+        project.start()
+        self.assertEqual(len(project.containers()), 0)
+
+        project.up()
+        self.assertEqual(len(project.containers()), 2)
+        self.assertEqual(len(db.containers()), 1)
+        self.assertEqual(len(console.containers()), 1)
+
+        project.kill()
+        project.remove_stopped()
+
+    def test_project_up_starts_links(self):
+        console = self.create_service('console')
+        db = self.create_service('db', volumes=['/var/db'])
+        web = self.create_service('web', links=[(db, 'db')])
+
+        project = Project('figtest', [web, db, console], self.client)
+        project.start()
+        self.assertEqual(len(project.containers()), 0)
+
+        project.up(['web'])
+        self.assertEqual(len(project.containers()), 2)
+        self.assertEqual(len(web.containers()), 1)
+        self.assertEqual(len(db.containers()), 1)
+        self.assertEqual(len(console.containers()), 0)
+
+        project.kill()
+        project.remove_stopped()
+
+    def test_project_up_with_no_deps(self):
+        console = self.create_service('console')
+        db = self.create_service('db', volumes=['/var/db'])
+        web = self.create_service('web', links=[(db, 'db')])
+
+        project = Project('figtest', [web, db, console], self.client)
+        project.start()
+        self.assertEqual(len(project.containers()), 0)
+
+        project.up(['web'], start_links=False)
+        self.assertEqual(len(project.containers()), 1)
+        self.assertEqual(len(web.containers()), 1)
+        self.assertEqual(len(db.containers()), 0)
+        self.assertEqual(len(console.containers()), 0)
+
+        project.kill()
+        project.remove_stopped()
+
     def test_unscale_after_restart(self):
         web = self.create_service('web')
         project = Project('figtest', [web], self.client)

+ 65 - 0
tests/unit/project_test.py

@@ -67,3 +67,68 @@ class ProjectTest(unittest.TestCase):
         )
         project = Project('test', [web], None)
         self.assertEqual(project.get_service('web'), web)
+
+    def test_get_services_returns_all_services_without_args(self):
+        web = Service(
+            project='figtest',
+            name='web',
+        )
+        console = Service(
+            project='figtest',
+            name='console',
+        )
+        project = Project('test', [web, console], None)
+        self.assertEqual(project.get_services(), [web, console])
+
+    def test_get_services_returns_listed_services_with_args(self):
+        web = Service(
+            project='figtest',
+            name='web',
+        )
+        console = Service(
+            project='figtest',
+            name='console',
+        )
+        project = Project('test', [web, console], None)
+        self.assertEqual(project.get_services(['console']), [console])
+
+    def test_get_services_with_include_links(self):
+        db = Service(
+            project='figtest',
+            name='db',
+        )
+        web = Service(
+            project='figtest',
+            name='web',
+            links=[(db, 'database')]
+        )
+        cache = Service(
+            project='figtest',
+            name='cache'
+        )
+        console = Service(
+            project='figtest',
+            name='console',
+            links=[(web, 'web')]
+        )
+        project = Project('test', [web, db, cache, console], None)
+        self.assertEqual(
+            project.get_services(['console'], include_links=True),
+            [db, web, console]
+        )
+
+    def test_get_services_removes_duplicates_following_links(self):
+        db = Service(
+            project='figtest',
+            name='db',
+        )
+        web = Service(
+            project='figtest',
+            name='web',
+            links=[(db, 'database')]
+        )
+        project = Project('test', [web, db], None)
+        self.assertEqual(
+            project.get_services(['web', 'db'], include_links=True),
+            [db, web]
+        )