diff --git a/README.md b/README.md index e2e87f755..2f9599a8b 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Run `fig [COMMAND] --help` for full usage. Build or rebuild services. -Services are built once and then tagged as `project_service`. If you change a service's `Dockerfile` or its configuration in `fig.yml`, you will probably need to run `fig build` to rebuild it, then run `fig rm` to make `fig up` recreate your containers. +Services are built once and then tagged as `project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it. #### kill @@ -237,9 +237,11 @@ Stop running containers without removing them. They can be started again with `f #### up -Build, create, start and attach to containers for a service. +Build, (re)create, start and attach to containers for a service. -If there are stopped containers for a service, `fig up` will start those again instead of creating new containers. When it exits, the containers it started will be stopped. This means if you want to recreate containers, you will need to explicitly run `fig rm`. +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. ### Environment variables @@ -265,3 +267,4 @@ Fully qualified container name, e.g. `MYAPP_DB_1_NAME=/myapp_web_1/myapp_db_1` [Docker links]: http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container +[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/ diff --git a/fig/cli/errors.py b/fig/cli/errors.py index bb1702fda..874d35918 100644 --- a/fig/cli/errors.py +++ b/fig/cli/errors.py @@ -5,3 +5,6 @@ from textwrap import dedent class UserError(Exception): def __init__(self, msg): self.msg = dedent(msg).strip() + + def __unicode__(self): + return self.msg diff --git a/fig/cli/main.py b/fig/cli/main.py index d2d0af156..51c4d27fb 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -220,14 +220,18 @@ class TopLevelCommand(Command): """ detached = options['-d'] - self.project.create_containers(service_names=options['SERVICE']) - containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + (old, new) = self.project.recreate_containers(service_names=options['SERVICE']) if not detached: - print("Attaching to", list_containers(containers)) - log_printer = LogPrinter(containers) + to_attach = [c for (s, c) in new] + print("Attaching to", list_containers(to_attach)) + log_printer = LogPrinter(to_attach) - self.project.start(service_names=options['SERVICE']) + for (service, container) in new: + service.start_container(container) + + for (service, container) in old: + container.remove() if not detached: try: diff --git a/fig/container.py b/fig/container.py index 9556ec1f6..24b239ac6 100644 --- a/fig/container.py +++ b/fig/container.py @@ -1,8 +1,5 @@ from __future__ import unicode_literals from __future__ import absolute_import -import logging - -log = logging.getLogger(__name__) class Container(object): """ @@ -91,19 +88,15 @@ class Container(object): return self.dictionary['State']['Running'] def start(self, **options): - log.info("Starting %s..." % self.name) return self.client.start(self.id, **options) def stop(self, **options): - log.info("Stopping %s..." % self.name) return self.client.stop(self.id, **options) def kill(self): - log.info("Killing %s..." % self.name) return self.client.kill(self.id) def remove(self): - log.info("Removing %s..." % self.name) return self.client.remove_container(self.id) def inspect_if_not_inspected(self): diff --git a/fig/project.py b/fig/project.py index 7c05b2c73..f77da5f7d 100644 --- a/fig/project.py +++ b/fig/project.py @@ -79,13 +79,22 @@ class Project(object): unsorted = [self.get_service(name) for name in service_names] return [s for s in self.services if s in unsorted] - def create_containers(self, service_names=None): + def recreate_containers(self, service_names=None): """ - For each service, creates a container if there are none. + For each service, create or recreate their containers. + Returns a tuple with two lists. The first is a list of + (service, old_container) tuples; the second is a list + of (service, new_container) tuples. """ + old = [] + new = [] + for service in self.get_services(service_names): - if len(service.containers(stopped=True)) == 0: - service.create_container() + (s_old, s_new) = service.recreate_containers() + old += [(service, container) for container in s_old] + new += [(service, container) for container in s_new] + + return (old, new) def start(self, service_names=None, **options): for service in self.get_services(service_names): diff --git a/fig/service.py b/fig/service.py index 5b7b7663e..e73004101 100644 --- a/fig/service.py +++ b/fig/service.py @@ -43,19 +43,23 @@ 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) def stop(self, **options): for c in self.containers(): + log.info("Stopping %s..." % c.name) c.stop(**options) def kill(self, **options): for c in self.containers(): + log.info("Killing %s..." % c.name) c.kill(**options) def remove_stopped(self, **options): for c in self.containers(stopped=True): if not c.is_running: + log.info("Removing %s..." % c.name) c.remove(**options) def create_container(self, one_off=False, **override_options): @@ -73,6 +77,48 @@ class Service(object): return Container.create(self.client, **container_options) raise + def recreate_containers(self, **override_options): + """ + If a container for this service doesn't exist, create one. If there are + any, stop them and create new ones. Does not remove the old containers. + """ + containers = self.containers(stopped=True) + + if len(containers) == 0: + log.info("Creating %s..." % self.next_container_name()) + return ([], [self.create_container(**override_options)]) + else: + old_containers = [] + new_containers = [] + + for c in containers: + log.info("Recreating %s..." % c.name) + (old_container, new_container) = self.recreate_container(c, **override_options) + old_containers.append(old_container) + new_containers.append(new_container) + + return (old_containers, new_containers) + + def recreate_container(self, container, **override_options): + if container.is_running: + container.stop(timeout=1) + + intermediate_container = Container.create( + self.client, + image='ubuntu', + command='echo', + volumes_from=container.id, + ) + intermediate_container.start() + intermediate_container.wait() + container.remove() + + options = dict(override_options) + options['volumes_from'] = intermediate_container.id + new_container = self.create_container(**options) + + return (intermediate_container, new_container) + def start_container(self, container=None, **override_options): if container is None: container = self.create_container(**override_options) @@ -95,8 +141,9 @@ class Service(object): if options.get('volumes', None) is not None: for volume in options['volumes']: - external_dir, internal_dir = volume.split(':') - volume_bindings[os.path.abspath(external_dir)] = internal_dir + if ':' in volume: + external_dir, internal_dir = volume.split(':') + volume_bindings[os.path.abspath(external_dir)] = internal_dir container.start( links=self._get_links(), @@ -143,7 +190,7 @@ class Service(object): container_options['ports'] = ports if 'volumes' in container_options: - container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes']) + container_options['volumes'] = dict((split_volume(v)[1], {}) for v in container_options['volumes']) if self.can_be_built(): if len(self.client.images(name=self._build_tag_name())) == 0: @@ -214,3 +261,14 @@ def get_container_name(container): for name in container['Names']: if len(name.split('/')) == 2: return name[1:] + + +def split_volume(v): + """ + If v is of the format EXTERNAL:INTERNAL, returns (EXTERNAL, INTERNAL). + If v is of the format INTERNAL, returns (None, INTERNAL). + """ + if ':' in v: + return v.split(':', 1) + else: + return (None, v) diff --git a/tests/project_test.py b/tests/project_test.py index 09792fabd..a73aa2527 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -42,16 +42,22 @@ class ProjectTest(DockerClientTestCase): project = Project('test', [web], self.client) self.assertEqual(project.get_service('web'), web) - def test_create_containers(self): + def test_recreate_containers(self): web = self.create_service('web') db = self.create_service('db') project = Project('test', [web, db], self.client) - project.create_containers(service_names=['web']) + old_web_container = web.create_container() self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 0) - project.create_containers() + (old, new) = project.recreate_containers() + self.assertEqual(len(old), 1) + self.assertEqual(old[0][0], web) + self.assertEqual(len(new), 2) + self.assertEqual(new[0][0], web) + self.assertEqual(new[1][0], db) + self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 1) diff --git a/tests/service_test.py b/tests/service_test.py index d2078414a..2ccf17555 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -102,6 +102,36 @@ class ServiceTest(DockerClientTestCase): container = db.create_container(one_off=True) self.assertEqual(container.name, 'figtest_db_run_1') + def test_create_container_with_unspecified_volume(self): + service = self.create_service('db', volumes=['/var/db']) + container = service.create_container() + service.start_container(container) + self.assertIn('/var/db', container.inspect()['Volumes']) + + def test_recreate_containers(self): + service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db']) + old_container = service.create_container() + self.assertEqual(old_container.dictionary['Config']['Env'], ['FOO=1']) + self.assertEqual(old_container.name, 'figtest_db_1') + service.start_container(old_container) + volume_path = old_container.inspect()['Volumes']['/var/db'] + + num_containers_before = len(self.client.containers(all=True)) + + service.options['environment']['FOO'] = '2' + (old, new) = service.recreate_containers() + self.assertEqual(len(old), 1) + self.assertEqual(len(new), 1) + + new_container = new[0] + self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2']) + self.assertEqual(new_container.name, 'figtest_db_1') + service.start_container(new_container) + self.assertEqual(new_container.inspect()['Volumes']['/var/db'], volume_path) + + self.assertEqual(len(self.client.containers(all=True)), num_containers_before + 1) + self.assertNotEqual(old_container.id, new_container.id) + def test_start_container_passes_through_options(self): db = self.create_service('db') db.start_container(environment={'FOO': 'BAR'})