Merge pull request #13 from orchardup/recreate-containers

Recreate containers on each `fig up`
This commit is contained in:
Ben Firshman 2014-01-16 04:57:48 -08:00
commit c06456da37
8 changed files with 131 additions and 25 deletions

View File

@ -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/

View File

@ -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

View File

@ -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:

View File

@ -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):

View File

@ -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):

View File

@ -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)

View File

@ -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)

View File

@ -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'})