From 0eb7d308615bae1ad4be1ca5112ac7b6b6cbfbaf Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 11:41:05 +0000 Subject: [PATCH 0001/2154] yay --- .gitignore | 3 +++ plum/__init__.py | 3 +++ plum/service.py | 30 +++++++++++++++++++++++++ plum/tests/__init__.py | 0 plum/tests/service_test.py | 39 ++++++++++++++++++++++++++++++++ setup.py | 46 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+) create mode 100644 .gitignore create mode 100644 plum/__init__.py create mode 100644 plum/service.py create mode 100644 plum/tests/__init__.py create mode 100644 plum/tests/service_test.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..21a256df --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.egg-info +*.pyc +/dist diff --git a/plum/__init__.py b/plum/__init__.py new file mode 100644 index 00000000..404eba85 --- /dev/null +++ b/plum/__init__.py @@ -0,0 +1,3 @@ +from .service import Service + +__version__ = '1.0.0' diff --git a/plum/service.py b/plum/service.py new file mode 100644 index 00000000..e9777be7 --- /dev/null +++ b/plum/service.py @@ -0,0 +1,30 @@ +class Service(object): + def __init__(self, client, image, command): + self.client = client + self.image = image + self.command = command + + @property + def containers(self): + return self.client.containers() + + def start(self): + if len(self.containers) == 0: + self.start_container() + + def stop(self): + self.scale(0) + + def scale(self, num): + while len(self.containers) < num: + self.start_container() + + while len(self.containers) > num: + self.stop_container() + + def start_container(self): + container = self.client.create_container(self.image, self.command) + self.client.start(container['Id']) + + def stop_container(self): + self.client.kill(self.containers[0]['Id']) diff --git a/plum/tests/__init__.py b/plum/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py new file mode 100644 index 00000000..6075965e --- /dev/null +++ b/plum/tests/service_test.py @@ -0,0 +1,39 @@ +from unittest import TestCase +from docker import Client +from plum import Service + + +class ServiceTestCase(TestCase): + def setUp(self): + self.client = Client('http://127.0.0.1:4243') + self.client.pull('ubuntu') + + for c in self.client.containers(): + self.client.kill(c['Id']) + + self.service = Service( + client=self.client, + image="ubuntu", + command=["/bin/sleep", "300"], + ) + + def test_up_scale_down(self): + self.assertEqual(len(self.service.containers), 0) + + self.service.start() + self.assertEqual(len(self.service.containers), 1) + + self.service.start() + self.assertEqual(len(self.service.containers), 1) + + self.service.scale(2) + self.assertEqual(len(self.service.containers), 2) + + self.service.scale(1) + self.assertEqual(len(self.service.containers), 1) + + self.service.stop() + self.assertEqual(len(self.service.containers), 0) + + self.service.stop() + self.assertEqual(len(self.service.containers), 0) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..b4b80f12 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from setuptools import setup +import re +import os +import codecs + + +# Borrowed from +# https://github.com/jezdez/django_compressor/blob/develop/setup.py +def read(*parts): + return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read() + + +def find_version(*file_paths): + version_file = read(*file_paths) + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + + +setup( + name='plum', + version=find_version("plum", "__init__.py"), + description='', + url='https://github.com/orchardup.plum', + author='Orchard Laboratories Ltd.', + author_email='hello@orchardup.com', + packages=['plum'], + package_data={}, + include_package_data=True, + install_requires=[ + 'docopt==0.6.1', + 'docker-py==0.2.2', + 'requests==2.0.1', + 'texttable==0.8.1', + ], + dependency_links=[], + entry_points=""" + [console_scripts] + plum=plum:main + """, +) From 950bfda579d7bc1a7985d1a849c12b576aac478c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 12:01:15 +0000 Subject: [PATCH 0002/2154] Add readme --- README.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..22ad71bf --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +Plum +==== + +Plum is tool for defining and running apps with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: + +```yaml + db: + image: orchardup/postgresql + + web: + build: web/ + link: db +``` + +Installing +---------- + +```bash + $ sudo pip install plum +``` + +Defining your app +----------------- + +Put a `plum.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Plum will start a Docker container, so at minimum it needs to know what image to use. + +The way to get started is to just give it an image name: + +```yaml + db: + image: orchardup/postgresql +``` + +Alternatively, you can give it the location of a directory with a Dockerfile (or a Git URL, as per the `docker build` command), and it'll automatically build the image for you: + +```yaml + db: + build: /path/to/postgresql/build/directory +``` + +You've now given Plum the minimal amount of configuration it needs to run: + +```bash + $ plum up + Building db... done + db is running at 127.0.0.1:45678 + <...output from postgresql server...> +``` + +For each service you've defined, Plum will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. + +By default, `plum up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: + +```bash + $ plum run -d + Building db... done + db is running at 127.0.0.1:45678 + + $ plum ps + SERVICE STATE PORT + db up 45678 +``` + + +### Getting your code in + +Some services may include your own code. To get that code into the container, ADD it in a Dockerfile. + +`plum.yml`: + +```yaml + web: + build: web/ +``` + +`web/Dockerfile`: + + FROM orchardup/rails + ADD . /code + CMD: bundle exec rackup + + +### Communicating between containers + +Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicatecommunicate, pass in the right IP address and port via environment variables: + +```yaml + db: + image: orchardup/postgresql + + web: + build: web/ + link: db +``` + +This will pass an environment variable called DB_PORT into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can then use that to connect to the database. + +You can pass in multiple links, too: + +```yaml + link: + - db + - memcached + - redis +``` + + +In each case, the resulting environment variable will begin with the uppercased name of the linked service (`DB_PORT`, `MEMCACHED_PORT`, `REDIS_PORT`). + + +### Container configuration options + +You can pass extra configuration options to a container, much like with `docker run`: + +```yaml + web: + build: web/ + + -- override the default run command + run: bundle exec thin -p 3000 + + -- expose ports - can also be an array + ports: 3000 + + -- map volumes - can also be an array + volumes: /tmp/cache + + -- add environment variables - can also be a dictionary + environment: + - RACK_ENV=development +``` + + +Running a one-off command +------------------------- + +If you want to run a management command, use `plum run` to start a one-off container: + + $ plum run db createdb myapp_development + $ plum run web rake db:migrate + $ plum run web bash + + +Running more than one container for a service +--------------------------------------------- + +You can set the number of containers to run for each service with `plum scale`: + + $ plum up -d + db is running at 127.0.0.1:45678 + web is running at 127.0.0.1:45679 + + $ plum scale db=0,web=3 + Stopped db (127.0.0.1:45678) + Started web (127.0.0.1:45680) + Started web (127.0.0.1:45681) + + + From 84ea31dc92916da69fae2a2cdad3994b74350e77 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 12:01:44 +0000 Subject: [PATCH 0003/2154] Add license --- LICENSE | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..a75bd571 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2013, Orchard Laboratories Ltd. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * The names of its contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From ed1918ef0972f810fc68a2b16452cbfa02cf537f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 12:03:26 +0000 Subject: [PATCH 0004/2154] Fix readme formatting --- README.md | 117 +++++++++++++++++++++++++++--------------------------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 22ad71bf..05560406 100644 --- a/README.md +++ b/README.md @@ -4,19 +4,19 @@ Plum Plum is tool for defining and running apps with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: ```yaml - db: - image: orchardup/postgresql +db: + image: orchardup/postgresql - web: - build: web/ - link: db +web: + build: web/ + link: db ``` Installing ---------- ```bash - $ sudo pip install plum +$ sudo pip install plum ``` Defining your app @@ -27,24 +27,24 @@ Put a `plum.yml` in your app's directory. Each top-level key defines a "service" The way to get started is to just give it an image name: ```yaml - db: - image: orchardup/postgresql +db: + image: orchardup/postgresql ``` Alternatively, you can give it the location of a directory with a Dockerfile (or a Git URL, as per the `docker build` command), and it'll automatically build the image for you: ```yaml - db: - build: /path/to/postgresql/build/directory +db: + build: /path/to/postgresql/build/directory ``` You've now given Plum the minimal amount of configuration it needs to run: ```bash - $ plum up - Building db... done - db is running at 127.0.0.1:45678 - <...output from postgresql server...> +$ plum up +Building db... done +db is running at 127.0.0.1:45678 +<...output from postgresql server...> ``` For each service you've defined, Plum will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. @@ -52,13 +52,13 @@ For each service you've defined, Plum will start a Docker container with the spe By default, `plum up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: ```bash - $ plum run -d - Building db... done - db is running at 127.0.0.1:45678 +$ plum run -d +Building db... done +db is running at 127.0.0.1:45678 - $ plum ps - SERVICE STATE PORT - db up 45678 +$ plum ps +SERVICE STATE PORT +db up 45678 ``` @@ -69,15 +69,15 @@ Some services may include your own code. To get that code into the container, AD `plum.yml`: ```yaml - web: - build: web/ +web: + build: web/ ``` `web/Dockerfile`: - FROM orchardup/rails - ADD . /code - CMD: bundle exec rackup +FROM orchardup/rails +ADD . /code +CMD: bundle exec rackup ### Communicating between containers @@ -85,12 +85,12 @@ Some services may include your own code. To get that code into the container, AD Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicatecommunicate, pass in the right IP address and port via environment variables: ```yaml - db: - image: orchardup/postgresql +db: + image: orchardup/postgresql - web: - build: web/ - link: db +web: + build: web/ + link: db ``` This will pass an environment variable called DB_PORT into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can then use that to connect to the database. @@ -98,10 +98,10 @@ This will pass an environment variable called DB_PORT into the web container, wh You can pass in multiple links, too: ```yaml - link: - - db - - memcached - - redis +link: + - db + - memcached + - redis ``` @@ -113,21 +113,21 @@ In each case, the resulting environment variable will begin with the uppercased You can pass extra configuration options to a container, much like with `docker run`: ```yaml - web: - build: web/ +web: + build: web/ - -- override the default run command - run: bundle exec thin -p 3000 +-- override the default run command +run: bundle exec thin -p 3000 - -- expose ports - can also be an array - ports: 3000 +-- expose ports - can also be an array +ports: 3000 - -- map volumes - can also be an array - volumes: /tmp/cache +-- map volumes - can also be an array +volumes: /tmp/cache - -- add environment variables - can also be a dictionary - environment: - - RACK_ENV=development +-- add environment variables - can also be a dictionary +environment: + - RACK_ENV=development ``` @@ -136,9 +136,11 @@ Running a one-off command If you want to run a management command, use `plum run` to start a one-off container: - $ plum run db createdb myapp_development - $ plum run web rake db:migrate - $ plum run web bash +```bash +$ plum run db createdb myapp_development +$ plum run web rake db:migrate +$ plum run web bash +``` Running more than one container for a service @@ -146,14 +148,13 @@ Running more than one container for a service You can set the number of containers to run for each service with `plum scale`: - $ plum up -d - db is running at 127.0.0.1:45678 - web is running at 127.0.0.1:45679 - - $ plum scale db=0,web=3 - Stopped db (127.0.0.1:45678) - Started web (127.0.0.1:45680) - Started web (127.0.0.1:45681) - - +```bash +$ plum up -d +db is running at 127.0.0.1:45678 +web is running at 127.0.0.1:45679 +$ plum scale db=0,web=3 +Stopped db (127.0.0.1:45678) +Started web (127.0.0.1:45680) +Started web (127.0.0.1:45681) +``` From f80ccab9d689274a91d5b83c1e95b8765c7893ed Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 12:06:41 +0000 Subject: [PATCH 0005/2154] Add .travis.yml --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..117b1c0f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: python +python: + - "2.6" + - "2.7" + - "3.2" + - "3.3" +install: pip install nose==1.3.0 +script: nosetests + From 83a086b865c2db791a208d3854c15963ed3fc693 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 11:46:53 +0000 Subject: [PATCH 0006/2154] Delete all containers on the Docker daemon before running test --- plum/tests/service_test.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 6075965e..0c03a2ab 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -8,8 +8,9 @@ class ServiceTestCase(TestCase): self.client = Client('http://127.0.0.1:4243') self.client.pull('ubuntu') - for c in self.client.containers(): + for c in self.client.containers(all=True): self.client.kill(c['Id']) + self.client.remove_container(c['Id']) self.service = Service( client=self.client, From ffdebb84bbce70cb297da8d31745141e20298f81 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 12:19:27 +0000 Subject: [PATCH 0007/2154] Services have names --- plum/service.py | 9 ++++++++- plum/tests/service_test.py | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/plum/service.py b/plum/service.py index e9777be7..8bc5a84e 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,5 +1,12 @@ +import re + + class Service(object): - def __init__(self, client, image, command): + def __init__(self, name, client=None, image=None, command=None): + if not re.match('^[a-zA-Z0-9_]+$', name): + raise ValueError('Invalid name: %s' % name) + + self.name = name self.client = client self.image = image self.command = command diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 0c03a2ab..eac2d498 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -3,7 +3,24 @@ from docker import Client from plum import Service -class ServiceTestCase(TestCase): +class NameTestCase(TestCase): + def test_name_validations(self): + self.assertRaises(ValueError, lambda: Service(name='')) + + self.assertRaises(ValueError, lambda: Service(name=' ')) + self.assertRaises(ValueError, lambda: Service(name='/')) + self.assertRaises(ValueError, lambda: Service(name='!')) + self.assertRaises(ValueError, lambda: Service(name='\xe2')) + + Service('a') + Service('foo') + Service('foo_bar') + Service('__foo_bar__') + Service('_') + Service('_____') + + +class ScalingTestCase(TestCase): def setUp(self): self.client = Client('http://127.0.0.1:4243') self.client.pull('ubuntu') @@ -13,6 +30,7 @@ class ServiceTestCase(TestCase): self.client.remove_container(c['Id']) self.service = Service( + name="test", client=self.client, image="ubuntu", command=["/bin/sleep", "300"], From 973760a5016ccf5673164fa77b11c3abeb5d1941 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 14:09:18 +0000 Subject: [PATCH 0008/2154] Add links argument to service --- plum/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 8bc5a84e..2d832182 100644 --- a/plum/service.py +++ b/plum/service.py @@ -2,7 +2,7 @@ import re class Service(object): - def __init__(self, name, client=None, image=None, command=None): + def __init__(self, name, client=None, image=None, command=None, links=None): if not re.match('^[a-zA-Z0-9_]+$', name): raise ValueError('Invalid name: %s' % name) @@ -10,6 +10,7 @@ class Service(object): self.client = client self.image = image self.command = command + self.links = links or [] @property def containers(self): From bc51134ceb47f5830d2bc14afc24a8ee65a0b00b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 14:10:23 +0000 Subject: [PATCH 0009/2154] Add ServiceCollection --- plum/service_collection.py | 37 ++++++++++++++++++++++ plum/tests/service_collection_test.py | 44 +++++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 plum/service_collection.py create mode 100644 plum/tests/service_collection_test.py diff --git a/plum/service_collection.py b/plum/service_collection.py new file mode 100644 index 00000000..b73511a9 --- /dev/null +++ b/plum/service_collection.py @@ -0,0 +1,37 @@ +from .service import Service + +def sort_service_dicts(services): + # Sort in dependency order + def cmp(x, y): + x_deps_y = y['name'] in x.get('links', []) + y_deps_x = x['name'] in y.get('links', []) + if x_deps_y and not y_deps_x: + return 1 + elif y_deps_x and not x_deps_y: + return -1 + return 0 + return sorted(services, cmp=cmp) + +class ServiceCollection(list): + @classmethod + def from_dicts(cls, client, service_dicts): + """ + Construct a ServiceCollection from a list of dicts representing services. + """ + collection = ServiceCollection() + for service_dict in sort_service_dicts(service_dicts): + # Reference links by object + links = [] + if 'links' in service_dict: + for name in service_dict.get('links', []): + links.append(collection.get(name)) + del service_dict['links'] + collection.append(Service(client=client, links=links, **service_dict)) + return collection + + def get(self, name): + for service in self: + if service.name == name: + return service + + diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py new file mode 100644 index 00000000..88641c0a --- /dev/null +++ b/plum/tests/service_collection_test.py @@ -0,0 +1,44 @@ +from plum.service import Service +from plum.service_collection import ServiceCollection +from unittest import TestCase + + +class ServiceCollectionTest(TestCase): + def test_from_dict(self): + collection = ServiceCollection.from_dicts(None, [ + { + 'name': 'web', + 'image': 'ubuntu' + }, + { + 'name': 'db', + 'image': 'ubuntu' + } + ]) + self.assertEqual(len(collection), 2) + web = [s for s in collection if s.name == 'web'][0] + self.assertEqual(web.name, 'web') + self.assertEqual(web.image, 'ubuntu') + db = [s for s in collection if s.name == 'db'][0] + self.assertEqual(db.name, 'db') + self.assertEqual(db.image, 'ubuntu') + + def test_from_dict_sorts_in_dependency_order(self): + collection = ServiceCollection.from_dicts(None, [ + { + 'name': 'web', + 'image': 'ubuntu', + 'links': ['db'], + }, + { + 'name': 'db', + 'image': 'ubuntu' + } + ]) + + self.assertEqual(collection[0].name, 'db') + self.assertEqual(collection[1].name, 'web') + + + + From 630f6fcf02f51a3c257c69c65127597c343500cf Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 15:00:01 +0000 Subject: [PATCH 0010/2154] Extract test setup --- plum/tests/service_test.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index eac2d498..5870c30d 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -3,7 +3,26 @@ from docker import Client from plum import Service -class NameTestCase(TestCase): +client = Client('http://127.0.0.1:4243') +client.pull('ubuntu') + + +class ServiceTestCase(TestCase): + def setUp(self): + for c in client.containers(all=True): + client.kill(c['Id']) + client.remove_container(c['Id']) + + def create_service(self, name): + return Service( + name=name, + client=client, + image="ubuntu", + command=["/bin/sleep", "300"], + ) + + +class NameTestCase(ServiceTestCase): def test_name_validations(self): self.assertRaises(ValueError, lambda: Service(name='')) @@ -20,21 +39,10 @@ class NameTestCase(TestCase): Service('_____') -class ScalingTestCase(TestCase): +class ScalingTestCase(ServiceTestCase): def setUp(self): - self.client = Client('http://127.0.0.1:4243') - self.client.pull('ubuntu') - - for c in self.client.containers(all=True): - self.client.kill(c['Id']) - self.client.remove_container(c['Id']) - - self.service = Service( - name="test", - client=self.client, - image="ubuntu", - command=["/bin/sleep", "300"], - ) + super(ServiceTestCase, self).setUp() + self.service = self.create_service("scaling_test") def test_up_scale_down(self): self.assertEqual(len(self.service.containers), 0) From 4e426e77fa5b8d38730a37695a8892a53e73754a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 15:00:41 +0000 Subject: [PATCH 0011/2154] Containers have names which are used to isolate them to their service --- plum/service.py | 33 +++++++++++++++++++++++++++++++-- plum/tests/service_test.py | 16 ++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/plum/service.py b/plum/service.py index 2d832182..6b928564 100644 --- a/plum/service.py +++ b/plum/service.py @@ -14,7 +14,7 @@ class Service(object): @property def containers(self): - return self.client.containers() + return [c for c in self.client.containers() if parse_name(get_container_name(c))[0] == self.name] def start(self): if len(self.containers) == 0: @@ -31,8 +31,37 @@ class Service(object): self.stop_container() def start_container(self): - container = self.client.create_container(self.image, self.command) + number = self.next_container_number() + name = make_name(self.name, number) + container = self.client.create_container(self.image, self.command, name=name) self.client.start(container['Id']) def stop_container(self): self.client.kill(self.containers[0]['Id']) + + def next_container_number(self): + numbers = [parse_name(get_container_name(c))[1] for c in self.containers] + + if len(numbers) == 0: + return 1 + else: + return max(numbers) + 1 + + +def make_name(prefix, number): + return '%s_%s' % (prefix, number) + + +def parse_name(name): + match = re.match('^(.+)_(\d+)$', name) + + if match is None: + raise ValueError("Invalid name: %s" % name) + + (service_name, suffix) = match.groups() + + return (service_name, int(suffix)) + + +def get_container_name(container): + return container['Names'][0][1:] diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 5870c30d..e8db26ff 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -39,6 +39,22 @@ class NameTestCase(ServiceTestCase): Service('_____') +class ContainersTestCase(ServiceTestCase): + def test_containers(self): + foo = self.create_service('foo') + bar = self.create_service('bar') + + foo.start() + + self.assertEqual(len(foo.containers), 1) + self.assertEqual(len(bar.containers), 0) + + bar.scale(2) + + self.assertEqual(len(foo.containers), 1) + self.assertEqual(len(bar.containers), 2) + + class ScalingTestCase(ServiceTestCase): def setUp(self): super(ServiceTestCase, self).setUp() From e116e7db99d1103318ab1514b452b2ae89b5ebf3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 16:16:15 +0000 Subject: [PATCH 0012/2154] Add setup.py install to travis.yml --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 117b1c0f..da53766e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ python: - "2.7" - "3.2" - "3.3" -install: pip install nose==1.3.0 +install: + - python setup.py install + - pip install nose==1.3.0 script: nosetests From 084b06cd128c1166bdaa51752da883e53fde8580 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 16:25:50 +0000 Subject: [PATCH 0013/2154] Install docker-py from GitHub Just use requirements.txt for now, as doing it in setup.py is a pain. --- requirements.txt | 1 + setup.py | 7 +------ 2 files changed, 2 insertions(+), 6 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..e560af40 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52 diff --git a/setup.py b/setup.py index b4b80f12..db17156e 100644 --- a/setup.py +++ b/setup.py @@ -32,12 +32,7 @@ setup( packages=['plum'], package_data={}, include_package_data=True, - install_requires=[ - 'docopt==0.6.1', - 'docker-py==0.2.2', - 'requests==2.0.1', - 'texttable==0.8.1', - ], + install_requires=[], dependency_links=[], entry_points=""" [console_scripts] From e2e5172a59216e304d36968191bd6a14bb12e6e1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 9 Dec 2013 16:41:47 +0000 Subject: [PATCH 0014/2154] READMEREADME fixfix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05560406..c6ae7891 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ CMD: bundle exec rackup ### Communicating between containers -Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicatecommunicate, pass in the right IP address and port via environment variables: +Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicate, pass in the right IP address and port via environment variables: ```yaml db: From dc4f90f3edf0737675b1f9cd9e31212d9e72881f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 16:57:12 +0000 Subject: [PATCH 0015/2154] Add envvar to set Docker URL in tests --- plum/tests/service_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index e8db26ff..99285233 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -1,9 +1,13 @@ from unittest import TestCase from docker import Client from plum import Service +import os -client = Client('http://127.0.0.1:4243') +if os.environ.get('DOCKER_URL'): + client = Client(os.environ['DOCKER_URL']) +else: + client = Client() client.pull('ubuntu') From 4cf072a013c4fb078e105a05d71543f4e89bde0a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 17:36:44 +0000 Subject: [PATCH 0016/2154] Move ServiceTestCase to separate module --- plum/tests/service_collection_test.py | 8 ++----- plum/tests/service_test.py | 26 +---------------------- plum/tests/testcases.py | 30 +++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 plum/tests/testcases.py diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py index 88641c0a..1715c299 100644 --- a/plum/tests/service_collection_test.py +++ b/plum/tests/service_collection_test.py @@ -1,9 +1,9 @@ from plum.service import Service from plum.service_collection import ServiceCollection -from unittest import TestCase +from .testcases import ServiceTestCase -class ServiceCollectionTest(TestCase): +class ServiceCollectionTest(ServiceTestCase): def test_from_dict(self): collection = ServiceCollection.from_dicts(None, [ { @@ -38,7 +38,3 @@ class ServiceCollectionTest(TestCase): self.assertEqual(collection[0].name, 'db') self.assertEqual(collection[1].name, 'web') - - - - diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 99285233..49c4d585 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -1,29 +1,5 @@ -from unittest import TestCase -from docker import Client from plum import Service -import os - - -if os.environ.get('DOCKER_URL'): - client = Client(os.environ['DOCKER_URL']) -else: - client = Client() -client.pull('ubuntu') - - -class ServiceTestCase(TestCase): - def setUp(self): - for c in client.containers(all=True): - client.kill(c['Id']) - client.remove_container(c['Id']) - - def create_service(self, name): - return Service( - name=name, - client=client, - image="ubuntu", - command=["/bin/sleep", "300"], - ) +from .testcases import ServiceTestCase class NameTestCase(ServiceTestCase): diff --git a/plum/tests/testcases.py b/plum/tests/testcases.py new file mode 100644 index 00000000..7be5b99b --- /dev/null +++ b/plum/tests/testcases.py @@ -0,0 +1,30 @@ +from docker import Client +from plum.service import Service +import os +from unittest import TestCase + + +class ServiceTestCase(TestCase): + @classmethod + def setUpClass(cls): + if os.environ.get('DOCKER_URL'): + cls.client = Client(os.environ['DOCKER_URL']) + else: + cls.client = Client() + cls.client.pull('ubuntu') + + def setUp(self): + for c in self.client.containers(all=True): + self.client.kill(c['Id']) + self.client.remove_container(c['Id']) + + def create_service(self, name): + return Service( + name=name, + client=self.client, + image="ubuntu", + command=["/bin/sleep", "300"], + ) + + + From 39497f6ee7692203de7cdd68eea0ade46499c185 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 17:48:15 +0000 Subject: [PATCH 0017/2154] Add start and stop to ServiceCollections --- plum/service_collection.py | 9 +++++++++ plum/tests/service_collection_test.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/plum/service_collection.py b/plum/service_collection.py index b73511a9..3803bafc 100644 --- a/plum/service_collection.py +++ b/plum/service_collection.py @@ -34,4 +34,13 @@ class ServiceCollection(list): if service.name == name: return service + def start(self): + for container in self: + container.start() + + def stop(self): + for container in self: + container.stop() + + diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py index 1715c299..080ee2cf 100644 --- a/plum/tests/service_collection_test.py +++ b/plum/tests/service_collection_test.py @@ -38,3 +38,21 @@ class ServiceCollectionTest(ServiceTestCase): self.assertEqual(collection[0].name, 'db') self.assertEqual(collection[1].name, 'web') + + def test_start_stop(self): + collection = ServiceCollection([ + self.create_service('web'), + self.create_service('db'), + ]) + + collection.start() + + self.assertEqual(len(collection[0].containers), 1) + self.assertEqual(len(collection[1].containers), 1) + + collection.stop() + + self.assertEqual(len(collection[0].containers), 0) + self.assertEqual(len(collection[1].containers), 0) + + From 2199b62783fbaaa5003a8a54e46751dd10cad39d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 18:42:33 +0000 Subject: [PATCH 0018/2154] Test that names are created correctly --- plum/tests/service_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 49c4d585..5af3ef7b 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -27,6 +27,7 @@ class ContainersTestCase(ServiceTestCase): foo.start() self.assertEqual(len(foo.containers), 1) + self.assertEqual(foo.containers[0]['Names'], ['/foo_1']) self.assertEqual(len(bar.containers), 0) bar.scale(2) @@ -34,6 +35,10 @@ class ContainersTestCase(ServiceTestCase): self.assertEqual(len(foo.containers), 1) self.assertEqual(len(bar.containers), 2) + names = [c['Names'] for c in bar.containers] + self.assertIn(['/bar_1'], names) + self.assertIn(['/bar_2'], names) + class ScalingTestCase(ServiceTestCase): def setUp(self): From f0768d4dca87bf249a35782bbaa5ddbd8f40fab4 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 18:43:10 +0000 Subject: [PATCH 0019/2154] Add test for ServiceCollection.get --- plum/tests/service_collection_test.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py index 080ee2cf..0125511e 100644 --- a/plum/tests/service_collection_test.py +++ b/plum/tests/service_collection_test.py @@ -16,12 +16,10 @@ class ServiceCollectionTest(ServiceTestCase): } ]) self.assertEqual(len(collection), 2) - web = [s for s in collection if s.name == 'web'][0] - self.assertEqual(web.name, 'web') - self.assertEqual(web.image, 'ubuntu') - db = [s for s in collection if s.name == 'db'][0] - self.assertEqual(db.name, 'db') - self.assertEqual(db.image, 'ubuntu') + self.assertEqual(collection.get('web').name, 'web') + self.assertEqual(collection.get('web').image, 'ubuntu') + self.assertEqual(collection.get('db').name, 'db') + self.assertEqual(collection.get('db').image, 'ubuntu') def test_from_dict_sorts_in_dependency_order(self): collection = ServiceCollection.from_dicts(None, [ @@ -35,10 +33,15 @@ class ServiceCollectionTest(ServiceTestCase): 'image': 'ubuntu' } ]) - + self.assertEqual(collection[0].name, 'db') self.assertEqual(collection[1].name, 'web') + def test_get(self): + web = self.create_service('web') + collection = ServiceCollection([web]) + self.assertEqual(collection.get('web'), web) + def test_start_stop(self): collection = ServiceCollection([ self.create_service('web'), From 9a81623581f0488b3f8230dcbbd82a563e84346b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 21:39:11 +0000 Subject: [PATCH 0020/2154] Create links when creating containers --- plum/service.py | 19 ++++++++++++++++++- plum/tests/service_collection_test.py | 1 + plum/tests/service_test.py | 13 +++++++++++++ plum/tests/testcases.py | 3 ++- 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/plum/service.py b/plum/service.py index 6b928564..3f9d95f5 100644 --- a/plum/service.py +++ b/plum/service.py @@ -34,7 +34,10 @@ class Service(object): number = self.next_container_number() name = make_name(self.name, number) container = self.client.create_container(self.image, self.command, name=name) - self.client.start(container['Id']) + self.client.start( + container['Id'], + links=self._get_links(), + ) def stop_container(self): self.client.kill(self.containers[0]['Id']) @@ -47,6 +50,20 @@ class Service(object): else: return max(numbers) + 1 + def get_names(self): + return [get_container_name(c) for c in self.containers] + + def inspect(self): + return [self.client.inspect_container(c['Id']) for c in self.containers] + + def _get_links(self): + links = {} + for service in self.links: + for name in service.get_names(): + links[name] = name + return links + + def make_name(prefix, number): return '%s_%s' % (prefix, number) diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py index 0125511e..8029b6e9 100644 --- a/plum/tests/service_collection_test.py +++ b/plum/tests/service_collection_test.py @@ -59,3 +59,4 @@ class ServiceCollectionTest(ServiceTestCase): self.assertEqual(len(collection[1].containers), 0) + diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 5af3ef7b..9c3726bc 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -65,3 +65,16 @@ class ScalingTestCase(ServiceTestCase): self.service.stop() self.assertEqual(len(self.service.containers), 0) + + +class LinksTestCase(ServiceTestCase): + def test_links_are_created_when_starting(self): + db = self.create_service('db') + web = self.create_service('web', links=[db]) + db.start() + web.start() + self.assertIn('/web_1/db_1', db.containers[0]['Names']) + db.stop() + web.stop() + + diff --git a/plum/tests/testcases.py b/plum/tests/testcases.py index 7be5b99b..df13e752 100644 --- a/plum/tests/testcases.py +++ b/plum/tests/testcases.py @@ -18,12 +18,13 @@ class ServiceTestCase(TestCase): self.client.kill(c['Id']) self.client.remove_container(c['Id']) - def create_service(self, name): + def create_service(self, name, **kwargs): return Service( name=name, client=self.client, image="ubuntu", command=["/bin/sleep", "300"], + **kwargs ) From 3e6b3bae83734114aa9db208c04d42338573c40d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 21:50:36 +0000 Subject: [PATCH 0021/2154] Add warning to readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c6ae7891..fa491541 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Plum ==== +**WARNING**: This is a work in progress and probably won't work yet. Feedback welcome. + Plum is tool for defining and running apps with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: ```yaml From 3a3767f59d83e02e4d1456b7afe49872fb70a1b8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 9 Dec 2013 21:51:17 +0000 Subject: [PATCH 0022/2154] Fix readme formatting --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fa491541..8a2fe7dc 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ web: `web/Dockerfile`: -FROM orchardup/rails -ADD . /code -CMD: bundle exec rackup + FROM orchardup/rails + ADD . /code + CMD: bundle exec rackup ### Communicating between containers From b59436742b90bab56e08a8ae4afaafe2d1efa863 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 10 Dec 2013 20:47:08 +0000 Subject: [PATCH 0023/2154] Reorganise tests --- plum/tests/service_collection_test.py | 4 +-- plum/tests/service_test.py | 41 +++++++++++---------------- plum/tests/testcases.py | 2 +- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py index 8029b6e9..8d36e996 100644 --- a/plum/tests/service_collection_test.py +++ b/plum/tests/service_collection_test.py @@ -1,9 +1,9 @@ from plum.service import Service from plum.service_collection import ServiceCollection -from .testcases import ServiceTestCase +from .testcases import DockerClientTestCase -class ServiceCollectionTest(ServiceTestCase): +class ServiceCollectionTest(DockerClientTestCase): def test_from_dict(self): collection = ServiceCollection.from_dicts(None, [ { diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 9c3726bc..51289e68 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -1,8 +1,8 @@ from plum import Service -from .testcases import ServiceTestCase +from .testcases import DockerClientTestCase -class NameTestCase(ServiceTestCase): +class NameTestCase(DockerClientTestCase): def test_name_validations(self): self.assertRaises(ValueError, lambda: Service(name='')) @@ -18,8 +18,6 @@ class NameTestCase(ServiceTestCase): Service('_') Service('_____') - -class ContainersTestCase(ServiceTestCase): def test_containers(self): foo = self.create_service('foo') bar = self.create_service('bar') @@ -39,35 +37,28 @@ class ContainersTestCase(ServiceTestCase): self.assertIn(['/bar_1'], names) self.assertIn(['/bar_2'], names) - -class ScalingTestCase(ServiceTestCase): - def setUp(self): - super(ServiceTestCase, self).setUp() - self.service = self.create_service("scaling_test") - def test_up_scale_down(self): - self.assertEqual(len(self.service.containers), 0) + service = self.create_service('scaling_test') + self.assertEqual(len(service.containers), 0) - self.service.start() - self.assertEqual(len(self.service.containers), 1) + service.start() + self.assertEqual(len(service.containers), 1) - self.service.start() - self.assertEqual(len(self.service.containers), 1) + service.start() + self.assertEqual(len(service.containers), 1) - self.service.scale(2) - self.assertEqual(len(self.service.containers), 2) + service.scale(2) + self.assertEqual(len(service.containers), 2) - self.service.scale(1) - self.assertEqual(len(self.service.containers), 1) + service.scale(1) + self.assertEqual(len(service.containers), 1) - self.service.stop() - self.assertEqual(len(self.service.containers), 0) + service.stop() + self.assertEqual(len(service.containers), 0) - self.service.stop() - self.assertEqual(len(self.service.containers), 0) + service.stop() + self.assertEqual(len(service.containers), 0) - -class LinksTestCase(ServiceTestCase): def test_links_are_created_when_starting(self): db = self.create_service('db') web = self.create_service('web', links=[db]) diff --git a/plum/tests/testcases.py b/plum/tests/testcases.py index df13e752..8fc65ee8 100644 --- a/plum/tests/testcases.py +++ b/plum/tests/testcases.py @@ -4,7 +4,7 @@ import os from unittest import TestCase -class ServiceTestCase(TestCase): +class DockerClientTestCase(TestCase): @classmethod def setUpClass(cls): if os.environ.get('DOCKER_URL'): From bf2505d15def76e853a54682f0ade9d358d0c7cf Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 10 Dec 2013 20:51:55 +0000 Subject: [PATCH 0024/2154] Add options for containers to Service --- plum/service.py | 7 +++---- plum/tests/service_collection_test.py | 4 ++-- plum/tests/service_test.py | 11 ++++++++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/plum/service.py b/plum/service.py index 3f9d95f5..8e7660a1 100644 --- a/plum/service.py +++ b/plum/service.py @@ -2,15 +2,14 @@ import re class Service(object): - def __init__(self, name, client=None, image=None, command=None, links=None): + def __init__(self, name, client=None, links=[], **options): if not re.match('^[a-zA-Z0-9_]+$', name): raise ValueError('Invalid name: %s' % name) self.name = name self.client = client - self.image = image - self.command = command self.links = links or [] + self.options = options @property def containers(self): @@ -33,7 +32,7 @@ class Service(object): def start_container(self): number = self.next_container_number() name = make_name(self.name, number) - container = self.client.create_container(self.image, self.command, name=name) + container = self.client.create_container(name=name, **self.options) self.client.start( container['Id'], links=self._get_links(), diff --git a/plum/tests/service_collection_test.py b/plum/tests/service_collection_test.py index 8d36e996..1c764eca 100644 --- a/plum/tests/service_collection_test.py +++ b/plum/tests/service_collection_test.py @@ -17,9 +17,9 @@ class ServiceCollectionTest(DockerClientTestCase): ]) self.assertEqual(len(collection), 2) self.assertEqual(collection.get('web').name, 'web') - self.assertEqual(collection.get('web').image, 'ubuntu') + self.assertEqual(collection.get('web').options['image'], 'ubuntu') self.assertEqual(collection.get('db').name, 'db') - self.assertEqual(collection.get('db').image, 'ubuntu') + self.assertEqual(collection.get('db').options['image'], 'ubuntu') def test_from_dict_sorts_in_dependency_order(self): collection = ServiceCollection.from_dicts(None, [ diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 51289e68..7bd0e0b2 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -59,11 +59,16 @@ class NameTestCase(DockerClientTestCase): service.stop() self.assertEqual(len(service.containers), 0) - def test_links_are_created_when_starting(self): + def test_start_container_passes_through_options(self): + db = self.create_service('db', environment={'FOO': 'BAR'}) + db.start_container() + self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) + + def test_start_container_creates_links(self): db = self.create_service('db') web = self.create_service('web', links=[db]) - db.start() - web.start() + db.start_container() + web.start_container() self.assertIn('/web_1/db_1', db.containers[0]['Names']) db.stop() web.stop() From c2e935376072dac05bb97c7e85399e157960691f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 10 Dec 2013 21:01:39 +0000 Subject: [PATCH 0025/2154] Allow options to passed to start_container --- plum/service.py | 6 ++++-- plum/tests/service_test.py | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/plum/service.py b/plum/service.py index 8e7660a1..254bc9d9 100644 --- a/plum/service.py +++ b/plum/service.py @@ -29,10 +29,12 @@ class Service(object): while len(self.containers) > num: self.stop_container() - def start_container(self): + def start_container(self, **override_options): + options = dict(self.options) + options.update(override_options) number = self.next_container_number() name = make_name(self.name, number) - container = self.client.create_container(name=name, **self.options) + container = self.client.create_container(name=name, **options) self.client.start( container['Id'], links=self._get_links(), diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index 7bd0e0b2..ed2c3db7 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -60,6 +60,11 @@ class NameTestCase(DockerClientTestCase): self.assertEqual(len(service.containers), 0) def test_start_container_passes_through_options(self): + db = self.create_service('db') + db.start_container(environment={'FOO': 'BAR'}) + self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) + + def test_start_container_inherits_options_from_constructor(self): db = self.create_service('db', environment={'FOO': 'BAR'}) db.start_container() self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) From 523fb99d7919cc7d11e8d0cdb1e5c05c07a4319f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 11 Dec 2013 09:39:17 +0000 Subject: [PATCH 0026/2154] Fix URL in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index db17156e..658e3907 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( name='plum', version=find_version("plum", "__init__.py"), description='', - url='https://github.com/orchardup.plum', + url='https://github.com/orchardup/plum', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', packages=['plum'], From 3b654ad349600439cd7b639106e3f3eb25790a72 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 11 Dec 2013 14:25:32 +0000 Subject: [PATCH 0027/2154] Add basic CLI --- plum/cli/__init__.py | 0 plum/cli/command.py | 29 +++++++++++++ plum/cli/docopt_command.py | 46 +++++++++++++++++++++ plum/cli/errors.py | 6 +++ plum/cli/formatter.py | 15 +++++++ plum/cli/main.py | 83 ++++++++++++++++++++++++++++++++++++++ plum/cli/utils.py | 76 ++++++++++++++++++++++++++++++++++ plum/service_collection.py | 8 ++++ requirements.txt | 2 + setup.py | 2 +- 10 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 plum/cli/__init__.py create mode 100644 plum/cli/command.py create mode 100644 plum/cli/docopt_command.py create mode 100644 plum/cli/errors.py create mode 100644 plum/cli/formatter.py create mode 100644 plum/cli/main.py create mode 100644 plum/cli/utils.py diff --git a/plum/cli/__init__.py b/plum/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plum/cli/command.py b/plum/cli/command.py new file mode 100644 index 00000000..e59d1869 --- /dev/null +++ b/plum/cli/command.py @@ -0,0 +1,29 @@ +from docker import Client +import logging +import os +import yaml + +from ..service_collection import ServiceCollection +from .docopt_command import DocoptCommand +from .formatter import Formatter +from .utils import cached_property, mkdir + +log = logging.getLogger(__name__) + +class Command(DocoptCommand): + @cached_property + def client(self): + if os.environ.get('DOCKER_URL'): + return Client(os.environ['DOCKER_URL']) + else: + return Client() + + @cached_property + def service_collection(self): + config = yaml.load(open('plum.yml')) + return ServiceCollection.from_config(self.client, config) + + @cached_property + def formatter(self): + return Formatter() + diff --git a/plum/cli/docopt_command.py b/plum/cli/docopt_command.py new file mode 100644 index 00000000..0a11d8ee --- /dev/null +++ b/plum/cli/docopt_command.py @@ -0,0 +1,46 @@ +import sys + +from inspect import getdoc +from docopt import docopt, DocoptExit + + +def docopt_full_help(docstring, *args, **kwargs): + try: + return docopt(docstring, *args, **kwargs) + except DocoptExit: + raise SystemExit(docstring) + + +class DocoptCommand(object): + def sys_dispatch(self): + self.dispatch(sys.argv[1:], None) + + def dispatch(self, argv, global_options): + self.perform_command(*self.parse(argv, global_options)) + + def perform_command(self, options, command, handler, command_options): + handler(command_options) + + def parse(self, argv, global_options): + options = docopt_full_help(getdoc(self), argv, options_first=True) + command = options['COMMAND'] + + if not hasattr(self, command): + raise NoSuchCommand(command, self) + + handler = getattr(self, command) + docstring = getdoc(handler) + + if docstring is None: + raise NoSuchCommand(command, self) + + command_options = docopt_full_help(docstring, options['ARGS'], options_first=True) + return (options, command, handler, command_options) + + +class NoSuchCommand(Exception): + def __init__(self, command, supercommand): + super(NoSuchCommand, self).__init__("No such command: %s" % command) + + self.command = command + self.supercommand = supercommand diff --git a/plum/cli/errors.py b/plum/cli/errors.py new file mode 100644 index 00000000..038a7ea1 --- /dev/null +++ b/plum/cli/errors.py @@ -0,0 +1,6 @@ +from textwrap import dedent + + +class UserError(Exception): + def __init__(self, msg): + self.msg = dedent(msg).strip() diff --git a/plum/cli/formatter.py b/plum/cli/formatter.py new file mode 100644 index 00000000..55a967f9 --- /dev/null +++ b/plum/cli/formatter.py @@ -0,0 +1,15 @@ +import texttable +import os + + +class Formatter(object): + def table(self, headers, rows): + height, width = os.popen('stty size', 'r').read().split() + + table = texttable.Texttable(max_width=width) + table.set_cols_dtype(['t' for h in headers]) + table.add_rows([headers] + rows) + table.set_deco(table.HEADER) + table.set_chars(['-', '|', '+', '-']) + + return table.draw() diff --git a/plum/cli/main.py b/plum/cli/main.py new file mode 100644 index 00000000..2bd8f3ba --- /dev/null +++ b/plum/cli/main.py @@ -0,0 +1,83 @@ +import datetime +import logging +import sys +import os +import re + +from docopt import docopt +from inspect import getdoc + +from .. import __version__ +from ..service_collection import ServiceCollection +from .command import Command + +from .errors import UserError +from .docopt_command import NoSuchCommand + +log = logging.getLogger(__name__) + +def main(): + try: + command = TopLevelCommand() + command.sys_dispatch() + except KeyboardInterrupt: + log.error("\nAborting.") + exit(1) + except UserError, e: + log.error(e.msg) + exit(1) + except NoSuchCommand, e: + log.error("No such command: %s", e.command) + log.error("") + log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))) + exit(1) + + +# stolen from docopt master +def parse_doc_section(name, source): + pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', + re.IGNORECASE | re.MULTILINE) + return [s.strip() for s in pattern.findall(source)] + + +class TopLevelCommand(Command): + """. + + Usage: + plum [options] [COMMAND] [ARGS...] + plum -h|--help + + Options: + --verbose Show more output + --version Print version and exit + + Commands: + ps List services and containers + + """ + def ps(self, options): + """ + List services and containers. + + Usage: ps + """ + for service in self.service_collection: + for container in service.containers: + print container['Names'][0] + + def start(self, options): + """ + Start all services + + Usage: start + """ + self.service_collection.start() + + def stop(self, options): + """ + Stop all services + + Usage: stop + """ + self.service_collection.stop() + diff --git a/plum/cli/utils.py b/plum/cli/utils.py new file mode 100644 index 00000000..8d176425 --- /dev/null +++ b/plum/cli/utils.py @@ -0,0 +1,76 @@ +import datetime +import os + + +def cached_property(f): + """ + returns a cached property that is calculated by function f + http://code.activestate.com/recipes/576563-cached-property/ + """ + def get(self): + try: + return self._property_cache[f] + except AttributeError: + self._property_cache = {} + x = self._property_cache[f] = f(self) + return x + except KeyError: + x = self._property_cache[f] = f(self) + return x + + return property(get) + + +def yesno(prompt, default=None): + """ + Prompt the user for a yes or no. + + Can optionally specify a default value, which will only be + used if they enter a blank line. + + Unrecognised input (anything other than "y", "n", "yes", + "no" or "") will return None. + """ + answer = raw_input(prompt).strip().lower() + + if answer == "y" or answer == "yes": + return True + elif answer == "n" or answer == "no": + return False + elif answer == "": + return default + else: + return None + + +# http://stackoverflow.com/a/5164027 +def prettydate(d): + diff = datetime.datetime.utcnow() - d + s = diff.seconds + if diff.days > 7 or diff.days < 0: + return d.strftime('%d %b %y') + elif diff.days == 1: + return '1 day ago' + elif diff.days > 1: + return '{0} days ago'.format(diff.days) + elif s <= 1: + return 'just now' + elif s < 60: + return '{0} seconds ago'.format(s) + elif s < 120: + return '1 minute ago' + elif s < 3600: + return '{0} minutes ago'.format(s/60) + elif s < 7200: + return '1 hour ago' + else: + return '{0} hours ago'.format(s/3600) + + +def mkdir(path, permissions=0700): + if not os.path.exists(path): + os.mkdir(path) + + os.chmod(path, permissions) + + return path diff --git a/plum/service_collection.py b/plum/service_collection.py index 3803bafc..82ea78ff 100644 --- a/plum/service_collection.py +++ b/plum/service_collection.py @@ -29,6 +29,14 @@ class ServiceCollection(list): collection.append(Service(client=client, links=links, **service_dict)) return collection + @classmethod + def from_config(cls, client, config): + dicts = [] + for name, service in config.items(): + service['name'] = name + dicts.append(service) + return cls.from_dicts(client, dicts) + def get(self, name): for service in self: if service.name == name: diff --git a/requirements.txt b/requirements.txt index e560af40..6d769742 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52 +docopt==0.6.1 +PyYAML==3.10 diff --git a/setup.py b/setup.py index 658e3907..5b01a47e 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,6 @@ setup( dependency_links=[], entry_points=""" [console_scripts] - plum=plum:main + plum=plum.cli.main:main """, ) From 8005254138c3b7d02b24e047374d7579cc7f0612 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 13 Dec 2013 19:19:44 +0000 Subject: [PATCH 0028/2154] Set up default logging --- plum/cli/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plum/cli/main.py b/plum/cli/main.py index 2bd8f3ba..191b2f48 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -16,7 +16,18 @@ from .docopt_command import NoSuchCommand log = logging.getLogger(__name__) + def main(): + console_handler = logging.StreamHandler() + console_handler.setFormatter(logging.Formatter()) + console_handler.setLevel(logging.INFO) + root_logger = logging.getLogger() + root_logger.addHandler(console_handler) + root_logger.setLevel(logging.DEBUG) + + # Disable requests logging + logging.getLogger("requests").propagate = False + try: command = TopLevelCommand() command.sys_dispatch() From 1727586dd06fb93aa4ff65a1e6746e6ee2f00507 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 13 Dec 2013 20:35:54 +0000 Subject: [PATCH 0029/2154] Add start and stop to docs --- plum/cli/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plum/cli/main.py b/plum/cli/main.py index 191b2f48..9f18f8ff 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -64,6 +64,8 @@ class TopLevelCommand(Command): Commands: ps List services and containers + start Start services + stop Stop services """ def ps(self, options): From 88b04aaa9c56fe26dfdfe31d49450dc0947a1c5d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 13 Dec 2013 20:36:10 +0000 Subject: [PATCH 0030/2154] Add support for building images --- plum/service.py | 23 ++++++++++++++----- .../fixtures/simple-dockerfile/Dockerfile | 2 ++ plum/tests/service_test.py | 10 ++++++++ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 plum/tests/fixtures/simple-dockerfile/Dockerfile diff --git a/plum/service.py b/plum/service.py index 254bc9d9..3bcbdb2b 100644 --- a/plum/service.py +++ b/plum/service.py @@ -5,6 +5,8 @@ class Service(object): def __init__(self, name, client=None, links=[], **options): if not re.match('^[a-zA-Z0-9_]+$', name): raise ValueError('Invalid name: %s' % name) + if 'image' in options and 'build' in options: + raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.') self.name = name self.client = client @@ -17,7 +19,7 @@ class Service(object): def start(self): if len(self.containers) == 0: - self.start_container() + return self.start_container() def stop(self): self.scale(0) @@ -30,15 +32,12 @@ class Service(object): self.stop_container() def start_container(self, **override_options): - options = dict(self.options) - options.update(override_options) - number = self.next_container_number() - name = make_name(self.name, number) - container = self.client.create_container(name=name, **options) + container = self.client.create_container(**self._get_container_options(override_options)) self.client.start( container['Id'], links=self._get_links(), ) + return container['Id'] def stop_container(self): self.client.kill(self.containers[0]['Id']) @@ -64,6 +63,18 @@ class Service(object): links[name] = name return links + def _get_container_options(self, override_options): + keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from'] + container_options = dict((k, self.options[k]) for k in keys if k in self.options) + container_options.update(override_options) + + number = self.next_container_number() + container_options['name'] = make_name(self.name, number) + + if 'build' in self.options: + container_options['image'] = self.client.build(self.options['build'])[0] + + return container_options def make_name(prefix, number): diff --git a/plum/tests/fixtures/simple-dockerfile/Dockerfile b/plum/tests/fixtures/simple-dockerfile/Dockerfile new file mode 100644 index 00000000..b7fd870f --- /dev/null +++ b/plum/tests/fixtures/simple-dockerfile/Dockerfile @@ -0,0 +1,2 @@ +FROM ubuntu +CMD echo "success" diff --git a/plum/tests/service_test.py b/plum/tests/service_test.py index ed2c3db7..571e0efb 100644 --- a/plum/tests/service_test.py +++ b/plum/tests/service_test.py @@ -78,4 +78,14 @@ class NameTestCase(DockerClientTestCase): db.stop() web.stop() + def test_start_container_builds_images(self): + service = Service( + name='test', + client=self.client, + build='plum/tests/fixtures/simple-dockerfile', + ) + container = service.start() + self.client.wait(container) + self.assertIn('success', self.client.logs(container)) + From 539f1acdb8307d18e591442927b978043f2786e0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 13 Dec 2013 20:43:52 +0000 Subject: [PATCH 0031/2154] Use RUN in readme example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a2fe7dc..625131ec 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ web: FROM orchardup/rails ADD . /code - CMD: bundle exec rackup + RUN bundle exec rackup ### Communicating between containers From 21159b801fcd8498e8cc810822eee127b6bc0dfc Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 13 Dec 2013 20:44:35 +0000 Subject: [PATCH 0032/2154] Fix readme formatting --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 625131ec..efe80d93 100644 --- a/README.md +++ b/README.md @@ -118,18 +118,18 @@ You can pass extra configuration options to a container, much like with `docker web: build: web/ --- override the default run command -run: bundle exec thin -p 3000 + -- override the default run command + run: bundle exec thin -p 3000 --- expose ports - can also be an array -ports: 3000 + -- expose ports - can also be an array + ports: 3000 --- map volumes - can also be an array -volumes: /tmp/cache + -- map volumes - can also be an array + volumes: /tmp/cache --- add environment variables - can also be a dictionary -environment: - - RACK_ENV=development + -- add environment variables - can also be a dictionary + environment: + RACK_ENV: development ``` From 5c5bb9a02f735620d4e5fb2e135f32e83e962bfb Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 13 Dec 2013 20:55:28 +0000 Subject: [PATCH 0033/2154] Add basic run command --- plum/cli/main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plum/cli/main.py b/plum/cli/main.py index 9f18f8ff..6985b1fe 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -64,6 +64,7 @@ class TopLevelCommand(Command): Commands: ps List services and containers + run Run a one-off command start Start services stop Stop services @@ -78,6 +79,15 @@ class TopLevelCommand(Command): for container in service.containers: print container['Names'][0] + def run(self, options): + """ + Run a one-off command. + + Usage: run SERVICE COMMAND [ARGS...] + """ + service = self.service_collection.get(options['SERVICE']) + service.start_container(command=[options['COMMAND']] + options['ARGS']) + def start(self, options): """ Start all services From 772585109dfb127f1937963bba30ae0ca13c9940 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 14 Dec 2013 16:34:24 +0000 Subject: [PATCH 0034/2154] Moved tests to root directory --- {plum/tests => tests}/__init__.py | 0 {plum/tests => tests}/fixtures/simple-dockerfile/Dockerfile | 0 {plum/tests => tests}/service_collection_test.py | 0 {plum/tests => tests}/service_test.py | 2 +- {plum/tests => tests}/testcases.py | 0 5 files changed, 1 insertion(+), 1 deletion(-) rename {plum/tests => tests}/__init__.py (100%) rename {plum/tests => tests}/fixtures/simple-dockerfile/Dockerfile (100%) rename {plum/tests => tests}/service_collection_test.py (100%) rename {plum/tests => tests}/service_test.py (97%) rename {plum/tests => tests}/testcases.py (100%) diff --git a/plum/tests/__init__.py b/tests/__init__.py similarity index 100% rename from plum/tests/__init__.py rename to tests/__init__.py diff --git a/plum/tests/fixtures/simple-dockerfile/Dockerfile b/tests/fixtures/simple-dockerfile/Dockerfile similarity index 100% rename from plum/tests/fixtures/simple-dockerfile/Dockerfile rename to tests/fixtures/simple-dockerfile/Dockerfile diff --git a/plum/tests/service_collection_test.py b/tests/service_collection_test.py similarity index 100% rename from plum/tests/service_collection_test.py rename to tests/service_collection_test.py diff --git a/plum/tests/service_test.py b/tests/service_test.py similarity index 97% rename from plum/tests/service_test.py rename to tests/service_test.py index 571e0efb..33789448 100644 --- a/plum/tests/service_test.py +++ b/tests/service_test.py @@ -82,7 +82,7 @@ class NameTestCase(DockerClientTestCase): service = Service( name='test', client=self.client, - build='plum/tests/fixtures/simple-dockerfile', + build='tests/fixtures/simple-dockerfile', ) container = service.start() self.client.wait(container) diff --git a/plum/tests/testcases.py b/tests/testcases.py similarity index 100% rename from plum/tests/testcases.py rename to tests/testcases.py From 03e16c49815bc50477734f9f5a334f95e985f48a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 14 Dec 2013 16:46:34 +0000 Subject: [PATCH 0035/2154] Revert "Use RUN in readme example" This reverts commit 539f1acdb8307d18e591442927b978043f2786e0. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efe80d93..5d77ec6a 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ web: FROM orchardup/rails ADD . /code - RUN bundle exec rackup + CMD: bundle exec rackup ### Communicating between containers From 96ca74ccc8a94bad76f409bd0ca2e08bd4a5cdf1 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 14 Dec 2013 16:46:50 +0000 Subject: [PATCH 0036/2154] Fix readme example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d77ec6a..e7d5387c 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ web: FROM orchardup/rails ADD . /code - CMD: bundle exec rackup + CMD bundle exec rackup ### Communicating between containers From ee0ac206e008131c6729f863e5c024c359203b37 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 16 Dec 2013 10:51:22 +0000 Subject: [PATCH 0037/2154] Add build log message --- plum/service.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plum/service.py b/plum/service.py index 3bcbdb2b..ff198ee4 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,5 +1,7 @@ +import logging import re +log = logging.getLogger(__name__) class Service(object): def __init__(self, name, client=None, links=[], **options): @@ -72,6 +74,7 @@ class Service(object): container_options['name'] = make_name(self.name, number) if 'build' in self.options: + log.info('Building %s from %s...' % (self.name, self.options['build'])) container_options['image'] = self.client.build(self.options['build'])[0] return container_options From 6a2d528d2e278cb93cc76bdce38087d867804b7e Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 16 Dec 2013 11:22:54 +0000 Subject: [PATCH 0038/2154] Add port binding --- plum/service.py | 7 ++++++- requirements.txt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plum/service.py b/plum/service.py index ff198ee4..940859d3 100644 --- a/plum/service.py +++ b/plum/service.py @@ -34,10 +34,15 @@ class Service(object): self.stop_container() def start_container(self, **override_options): - container = self.client.create_container(**self._get_container_options(override_options)) + container_options = self._get_container_options(override_options) + container = self.client.create_container(**container_options) + port_bindings = {} + for port in container_options.get('ports', []): + port_bindings[port] = None self.client.start( container['Id'], links=self._get_links(), + port_bindings=port_bindings, ) return container['Id'] diff --git a/requirements.txt b/requirements.txt index 6d769742..8523db07 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -git+git://github.com/dotcloud/docker-py.git@4fde1a242e1853cbf83e5a36371d8b4a49501c52 +git+git://github.com/dotcloud/docker-py.git@5c928dcab51a276f421a36d584c37b745b3b9a3d docopt==0.6.1 PyYAML==3.10 From 6abec8570389e0f262910ae1ead8c39340e06560 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 17 Dec 2013 11:44:36 +0000 Subject: [PATCH 0039/2154] Fix variable naming in service collection --- plum/service_collection.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plum/service_collection.py b/plum/service_collection.py index 82ea78ff..3c59c6ed 100644 --- a/plum/service_collection.py +++ b/plum/service_collection.py @@ -43,12 +43,12 @@ class ServiceCollection(list): return service def start(self): - for container in self: - container.start() + for service in self: + service.start() def stop(self): - for container in self: - container.stop() + for service in self: + service.stop() From accc1a219ac9dde6e9f9eeaef11acbfd6c8cb954 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 17 Dec 2013 12:09:45 +0000 Subject: [PATCH 0040/2154] Perform all operations against stopped containers --- plum/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 940859d3..09e297b0 100644 --- a/plum/service.py +++ b/plum/service.py @@ -17,7 +17,7 @@ class Service(object): @property def containers(self): - return [c for c in self.client.containers() if parse_name(get_container_name(c))[0] == self.name] + return [c for c in self.client.containers(all=True) if parse_name(get_container_name(c))[0] == self.name] def start(self): if len(self.containers) == 0: From d3bd7f3239b05bbde48fd25b3ca847e8e12a29d2 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 17 Dec 2013 12:12:05 +0000 Subject: [PATCH 0041/2154] Remove containers after stopping them --- plum/service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 09e297b0..144f3d54 100644 --- a/plum/service.py +++ b/plum/service.py @@ -47,7 +47,9 @@ class Service(object): return container['Id'] def stop_container(self): - self.client.kill(self.containers[0]['Id']) + container_id = self.containers[-1]['Id'] + self.client.kill(container_id) + self.client.remove_container(container_id) def next_container_number(self): numbers = [parse_name(get_container_name(c))[1] for c in self.containers] From 3e680a2c7ab78345c8db4bb5cd19ec62be172ac3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 17 Dec 2013 12:12:13 +0000 Subject: [PATCH 0042/2154] Fix container naming --- plum/cli/main.py | 3 ++- plum/service.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 6985b1fe..95eef03b 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -8,6 +8,7 @@ from docopt import docopt from inspect import getdoc from .. import __version__ +from ..service import get_container_name from ..service_collection import ServiceCollection from .command import Command @@ -77,7 +78,7 @@ class TopLevelCommand(Command): """ for service in self.service_collection: for container in service.containers: - print container['Names'][0] + print get_container_name(container) def run(self, options): """ diff --git a/plum/service.py b/plum/service.py index 144f3d54..f734c41e 100644 --- a/plum/service.py +++ b/plum/service.py @@ -103,4 +103,6 @@ def parse_name(name): def get_container_name(container): - return container['Names'][0][1:] + for name in container['Names']: + if len(name.split('/')) == 2: + return name[1:] From 4fdd2dc077c398a15d1c0db84e96790aae4548c5 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 17 Dec 2013 14:13:12 +0000 Subject: [PATCH 0043/2154] Print output from run --- plum/cli/main.py | 11 ++++++++++- plum/service.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 95eef03b..c43cf7a7 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -87,7 +87,16 @@ class TopLevelCommand(Command): Usage: run SERVICE COMMAND [ARGS...] """ service = self.service_collection.get(options['SERVICE']) - service.start_container(command=[options['COMMAND']] + options['ARGS']) + container_options = { + 'command': [options['COMMAND']] + options['ARGS'], + } + container = service.create_container(**container_options) + stream = self.client.logs(container, stream=True) + service.start_container(container, **container_options) + for data in stream: + if data is None: + break + print data def start(self, options): """ diff --git a/plum/service.py b/plum/service.py index f734c41e..e81ed010 100644 --- a/plum/service.py +++ b/plum/service.py @@ -33,9 +33,14 @@ class Service(object): while len(self.containers) > num: self.stop_container() - def start_container(self, **override_options): + def create_container(self, **override_options): container_options = self._get_container_options(override_options) - container = self.client.create_container(**container_options) + return self.client.create_container(**container_options) + + def start_container(self, container=None, **override_options): + container_options = self._get_container_options(override_options) + if container is None: + container = self.create_container(**override_options) port_bindings = {} for port in container_options.get('ports', []): port_bindings[port] = None @@ -44,7 +49,7 @@ class Service(object): links=self._get_links(), port_bindings=port_bindings, ) - return container['Id'] + return container def stop_container(self): container_id = self.containers[-1]['Id'] From 120d57e856ac5955eedbee41cc59c04b8b1952f0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 17 Dec 2013 15:42:46 +0000 Subject: [PATCH 0044/2154] Change plum up to plum start --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e7d5387c..c91370cd 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ db: You've now given Plum the minimal amount of configuration it needs to run: ```bash -$ plum up +$ plum start Building db... done db is running at 127.0.0.1:45678 <...output from postgresql server...> @@ -51,10 +51,10 @@ db is running at 127.0.0.1:45678 For each service you've defined, Plum will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. -By default, `plum up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: +By default, `plum start` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: ```bash -$ plum run -d +$ plum start -d Building db... done db is running at 127.0.0.1:45678 @@ -151,7 +151,7 @@ Running more than one container for a service You can set the number of containers to run for each service with `plum scale`: ```bash -$ plum up -d +$ plum start -d db is running at 127.0.0.1:45678 web is running at 127.0.0.1:45679 From 87c46e281ce8c45f7047008eab31a9da6c1d2788 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 11:14:14 +0000 Subject: [PATCH 0045/2154] Add support for specifying external port --- plum/cli/main.py | 1 - plum/service.py | 12 ++++++++++-- tests/service_test.py | 16 +++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index c43cf7a7..93196edf 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -92,7 +92,6 @@ class TopLevelCommand(Command): } container = service.create_container(**container_options) stream = self.client.logs(container, stream=True) - service.start_container(container, **container_options) for data in stream: if data is None: break diff --git a/plum/service.py b/plum/service.py index e81ed010..2d12dbad 100644 --- a/plum/service.py +++ b/plum/service.py @@ -42,8 +42,13 @@ class Service(object): if container is None: container = self.create_container(**override_options) port_bindings = {} - for port in container_options.get('ports', []): - port_bindings[port] = None + for port in self.options.get('ports', []): + port = unicode(port) + if ':' in port: + internal_port, external_port = port.split(':', 1) + port_bindings[int(internal_port)] = int(external_port) + else: + port_bindings[int(port)] = None self.client.start( container['Id'], links=self._get_links(), @@ -85,6 +90,9 @@ class Service(object): number = self.next_container_number() container_options['name'] = make_name(self.name, number) + if 'ports' in container_options: + container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']] + if 'build' in self.options: log.info('Building %s from %s...' % (self.name, self.options['build'])) container_options['image'] = self.client.build(self.options['build'])[0] diff --git a/tests/service_test.py b/tests/service_test.py index 33789448..f2c32507 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -87,5 +87,19 @@ class NameTestCase(DockerClientTestCase): container = service.start() self.client.wait(container) self.assertIn('success', self.client.logs(container)) - + + def test_start_container_creates_ports(self): + service = self.create_service('web', ports=[8000]) + service.start_container() + container = service.inspect()[0] + self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) + self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') + + def test_start_container_creates_fixed_external_ports(self): + service = self.create_service('web', ports=['8000:8000']) + service.start_container() + container = service.inspect()[0] + self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) + self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') + From 785cb12833c586e8165608e1d45ccf2ca4c6c762 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 11:15:59 +0000 Subject: [PATCH 0046/2154] Add missing format var in error --- plum/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 2d12dbad..4f2a1083 100644 --- a/plum/service.py +++ b/plum/service.py @@ -8,7 +8,7 @@ class Service(object): if not re.match('^[a-zA-Z0-9_]+$', name): raise ValueError('Invalid name: %s' % name) if 'image' in options and 'build' in options: - raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.') + raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) self.name = name self.client = client From 24a98b0552c2972b550d7f6a8a903888033d8b94 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 11:37:51 +0000 Subject: [PATCH 0047/2154] Pull images if they do not exist --- plum/service.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 4f2a1083..58dc8294 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,3 +1,4 @@ +from docker.client import APIError import logging import re @@ -34,8 +35,19 @@ class Service(object): self.stop_container() def create_container(self, **override_options): + """ + Create a container for this service. If the image doesn't exist, attempt to pull + it. + """ container_options = self._get_container_options(override_options) - return self.client.create_container(**container_options) + try: + return self.client.create_container(**container_options) + except APIError, e: + if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation: + log.info('Pulling image %s...' % container_options['image']) + self.client.pull(container_options['image']) + return self.client.create_container(**container_options) + raise def start_container(self, container=None, **override_options): container_options = self._get_container_options(override_options) From 90130eec65b6bfa2e15e6a9d0d3002ae281f87c7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 12:01:54 +0000 Subject: [PATCH 0048/2154] Ignore non-plum containers --- plum/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 58dc8294..5034fd2e 100644 --- a/plum/service.py +++ b/plum/service.py @@ -120,7 +120,7 @@ def parse_name(name): match = re.match('^(.+)_(\d+)$', name) if match is None: - raise ValueError("Invalid name: %s" % name) + return (None, None) (service_name, suffix) = match.groups() From cb366eed7adcf2f1aaf322655af708b0f8e712ae Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 13:13:40 +0000 Subject: [PATCH 0049/2154] Add logging to start and stop --- plum/service.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plum/service.py b/plum/service.py index 5034fd2e..d8e7f235 100644 --- a/plum/service.py +++ b/plum/service.py @@ -61,6 +61,7 @@ class Service(object): port_bindings[int(internal_port)] = int(external_port) else: port_bindings[int(port)] = None + log.info("Starting %s..." % container_options['name']) self.client.start( container['Id'], links=self._get_links(), @@ -69,9 +70,10 @@ class Service(object): return container def stop_container(self): - container_id = self.containers[-1]['Id'] - self.client.kill(container_id) - self.client.remove_container(container_id) + container = self.containers[-1] + log.info("Stopping and removing %s..." % get_container_name(container)) + self.client.kill(container) + self.client.remove_container(container) def next_container_number(self): numbers = [parse_name(get_container_name(c))[1] for c in self.containers] From 3458dd2fad1176665262d9ccbd1e17a8d083450c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 16:12:53 +0000 Subject: [PATCH 0050/2154] Print build output --- plum/service.py | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/plum/service.py b/plum/service.py index d8e7f235..8a862bc1 100644 --- a/plum/service.py +++ b/plum/service.py @@ -4,6 +4,11 @@ import re log = logging.getLogger(__name__) + +class BuildError(Exception): + pass + + class Service(object): def __init__(self, name, client=None, links=[], **options): if not re.match('^[a-zA-Z0-9_]+$', name): @@ -50,7 +55,6 @@ class Service(object): raise def start_container(self, container=None, **override_options): - container_options = self._get_container_options(override_options) if container is None: container = self.create_container(**override_options) port_bindings = {} @@ -61,7 +65,7 @@ class Service(object): port_bindings[int(internal_port)] = int(external_port) else: port_bindings[int(port)] = None - log.info("Starting %s..." % container_options['name']) + log.info("Starting %s..." % container['Id']) self.client.start( container['Id'], links=self._get_links(), @@ -108,11 +112,29 @@ class Service(object): container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']] if 'build' in self.options: - log.info('Building %s from %s...' % (self.name, self.options['build'])) - container_options['image'] = self.client.build(self.options['build'])[0] + container_options['image'] = self.build() return container_options + def build(self): + log.info('Building %s from %s...' % (self.name, self.options['build'])) + + build_output = self.client.build(self.options['build'], stream=True) + + image_id = None + + for line in build_output: + if line: + match = re.search(r'Successfully built ([0-9a-f]+)', line) + if match: + image_id = match.group(1) + print line + + if image_id is None: + raise BuildError() + + return image_id + def make_name(prefix, number): return '%s_%s' % (prefix, number) @@ -130,6 +152,10 @@ def parse_name(name): def get_container_name(container): + # inspect + if 'Name' in container: + return container['Name'] + # ps for name in container['Names']: if len(name.split('/')) == 2: return name[1:] From f0df5c60796f05b79ec3c2435d8fc3b57de3b0b9 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Dec 2013 14:54:28 +0000 Subject: [PATCH 0051/2154] Refactor container retrieval / name parsing --- plum/service.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plum/service.py b/plum/service.py index 8a862bc1..b3fadbd9 100644 --- a/plum/service.py +++ b/plum/service.py @@ -23,7 +23,13 @@ class Service(object): @property def containers(self): - return [c for c in self.client.containers(all=True) if parse_name(get_container_name(c))[0] == self.name] + return list(self.get_containers(all=True)) + + def get_containers(self, all): + for container in self.client.containers(all=all): + name = get_container_name(container) + if is_valid_name(name) and parse_name(name)[0] == self.name: + yield container def start(self): if len(self.containers) == 0: @@ -136,18 +142,20 @@ class Service(object): return image_id +name_regex = '^(.+)_(\d+)$' + + def make_name(prefix, number): return '%s_%s' % (prefix, number) +def is_valid_name(name): + return (re.match(name_regex, name) is not None) + + def parse_name(name): - match = re.match('^(.+)_(\d+)$', name) - - if match is None: - return (None, None) - + match = re.match(name_regex, name) (service_name, suffix) = match.groups() - return (service_name, int(suffix)) From 23c3dc430b4e803b0eeaadf168f1e19466890f14 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Dec 2013 14:54:51 +0000 Subject: [PATCH 0052/2154] Add texttable to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 8523db07..f8e53737 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ git+git://github.com/dotcloud/docker-py.git@5c928dcab51a276f421a36d584c37b745b3b9a3d docopt==0.6.1 PyYAML==3.10 +texttable==0.8.1 From 64253a8290b073f5238ca4d49517de6cd7c500e5 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Dec 2013 14:58:58 +0000 Subject: [PATCH 0053/2154] Basic log output --- plum/cli/colors.py | 41 +++++++++++++++++++++++++++++++ plum/cli/log_printer.py | 53 +++++++++++++++++++++++++++++++++++++++++ plum/cli/main.py | 13 ++++++++++ plum/cli/multiplexer.py | 32 +++++++++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 plum/cli/colors.py create mode 100644 plum/cli/log_printer.py create mode 100644 plum/cli/multiplexer.py diff --git a/plum/cli/colors.py b/plum/cli/colors.py new file mode 100644 index 00000000..09ec84bd --- /dev/null +++ b/plum/cli/colors.py @@ -0,0 +1,41 @@ +NAMES = [ + 'grey', + 'red', + 'green', + 'yellow', + 'blue', + 'magenta', + 'cyan', + 'white' +] + + +def get_pairs(): + for i, name in enumerate(NAMES): + yield(name, str(30 + i)) + yield('intense_' + name, str(30 + i) + ';1') + + +def ansi(code): + return '\033[{0}m'.format(code) + + +def ansi_color(code, s): + return '{0}{1}{2}'.format(ansi(code), s, ansi(0)) + + +def make_color_fn(code): + return lambda s: ansi_color(code, s) + + +for (name, code) in get_pairs(): + globals()[name] = make_color_fn(code) + + +def rainbow(): + cs = ['cyan', 'yellow', 'green', 'magenta', 'red', 'blue', + 'intense_cyan', 'intense_yellow', 'intense_green', + 'intense_magenta', 'intense_red', 'intense_blue'] + + for c in cs: + yield globals()[c] diff --git a/plum/cli/log_printer.py b/plum/cli/log_printer.py new file mode 100644 index 00000000..9fced4f3 --- /dev/null +++ b/plum/cli/log_printer.py @@ -0,0 +1,53 @@ +import sys + +from itertools import cycle + +from ..service import get_container_name +from .multiplexer import Multiplexer +from . import colors + + +class LogPrinter(object): + def __init__(self, client): + self.client = client + + def attach(self, containers): + generators = self._make_log_generators(containers) + mux = Multiplexer(generators) + for line in mux.loop(): + sys.stdout.write(line) + + def _make_log_generators(self, containers): + color_fns = cycle(colors.rainbow()) + generators = [] + + for container in containers: + color_fn = color_fns.next() + generators.append(self._make_log_generator(container, color_fn)) + + return generators + + def _make_log_generator(self, container, color_fn): + container_name = get_container_name(container) + format = lambda line: color_fn(container_name + " | ") + line + return (format(line) for line in self._readlines(container)) + + def _readlines(self, container, logs=False, stream=True): + socket = self.client.attach_socket( + container['Id'], + params={ + 'stdin': 0, + 'stdout': 1, + 'stderr': 1, + 'logs': 1 if logs else 0, + 'stream': 1 if stream else 0 + }, + ) + + for line in iter(socket.makefile().readline, b''): + if not line.endswith('\n'): + line += '\n' + + yield line + + socket.close() diff --git a/plum/cli/main.py b/plum/cli/main.py index 93196edf..9c14e8f5 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -11,6 +11,7 @@ from .. import __version__ from ..service import get_container_name from ..service_collection import ServiceCollection from .command import Command +from .log_printer import LogPrinter from .errors import UserError from .docopt_command import NoSuchCommand @@ -113,3 +114,15 @@ class TopLevelCommand(Command): """ self.service_collection.stop() + def logs(self, options): + """ + View containers' output + + Usage: logs + """ + containers = self._get_containers(all=False) + print "Attaching to", ", ".join(get_container_name(c) for c in containers) + LogPrinter(client=self.client).attach(containers) + + def _get_containers(self, all): + return [c for s in self.service_collection for c in s.get_containers(all=all)] diff --git a/plum/cli/multiplexer.py b/plum/cli/multiplexer.py new file mode 100644 index 00000000..cfcc3b6c --- /dev/null +++ b/plum/cli/multiplexer.py @@ -0,0 +1,32 @@ +from threading import Thread + +try: + from Queue import Queue, Empty +except ImportError: + from queue import Queue, Empty # Python 3.x + + +class Multiplexer(object): + def __init__(self, generators): + self.generators = generators + self.queue = Queue() + + def loop(self): + self._init_readers() + + while True: + try: + yield self.queue.get(timeout=0.1) + except Empty: + pass + + def _init_readers(self): + for generator in self.generators: + t = Thread(target=_enqueue_output, args=(generator, self.queue)) + t.daemon = True + t.start() + + +def _enqueue_output(generator, queue): + for item in generator: + queue.put(item) From 4cc906fcd29dbec4683a3ab2dba6c7a88d40c216 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Dec 2013 15:12:45 +0000 Subject: [PATCH 0054/2154] ps only lists running containers --- plum/cli/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 9c14e8f5..5650cdeb 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -77,9 +77,8 @@ class TopLevelCommand(Command): Usage: ps """ - for service in self.service_collection: - for container in service.containers: - print get_container_name(container) + for container in self._get_containers(all=False): + print get_container_name(container) def run(self, options): """ From e5642bd8b71d7a4a413565c95f39399a097b347d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Dec 2013 15:13:02 +0000 Subject: [PATCH 0055/2154] Show a sensible error when an unknown service name is given to 'run' --- plum/cli/main.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plum/cli/main.py b/plum/cli/main.py index 5650cdeb..f4ba5766 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -87,6 +87,8 @@ class TopLevelCommand(Command): Usage: run SERVICE COMMAND [ARGS...] """ service = self.service_collection.get(options['SERVICE']) + if service is None: + raise UserError("No such service: %s" % options['SERVICE']) container_options = { 'command': [options['COMMAND']] + options['ARGS'], } From 5e1e4a71e00831a5151c3091af7a3c024b6cfa46 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 17:01:50 +0000 Subject: [PATCH 0056/2154] Rename ServiceTest --- tests/service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/service_test.py b/tests/service_test.py index f2c32507..321f550b 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -2,7 +2,7 @@ from plum import Service from .testcases import DockerClientTestCase -class NameTestCase(DockerClientTestCase): +class ServiceTest(DockerClientTestCase): def test_name_validations(self): self.assertRaises(ValueError, lambda: Service(name='')) From a5fc880d1046d28543cbdebd8d918301e4cc3030 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 18:37:48 +0000 Subject: [PATCH 0057/2154] Refactor service to add a container object --- plum/cli/main.py | 4 +- plum/container.py | 86 ++++++++++++++++++++++++++++++++ plum/service.py | 46 +++++++---------- tests/container_test.py | 36 +++++++++++++ tests/service_collection_test.py | 8 +-- tests/service_test.py | 46 ++++++++--------- 6 files changed, 169 insertions(+), 57 deletions(-) create mode 100644 plum/container.py create mode 100644 tests/container_test.py diff --git a/plum/cli/main.py b/plum/cli/main.py index f4ba5766..56ef3e57 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -78,7 +78,7 @@ class TopLevelCommand(Command): Usage: ps """ for container in self._get_containers(all=False): - print get_container_name(container) + print container.name def run(self, options): """ @@ -126,4 +126,4 @@ class TopLevelCommand(Command): LogPrinter(client=self.client).attach(containers) def _get_containers(self, all): - return [c for s in self.service_collection for c in s.get_containers(all=all)] + return [c for s in self.service_collection for c in s.containers(all=all)] diff --git a/plum/container.py b/plum/container.py new file mode 100644 index 00000000..9ce6509d --- /dev/null +++ b/plum/container.py @@ -0,0 +1,86 @@ + + +class Container(object): + """ + Represents a Docker container, constructed from the output of + GET /containers/:id:/json. + """ + def __init__(self, client, dictionary, has_been_inspected=False): + self.client = client + self.dictionary = dictionary + self.has_been_inspected = has_been_inspected + + @classmethod + def from_ps(cls, client, dictionary, **kwargs): + """ + Construct a container object from the output of GET /containers/json. + """ + new_dictionary = { + 'ID': dictionary['Id'], + 'Image': dictionary['Image'], + } + for name in dictionary.get('Names', []): + if len(name.split('/')) == 2: + new_dictionary['Name'] = name + return cls(client, new_dictionary, **kwargs) + + @classmethod + def from_id(cls, client, id): + return cls(client, client.inspect_container(id)) + + @classmethod + def create(cls, client, **options): + response = client.create_container(**options) + return cls.from_id(client, response['Id']) + + @property + def id(self): + return self.dictionary['ID'] + + @property + def name(self): + return self.dictionary['Name'] + + @property + def environment(self): + self.inspect_if_not_inspected() + out = {} + for var in self.dictionary.get('Config', {}).get('Env', []): + k, v = var.split('=', 1) + out[k] = v + return out + + def start(self, **options): + return self.client.start(self.id, **options) + + def stop(self): + return self.client.stop(self.id) + + def kill(self): + return self.client.kill(self.id) + + def remove(self): + return self.client.remove_container(self.id) + + def inspect_if_not_inspected(self): + if not self.has_been_inspected: + self.inspect() + + def wait(self): + return self.client.wait(self.id) + + def logs(self, *args, **kwargs): + return self.client.logs(self.id, *args, **kwargs) + + def inspect(self): + self.dictionary = self.client.inspect_container(self.id) + return self.dictionary + + def links(self): + links = [] + for container in self.client.containers(): + for name in container['Names']: + bits = name.split('/') + if len(bits) > 2 and bits[1] == self.name[1:]: + links.append(bits[2]) + return links diff --git a/plum/service.py b/plum/service.py index b3fadbd9..29647051 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,6 +1,7 @@ from docker.client import APIError import logging import re +from .container import Container log = logging.getLogger(__name__) @@ -21,28 +22,26 @@ class Service(object): self.links = links or [] self.options = options - @property - def containers(self): - return list(self.get_containers(all=True)) - - def get_containers(self, all): + def containers(self, all=False): + l = [] for container in self.client.containers(all=all): name = get_container_name(container) if is_valid_name(name) and parse_name(name)[0] == self.name: - yield container + l.append(Container.from_ps(self.client, container)) + return l def start(self): - if len(self.containers) == 0: + if len(self.containers()) == 0: return self.start_container() def stop(self): self.scale(0) def scale(self, num): - while len(self.containers) < num: + while len(self.containers()) < num: self.start_container() - while len(self.containers) > num: + while len(self.containers()) > num: self.stop_container() def create_container(self, **override_options): @@ -52,12 +51,12 @@ class Service(object): """ container_options = self._get_container_options(override_options) try: - return self.client.create_container(**container_options) + return Container.create(self.client, **container_options) except APIError, e: if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation: log.info('Pulling image %s...' % container_options['image']) self.client.pull(container_options['image']) - return self.client.create_container(**container_options) + return Container.create(self.client, **container_options) raise def start_container(self, container=None, **override_options): @@ -71,39 +70,32 @@ class Service(object): port_bindings[int(internal_port)] = int(external_port) else: port_bindings[int(port)] = None - log.info("Starting %s..." % container['Id']) - self.client.start( - container['Id'], + log.info("Starting %s..." % container.name) + container.start( links=self._get_links(), port_bindings=port_bindings, ) return container def stop_container(self): - container = self.containers[-1] - log.info("Stopping and removing %s..." % get_container_name(container)) - self.client.kill(container) - self.client.remove_container(container) + container = self.containers()[-1] + log.info("Stopping and removing %s..." % container.name) + container.kill() + container.remove() def next_container_number(self): - numbers = [parse_name(get_container_name(c))[1] for c in self.containers] + numbers = [parse_name(c.name)[1] for c in self.containers(all=True)] if len(numbers) == 0: return 1 else: return max(numbers) + 1 - def get_names(self): - return [get_container_name(c) for c in self.containers] - - def inspect(self): - return [self.client.inspect_container(c['Id']) for c in self.containers] - def _get_links(self): links = {} for service in self.links: - for name in service.get_names(): - links[name] = name + for container in service.containers(): + links[container.name[1:]] = container.name[1:] return links def _get_container_options(self, override_options): diff --git a/tests/container_test.py b/tests/container_test.py new file mode 100644 index 00000000..8628b04d --- /dev/null +++ b/tests/container_test.py @@ -0,0 +1,36 @@ +from .testcases import DockerClientTestCase +from plum.container import Container + +class ContainerTest(DockerClientTestCase): + def test_from_ps(self): + container = Container.from_ps(self.client, { + "Id":"abc", + "Image":"ubuntu:12.04", + "Command":"sleep 300", + "Created":1387384730, + "Status":"Up 8 seconds", + "Ports":None, + "SizeRw":0, + "SizeRootFs":0, + "Names":["/db_1"] + }, has_been_inspected=True) + self.assertEqual(container.dictionary, { + "ID": "abc", + "Image":"ubuntu:12.04", + "Name": "/db_1", + }) + + def test_environment(self): + container = Container(self.client, { + 'ID': 'abc', + 'Config': { + 'Env': [ + 'FOO=BAR', + 'BAZ=DOGE', + ] + } + }, has_been_inspected=True) + self.assertEqual(container.environment, { + 'FOO': 'BAR', + 'BAZ': 'DOGE', + }) diff --git a/tests/service_collection_test.py b/tests/service_collection_test.py index 1c764eca..7dbffdce 100644 --- a/tests/service_collection_test.py +++ b/tests/service_collection_test.py @@ -50,13 +50,13 @@ class ServiceCollectionTest(DockerClientTestCase): collection.start() - self.assertEqual(len(collection[0].containers), 1) - self.assertEqual(len(collection[1].containers), 1) + self.assertEqual(len(collection[0].containers()), 1) + self.assertEqual(len(collection[1].containers()), 1) collection.stop() - self.assertEqual(len(collection[0].containers), 0) - self.assertEqual(len(collection[1].containers), 0) + self.assertEqual(len(collection[0].containers()), 0) + self.assertEqual(len(collection[1].containers()), 0) diff --git a/tests/service_test.py b/tests/service_test.py index 321f550b..62d53e56 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -24,57 +24,57 @@ class ServiceTest(DockerClientTestCase): foo.start() - self.assertEqual(len(foo.containers), 1) - self.assertEqual(foo.containers[0]['Names'], ['/foo_1']) - self.assertEqual(len(bar.containers), 0) + self.assertEqual(len(foo.containers()), 1) + self.assertEqual(foo.containers()[0].name, '/foo_1') + self.assertEqual(len(bar.containers()), 0) bar.scale(2) - self.assertEqual(len(foo.containers), 1) - self.assertEqual(len(bar.containers), 2) + self.assertEqual(len(foo.containers()), 1) + self.assertEqual(len(bar.containers()), 2) - names = [c['Names'] for c in bar.containers] - self.assertIn(['/bar_1'], names) - self.assertIn(['/bar_2'], names) + names = [c.name for c in bar.containers()] + self.assertIn('/bar_1', names) + self.assertIn('/bar_2', names) def test_up_scale_down(self): service = self.create_service('scaling_test') - self.assertEqual(len(service.containers), 0) + self.assertEqual(len(service.containers()), 0) service.start() - self.assertEqual(len(service.containers), 1) + self.assertEqual(len(service.containers()), 1) service.start() - self.assertEqual(len(service.containers), 1) + self.assertEqual(len(service.containers()), 1) service.scale(2) - self.assertEqual(len(service.containers), 2) + self.assertEqual(len(service.containers()), 2) service.scale(1) - self.assertEqual(len(service.containers), 1) + self.assertEqual(len(service.containers()), 1) service.stop() - self.assertEqual(len(service.containers), 0) + self.assertEqual(len(service.containers()), 0) service.stop() - self.assertEqual(len(service.containers), 0) + self.assertEqual(len(service.containers()), 0) def test_start_container_passes_through_options(self): db = self.create_service('db') db.start_container(environment={'FOO': 'BAR'}) - self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) + self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') def test_start_container_inherits_options_from_constructor(self): db = self.create_service('db', environment={'FOO': 'BAR'}) db.start_container() - self.assertEqual(db.inspect()[0]['Config']['Env'], ['FOO=BAR']) + self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') def test_start_container_creates_links(self): db = self.create_service('db') web = self.create_service('web', links=[db]) db.start_container() web.start_container() - self.assertIn('/web_1/db_1', db.containers[0]['Names']) + self.assertIn('db_1', web.containers()[0].links()) db.stop() web.stop() @@ -85,20 +85,18 @@ class ServiceTest(DockerClientTestCase): build='tests/fixtures/simple-dockerfile', ) container = service.start() - self.client.wait(container) - self.assertIn('success', self.client.logs(container)) + container.wait() + self.assertIn('success', container.logs()) def test_start_container_creates_ports(self): service = self.create_service('web', ports=[8000]) - service.start_container() - container = service.inspect()[0] + container = service.start_container().inspect() self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_creates_fixed_external_ports(self): service = self.create_service('web', ports=['8000:8000']) - service.start_container() - container = service.inspect()[0] + container = service.start_container().inspect() self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') From f89e4bc70f8c8098a77bd5e4a7cc77575ea8aa9c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 18:44:33 +0000 Subject: [PATCH 0058/2154] Add quotes to build output --- plum/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 29647051..3b16a084 100644 --- a/plum/service.py +++ b/plum/service.py @@ -115,7 +115,7 @@ class Service(object): return container_options def build(self): - log.info('Building %s from %s...' % (self.name, self.options['build'])) + log.info('Building %s from "%s"...' % (self.name, self.options['build'])) build_output = self.client.build(self.options['build'], stream=True) From dd767aef34b40b353ea56d3615b9274b259160c4 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 18:45:25 +0000 Subject: [PATCH 0059/2154] Remove extraneous new lines when building --- plum/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 3b16a084..0badd48c 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,6 +1,7 @@ from docker.client import APIError import logging import re +import sys from .container import Container log = logging.getLogger(__name__) @@ -126,7 +127,7 @@ class Service(object): match = re.search(r'Successfully built ([0-9a-f]+)', line) if match: image_id = match.group(1) - print line + sys.stdout.write(line) if image_id is None: raise BuildError() From 26ea08087a951f035d82852edae7deb347fbb18d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Dec 2013 18:46:53 +0000 Subject: [PATCH 0060/2154] Remove build target from logs --- plum/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 0badd48c..bdca97ed 100644 --- a/plum/service.py +++ b/plum/service.py @@ -116,7 +116,7 @@ class Service(object): return container_options def build(self): - log.info('Building %s from "%s"...' % (self.name, self.options['build'])) + log.info('Building %s...' % self.name) build_output = self.client.build(self.options['build'], stream=True) From 730f9772f9c460905c3c93fee0939c084e5fa77d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Dec 2013 19:01:53 +0000 Subject: [PATCH 0061/2154] plum start runs in foreground by default Also fixed LogPrinter regressions. Sorry for not doing that in a separate commit. Also made 'plum logs' show backlog. Yep, rolled that right in too. Gonna go whip myself now. --- plum/cli/log_printer.py | 44 ++++++++++++++++++++--------------------- plum/cli/main.py | 34 ++++++++++++++++++++++++++----- plum/container.py | 3 +++ 3 files changed, 54 insertions(+), 27 deletions(-) diff --git a/plum/cli/log_printer.py b/plum/cli/log_printer.py index 9fced4f3..653c0f50 100644 --- a/plum/cli/log_printer.py +++ b/plum/cli/log_printer.py @@ -2,48 +2,48 @@ import sys from itertools import cycle -from ..service import get_container_name from .multiplexer import Multiplexer from . import colors class LogPrinter(object): - def __init__(self, client): - self.client = client + def __init__(self, containers, attach_params=None): + self.containers = containers + self.attach_params = attach_params or {} + self.generators = self._make_log_generators() - def attach(self, containers): - generators = self._make_log_generators(containers) - mux = Multiplexer(generators) + def run(self): + mux = Multiplexer(self.generators) for line in mux.loop(): sys.stdout.write(line) - def _make_log_generators(self, containers): + def _make_log_generators(self): color_fns = cycle(colors.rainbow()) generators = [] - for container in containers: + for container in self.containers: color_fn = color_fns.next() generators.append(self._make_log_generator(container, color_fn)) return generators def _make_log_generator(self, container, color_fn): - container_name = get_container_name(container) - format = lambda line: color_fn(container_name + " | ") + line - return (format(line) for line in self._readlines(container)) + format = lambda line: color_fn(container.name + " | ") + line + return (format(line) for line in self._readlines(self._attach(container))) - def _readlines(self, container, logs=False, stream=True): - socket = self.client.attach_socket( - container['Id'], - params={ - 'stdin': 0, - 'stdout': 1, - 'stderr': 1, - 'logs': 1 if logs else 0, - 'stream': 1 if stream else 0 - }, - ) + def _attach(self, container): + params = { + 'stdin': False, + 'stdout': True, + 'stderr': True, + 'logs': False, + 'stream': True, + } + params.update(self.attach_params) + params = dict((name, 1 if value else 0) for (name, value) in params.items()) + return container.attach_socket(params=params) + def _readlines(self, socket): for line in iter(socket.makefile().readline, b''): if not line.endswith('\n'): line += '\n' diff --git a/plum/cli/main.py b/plum/cli/main.py index 56ef3e57..14737898 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -8,7 +8,6 @@ from docopt import docopt from inspect import getdoc from .. import __version__ -from ..service import get_container_name from ..service_collection import ServiceCollection from .command import Command from .log_printer import LogPrinter @@ -103,9 +102,30 @@ class TopLevelCommand(Command): """ Start all services - Usage: start + Usage: start [-d] """ - self.service_collection.start() + if options['-d']: + self.service_collection.start() + return + + running = [] + unstarted = [] + + for s in self.service_collection: + if len(s.containers()) == 0: + unstarted.append((s, s.create_container())) + else: + running += s.get_containers(all=False) + + log_printer = LogPrinter(running + [c for (s, c) in unstarted]) + + for (s, c) in unstarted: + s.start_container(c) + + try: + log_printer.run() + finally: + self.service_collection.stop() def stop(self, options): """ @@ -122,8 +142,12 @@ class TopLevelCommand(Command): Usage: logs """ containers = self._get_containers(all=False) - print "Attaching to", ", ".join(get_container_name(c) for c in containers) - LogPrinter(client=self.client).attach(containers) + print "Attaching to", list_containers(containers) + LogPrinter(containers, attach_params={'logs': True}).run() def _get_containers(self, all): return [c for s in self.service_collection for c in s.containers(all=all)] + + +def list_containers(containers): + return ", ".join(c.name for c in containers) diff --git a/plum/container.py b/plum/container.py index 9ce6509d..abd1c3f5 100644 --- a/plum/container.py +++ b/plum/container.py @@ -84,3 +84,6 @@ class Container(object): if len(bits) > 2 and bits[1] == self.name[1:]: links.append(bits[2]) return links + + def attach_socket(self, **kwargs): + return self.client.attach_socket(self.id, **kwargs) From beaa1dbc14c94aa76b0649b698c3ff27b60ac851 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 12:26:13 +0000 Subject: [PATCH 0062/2154] Fix run: use Container.logs(), explicitly start container --- plum/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 14737898..6f3aa9fd 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -92,7 +92,8 @@ class TopLevelCommand(Command): 'command': [options['COMMAND']] + options['ARGS'], } container = service.create_container(**container_options) - stream = self.client.logs(container, stream=True) + stream = container.logs(stream=True) + service.start_container(container) for data in stream: if data is None: break From fb69512008afb9973baf00b2de48767fa53e39e3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 12:26:58 +0000 Subject: [PATCH 0063/2154] Set port_bindings to None when starting a one-off container in 'plum run' --- plum/cli/main.py | 2 +- plum/service.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 6f3aa9fd..8dd46555 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -93,7 +93,7 @@ class TopLevelCommand(Command): } container = service.create_container(**container_options) stream = container.logs(stream=True) - service.start_container(container) + service.start_container(container, ports=None) for data in stream: if data is None: break diff --git a/plum/service.py b/plum/service.py index bdca97ed..14e2d8ac 100644 --- a/plum/service.py +++ b/plum/service.py @@ -63,14 +63,21 @@ class Service(object): def start_container(self, container=None, **override_options): if container is None: container = self.create_container(**override_options) + + options = self.options.copy() + options.update(override_options) + port_bindings = {} - for port in self.options.get('ports', []): - port = unicode(port) - if ':' in port: - internal_port, external_port = port.split(':', 1) - port_bindings[int(internal_port)] = int(external_port) - else: - port_bindings[int(port)] = None + + if options.get('ports', None) is not None: + for port in options['ports']: + port = unicode(port) + if ':' in port: + internal_port, external_port = port.split(':', 1) + port_bindings[int(internal_port)] = int(external_port) + else: + port_bindings[int(port)] = None + log.info("Starting %s..." % container.name) container.start( links=self._get_links(), From ae0fa0c447dea43d03684ff740a5083da5d8cb38 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 12:36:38 +0000 Subject: [PATCH 0064/2154] Hide stack traces for Docker API errors --- plum/cli/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plum/cli/main.py b/plum/cli/main.py index 8dd46555..2de1cd13 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -12,6 +12,7 @@ from ..service_collection import ServiceCollection from .command import Command from .log_printer import LogPrinter +from docker.client import APIError from .errors import UserError from .docopt_command import NoSuchCommand @@ -43,6 +44,9 @@ def main(): log.error("") log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))) exit(1) + except APIError, e: + log.error(e.explanation) + exit(1) # stolen from docopt master From bac37a19e305af6b1a505a56da3d0be242640131 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 12:39:23 +0000 Subject: [PATCH 0065/2154] Fix method name in start() --- plum/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 2de1cd13..1db3873a 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -120,7 +120,7 @@ class TopLevelCommand(Command): if len(s.containers()) == 0: unstarted.append((s, s.create_container())) else: - running += s.get_containers(all=False) + running += s.containers(all=False) log_printer = LogPrinter(running + [c for (s, c) in unstarted]) From 9cf1d232b2fef4d47e2569a751d27633bb3faab3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 13:02:04 +0000 Subject: [PATCH 0066/2154] Better ps output --- plum/cli/main.py | 32 +++++++++++++++++++++++++------- plum/container.py | 31 +++++++++++++++++++++++++++++++ plum/service_collection.py | 6 ++++++ 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 1db3873a..688bb180 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -10,6 +10,7 @@ from inspect import getdoc from .. import __version__ from ..service_collection import ServiceCollection from .command import Command +from .formatter import Formatter from .log_printer import LogPrinter from docker.client import APIError @@ -78,10 +79,30 @@ class TopLevelCommand(Command): """ List services and containers. - Usage: ps + Usage: ps [options] + + Options: + -q Only display IDs """ - for container in self._get_containers(all=False): - print container.name + if options['-q']: + for container in self.service_collection.containers(all=True): + print container.id + else: + headers = [ + 'Name', + 'Command', + 'State', + 'Ports', + ] + rows = [] + for container in self.service_collection.containers(all=True): + rows.append([ + container.name, + container.human_readable_command, + container.human_readable_state, + container.human_readable_ports, + ]) + print Formatter().table(headers, rows) def run(self, options): """ @@ -146,13 +167,10 @@ class TopLevelCommand(Command): Usage: logs """ - containers = self._get_containers(all=False) + containers = self.service_collection.containers(all=False) print "Attaching to", list_containers(containers) LogPrinter(containers, attach_params={'logs': True}).run() - def _get_containers(self, all): - return [c for s in self.service_collection for c in s.containers(all=all)] - def list_containers(containers): return ", ".join(c.name for c in containers) diff --git a/plum/container.py b/plum/container.py index abd1c3f5..eb65fc12 100644 --- a/plum/container.py +++ b/plum/container.py @@ -37,10 +37,41 @@ class Container(object): def id(self): return self.dictionary['ID'] + @property + def short_id(self): + return self.id[:10] + @property def name(self): return self.dictionary['Name'] + @property + def human_readable_ports(self): + self.inspect_if_not_inspected() + if not self.dictionary['NetworkSettings']['Ports']: + return '' + ports = [] + for private, public in self.dictionary['NetworkSettings']['Ports'].items(): + if public: + ports.append('%s->%s' % (public[0]['HostPort'], private)) + return ', '.join(ports) + + @property + def human_readable_state(self): + self.inspect_if_not_inspected() + if self.dictionary['State']['Running']: + if self.dictionary['State']['Ghost']: + return 'Ghost' + else: + return 'Up' + else: + return 'Exit %s' % self.dictionary['State']['ExitCode'] + + @property + def human_readable_command(self): + self.inspect_if_not_inspected() + return ' '.join(self.dictionary['Config']['Cmd']) + @property def environment(self): self.inspect_if_not_inspected() diff --git a/plum/service_collection.py b/plum/service_collection.py index 3c59c6ed..a72835b0 100644 --- a/plum/service_collection.py +++ b/plum/service_collection.py @@ -50,5 +50,11 @@ class ServiceCollection(list): for service in self: service.stop() + def containers(self, *args, **kwargs): + l = [] + for service in self: + for container in service.containers(*args, **kwargs): + l.append(container) + return l From 9e9a20b227145cd3c0ce274d187270c3044ce28e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 13:02:42 +0000 Subject: [PATCH 0067/2154] Remove unused imports --- plum/cli/main.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 688bb180..0e94f438 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -1,14 +1,9 @@ -import datetime import logging -import sys -import os import re -from docopt import docopt from inspect import getdoc from .. import __version__ -from ..service_collection import ServiceCollection from .command import Command from .formatter import Formatter from .log_printer import LogPrinter From 9f1d08c54be21487476a9e7b5779bc05ff862ee3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 13:06:26 +0000 Subject: [PATCH 0068/2154] Implement --version flag --- plum/cli/docopt_command.py | 5 ++++- plum/cli/main.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plum/cli/docopt_command.py b/plum/cli/docopt_command.py index 0a11d8ee..93cf889e 100644 --- a/plum/cli/docopt_command.py +++ b/plum/cli/docopt_command.py @@ -12,6 +12,9 @@ def docopt_full_help(docstring, *args, **kwargs): class DocoptCommand(object): + def docopt_options(self): + return {'options_first': True} + def sys_dispatch(self): self.dispatch(sys.argv[1:], None) @@ -22,7 +25,7 @@ class DocoptCommand(object): handler(command_options) def parse(self, argv, global_options): - options = docopt_full_help(getdoc(self), argv, options_first=True) + options = docopt_full_help(getdoc(self), argv, **self.docopt_options()) command = options['COMMAND'] if not hasattr(self, command): diff --git a/plum/cli/main.py b/plum/cli/main.py index 0e94f438..0e0cb8a5 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -1,4 +1,5 @@ import logging +import sys import re from inspect import getdoc @@ -70,6 +71,11 @@ class TopLevelCommand(Command): stop Stop services """ + def docopt_options(self): + options = super(TopLevelCommand, self).docopt_options() + options['version'] = "plum %s" % __version__ + return options + def ps(self, options): """ List services and containers. From 6c551a200b7abf49cd0574847e7f746ecf6fb27a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 14:47:43 +0000 Subject: [PATCH 0069/2154] Do not allow underscores in names --- plum/service.py | 2 +- tests/service_test.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plum/service.py b/plum/service.py index 14e2d8ac..3d6465f1 100644 --- a/plum/service.py +++ b/plum/service.py @@ -13,7 +13,7 @@ class BuildError(Exception): class Service(object): def __init__(self, name, client=None, links=[], **options): - if not re.match('^[a-zA-Z0-9_]+$', name): + if not re.match('^[a-zA-Z0-9]+$', name): raise ValueError('Invalid name: %s' % name) if 'image' in options and 'build' in options: raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) diff --git a/tests/service_test.py b/tests/service_test.py index 62d53e56..51559b4b 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -10,13 +10,13 @@ class ServiceTest(DockerClientTestCase): self.assertRaises(ValueError, lambda: Service(name='/')) self.assertRaises(ValueError, lambda: Service(name='!')) self.assertRaises(ValueError, lambda: Service(name='\xe2')) + self.assertRaises(ValueError, lambda: Service(name='_')) + self.assertRaises(ValueError, lambda: Service(name='____')) + self.assertRaises(ValueError, lambda: Service(name='foo_bar')) + self.assertRaises(ValueError, lambda: Service(name='__foo_bar__')) Service('a') Service('foo') - Service('foo_bar') - Service('__foo_bar__') - Service('_') - Service('_____') def test_containers(self): foo = self.create_service('foo') @@ -38,7 +38,7 @@ class ServiceTest(DockerClientTestCase): self.assertIn('/bar_2', names) def test_up_scale_down(self): - service = self.create_service('scaling_test') + service = self.create_service('scalingtest') self.assertEqual(len(service.containers()), 0) service.start() From c4887106258a0d1c02f22ddfcfe05bcc5f70e0f2 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 15:16:17 +0000 Subject: [PATCH 0070/2154] Add project option to services --- plum/service.py | 28 ++++++++++++++++------------ tests/service_test.py | 17 +++++++++++++---- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/plum/service.py b/plum/service.py index 3d6465f1..26ebb167 100644 --- a/plum/service.py +++ b/plum/service.py @@ -12,14 +12,17 @@ class BuildError(Exception): class Service(object): - def __init__(self, name, client=None, links=[], **options): + def __init__(self, name, client=None, project='default', links=[], **options): if not re.match('^[a-zA-Z0-9]+$', name): raise ValueError('Invalid name: %s' % name) + if not re.match('^[a-zA-Z0-9]+$', project): + raise ValueError('Invalid project: %s' % project) if 'image' in options and 'build' in options: raise ValueError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) self.name = name self.client = client + self.project = project self.links = links or [] self.options = options @@ -27,7 +30,10 @@ class Service(object): l = [] for container in self.client.containers(all=all): name = get_container_name(container) - if is_valid_name(name) and parse_name(name)[0] == self.name: + if not is_valid_name(name): + continue + project, name, number = parse_name(name) + if project == self.project and name == self.name: l.append(Container.from_ps(self.client, container)) return l @@ -91,8 +97,11 @@ class Service(object): container.kill() container.remove() + def next_container_name(self): + return '%s_%s_%s' % (self.project, self.name, self.next_container_number()) + def next_container_number(self): - numbers = [parse_name(c.name)[1] for c in self.containers(all=True)] + numbers = [parse_name(c.name)[2] for c in self.containers(all=True)] if len(numbers) == 0: return 1 @@ -111,8 +120,7 @@ class Service(object): container_options = dict((k, self.options[k]) for k in keys if k in self.options) container_options.update(override_options) - number = self.next_container_number() - container_options['name'] = make_name(self.name, number) + container_options['name'] = self.next_container_name() if 'ports' in container_options: container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']] @@ -142,11 +150,7 @@ class Service(object): return image_id -name_regex = '^(.+)_(\d+)$' - - -def make_name(prefix, number): - return '%s_%s' % (prefix, number) +name_regex = '^([^_]+)_([^_]+)_(\d+)$' def is_valid_name(name): @@ -155,8 +159,8 @@ def is_valid_name(name): def parse_name(name): match = re.match(name_regex, name) - (service_name, suffix) = match.groups() - return (service_name, int(suffix)) + (project, service_name, suffix) = match.groups() + return (project, service_name, int(suffix)) def get_container_name(container): diff --git a/tests/service_test.py b/tests/service_test.py index 51559b4b..7ad89021 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -18,6 +18,10 @@ class ServiceTest(DockerClientTestCase): Service('a') Service('foo') + def test_project_validation(self): + self.assertRaises(ValueError, lambda: Service(name='foo', project='_')) + Service(name='foo', project='bar') + def test_containers(self): foo = self.create_service('foo') bar = self.create_service('bar') @@ -25,7 +29,7 @@ class ServiceTest(DockerClientTestCase): foo.start() self.assertEqual(len(foo.containers()), 1) - self.assertEqual(foo.containers()[0].name, '/foo_1') + self.assertEqual(foo.containers()[0].name, '/default_foo_1') self.assertEqual(len(bar.containers()), 0) bar.scale(2) @@ -34,8 +38,13 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(bar.containers()), 2) names = [c.name for c in bar.containers()] - self.assertIn('/bar_1', names) - self.assertIn('/bar_2', names) + self.assertIn('/default_bar_1', names) + self.assertIn('/default_bar_2', names) + + def test_project_is_added_to_container_name(self): + service = self.create_service('web', project='myproject') + service.start() + self.assertEqual(service.containers()[0].name, '/myproject_web_1') def test_up_scale_down(self): service = self.create_service('scalingtest') @@ -74,7 +83,7 @@ class ServiceTest(DockerClientTestCase): web = self.create_service('web', links=[db]) db.start_container() web.start_container() - self.assertIn('db_1', web.containers()[0].links()) + self.assertIn('default_db_1', web.containers()[0].links()) db.stop() web.stop() From d6db049b421407ae67ee3ed214f9e4094e7aa43d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 15:32:24 +0000 Subject: [PATCH 0071/2154] Generate project name based on current dir --- plum/cli/command.py | 15 ++++++++++++++- plum/service_collection.py | 8 ++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/plum/cli/command.py b/plum/cli/command.py index e59d1869..78e28730 100644 --- a/plum/cli/command.py +++ b/plum/cli/command.py @@ -1,6 +1,7 @@ from docker import Client import logging import os +import re import yaml from ..service_collection import ServiceCollection @@ -21,7 +22,19 @@ class Command(DocoptCommand): @cached_property def service_collection(self): config = yaml.load(open('plum.yml')) - return ServiceCollection.from_config(self.client, config) + return ServiceCollection.from_config( + config, + client=self.client, + project=self.project + ) + + @cached_property + def project(self): + project = os.path.basename(os.getcwd()) + project = re.sub(r'[^a-zA-Z0-9]', '', project) + if not project: + project = 'default' + return project @cached_property def formatter(self): diff --git a/plum/service_collection.py b/plum/service_collection.py index a72835b0..feef3de5 100644 --- a/plum/service_collection.py +++ b/plum/service_collection.py @@ -14,7 +14,7 @@ def sort_service_dicts(services): class ServiceCollection(list): @classmethod - def from_dicts(cls, client, service_dicts): + def from_dicts(cls, service_dicts, client, project='default'): """ Construct a ServiceCollection from a list of dicts representing services. """ @@ -26,16 +26,16 @@ class ServiceCollection(list): for name in service_dict.get('links', []): links.append(collection.get(name)) del service_dict['links'] - collection.append(Service(client=client, links=links, **service_dict)) + collection.append(Service(client=client, project=project, links=links, **service_dict)) return collection @classmethod - def from_config(cls, client, config): + def from_config(cls, config, client, project='default'): dicts = [] for name, service in config.items(): service['name'] = name dicts.append(service) - return cls.from_dicts(client, dicts) + return cls.from_dicts(dicts, client, project) def get(self, name): for service in self: From 818728b825320e77760afeda55cf2e13f0abb733 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Dec 2013 15:53:39 +0000 Subject: [PATCH 0072/2154] Mount volumes --- plum/service.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/plum/service.py b/plum/service.py index 26ebb167..5371bd57 100644 --- a/plum/service.py +++ b/plum/service.py @@ -1,6 +1,7 @@ from docker.client import APIError import logging import re +import os import sys from .container import Container @@ -84,10 +85,18 @@ class Service(object): else: port_bindings[int(port)] = None + volume_bindings = {} + + 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 + log.info("Starting %s..." % container.name) container.start( links=self._get_links(), port_bindings=port_bindings, + binds=volume_bindings, ) return container @@ -125,6 +134,9 @@ class Service(object): if 'ports' in container_options: container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']] + if 'volumes' in container_options: + container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes']) + if 'build' in self.options: container_options['image'] = self.build() From 2d2d81d33fe31285db3578eb7202354e36ec17db Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 16:55:12 +0000 Subject: [PATCH 0073/2154] Rename "service collection" to "project" --- plum/cli/command.py | 12 ++--- plum/cli/main.py | 16 +++--- plum/{service_collection.py => project.py} | 34 +++++++----- tests/project_test.py | 59 ++++++++++++++++++++ tests/service_collection_test.py | 62 ---------------------- 5 files changed, 92 insertions(+), 91 deletions(-) rename plum/{service_collection.py => project.py} (62%) create mode 100644 tests/project_test.py delete mode 100644 tests/service_collection_test.py diff --git a/plum/cli/command.py b/plum/cli/command.py index 78e28730..5bbed26b 100644 --- a/plum/cli/command.py +++ b/plum/cli/command.py @@ -4,7 +4,7 @@ import os import re import yaml -from ..service_collection import ServiceCollection +from ..project import Project from .docopt_command import DocoptCommand from .formatter import Formatter from .utils import cached_property, mkdir @@ -20,16 +20,12 @@ class Command(DocoptCommand): return Client() @cached_property - def service_collection(self): + def project(self): config = yaml.load(open('plum.yml')) - return ServiceCollection.from_config( - config, - client=self.client, - project=self.project - ) + return Project.from_config(self.project_name, config, self.client) @cached_property - def project(self): + def project_name(self): project = os.path.basename(os.getcwd()) project = re.sub(r'[^a-zA-Z0-9]', '', project) if not project: diff --git a/plum/cli/main.py b/plum/cli/main.py index 0e0cb8a5..05b90c8f 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -86,7 +86,7 @@ class TopLevelCommand(Command): -q Only display IDs """ if options['-q']: - for container in self.service_collection.containers(all=True): + for container in self.project.containers(all=True): print container.id else: headers = [ @@ -96,7 +96,7 @@ class TopLevelCommand(Command): 'Ports', ] rows = [] - for container in self.service_collection.containers(all=True): + for container in self.project.containers(all=True): rows.append([ container.name, container.human_readable_command, @@ -111,7 +111,7 @@ class TopLevelCommand(Command): Usage: run SERVICE COMMAND [ARGS...] """ - service = self.service_collection.get(options['SERVICE']) + service = self.project.get_service(options['SERVICE']) if service is None: raise UserError("No such service: %s" % options['SERVICE']) container_options = { @@ -132,13 +132,13 @@ class TopLevelCommand(Command): Usage: start [-d] """ if options['-d']: - self.service_collection.start() + self.project.start() return running = [] unstarted = [] - for s in self.service_collection: + for s in self.project.services: if len(s.containers()) == 0: unstarted.append((s, s.create_container())) else: @@ -152,7 +152,7 @@ class TopLevelCommand(Command): try: log_printer.run() finally: - self.service_collection.stop() + self.project.stop() def stop(self, options): """ @@ -160,7 +160,7 @@ class TopLevelCommand(Command): Usage: stop """ - self.service_collection.stop() + self.project.stop() def logs(self, options): """ @@ -168,7 +168,7 @@ class TopLevelCommand(Command): Usage: logs """ - containers = self.service_collection.containers(all=False) + containers = self.project.containers(all=False) print "Attaching to", list_containers(containers) LogPrinter(containers, attach_params={'logs': True}).run() diff --git a/plum/service_collection.py b/plum/project.py similarity index 62% rename from plum/service_collection.py rename to plum/project.py index feef3de5..f4c1d018 100644 --- a/plum/service_collection.py +++ b/plum/project.py @@ -12,47 +12,55 @@ def sort_service_dicts(services): return 0 return sorted(services, cmp=cmp) -class ServiceCollection(list): +class Project(object): + """ + A collection of services. + """ + def __init__(self, name, services, client): + self.name = name + self.services = services + self.client = client + @classmethod - def from_dicts(cls, service_dicts, client, project='default'): + def from_dicts(cls, name, service_dicts, client): """ Construct a ServiceCollection from a list of dicts representing services. """ - collection = ServiceCollection() + project = cls(name, [], client) for service_dict in sort_service_dicts(service_dicts): # Reference links by object links = [] if 'links' in service_dict: for name in service_dict.get('links', []): - links.append(collection.get(name)) + links.append(project.get_service(name)) del service_dict['links'] - collection.append(Service(client=client, project=project, links=links, **service_dict)) - return collection + project.services.append(Service(client=client, project=name, links=links, **service_dict)) + return project @classmethod - def from_config(cls, config, client, project='default'): + def from_config(cls, name, config, client): dicts = [] for name, service in config.items(): service['name'] = name dicts.append(service) - return cls.from_dicts(dicts, client, project) + return cls.from_dicts(name, dicts, client) - def get(self, name): - for service in self: + def get_service(self, name): + for service in self.services: if service.name == name: return service def start(self): - for service in self: + for service in self.services: service.start() def stop(self): - for service in self: + for service in self.services: service.stop() def containers(self, *args, **kwargs): l = [] - for service in self: + for service in self.services: for container in service.containers(*args, **kwargs): l.append(container) return l diff --git a/tests/project_test.py b/tests/project_test.py new file mode 100644 index 00000000..aa56b407 --- /dev/null +++ b/tests/project_test.py @@ -0,0 +1,59 @@ +from plum.project import Project +from plum.service import Service +from .testcases import DockerClientTestCase + + +class ProjectTest(DockerClientTestCase): + def test_from_dict(self): + project = Project.from_dicts('test', [ + { + 'name': 'web', + 'image': 'ubuntu' + }, + { + 'name': 'db', + 'image': 'ubuntu' + } + ], self.client) + self.assertEqual(len(project.services), 2) + self.assertEqual(project.get_service('web').name, 'web') + self.assertEqual(project.get_service('web').options['image'], 'ubuntu') + self.assertEqual(project.get_service('db').name, 'db') + self.assertEqual(project.get_service('db').options['image'], 'ubuntu') + + def test_from_dict_sorts_in_dependency_order(self): + project = Project.from_dicts('test', [ + { + 'name': 'web', + 'image': 'ubuntu', + 'links': ['db'], + }, + { + 'name': 'db', + 'image': 'ubuntu' + } + ], self.client) + + self.assertEqual(project.services[0].name, 'db') + self.assertEqual(project.services[1].name, 'web') + + def test_get_service(self): + web = self.create_service('web') + project = Project('test', [web], self.client) + self.assertEqual(project.get_service('web'), web) + + def test_start_stop(self): + project = Project('test', [ + self.create_service('web'), + self.create_service('db'), + ], self.client) + + project.start() + + self.assertEqual(len(project.get_service('web').containers()), 1) + self.assertEqual(len(project.get_service('db').containers()), 1) + + project.stop() + + self.assertEqual(len(project.get_service('web').containers()), 0) + self.assertEqual(len(project.get_service('db').containers()), 0) diff --git a/tests/service_collection_test.py b/tests/service_collection_test.py deleted file mode 100644 index 7dbffdce..00000000 --- a/tests/service_collection_test.py +++ /dev/null @@ -1,62 +0,0 @@ -from plum.service import Service -from plum.service_collection import ServiceCollection -from .testcases import DockerClientTestCase - - -class ServiceCollectionTest(DockerClientTestCase): - def test_from_dict(self): - collection = ServiceCollection.from_dicts(None, [ - { - 'name': 'web', - 'image': 'ubuntu' - }, - { - 'name': 'db', - 'image': 'ubuntu' - } - ]) - self.assertEqual(len(collection), 2) - self.assertEqual(collection.get('web').name, 'web') - self.assertEqual(collection.get('web').options['image'], 'ubuntu') - self.assertEqual(collection.get('db').name, 'db') - self.assertEqual(collection.get('db').options['image'], 'ubuntu') - - def test_from_dict_sorts_in_dependency_order(self): - collection = ServiceCollection.from_dicts(None, [ - { - 'name': 'web', - 'image': 'ubuntu', - 'links': ['db'], - }, - { - 'name': 'db', - 'image': 'ubuntu' - } - ]) - - self.assertEqual(collection[0].name, 'db') - self.assertEqual(collection[1].name, 'web') - - def test_get(self): - web = self.create_service('web') - collection = ServiceCollection([web]) - self.assertEqual(collection.get('web'), web) - - def test_start_stop(self): - collection = ServiceCollection([ - self.create_service('web'), - self.create_service('db'), - ]) - - collection.start() - - self.assertEqual(len(collection[0].containers()), 1) - self.assertEqual(len(collection[1].containers()), 1) - - collection.stop() - - self.assertEqual(len(collection[0].containers()), 0) - self.assertEqual(len(collection[1].containers()), 0) - - - From 5a46278f797665a078acc4f58f6d74dc81d46e8b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 16:56:58 +0000 Subject: [PATCH 0074/2154] Fix project name getting overridden with service --- plum/project.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plum/project.py b/plum/project.py index f4c1d018..7a72e4a6 100644 --- a/plum/project.py +++ b/plum/project.py @@ -31,8 +31,8 @@ class Project(object): # Reference links by object links = [] if 'links' in service_dict: - for name in service_dict.get('links', []): - links.append(project.get_service(name)) + for service_name in service_dict.get('links', []): + links.append(project.get_service(service_name)) del service_dict['links'] project.services.append(Service(client=client, project=name, links=links, **service_dict)) return project @@ -40,8 +40,8 @@ class Project(object): @classmethod def from_config(cls, name, config, client): dicts = [] - for name, service in config.items(): - service['name'] = name + for service_name, service in config.items(): + service['name'] = service_name dicts.append(service) return cls.from_dicts(name, dicts, client) From bdf99cd443505d2996b5cbc79ac4bcf702a410f6 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 18:20:48 +0000 Subject: [PATCH 0075/2154] Move log messages to container --- plum/container.py | 6 ++++++ plum/service.py | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plum/container.py b/plum/container.py index eb65fc12..0bf6fccd 100644 --- a/plum/container.py +++ b/plum/container.py @@ -1,4 +1,6 @@ +import logging +log = logging.getLogger(__name__) class Container(object): """ @@ -82,15 +84,19 @@ class Container(object): return out def start(self, **options): + log.info("Starting %s..." % self.name) return self.client.start(self.id, **options) def stop(self): + log.info("Stopping %s..." % self.name) return self.client.stop(self.id) 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/plum/service.py b/plum/service.py index 5371bd57..4cec3999 100644 --- a/plum/service.py +++ b/plum/service.py @@ -92,7 +92,6 @@ class Service(object): external_dir, internal_dir = volume.split(':') volume_bindings[os.path.abspath(external_dir)] = internal_dir - log.info("Starting %s..." % container.name) container.start( links=self._get_links(), port_bindings=port_bindings, @@ -102,7 +101,6 @@ class Service(object): def stop_container(self): container = self.containers()[-1] - log.info("Stopping and removing %s..." % container.name) container.kill() container.remove() From 68e4341fbf01c074851c3ebce475ef98adf17722 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Dec 2013 20:09:54 +0000 Subject: [PATCH 0076/2154] Compile name regex --- plum/service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plum/service.py b/plum/service.py index 4cec3999..de1d5440 100644 --- a/plum/service.py +++ b/plum/service.py @@ -160,15 +160,15 @@ class Service(object): return image_id -name_regex = '^([^_]+)_([^_]+)_(\d+)$' +NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(\d+)$') def is_valid_name(name): - return (re.match(name_regex, name) is not None) + return (NAME_RE.match(name) is not None) def parse_name(name): - match = re.match(name_regex, name) + match = NAME_RE.match(name) (project, service_name, suffix) = match.groups() return (project, service_name, int(suffix)) From 2f28265d1078b2fe49527a5689ec60f39f933193 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 10:46:55 +0000 Subject: [PATCH 0077/2154] Add support for differentiating one-off containers This is a basic start, the API is pretty shonky. --- plum/cli/main.py | 12 +++++++----- plum/container.py | 8 ++++++++ plum/service.py | 43 ++++++++++++++++++++++++++----------------- tests/service_test.py | 11 +++++++++++ 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 05b90c8f..0fd68656 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -85,8 +85,10 @@ class TopLevelCommand(Command): Options: -q Only display IDs """ + containers = self.project.containers(stopped=True) + self.project.containers(one_off=True) + if options['-q']: - for container in self.project.containers(all=True): + for container in containers: print container.id else: headers = [ @@ -96,7 +98,7 @@ class TopLevelCommand(Command): 'Ports', ] rows = [] - for container in self.project.containers(all=True): + for container in containers: rows.append([ container.name, container.human_readable_command, @@ -117,7 +119,7 @@ class TopLevelCommand(Command): container_options = { 'command': [options['COMMAND']] + options['ARGS'], } - container = service.create_container(**container_options) + container = service.create_container(one_off=True, **container_options) stream = container.logs(stream=True) service.start_container(container, ports=None) for data in stream: @@ -142,7 +144,7 @@ class TopLevelCommand(Command): if len(s.containers()) == 0: unstarted.append((s, s.create_container())) else: - running += s.containers(all=False) + running += s.containers(stopped=False) log_printer = LogPrinter(running + [c for (s, c) in unstarted]) @@ -168,7 +170,7 @@ class TopLevelCommand(Command): Usage: logs """ - containers = self.project.containers(all=False) + containers = self.project.containers(stopped=False) print "Attaching to", list_containers(containers) LogPrinter(containers, attach_params={'logs': True}).run() diff --git a/plum/container.py b/plum/container.py index 0bf6fccd..61695216 100644 --- a/plum/container.py +++ b/plum/container.py @@ -124,3 +124,11 @@ class Container(object): def attach_socket(self, **kwargs): return self.client.attach_socket(self.id, **kwargs) + + def __repr__(self): + return '' % self.name + + def __eq__(self, other): + if type(self) != type(other): + return False + return self.id == other.id diff --git a/plum/service.py b/plum/service.py index de1d5440..486015e1 100644 --- a/plum/service.py +++ b/plum/service.py @@ -27,11 +27,11 @@ class Service(object): self.links = links or [] self.options = options - def containers(self, all=False): + def containers(self, stopped=False, one_off=False): l = [] - for container in self.client.containers(all=all): + for container in self.client.containers(all=stopped): name = get_container_name(container) - if not is_valid_name(name): + if not is_valid_name(name, one_off): continue project, name, number = parse_name(name) if project == self.project and name == self.name: @@ -52,12 +52,12 @@ class Service(object): while len(self.containers()) > num: self.stop_container() - def create_container(self, **override_options): + def create_container(self, one_off=False, **override_options): """ Create a container for this service. If the image doesn't exist, attempt to pull it. """ - container_options = self._get_container_options(override_options) + container_options = self._get_container_options(override_options, one_off=one_off) try: return Container.create(self.client, **container_options) except APIError, e: @@ -104,11 +104,14 @@ class Service(object): container.kill() container.remove() - def next_container_name(self): - return '%s_%s_%s' % (self.project, self.name, self.next_container_number()) + def next_container_name(self, one_off=False): + bits = [self.project, self.name] + if one_off: + bits.append('run') + return '_'.join(bits + [unicode(self.next_container_number())]) def next_container_number(self): - numbers = [parse_name(c.name)[2] for c in self.containers(all=True)] + numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True)] if len(numbers) == 0: return 1 @@ -122,12 +125,12 @@ class Service(object): links[container.name[1:]] = container.name[1:] return links - def _get_container_options(self, override_options): + def _get_container_options(self, override_options, one_off=False): keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from'] container_options = dict((k, self.options[k]) for k in keys if k in self.options) container_options.update(override_options) - container_options['name'] = self.next_container_name() + container_options['name'] = self.next_container_name(one_off) if 'ports' in container_options: container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']] @@ -160,16 +163,22 @@ class Service(object): return image_id -NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(\d+)$') +NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') -def is_valid_name(name): - return (NAME_RE.match(name) is not None) - - -def parse_name(name): +def is_valid_name(name, one_off=False): match = NAME_RE.match(name) - (project, service_name, suffix) = match.groups() + if match is None: + return False + if one_off: + return match.group(3) == 'run_' + else: + return match.group(3) is None + + +def parse_name(name, one_off=False): + match = NAME_RE.match(name) + (project, service_name, _, suffix) = match.groups() return (project, service_name, int(suffix)) diff --git a/tests/service_test.py b/tests/service_test.py index 7ad89021..c09e17e3 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -41,6 +41,12 @@ class ServiceTest(DockerClientTestCase): self.assertIn('/default_bar_1', names) self.assertIn('/default_bar_2', names) + def test_containers_one_off(self): + db = self.create_service('db') + container = db.create_container(one_off=True) + self.assertEqual(db.containers(stopped=True), []) + self.assertEqual(db.containers(one_off=True, stopped=True), [container]) + def test_project_is_added_to_container_name(self): service = self.create_service('web', project='myproject') service.start() @@ -68,6 +74,11 @@ class ServiceTest(DockerClientTestCase): service.stop() self.assertEqual(len(service.containers()), 0) + def test_create_container_with_one_off(self): + db = self.create_service('db') + container = db.create_container(one_off=True) + self.assertEqual(container.name, '/default_db_run_1') + def test_start_container_passes_through_options(self): db = self.create_service('db') db.start_container(environment={'FOO': 'BAR'}) From ea09ec672c1f6c819ed25a4d886c84386f6729e2 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 10:53:07 +0000 Subject: [PATCH 0078/2154] Add detached mode to run --- plum/cli/main.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 0fd68656..475cb407 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -111,7 +111,10 @@ class TopLevelCommand(Command): """ Run a one-off command. - Usage: run SERVICE COMMAND [ARGS...] + Usage: run [options] SERVICE COMMAND [ARGS...] + + Options: + -d Detached mode: Run container in the background, print new container name """ service = self.project.get_service(options['SERVICE']) if service is None: @@ -120,18 +123,25 @@ class TopLevelCommand(Command): 'command': [options['COMMAND']] + options['ARGS'], } container = service.create_container(one_off=True, **container_options) - stream = container.logs(stream=True) - service.start_container(container, ports=None) - for data in stream: - if data is None: - break - print data + if options['-d']: + service.start_container(container, ports=None) + print container.name + else: + stream = container.logs(stream=True) + service.start_container(container, ports=None) + for data in stream: + if data is None: + break + print data def start(self, options): """ Start all services - Usage: start [-d] + Usage: start [options] + + Options: + -d Detached mode: Run containers in the background, print new container names """ if options['-d']: self.project.start() From 4a729fe47f2658011942ea8f0b6a39eaa6fe3d57 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 10:57:28 +0000 Subject: [PATCH 0079/2154] Document logs command --- plum/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 475cb407..80646e33 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -65,6 +65,7 @@ class TopLevelCommand(Command): --version Print version and exit Commands: + logs View output from containers ps List services and containers run Run a one-off command start Start services @@ -176,7 +177,7 @@ class TopLevelCommand(Command): def logs(self, options): """ - View containers' output + View output from containers Usage: logs """ From aa7a5a1487aeed751282eba3cc6f090acd30328a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 10:52:46 +0000 Subject: [PATCH 0080/2154] Small refactor for clarity --- plum/cli/log_printer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plum/cli/log_printer.py b/plum/cli/log_printer.py index 653c0f50..e7f8eac8 100644 --- a/plum/cli/log_printer.py +++ b/plum/cli/log_printer.py @@ -28,8 +28,8 @@ class LogPrinter(object): return generators def _make_log_generator(self, container, color_fn): - format = lambda line: color_fn(container.name + " | ") + line - return (format(line) for line in self._readlines(self._attach(container))) + prefix = color_fn(container.name + " | ") + return (prefix + line for line in self._readlines(self._attach(container))) def _attach(self, container): params = { From 86e551f2e29c8d21b5fb9da62b4bacc9d39f5a99 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 11:08:40 +0000 Subject: [PATCH 0081/2154] Attach with websocket and do manual line buffering This works around the odd byte sequences we see at the beginning of every chunk when attaching via the streaming HTTP endpoint and a plain socket. --- plum/cli/log_printer.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/plum/cli/log_printer.py b/plum/cli/log_printer.py index e7f8eac8..480fc5eb 100644 --- a/plum/cli/log_printer.py +++ b/plum/cli/log_printer.py @@ -29,7 +29,8 @@ class LogPrinter(object): def _make_log_generator(self, container, color_fn): prefix = color_fn(container.name + " | ") - return (prefix + line for line in self._readlines(self._attach(container))) + websocket = self._attach(container) + return (prefix + line for line in split_buffer(read_websocket(websocket), '\n')) def _attach(self, container): params = { @@ -41,13 +42,25 @@ class LogPrinter(object): } params.update(self.attach_params) params = dict((name, 1 if value else 0) for (name, value) in params.items()) - return container.attach_socket(params=params) + return container.attach_socket(params=params, ws=True) - def _readlines(self, socket): - for line in iter(socket.makefile().readline, b''): - if not line.endswith('\n'): - line += '\n' +def read_websocket(websocket): + while True: + data = websocket.recv() + if data: + yield data + else: + break - yield line +def split_buffer(reader, separator): + buffered = '' - socket.close() + for data in reader: + lines = (buffered + data).split(separator) + for line in lines[:-1]: + yield line + separator + if len(lines) > 1: + buffered = lines[-1] + + if len(buffered) > 0: + yield buffered From 15f12c6e2c8c18a7d5912114f869fdd82df28950 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 12:51:20 +0000 Subject: [PATCH 0082/2154] Ignore containers without names --- plum/service.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plum/service.py b/plum/service.py index 486015e1..8b829ace 100644 --- a/plum/service.py +++ b/plum/service.py @@ -31,7 +31,7 @@ class Service(object): l = [] for container in self.client.containers(all=stopped): name = get_container_name(container) - if not is_valid_name(name, one_off): + if not name or not is_valid_name(name, one_off): continue project, name, number = parse_name(name) if project == self.project and name == self.name: @@ -183,6 +183,8 @@ def parse_name(name, one_off=False): def get_container_name(container): + if not container.get('Name') and not container.get('Names'): + return None # inspect if 'Name' in container: return container['Name'] From 326438b1706f62c74b703813d6fdd5ec0253b3ba Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 12:55:45 +0000 Subject: [PATCH 0083/2154] Pick correct numbers for one off containers --- plum/service.py | 6 +++--- tests/service_test.py | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plum/service.py b/plum/service.py index 8b829ace..6fab273b 100644 --- a/plum/service.py +++ b/plum/service.py @@ -108,10 +108,10 @@ class Service(object): bits = [self.project, self.name] if one_off: bits.append('run') - return '_'.join(bits + [unicode(self.next_container_number())]) + return '_'.join(bits + [unicode(self.next_container_number(one_off=one_off))]) - def next_container_number(self): - numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True)] + def next_container_number(self, one_off=False): + numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)] if len(numbers) == 0: return 1 diff --git a/tests/service_test.py b/tests/service_test.py index c09e17e3..2fdfba7d 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -79,6 +79,12 @@ class ServiceTest(DockerClientTestCase): container = db.create_container(one_off=True) self.assertEqual(container.name, '/default_db_run_1') + def test_create_container_with_one_off_when_existing_container_is_running(self): + db = self.create_service('db') + db.start() + container = db.create_container(one_off=True) + self.assertEqual(container.name, '/default_db_run_1') + def test_start_container_passes_through_options(self): db = self.create_service('db') db.start_container(environment={'FOO': 'BAR'}) From abfb3b800f7bcd5380189a34db8629270d4708e5 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 15:03:01 +0000 Subject: [PATCH 0084/2154] Interactive plum run --- plum/cli/main.py | 48 +++++++++++++-- plum/cli/socketclient.py | 129 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 6 deletions(-) create mode 100644 plum/cli/socketclient.py diff --git a/plum/cli/main.py b/plum/cli/main.py index 80646e33..b3e4f11e 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -12,6 +12,7 @@ from .log_printer import LogPrinter from docker.client import APIError from .errors import UserError from .docopt_command import NoSuchCommand +from .socketclient import SocketClient log = logging.getLogger(__name__) @@ -122,18 +123,22 @@ class TopLevelCommand(Command): raise UserError("No such service: %s" % options['SERVICE']) container_options = { 'command': [options['COMMAND']] + options['ARGS'], + 'tty': not options['-d'], + 'stdin_open': not options['-d'], } container = service.create_container(one_off=True, **container_options) if options['-d']: service.start_container(container, ports=None) print container.name else: - stream = container.logs(stream=True) - service.start_container(container, ports=None) - for data in stream: - if data is None: - break - print data + with self._attach_to_container( + container.id, + interactive=True, + logs=True, + raw=True + ) as c: + service.start_container(container, ports=None) + c.run() def start(self, options): """ @@ -185,6 +190,37 @@ class TopLevelCommand(Command): print "Attaching to", list_containers(containers) LogPrinter(containers, attach_params={'logs': True}).run() + def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False): + stdio = self.client.attach_socket( + container_id, + params={ + 'stdin': 1 if interactive else 0, + 'stdout': 1, + 'stderr': 0, + 'logs': 1 if logs else 0, + 'stream': 1 if stream else 0 + }, + ws=True, + ) + + stderr = self.client.attach_socket( + container_id, + params={ + 'stdin': 0, + 'stdout': 0, + 'stderr': 1, + 'logs': 1 if logs else 0, + 'stream': 1 if stream else 0 + }, + ws=True, + ) + + return SocketClient( + socket_in=stdio, + socket_out=stdio, + socket_err=stderr, + raw=raw, + ) def list_containers(containers): return ", ".join(c.name for c in containers) diff --git a/plum/cli/socketclient.py b/plum/cli/socketclient.py new file mode 100644 index 00000000..90ed8b58 --- /dev/null +++ b/plum/cli/socketclient.py @@ -0,0 +1,129 @@ +# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py + +from select import select +import sys +import tty +import fcntl +import os +import termios +import threading +import errno + +import logging +log = logging.getLogger(__name__) + + +class SocketClient: + def __init__(self, + socket_in=None, + socket_out=None, + socket_err=None, + raw=True, + ): + self.socket_in = socket_in + self.socket_out = socket_out + self.socket_err = socket_err + self.raw = raw + + self.stdin_fileno = sys.stdin.fileno() + + def __enter__(self): + self.create() + return self + + def __exit__(self, type, value, trace): + self.destroy() + + def create(self): + if os.isatty(sys.stdin.fileno()): + self.settings = termios.tcgetattr(sys.stdin.fileno()) + else: + self.settings = None + + if self.socket_in is not None: + self.set_blocking(sys.stdin, False) + self.set_blocking(sys.stdout, True) + self.set_blocking(sys.stderr, True) + + if self.raw: + tty.setraw(sys.stdin.fileno()) + + def set_blocking(self, file, blocking): + fd = file.fileno() + flags = fcntl.fcntl(fd, fcntl.F_GETFL) + flags = (flags & ~os.O_NONBLOCK) if blocking else (flags | os.O_NONBLOCK) + fcntl.fcntl(fd, fcntl.F_SETFL, flags) + + def run(self): + if self.socket_in is not None: + self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin)) + + recv_threads = [] + + if self.socket_out is not None: + recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout))) + + if self.socket_err is not None: + recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr))) + + for t in recv_threads: + t.join() + + def start_background_thread(self, **kwargs): + thread = threading.Thread(**kwargs) + thread.daemon = True + thread.start() + return thread + + def recv_ws(self, socket, stream): + try: + while True: + chunk = socket.recv() + + if chunk: + stream.write(chunk) + stream.flush() + else: + break + except Exception, e: + log.debug(e) + + def send_ws(self, socket, stream): + while True: + r, w, e = select([stream.fileno()], [], []) + + if r: + chunk = stream.read(1) + + if chunk == '': + socket.send_close() + break + else: + try: + socket.send(chunk) + except Exception, e: + if hasattr(e, 'errno') and e.errno == errno.EPIPE: + break + else: + raise e + + def destroy(self): + if self.settings is not None: + termios.tcsetattr(self.stdin_fileno, termios.TCSADRAIN, self.settings) + + sys.stdout.flush() + +if __name__ == '__main__': + import websocket + + if len(sys.argv) != 2: + sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n") + exit(1) + + url = sys.argv[1] + socket = websocket.create_connection(url) + + print "connected\r" + + with SocketClient(socket, interactive=True) as client: + client.run() From 76b6354173d4e355a0171bc09c1a549d97e644b1 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 16:18:44 +0000 Subject: [PATCH 0085/2154] Add requirements-dev.txt --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..f3c7e8e6 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +nose From 507940535fbb8f3932287dfd313ce01d99354abe Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 16:23:40 +0000 Subject: [PATCH 0086/2154] Tag built images and use them when starting A basic measure to get round the fact that adding isn't cached. Once Docker supports cached adds, this is probably redundant. --- plum/service.py | 16 ++++++++++++++-- tests/service_test.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/plum/service.py b/plum/service.py index 6fab273b..0550a7c0 100644 --- a/plum/service.py +++ b/plum/service.py @@ -139,14 +139,20 @@ class Service(object): container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes']) if 'build' in self.options: - container_options['image'] = self.build() + if len(self.client.images(name=self._build_tag_name())) == 0: + self.build() + container_options['image'] = self._build_tag_name() return container_options def build(self): log.info('Building %s...' % self.name) - build_output = self.client.build(self.options['build'], stream=True) + build_output = self.client.build( + self.options['build'], + tag=self._build_tag_name(), + stream=True + ) image_id = None @@ -162,6 +168,12 @@ class Service(object): return image_id + def _build_tag_name(self): + """ + The tag to give to images built for this service. + """ + return '%s_%s' % (self.project, self.name) + NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') diff --git a/tests/service_test.py b/tests/service_test.py index 2fdfba7d..3f84664d 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -113,6 +113,18 @@ class ServiceTest(DockerClientTestCase): container = service.start() container.wait() self.assertIn('success', container.logs()) + self.assertEqual(len(self.client.images(name='default_test')), 1) + + def test_start_container_uses_tagged_image_if_it_exists(self): + self.client.build('tests/fixtures/simple-dockerfile', tag='default_test') + service = Service( + name='test', + client=self.client, + build='this/does/not/exist/and/will/throw/error', + ) + container = service.start() + container.wait() + self.assertIn('success', container.logs()) def test_start_container_creates_ports(self): service = self.create_service('web', ports=[8000]) From 791028866c29f42e4c2aec9c13d5b6b3126195f0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 18:10:30 +0000 Subject: [PATCH 0087/2154] Update readme to reflect how it currently works --- README.md | 133 +++++++++++++++++++++++++++--------------------------- 1 file changed, 67 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index c91370cd..3ef299bf 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,17 @@ Plum **WARNING**: This is a work in progress and probably won't work yet. Feedback welcome. -Plum is tool for defining and running apps with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: +Plum is tool for defining and running application environments with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: ```yaml +web: + build: . + links: + - db + ports: + - 8000:8000 db: image: orchardup/postgresql - -web: - build: web/ - link: db ``` Installing @@ -26,26 +28,20 @@ Defining your app Put a `plum.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Plum will start a Docker container, so at minimum it needs to know what image to use. -The way to get started is to just give it an image name: +The simplest way to get started is to just give it an image name: ```yaml db: image: orchardup/postgresql ``` -Alternatively, you can give it the location of a directory with a Dockerfile (or a Git URL, as per the `docker build` command), and it'll automatically build the image for you: - -```yaml -db: - build: /path/to/postgresql/build/directory -``` - You've now given Plum the minimal amount of configuration it needs to run: ```bash $ plum start -Building db... done -db is running at 127.0.0.1:45678 +Pulling image orchardup/postgresql... +Starting myapp_db_1... +myapp_db_1 is running at 127.0.0.1:45678 <...output from postgresql server...> ``` @@ -55,79 +51,99 @@ By default, `plum start` will run until each container has shut down, and relay ```bash $ plum start -d -Building db... done -db is running at 127.0.0.1:45678 +Starting myapp_db_1... done +myapp_db_1 is running at 127.0.0.1:45678 $ plum ps -SERVICE STATE PORT -db up 45678 +Name State Ports +------------------------------------ +myapp_db_1 Up 5432->45678/tcp ``` +### Building services + +Plum can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command). + +This example will build an image with `app.py` inside it: + +#### app.py + +```python +print "Hello world!" +``` + +#### plum.yaml + +```yaml +web: + build: . +``` + +#### Dockerfile + + FROM ubuntu:12.04 + RUN apt-get install python + ADD . /opt + WORKDIR /opt + CMD python app.py + + ### Getting your code in -Some services may include your own code. To get that code into the container, ADD it in a Dockerfile. - -`plum.yml`: +If you want to work on an application being run by Plum, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately: ```yaml web: - build: web/ + build: . + volumes: + - .:/opt ``` -`web/Dockerfile`: - - FROM orchardup/rails - ADD . /code - CMD bundle exec rackup - ### Communicating between containers -Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicate, pass in the right IP address and port via environment variables: +Your dweb app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables: ```yaml db: image: orchardup/postgresql web: - build: web/ - link: db + build: . + links: + - db ``` -This will pass an environment variable called DB_PORT into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can then use that to connect to the database. +This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container: -You can pass in multiple links, too: - -```yaml -link: - - db - - memcached - - redis +```bash +$ plum start -d db +$ plum run web env ``` -In each case, the resulting environment variable will begin with the uppercased name of the linked service (`DB_PORT`, `MEMCACHED_PORT`, `REDIS_PORT`). - - ### Container configuration options You can pass extra configuration options to a container, much like with `docker run`: ```yaml web: - build: web/ + build: . - -- override the default run command - run: bundle exec thin -p 3000 + -- override the default command + command: bundle exec thin -p 3000 - -- expose ports - can also be an array - ports: 3000 + -- expose ports, optionally specifying both host and container ports (a random host port will be chosen otherwise) + ports: + - 3000 + - 8000:8000 - -- map volumes - can also be an array - volumes: /tmp/cache + -- map volumes + volumes: + - cache/:/tmp/cache - -- add environment variables - can also be a dictionary + -- add environment variables environment: RACK_ENV: development ``` @@ -145,18 +161,3 @@ $ plum run web bash ``` -Running more than one container for a service ---------------------------------------------- - -You can set the number of containers to run for each service with `plum scale`: - -```bash -$ plum start -d -db is running at 127.0.0.1:45678 -web is running at 127.0.0.1:45679 - -$ plum scale db=0,web=3 -Stopped db (127.0.0.1:45678) -Started web (127.0.0.1:45680) -Started web (127.0.0.1:45681) -``` From 3bebd18de79946bb3ebc1127817f7824f851df9d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 12:09:07 +0000 Subject: [PATCH 0088/2154] Show help banner if no command given --- plum/cli/docopt_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plum/cli/docopt_command.py b/plum/cli/docopt_command.py index 93cf889e..d2aeb035 100644 --- a/plum/cli/docopt_command.py +++ b/plum/cli/docopt_command.py @@ -28,6 +28,9 @@ class DocoptCommand(object): options = docopt_full_help(getdoc(self), argv, **self.docopt_options()) command = options['COMMAND'] + if command is None: + raise SystemExit(getdoc(self)) + if not hasattr(self, command): raise NoSuchCommand(command, self) From a4710fa9e149dc570fc9588d0e18e85e9a388d94 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 16:22:54 +0000 Subject: [PATCH 0089/2154] 'plum up' is now the special magic 'start' and 'stop' are now analogous to their Docker namesakes. --- plum/cli/main.py | 44 ++++++++++++++++++++---------------- plum/container.py | 9 ++++++-- plum/project.py | 24 ++++++++++++++++---- plum/service.py | 24 ++++++-------------- tests/project_test.py | 52 +++++++++++++++++++++++++++++++++++-------- tests/service_test.py | 35 ++++++++++++++--------------- 6 files changed, 119 insertions(+), 69 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index b3e4f11e..7819df61 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -140,37 +140,43 @@ class TopLevelCommand(Command): service.start_container(container, ports=None) c.run() - def start(self, options): + def up(self, options): """ - Start all services + Create and start containers - Usage: start [options] + Usage: up [options] Options: -d Detached mode: Run containers in the background, print new container names """ - if options['-d']: - self.project.start() - return + detached = options['-d'] - running = [] - unstarted = [] + unstarted = self.project.create_containers() - for s in self.project.services: - if len(s.containers()) == 0: - unstarted.append((s, s.create_container())) - else: - running += s.containers(stopped=False) - - log_printer = LogPrinter(running + [c for (s, c) in unstarted]) + if not detached: + to_attach = self.project.containers() + [c for (s, c) in unstarted] + print "Attaching to", list_containers(to_attach) + log_printer = LogPrinter(to_attach, attach_params={'logs': True}) for (s, c) in unstarted: s.start_container(c) - try: - log_printer.run() - finally: - self.project.stop() + if detached: + for (s, c) in unstarted: + print c.name + else: + try: + log_printer.run() + finally: + self.project.kill_and_remove(unstarted) + + def start(self, options): + """ + Start all services + + Usage: start + """ + self.project.start() def stop(self, options): """ diff --git a/plum/container.py b/plum/container.py index 61695216..5e025440 100644 --- a/plum/container.py +++ b/plum/container.py @@ -83,13 +83,18 @@ class Container(object): out[k] = v return out + @property + def is_running(self): + self.inspect_if_not_inspected() + 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): + def stop(self, **options): log.info("Stopping %s..." % self.name) - return self.client.stop(self.id) + return self.client.stop(self.id, **options) def kill(self): log.info("Killing %s..." % self.name) diff --git a/plum/project.py b/plum/project.py index 7a72e4a6..84761644 100644 --- a/plum/project.py +++ b/plum/project.py @@ -50,13 +50,29 @@ class Project(object): if service.name == name: return service - def start(self): + def create_containers(self): + """ + Returns a list of (service, container) tuples, + one for each service with no running containers. + """ + containers = [] for service in self.services: - service.start() + if len(service.containers()) == 0: + containers.append((service, service.create_container())) + return containers - def stop(self): + def kill_and_remove(self, tuples): + for (service, container) in tuples: + container.kill() + container.remove() + + def start(self, **options): for service in self.services: - service.stop() + service.start(**options) + + def stop(self, **options): + for service in self.services: + service.stop(**options) def containers(self, *args, **kwargs): l = [] diff --git a/plum/service.py b/plum/service.py index 0550a7c0..9caccc6f 100644 --- a/plum/service.py +++ b/plum/service.py @@ -38,19 +38,14 @@ class Service(object): l.append(Container.from_ps(self.client, container)) return l - def start(self): - if len(self.containers()) == 0: - return self.start_container() + def start(self, **options): + for c in self.containers(stopped=True): + if not c.is_running: + self.start_container(c, **options) - def stop(self): - self.scale(0) - - def scale(self, num): - while len(self.containers()) < num: - self.start_container() - - while len(self.containers()) > num: - self.stop_container() + def stop(self, **options): + for c in self.containers(): + c.stop(**options) def create_container(self, one_off=False, **override_options): """ @@ -99,11 +94,6 @@ class Service(object): ) return container - def stop_container(self): - container = self.containers()[-1] - container.kill() - container.remove() - def next_container_name(self, one_off=False): bits = [self.project, self.name] if one_off: diff --git a/tests/project_test.py b/tests/project_test.py index aa56b407..c982990a 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -42,18 +42,52 @@ class ProjectTest(DockerClientTestCase): project = Project('test', [web], self.client) self.assertEqual(project.get_service('web'), web) + def test_up(self): + web = self.create_service('web') + db = self.create_service('db') + project = Project('test', [web, db], self.client) + + web.create_container() + + self.assertEqual(len(web.containers()), 0) + self.assertEqual(len(db.containers()), 0) + self.assertEqual(len(web.containers(stopped=True)), 1) + self.assertEqual(len(db.containers(stopped=True)), 0) + + unstarted = project.create_containers() + self.assertEqual(len(unstarted), 2) + self.assertEqual(unstarted[0][0], web) + self.assertEqual(unstarted[1][0], db) + + self.assertEqual(len(web.containers()), 0) + self.assertEqual(len(db.containers()), 0) + self.assertEqual(len(web.containers(stopped=True)), 2) + self.assertEqual(len(db.containers(stopped=True)), 1) + + project.kill_and_remove(unstarted) + + self.assertEqual(len(web.containers()), 0) + self.assertEqual(len(db.containers()), 0) + self.assertEqual(len(web.containers(stopped=True)), 1) + self.assertEqual(len(db.containers(stopped=True)), 0) + def test_start_stop(self): - project = Project('test', [ - self.create_service('web'), - self.create_service('db'), - ], self.client) + web = self.create_service('web') + db = self.create_service('db') + project = Project('test', [web, db], self.client) project.start() - self.assertEqual(len(project.get_service('web').containers()), 1) - self.assertEqual(len(project.get_service('db').containers()), 1) + self.assertEqual(len(web.containers()), 0) + self.assertEqual(len(db.containers()), 0) - project.stop() + web.create_container() + project.start() - self.assertEqual(len(project.get_service('web').containers()), 0) - self.assertEqual(len(project.get_service('db').containers()), 0) + self.assertEqual(len(web.containers()), 1) + self.assertEqual(len(db.containers()), 0) + + project.stop(timeout=1) + + self.assertEqual(len(web.containers()), 0) + self.assertEqual(len(db.containers()), 0) diff --git a/tests/service_test.py b/tests/service_test.py index 3f84664d..77419630 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -26,13 +26,14 @@ class ServiceTest(DockerClientTestCase): foo = self.create_service('foo') bar = self.create_service('bar') - foo.start() + foo.start_container() self.assertEqual(len(foo.containers()), 1) self.assertEqual(foo.containers()[0].name, '/default_foo_1') self.assertEqual(len(bar.containers()), 0) - bar.scale(2) + bar.start_container() + bar.start_container() self.assertEqual(len(foo.containers()), 1) self.assertEqual(len(bar.containers()), 2) @@ -49,30 +50,28 @@ class ServiceTest(DockerClientTestCase): def test_project_is_added_to_container_name(self): service = self.create_service('web', project='myproject') - service.start() + service.start_container() self.assertEqual(service.containers()[0].name, '/myproject_web_1') - def test_up_scale_down(self): + def test_start_stop(self): service = self.create_service('scalingtest') + self.assertEqual(len(service.containers(stopped=True)), 0) + + service.create_container() self.assertEqual(len(service.containers()), 0) + self.assertEqual(len(service.containers(stopped=True)), 1) service.start() self.assertEqual(len(service.containers()), 1) + self.assertEqual(len(service.containers(stopped=True)), 1) - service.start() - self.assertEqual(len(service.containers()), 1) - - service.scale(2) - self.assertEqual(len(service.containers()), 2) - - service.scale(1) - self.assertEqual(len(service.containers()), 1) - - service.stop() + service.stop(timeout=1) self.assertEqual(len(service.containers()), 0) + self.assertEqual(len(service.containers(stopped=True)), 1) - service.stop() + service.stop(timeout=1) self.assertEqual(len(service.containers()), 0) + self.assertEqual(len(service.containers(stopped=True)), 1) def test_create_container_with_one_off(self): db = self.create_service('db') @@ -101,8 +100,8 @@ class ServiceTest(DockerClientTestCase): db.start_container() web.start_container() self.assertIn('default_db_1', web.containers()[0].links()) - db.stop() - web.stop() + db.stop(timeout=1) + web.stop(timeout=1) def test_start_container_builds_images(self): service = Service( @@ -110,7 +109,7 @@ class ServiceTest(DockerClientTestCase): client=self.client, build='tests/fixtures/simple-dockerfile', ) - container = service.start() + container = service.start_container() container.wait() self.assertIn('success', container.logs()) self.assertEqual(len(self.client.images(name='default_test')), 1) From 81093627fe9b1ed9be947a0178318798ca319f68 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 16:53:07 +0000 Subject: [PATCH 0090/2154] Implement kill and rm --- plum/cli/main.py | 18 ++++++++++++++++++ plum/project.py | 8 ++++++++ plum/service.py | 9 +++++++++ tests/service_test.py | 16 ++++++++++++++++ 4 files changed, 51 insertions(+) diff --git a/plum/cli/main.py b/plum/cli/main.py index 7819df61..fe03dbd3 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -71,6 +71,8 @@ class TopLevelCommand(Command): run Run a one-off command start Start services stop Stop services + kill Kill containers + rm Remove stopped containers """ def docopt_options(self): @@ -186,6 +188,22 @@ class TopLevelCommand(Command): """ self.project.stop() + def kill(self, options): + """ + Kill all containers + + Usage: kill + """ + self.project.kill() + + def rm(self, options): + """ + Remove all stopped containers + + Usage: rm + """ + self.project.remove_stopped() + def logs(self, options): """ View output from containers diff --git a/plum/project.py b/plum/project.py index 84761644..b9fbaabd 100644 --- a/plum/project.py +++ b/plum/project.py @@ -74,6 +74,14 @@ class Project(object): for service in self.services: service.stop(**options) + def kill(self, **options): + for service in self.services: + service.kill(**options) + + def remove_stopped(self, **options): + for service in self.services: + service.remove_stopped(**options) + def containers(self, *args, **kwargs): l = [] for service in self.services: diff --git a/plum/service.py b/plum/service.py index 9caccc6f..fab896f5 100644 --- a/plum/service.py +++ b/plum/service.py @@ -47,6 +47,15 @@ class Service(object): for c in self.containers(): c.stop(**options) + def kill(self, **options): + for c in self.containers(): + c.kill(**options) + + def remove_stopped(self, **options): + for c in self.containers(stopped=True): + if not c.is_running: + c.remove(**options) + def create_container(self, one_off=False, **override_options): """ Create a container for this service. If the image doesn't exist, attempt to pull diff --git a/tests/service_test.py b/tests/service_test.py index 77419630..e3a3e625 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -73,6 +73,22 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(service.containers()), 0) self.assertEqual(len(service.containers(stopped=True)), 1) + def test_kill_remove(self): + service = self.create_service('scalingtest') + + service.start_container() + self.assertEqual(len(service.containers()), 1) + + service.remove_stopped() + self.assertEqual(len(service.containers()), 1) + + service.kill() + self.assertEqual(len(service.containers()), 0) + self.assertEqual(len(service.containers(stopped=True)), 1) + + service.remove_stopped() + self.assertEqual(len(service.containers(stopped=True)), 0) + def test_create_container_with_one_off(self): db = self.create_service('db') container = db.create_container(one_off=True) From d3346fa174897498b5f86c9c4a8fadd4e961d345 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 18:30:23 +0000 Subject: [PATCH 0091/2154] up, start, stop, kill and rm all accept a list of services --- plum/cli/main.py | 28 +++++++++++----------- plum/project.py | 54 +++++++++++++++++++++++++++++++++---------- tests/project_test.py | 49 +++++++++++++++++++++++++++++++-------- 3 files changed, 97 insertions(+), 34 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index fe03dbd3..0c844040 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -5,6 +5,7 @@ import re from inspect import getdoc from .. import __version__ +from ..project import NoSuchService from .command import Command from .formatter import Formatter from .log_printer import LogPrinter @@ -37,6 +38,9 @@ def main(): except UserError, e: log.error(e.msg) exit(1) + except NoSuchService, e: + log.error(e.msg) + exit(1) except NoSuchCommand, e: log.error("No such command: %s", e.command) log.error("") @@ -121,8 +125,6 @@ class TopLevelCommand(Command): -d Detached mode: Run container in the background, print new container name """ service = self.project.get_service(options['SERVICE']) - if service is None: - raise UserError("No such service: %s" % options['SERVICE']) container_options = { 'command': [options['COMMAND']] + options['ARGS'], 'tty': not options['-d'], @@ -146,17 +148,17 @@ class TopLevelCommand(Command): """ Create and start containers - Usage: up [options] + Usage: up [options] [SERVICE...] Options: -d Detached mode: Run containers in the background, print new container names """ detached = options['-d'] - unstarted = self.project.create_containers() + unstarted = self.project.create_containers(service_names=options['SERVICE']) if not detached: - to_attach = self.project.containers() + [c for (s, c) in unstarted] + to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted] print "Attaching to", list_containers(to_attach) log_printer = LogPrinter(to_attach, attach_params={'logs': True}) @@ -176,33 +178,33 @@ class TopLevelCommand(Command): """ Start all services - Usage: start + Usage: start [SERVICE...] """ - self.project.start() + self.project.start(service_names=options['SERVICE']) def stop(self, options): """ Stop all services - Usage: stop + Usage: stop [SERVICE...] """ - self.project.stop() + self.project.stop(service_names=options['SERVICE']) def kill(self, options): """ Kill all containers - Usage: kill + Usage: kill [SERVICE...] """ - self.project.kill() + self.project.kill(service_names=options['SERVICE']) def rm(self, options): """ Remove all stopped containers - Usage: rm + Usage: rm [SERVICE...] """ - self.project.remove_stopped() + self.project.remove_stopped(service_names=options['SERVICE']) def logs(self, options): """ diff --git a/plum/project.py b/plum/project.py index b9fbaabd..52a050d2 100644 --- a/plum/project.py +++ b/plum/project.py @@ -46,17 +46,40 @@ class Project(object): return cls.from_dicts(name, dicts, client) def get_service(self, name): + """ + Retrieve a service by name. Raises NoSuchService + if the named service does not exist. + """ for service in self.services: if service.name == name: return service - def create_containers(self): + raise NoSuchService(name) + + def get_services(self, service_names=None): + """ + Returns a list of this project's services filtered + by the provided list of names, or all services if + service_names is None or []. + + Preserves the original order of self.services. + + Raises NoSuchService if any of the named services + do not exist. + """ + if service_names is None or len(service_names) == 0: + return self.services + else: + 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): """ Returns a list of (service, container) tuples, one for each service with no running containers. """ containers = [] - for service in self.services: + for service in self.get_services(service_names): if len(service.containers()) == 0: containers.append((service, service.create_container())) return containers @@ -66,27 +89,34 @@ class Project(object): container.kill() container.remove() - def start(self, **options): - for service in self.services: + def start(self, service_names=None, **options): + for service in self.get_services(service_names): service.start(**options) - def stop(self, **options): - for service in self.services: + def stop(self, service_names=None, **options): + for service in self.get_services(service_names): service.stop(**options) - def kill(self, **options): - for service in self.services: + def kill(self, service_names=None, **options): + for service in self.get_services(service_names): service.kill(**options) - def remove_stopped(self, **options): - for service in self.services: + def remove_stopped(self, service_names=None, **options): + for service in self.get_services(service_names): service.remove_stopped(**options) - def containers(self, *args, **kwargs): + def containers(self, service_names=None, *args, **kwargs): l = [] - for service in self.services: + for service in self.get_services(service_names): for container in service.containers(*args, **kwargs): l.append(container) return l +class NoSuchService(Exception): + def __init__(self, name): + self.name = name + self.msg = "No such service: %s" % self.name + + def __str__(self): + return self.msg diff --git a/tests/project_test.py b/tests/project_test.py index c982990a..e96923dd 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -42,11 +42,30 @@ class ProjectTest(DockerClientTestCase): project = Project('test', [web], self.client) self.assertEqual(project.get_service('web'), web) - def test_up(self): + def test_create_containers(self): web = self.create_service('web') db = self.create_service('db') project = Project('test', [web, db], self.client) + unstarted = project.create_containers(service_names=['web']) + self.assertEqual(len(unstarted), 1) + self.assertEqual(unstarted[0][0], web) + self.assertEqual(len(web.containers(stopped=True)), 1) + self.assertEqual(len(db.containers(stopped=True)), 0) + + unstarted = project.create_containers() + self.assertEqual(len(unstarted), 2) + self.assertEqual(unstarted[0][0], web) + self.assertEqual(unstarted[1][0], db) + self.assertEqual(len(web.containers(stopped=True)), 2) + self.assertEqual(len(db.containers(stopped=True)), 1) + + def test_up(self): + web = self.create_service('web') + db = self.create_service('db') + other = self.create_service('other') + project = Project('test', [web, db, other], self.client) + web.create_container() self.assertEqual(len(web.containers()), 0) @@ -54,7 +73,7 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 0) - unstarted = project.create_containers() + unstarted = project.create_containers(service_names=['web', 'db']) self.assertEqual(len(unstarted), 2) self.assertEqual(unstarted[0][0], web) self.assertEqual(unstarted[1][0], db) @@ -71,7 +90,7 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 0) - def test_start_stop(self): + def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') project = Project('test', [web, db], self.client) @@ -81,13 +100,25 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(web.containers()), 0) self.assertEqual(len(db.containers()), 0) - web.create_container() + web_container_1 = web.create_container() + web_container_2 = web.create_container() + db_container = db.create_container() + + project.start(service_names=['web']) + self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name])) + project.start() + self.assertEqual(set(c.name for c in project.containers()), set([web_container_1.name, web_container_2.name, db_container.name])) - self.assertEqual(len(web.containers()), 1) - self.assertEqual(len(db.containers()), 0) + project.stop(service_names=['web'], timeout=1) + self.assertEqual(set(c.name for c in project.containers()), set([db_container.name])) - project.stop(timeout=1) + project.kill(service_names=['db']) + self.assertEqual(len(project.containers()), 0) + self.assertEqual(len(project.containers(stopped=True)), 3) - self.assertEqual(len(web.containers()), 0) - self.assertEqual(len(db.containers()), 0) + project.remove_stopped(service_names=['web']) + self.assertEqual(len(project.containers(stopped=True)), 1) + + project.remove_stopped() + self.assertEqual(len(project.containers(stopped=True)), 0) From 94cae104173554496f0ba4c3ab104fb4652ecb6f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 19:13:55 +0000 Subject: [PATCH 0092/2154] ps and logs can filter by service too --- plum/cli/main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index 0c844040..b5289bc0 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -88,12 +88,12 @@ class TopLevelCommand(Command): """ List services and containers. - Usage: ps [options] + Usage: ps [options] [SERVICE...] Options: -q Only display IDs """ - containers = self.project.containers(stopped=True) + self.project.containers(one_off=True) + containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + self.project.containers(service_names=options['SERVICE'], one_off=True) if options['-q']: for container in containers: @@ -210,9 +210,9 @@ class TopLevelCommand(Command): """ View output from containers - Usage: logs + Usage: logs [SERVICE...] """ - containers = self.project.containers(stopped=False) + containers = self.project.containers(service_names=options['SERVICE'], stopped=False) print "Attaching to", list_containers(containers) LogPrinter(containers, attach_params={'logs': True}).run() From 08e4468bdbb80d07526393bb2ecc5d78f49473eb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 19:15:12 +0000 Subject: [PATCH 0093/2154] Clean up the help banners a bit --- plum/cli/main.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/plum/cli/main.py b/plum/cli/main.py index b5289bc0..1e91528e 100644 --- a/plum/cli/main.py +++ b/plum/cli/main.py @@ -70,8 +70,9 @@ class TopLevelCommand(Command): --version Print version and exit Commands: + up Create and start containers logs View output from containers - ps List services and containers + ps List containers run Run a one-off command start Start services stop Stop services @@ -86,7 +87,7 @@ class TopLevelCommand(Command): def ps(self, options): """ - List services and containers. + List containers. Usage: ps [options] [SERVICE...] @@ -146,7 +147,7 @@ class TopLevelCommand(Command): def up(self, options): """ - Create and start containers + Create and start containers. Usage: up [options] [SERVICE...] @@ -176,7 +177,7 @@ class TopLevelCommand(Command): def start(self, options): """ - Start all services + Start existing containers. Usage: start [SERVICE...] """ @@ -184,7 +185,7 @@ class TopLevelCommand(Command): def stop(self, options): """ - Stop all services + Stop running containers. Usage: stop [SERVICE...] """ @@ -192,7 +193,7 @@ class TopLevelCommand(Command): def kill(self, options): """ - Kill all containers + Kill containers. Usage: kill [SERVICE...] """ @@ -200,7 +201,7 @@ class TopLevelCommand(Command): def rm(self, options): """ - Remove all stopped containers + Remove stopped containers Usage: rm [SERVICE...] """ @@ -208,7 +209,7 @@ class TopLevelCommand(Command): def logs(self, options): """ - View output from containers + View output from containers. Usage: logs [SERVICE...] """ From 8291d36eafbba81e8afc6515a69418ca377785c8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 19:30:31 +0000 Subject: [PATCH 0094/2154] Fix stray test regression --- tests/service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/service_test.py b/tests/service_test.py index e3a3e625..841a585b 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -137,7 +137,7 @@ class ServiceTest(DockerClientTestCase): client=self.client, build='this/does/not/exist/and/will/throw/error', ) - container = service.start() + container = service.start_container() container.wait() self.assertIn('success', container.logs()) From 13a30c327a2c964b1167974766a0f664f0024248 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 19:33:41 +0000 Subject: [PATCH 0095/2154] Container.name strips the leading slash --- plum/container.py | 4 ++-- plum/service.py | 2 +- tests/service_test.py | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/plum/container.py b/plum/container.py index 5e025440..454ec91f 100644 --- a/plum/container.py +++ b/plum/container.py @@ -45,7 +45,7 @@ class Container(object): @property def name(self): - return self.dictionary['Name'] + return self.dictionary['Name'][1:] @property def human_readable_ports(self): @@ -123,7 +123,7 @@ class Container(object): for container in self.client.containers(): for name in container['Names']: bits = name.split('/') - if len(bits) > 2 and bits[1] == self.name[1:]: + if len(bits) > 2 and bits[1] == self.name: links.append(bits[2]) return links diff --git a/plum/service.py b/plum/service.py index fab896f5..537842db 100644 --- a/plum/service.py +++ b/plum/service.py @@ -121,7 +121,7 @@ class Service(object): links = {} for service in self.links: for container in service.containers(): - links[container.name[1:]] = container.name[1:] + links[container.name] = container.name return links def _get_container_options(self, override_options, one_off=False): diff --git a/tests/service_test.py b/tests/service_test.py index 841a585b..643473b0 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -29,7 +29,7 @@ class ServiceTest(DockerClientTestCase): foo.start_container() self.assertEqual(len(foo.containers()), 1) - self.assertEqual(foo.containers()[0].name, '/default_foo_1') + self.assertEqual(foo.containers()[0].name, 'default_foo_1') self.assertEqual(len(bar.containers()), 0) bar.start_container() @@ -39,8 +39,8 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(bar.containers()), 2) names = [c.name for c in bar.containers()] - self.assertIn('/default_bar_1', names) - self.assertIn('/default_bar_2', names) + self.assertIn('default_bar_1', names) + self.assertIn('default_bar_2', names) def test_containers_one_off(self): db = self.create_service('db') @@ -51,7 +51,7 @@ class ServiceTest(DockerClientTestCase): def test_project_is_added_to_container_name(self): service = self.create_service('web', project='myproject') service.start_container() - self.assertEqual(service.containers()[0].name, '/myproject_web_1') + self.assertEqual(service.containers()[0].name, 'myproject_web_1') def test_start_stop(self): service = self.create_service('scalingtest') @@ -92,13 +92,13 @@ class ServiceTest(DockerClientTestCase): def test_create_container_with_one_off(self): db = self.create_service('db') container = db.create_container(one_off=True) - self.assertEqual(container.name, '/default_db_run_1') + self.assertEqual(container.name, 'default_db_run_1') def test_create_container_with_one_off_when_existing_container_is_running(self): db = self.create_service('db') db.start() container = db.create_container(one_off=True) - self.assertEqual(container.name, '/default_db_run_1') + self.assertEqual(container.name, 'default_db_run_1') def test_start_container_passes_through_options(self): db = self.create_service('db') From 4d35d47969a386faaf3c988c091a9ac94c268004 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 20:04:37 +0000 Subject: [PATCH 0096/2154] Fix a couple of typos in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3ef299bf..ae61648b 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ This example will build an image with `app.py` inside it: print "Hello world!" ``` -#### plum.yaml +#### plum.yml ```yaml web: @@ -103,7 +103,7 @@ web: ### Communicating between containers -Your dweb app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables: +Your web app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables: ```yaml db: From 0cafdc9c6c19dab2ef2795979dc8b2f48f623379 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 20:28:24 +0000 Subject: [PATCH 0097/2154] plum -> fig --- README.md | 38 ++++++++++++++--------------- {plum => fig}/__init__.py | 0 {plum => fig}/cli/__init__.py | 0 {plum => fig}/cli/colors.py | 0 {plum => fig}/cli/command.py | 2 +- {plum => fig}/cli/docopt_command.py | 0 {plum => fig}/cli/errors.py | 0 {plum => fig}/cli/formatter.py | 0 {plum => fig}/cli/log_printer.py | 0 {plum => fig}/cli/main.py | 6 ++--- {plum => fig}/cli/multiplexer.py | 0 {plum => fig}/cli/socketclient.py | 0 {plum => fig}/cli/utils.py | 0 {plum => fig}/container.py | 0 {plum => fig}/project.py | 0 {plum => fig}/service.py | 0 setup.py | 10 ++++---- tests/container_test.py | 2 +- tests/project_test.py | 4 +-- tests/service_test.py | 2 +- tests/testcases.py | 2 +- 21 files changed, 33 insertions(+), 33 deletions(-) rename {plum => fig}/__init__.py (100%) rename {plum => fig}/cli/__init__.py (100%) rename {plum => fig}/cli/colors.py (100%) rename {plum => fig}/cli/command.py (95%) rename {plum => fig}/cli/docopt_command.py (100%) rename {plum => fig}/cli/errors.py (100%) rename {plum => fig}/cli/formatter.py (100%) rename {plum => fig}/cli/log_printer.py (100%) rename {plum => fig}/cli/main.py (98%) rename {plum => fig}/cli/multiplexer.py (100%) rename {plum => fig}/cli/socketclient.py (100%) rename {plum => fig}/cli/utils.py (100%) rename {plum => fig}/container.py (100%) rename {plum => fig}/project.py (100%) rename {plum => fig}/service.py (100%) diff --git a/README.md b/README.md index ae61648b..09e68649 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -Plum +Fig ==== **WARNING**: This is a work in progress and probably won't work yet. Feedback welcome. -Plum is tool for defining and running application environments with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: +Fig is tool for defining and running application environments with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: ```yaml web: @@ -20,13 +20,13 @@ Installing ---------- ```bash -$ sudo pip install plum +$ sudo pip install fig ``` Defining your app ----------------- -Put a `plum.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Plum will start a Docker container, so at minimum it needs to know what image to use. +Put a `fig.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use. The simplest way to get started is to just give it an image name: @@ -35,26 +35,26 @@ db: image: orchardup/postgresql ``` -You've now given Plum the minimal amount of configuration it needs to run: +You've now given Fig the minimal amount of configuration it needs to run: ```bash -$ plum start +$ fig start Pulling image orchardup/postgresql... Starting myapp_db_1... myapp_db_1 is running at 127.0.0.1:45678 <...output from postgresql server...> ``` -For each service you've defined, Plum will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. +For each service you've defined, Fig will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. -By default, `plum start` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: +By default, `fig start` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: ```bash -$ plum start -d +$ fig start -d Starting myapp_db_1... done myapp_db_1 is running at 127.0.0.1:45678 -$ plum ps +$ fig ps Name State Ports ------------------------------------ myapp_db_1 Up 5432->45678/tcp @@ -62,7 +62,7 @@ myapp_db_1 Up 5432->45678/tcp ### Building services -Plum can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command). +Fig can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command). This example will build an image with `app.py` inside it: @@ -72,7 +72,7 @@ This example will build an image with `app.py` inside it: print "Hello world!" ``` -#### plum.yml +#### fig.yml ```yaml web: @@ -91,7 +91,7 @@ web: ### Getting your code in -If you want to work on an application being run by Plum, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately: +If you want to work on an application being run by Fig, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately: ```yaml web: @@ -118,8 +118,8 @@ web: This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container: ```bash -$ plum start -d db -$ plum run web env +$ fig start -d db +$ fig run web env ``` @@ -152,12 +152,12 @@ web: Running a one-off command ------------------------- -If you want to run a management command, use `plum run` to start a one-off container: +If you want to run a management command, use `fig run` to start a one-off container: ```bash -$ plum run db createdb myapp_development -$ plum run web rake db:migrate -$ plum run web bash +$ fig run db createdb myapp_development +$ fig run web rake db:migrate +$ fig run web bash ``` diff --git a/plum/__init__.py b/fig/__init__.py similarity index 100% rename from plum/__init__.py rename to fig/__init__.py diff --git a/plum/cli/__init__.py b/fig/cli/__init__.py similarity index 100% rename from plum/cli/__init__.py rename to fig/cli/__init__.py diff --git a/plum/cli/colors.py b/fig/cli/colors.py similarity index 100% rename from plum/cli/colors.py rename to fig/cli/colors.py diff --git a/plum/cli/command.py b/fig/cli/command.py similarity index 95% rename from plum/cli/command.py rename to fig/cli/command.py index 5bbed26b..f8899c3f 100644 --- a/plum/cli/command.py +++ b/fig/cli/command.py @@ -21,7 +21,7 @@ class Command(DocoptCommand): @cached_property def project(self): - config = yaml.load(open('plum.yml')) + config = yaml.load(open('fig.yml')) return Project.from_config(self.project_name, config, self.client) @cached_property diff --git a/plum/cli/docopt_command.py b/fig/cli/docopt_command.py similarity index 100% rename from plum/cli/docopt_command.py rename to fig/cli/docopt_command.py diff --git a/plum/cli/errors.py b/fig/cli/errors.py similarity index 100% rename from plum/cli/errors.py rename to fig/cli/errors.py diff --git a/plum/cli/formatter.py b/fig/cli/formatter.py similarity index 100% rename from plum/cli/formatter.py rename to fig/cli/formatter.py diff --git a/plum/cli/log_printer.py b/fig/cli/log_printer.py similarity index 100% rename from plum/cli/log_printer.py rename to fig/cli/log_printer.py diff --git a/plum/cli/main.py b/fig/cli/main.py similarity index 98% rename from plum/cli/main.py rename to fig/cli/main.py index 1e91528e..f8687217 100644 --- a/plum/cli/main.py +++ b/fig/cli/main.py @@ -62,8 +62,8 @@ class TopLevelCommand(Command): """. Usage: - plum [options] [COMMAND] [ARGS...] - plum -h|--help + fig [options] [COMMAND] [ARGS...] + fig -h|--help Options: --verbose Show more output @@ -82,7 +82,7 @@ class TopLevelCommand(Command): """ def docopt_options(self): options = super(TopLevelCommand, self).docopt_options() - options['version'] = "plum %s" % __version__ + options['version'] = "fig %s" % __version__ return options def ps(self, options): diff --git a/plum/cli/multiplexer.py b/fig/cli/multiplexer.py similarity index 100% rename from plum/cli/multiplexer.py rename to fig/cli/multiplexer.py diff --git a/plum/cli/socketclient.py b/fig/cli/socketclient.py similarity index 100% rename from plum/cli/socketclient.py rename to fig/cli/socketclient.py diff --git a/plum/cli/utils.py b/fig/cli/utils.py similarity index 100% rename from plum/cli/utils.py rename to fig/cli/utils.py diff --git a/plum/container.py b/fig/container.py similarity index 100% rename from plum/container.py rename to fig/container.py diff --git a/plum/project.py b/fig/project.py similarity index 100% rename from plum/project.py rename to fig/project.py diff --git a/plum/service.py b/fig/service.py similarity index 100% rename from plum/service.py rename to fig/service.py diff --git a/setup.py b/setup.py index 5b01a47e..f1e3d324 100644 --- a/setup.py +++ b/setup.py @@ -23,19 +23,19 @@ def find_version(*file_paths): setup( - name='plum', - version=find_version("plum", "__init__.py"), + name='fig', + version=find_version("fig", "__init__.py"), description='', - url='https://github.com/orchardup/plum', + url='https://github.com/orchardup/fig', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', - packages=['plum'], + packages=['fig'], package_data={}, include_package_data=True, install_requires=[], dependency_links=[], entry_points=""" [console_scripts] - plum=plum.cli.main:main + fig=fig.cli.main:main """, ) diff --git a/tests/container_test.py b/tests/container_test.py index 8628b04d..0d6c5f0f 100644 --- a/tests/container_test.py +++ b/tests/container_test.py @@ -1,5 +1,5 @@ from .testcases import DockerClientTestCase -from plum.container import Container +from fig.container import Container class ContainerTest(DockerClientTestCase): def test_from_ps(self): diff --git a/tests/project_test.py b/tests/project_test.py index e96923dd..7dd3715f 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -1,5 +1,5 @@ -from plum.project import Project -from plum.service import Service +from fig.project import Project +from fig.service import Service from .testcases import DockerClientTestCase diff --git a/tests/service_test.py b/tests/service_test.py index 643473b0..02b96ab8 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -1,4 +1,4 @@ -from plum import Service +from fig import Service from .testcases import DockerClientTestCase diff --git a/tests/testcases.py b/tests/testcases.py index 8fc65ee8..a930d68e 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,5 +1,5 @@ from docker import Client -from plum.service import Service +from fig.service import Service import os from unittest import TestCase From 4182830e7e4bb8e34ee91f1fdb80ccc45a95e63f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 20 Dec 2013 20:30:59 +0000 Subject: [PATCH 0098/2154] README: change 'start' back to 'up' --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 09e68649..44eac9d0 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ db: You've now given Fig the minimal amount of configuration it needs to run: ```bash -$ fig start +$ fig up Pulling image orchardup/postgresql... Starting myapp_db_1... myapp_db_1 is running at 127.0.0.1:45678 @@ -47,10 +47,10 @@ myapp_db_1 is running at 127.0.0.1:45678 For each service you've defined, Fig will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. -By default, `fig start` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: +By default, `fig up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: ```bash -$ fig start -d +$ fig up -d Starting myapp_db_1... done myapp_db_1 is running at 127.0.0.1:45678 @@ -118,7 +118,7 @@ web: This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container: ```bash -$ fig start -d db +$ fig up -d db $ fig run web env ``` From 50d1a39b3a25487f2b9f976edbaa55b811a278f3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 20:45:27 +0000 Subject: [PATCH 0099/2154] Update description in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 44eac9d0..0ab7c110 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ Fig ==== -**WARNING**: This is a work in progress and probably won't work yet. Feedback welcome. +Punctual, lightweight development environments using Docker. -Fig is tool for defining and running application environments with Docker. It uses a simple, version-controllable YAML configuration file that looks something like this: +Fig is tool for defining and running isolated application environments. It uses a simple, version-controllable YAML configuration file that looks something like this: ```yaml web: From 206c338e142ba2af30a3c098b167f9e41d4282ad Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 20:57:14 +0000 Subject: [PATCH 0100/2154] Readme pluralisation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ab7c110..12dd61c0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Fig Punctual, lightweight development environments using Docker. -Fig is tool for defining and running isolated application environments. It uses a simple, version-controllable YAML configuration file that looks something like this: +Fig is tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this: ```yaml web: From 5cc4b59dc02dce0d8330370ae4665e1ecd2619a1 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:25:06 +0000 Subject: [PATCH 0101/2154] Switch to stable docker-py --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f8e53737..dccc0aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -git+git://github.com/dotcloud/docker-py.git@5c928dcab51a276f421a36d584c37b745b3b9a3d +docker-py==0.2.3 docopt==0.6.1 PyYAML==3.10 texttable==0.8.1 From dd920890f3510d69ea8780a5a4436fa4d3000e09 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:25:19 +0000 Subject: [PATCH 0102/2154] Read requirements in setup.py --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index f1e3d324..fdffe020 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,8 @@ def find_version(*file_paths): return version_match.group(1) raise RuntimeError("Unable to find version string.") +with open('requirements.txt') as f: + install_requires = f.read().splitlines() setup( name='fig', @@ -32,8 +34,7 @@ setup( packages=['fig'], package_data={}, include_package_data=True, - install_requires=[], - dependency_links=[], + install_requires=install_requires, entry_points=""" [console_scripts] fig=fig.cli.main:main From fb445b3a065287885587a855428ecc6be55936e0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:31:00 +0000 Subject: [PATCH 0103/2154] Version 0.0.1 --- fig/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/__init__.py b/fig/__init__.py index 404eba85..5282dce9 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,3 +1,3 @@ from .service import Service -__version__ = '1.0.0' +__version__ = '0.0.1' From 7b925b8eaca2864ad142ce04df16a0f693ca823f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:32:41 +0000 Subject: [PATCH 0104/2154] Add some helpful scripts --- script/clean | 3 +++ script/release | 14 ++++++++++++++ script/test | 2 ++ 3 files changed, 19 insertions(+) create mode 100755 script/clean create mode 100755 script/release create mode 100755 script/test diff --git a/script/clean b/script/clean new file mode 100755 index 00000000..f9f1b0da --- /dev/null +++ b/script/clean @@ -0,0 +1,3 @@ +#!/bin/sh +find . -type f -name '*.pyc' -delete + diff --git a/script/release b/script/release new file mode 100755 index 00000000..fdd4a960 --- /dev/null +++ b/script/release @@ -0,0 +1,14 @@ +#!/bin/bash + +set -xe + +if [ -z "$1" ]; then + echo 'pass a version as first argument' + exit 1 +fi + +git tag $1 +git push --tags +python setup.py sdist upload + + diff --git a/script/test b/script/test new file mode 100755 index 00000000..1ceefaad --- /dev/null +++ b/script/test @@ -0,0 +1,2 @@ +#!/bin/sh +nosetests From 23d6ae867d0602b2e99172fe30df0ed4e17a5ca7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:34:27 +0000 Subject: [PATCH 0105/2154] Add description to main help text --- fig/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index f8687217..6f3f57ea 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -59,7 +59,7 @@ def parse_doc_section(name, source): class TopLevelCommand(Command): - """. + """Punctual, lightweight development environments using Docker. Usage: fig [options] [COMMAND] [ARGS...] From 8998bd1adc3def9e6e55b654b16415a46e1ca28b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:35:00 +0000 Subject: [PATCH 0106/2154] Make setup.py actually work for release --- MANIFEST.in | 3 +++ setup.py | 8 ++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..a929a01c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include LICENSE +include *.md +include requirements.txt diff --git a/setup.py b/setup.py index fdffe020..aaf506ba 100644 --- a/setup.py +++ b/setup.py @@ -24,14 +24,18 @@ def find_version(*file_paths): with open('requirements.txt') as f: install_requires = f.read().splitlines() +with open('README.md') as f: + long_description = f.read() + setup( name='fig', version=find_version("fig", "__init__.py"), - description='', + description='Punctual, lightweight development environments using Docker', + long_description=long_description, url='https://github.com/orchardup/fig', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', - packages=['fig'], + packages=['fig', 'fig.cli'], package_data={}, include_package_data=True, install_requires=install_requires, From 89cd7d8db0117875a2aeae401ce724608b0cef47 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:36:06 +0000 Subject: [PATCH 0107/2154] Remove long description --- setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.py b/setup.py index aaf506ba..2f7131f6 100644 --- a/setup.py +++ b/setup.py @@ -24,14 +24,10 @@ def find_version(*file_paths): with open('requirements.txt') as f: install_requires = f.read().splitlines() -with open('README.md') as f: - long_description = f.read() - setup( name='fig', version=find_version("fig", "__init__.py"), description='Punctual, lightweight development environments using Docker', - long_description=long_description, url='https://github.com/orchardup/fig', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', From 2857631e9065851043e1987ec05c0849fb6449bb Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 20 Dec 2013 21:37:55 +0000 Subject: [PATCH 0108/2154] Add change log --- CHANGES.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 CHANGES.md diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 00000000..d360853d --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,9 @@ +Change log +========== + +0.0.1 (2013-12-20) +------------------ + +Initial release. + + From f3eff9a389aa2adca4b574c94c5ec3b386e0dcec Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 30 Dec 2013 18:57:27 +0000 Subject: [PATCH 0109/2154] Add _site to .gitignore (it's generated by Jekyll in the gh-pages branch) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 21a256df..c6e51dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.egg-info *.pyc /dist +/_site From fdc1e0f2e196d0172a52a71c5602f3ea3977166b Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 30 Dec 2013 18:57:50 +0000 Subject: [PATCH 0110/2154] Missing article in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 12dd61c0..873063d8 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Fig Punctual, lightweight development environments using Docker. -Fig is tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this: +Fig is a tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this: ```yaml web: From ebf9bf387c99e07bf5e0e376c177ebc7690dee80 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 31 Dec 2013 11:51:52 +0000 Subject: [PATCH 0111/2154] Remove unused import --- fig/cli/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index f8899c3f..b3e0583e 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -7,7 +7,7 @@ import yaml from ..project import Project from .docopt_command import DocoptCommand from .formatter import Formatter -from .utils import cached_property, mkdir +from .utils import cached_property log = logging.getLogger(__name__) From ff65a3e1b0f061100a20462dea4f654b02707a6f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 31 Dec 2013 12:18:27 +0000 Subject: [PATCH 0112/2154] Check default socket and localhost:4243 for Docker daemon --- fig/cli/command.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index b3e0583e..2bece913 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -3,11 +3,13 @@ import logging import os import re import yaml +import socket from ..project import Project from .docopt_command import DocoptCommand from .formatter import Formatter from .utils import cached_property +from .errors import UserError log = logging.getLogger(__name__) @@ -16,8 +18,26 @@ class Command(DocoptCommand): def client(self): if os.environ.get('DOCKER_URL'): return Client(os.environ['DOCKER_URL']) - else: - return Client() + + socket_path = '/var/run/docker.sock' + tcp_host = '127.0.0.1' + tcp_port = 4243 + + if os.path.exists(socket_path): + return Client('unix://%s' % socket_path) + + try: + s = socket.socket() + s.connect((tcp_host, tcp_port)) + s.close() + return Client('http://%s:%s' % (tcp_host, tcp_port)) + except: + pass + + raise UserError(""" + Couldn't find Docker daemon - tried %s and %s:%s. + If it's running elsewhere, specify a url with DOCKER_URL. + """ % (socket_path, tcp_host, tcp_port)) @cached_property def project(self): From 9ed653869330edd1d2183bfccfeac26412a998da Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 31 Dec 2013 12:37:17 +0000 Subject: [PATCH 0113/2154] Extract docker URL logic, use it in tests as well --- fig/cli/command.py | 27 ++------------------------- fig/cli/utils.py | 27 +++++++++++++++++++++++++++ tests/project_test.py | 3 +-- tests/testcases.py | 7 ++----- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index 2bece913..36d4c0c6 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -3,41 +3,18 @@ import logging import os import re import yaml -import socket from ..project import Project from .docopt_command import DocoptCommand from .formatter import Formatter -from .utils import cached_property -from .errors import UserError +from .utils import cached_property, docker_url log = logging.getLogger(__name__) class Command(DocoptCommand): @cached_property def client(self): - if os.environ.get('DOCKER_URL'): - return Client(os.environ['DOCKER_URL']) - - socket_path = '/var/run/docker.sock' - tcp_host = '127.0.0.1' - tcp_port = 4243 - - if os.path.exists(socket_path): - return Client('unix://%s' % socket_path) - - try: - s = socket.socket() - s.connect((tcp_host, tcp_port)) - s.close() - return Client('http://%s:%s' % (tcp_host, tcp_port)) - except: - pass - - raise UserError(""" - Couldn't find Docker daemon - tried %s and %s:%s. - If it's running elsewhere, specify a url with DOCKER_URL. - """ % (socket_path, tcp_host, tcp_port)) + return Client(docker_url()) @cached_property def project(self): diff --git a/fig/cli/utils.py b/fig/cli/utils.py index 8d176425..1094b5e7 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -1,5 +1,7 @@ import datetime import os +import socket +from .errors import UserError def cached_property(f): @@ -74,3 +76,28 @@ def mkdir(path, permissions=0700): os.chmod(path, permissions) return path + + +def docker_url(): + if os.environ.get('DOCKER_URL'): + return os.environ['DOCKER_URL'] + + socket_path = '/var/run/docker.sock' + tcp_host = '127.0.0.1' + tcp_port = 4243 + + if os.path.exists(socket_path): + return 'unix://%s' % socket_path + + try: + s = socket.socket() + s.connect((tcp_host, tcp_port)) + s.close() + return 'http://%s:%s' % (tcp_host, tcp_port) + except: + pass + + raise UserError(""" + Couldn't find Docker daemon - tried %s and %s:%s. + If it's running elsewhere, specify a url with DOCKER_URL. + """ % (socket_path, tcp_host, tcp_port)) diff --git a/tests/project_test.py b/tests/project_test.py index 7dd3715f..e8949a59 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -1,5 +1,4 @@ from fig.project import Project -from fig.service import Service from .testcases import DockerClientTestCase @@ -57,7 +56,7 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(unstarted), 2) self.assertEqual(unstarted[0][0], web) self.assertEqual(unstarted[1][0], db) - self.assertEqual(len(web.containers(stopped=True)), 2) + self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 1) def test_up(self): diff --git a/tests/testcases.py b/tests/testcases.py index a930d68e..b363bc00 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,16 +1,13 @@ from docker import Client from fig.service import Service -import os +from fig.cli.utils import docker_url from unittest import TestCase class DockerClientTestCase(TestCase): @classmethod def setUpClass(cls): - if os.environ.get('DOCKER_URL'): - cls.client = Client(os.environ['DOCKER_URL']) - else: - cls.client = Client() + cls.client = Client(docker_url()) cls.client.pull('ubuntu') def setUp(self): From d4f3ed1840d94c228c8c4c461a5cf14ff1822e89 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 31 Dec 2013 13:02:08 +0000 Subject: [PATCH 0114/2154] Fix 'fig up' behaviour - For each service, creates a container if there are none (stopped OR started) - Attaches to all containers (unless -d is passed) - Starts all containers - On ^C, kills all containers (unless -d is passed) --- fig/cli/main.py | 18 +++++++----------- fig/project.py | 14 +++----------- tests/project_test.py | 39 ++------------------------------------- 3 files changed, 12 insertions(+), 59 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 6f3f57ea..f6714f1e 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -156,24 +156,20 @@ class TopLevelCommand(Command): """ detached = options['-d'] - unstarted = self.project.create_containers(service_names=options['SERVICE']) + self.project.create_containers(service_names=options['SERVICE']) + containers = self.project.containers(service_names=options['SERVICE'], stopped=True) if not detached: - to_attach = self.project.containers(service_names=options['SERVICE']) + [c for (s, c) in unstarted] - print "Attaching to", list_containers(to_attach) - log_printer = LogPrinter(to_attach, attach_params={'logs': True}) + print "Attaching to", list_containers(containers) + log_printer = LogPrinter(containers) - for (s, c) in unstarted: - s.start_container(c) + self.project.start(service_names=options['SERVICE']) - if detached: - for (s, c) in unstarted: - print c.name - else: + if not detached: try: log_printer.run() finally: - self.project.kill_and_remove(unstarted) + self.project.kill(service_names=options['SERVICE']) def start(self, options): """ diff --git a/fig/project.py b/fig/project.py index 52a050d2..eb9e8d43 100644 --- a/fig/project.py +++ b/fig/project.py @@ -75,19 +75,11 @@ class Project(object): def create_containers(self, service_names=None): """ - Returns a list of (service, container) tuples, - one for each service with no running containers. + For each service, creates a container if there are none. """ - containers = [] for service in self.get_services(service_names): - if len(service.containers()) == 0: - containers.append((service, service.create_container())) - return containers - - def kill_and_remove(self, tuples): - for (service, container) in tuples: - container.kill() - container.remove() + if len(service.containers(stopped=True)) == 0: + service.create_container() def start(self, service_names=None, **options): for service in self.get_services(service_names): diff --git a/tests/project_test.py b/tests/project_test.py index e8949a59..5d0d1e72 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -46,49 +46,14 @@ class ProjectTest(DockerClientTestCase): db = self.create_service('db') project = Project('test', [web, db], self.client) - unstarted = project.create_containers(service_names=['web']) - self.assertEqual(len(unstarted), 1) - self.assertEqual(unstarted[0][0], web) + project.create_containers(service_names=['web']) self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 0) - unstarted = project.create_containers() - self.assertEqual(len(unstarted), 2) - self.assertEqual(unstarted[0][0], web) - self.assertEqual(unstarted[1][0], db) + project.create_containers() self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 1) - def test_up(self): - web = self.create_service('web') - db = self.create_service('db') - other = self.create_service('other') - project = Project('test', [web, db, other], self.client) - - web.create_container() - - self.assertEqual(len(web.containers()), 0) - self.assertEqual(len(db.containers()), 0) - self.assertEqual(len(web.containers(stopped=True)), 1) - self.assertEqual(len(db.containers(stopped=True)), 0) - - unstarted = project.create_containers(service_names=['web', 'db']) - self.assertEqual(len(unstarted), 2) - self.assertEqual(unstarted[0][0], web) - self.assertEqual(unstarted[1][0], db) - - self.assertEqual(len(web.containers()), 0) - self.assertEqual(len(db.containers()), 0) - self.assertEqual(len(web.containers(stopped=True)), 2) - self.assertEqual(len(db.containers(stopped=True)), 1) - - project.kill_and_remove(unstarted) - - self.assertEqual(len(web.containers()), 0) - self.assertEqual(len(db.containers()), 0) - self.assertEqual(len(web.containers(stopped=True)), 1) - self.assertEqual(len(db.containers(stopped=True)), 0) - def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') From c0676e3fa3b79cbf61d9f544f2ac7f97252b6793 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 31 Dec 2013 13:42:58 +0000 Subject: [PATCH 0115/2154] Add confirmation prompt to 'fig rm' --- fig/cli/main.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index f6714f1e..7028cf29 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -9,6 +9,7 @@ from ..project import NoSuchService from .command import Command from .formatter import Formatter from .log_printer import LogPrinter +from .utils import yesno from docker.client import APIError from .errors import UserError @@ -201,7 +202,15 @@ class TopLevelCommand(Command): Usage: rm [SERVICE...] """ - self.project.remove_stopped(service_names=options['SERVICE']) + all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + stopped_containers = [c for c in all_containers if not c.is_running] + + if len(stopped_containers) > 0: + print "Going to remove", list_containers(stopped_containers) + if yesno("Are you sure? [yN] ", default=False): + self.project.remove_stopped(service_names=options['SERVICE']) + else: + print "No stopped containers" def logs(self, options): """ From e5065bed1603110431c903e785faac68bdd075de Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 31 Dec 2013 16:31:40 +0000 Subject: [PATCH 0116/2154] Expand the intro a bit --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 873063d8..d4d539a0 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Fig Punctual, lightweight development environments using Docker. -Fig is a tool for defining and running isolated application environments. It uses simple, version-controllable YAML configuration files that look something like this: +Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this: ```yaml web: @@ -16,6 +16,15 @@ db: image: orchardup/postgresql ``` +Then type `fig up`, and Fig will start and run your entire app. + +There are commands to: + + - start, stop and rebuild services + - view the status of running services + - tail running services' log output + - run a one-off command on a service + Installing ---------- @@ -26,7 +35,7 @@ $ sudo pip install fig Defining your app ----------------- -Put a `fig.yml` in your app's directory. Each top-level key defines a "service", such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use. +Put a `fig.yml` in your app's directory. Each top-level key defines a service, such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use. The simplest way to get started is to just give it an image name: From 770e78fdce3cac7635f0eeb0b7c9b32ad8299bed Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 31 Dec 2013 17:05:20 +0000 Subject: [PATCH 0117/2154] Make usage alphabetical --- fig/cli/main.py | 106 ++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 7028cf29..f5b4f166 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -71,14 +71,14 @@ class TopLevelCommand(Command): --version Print version and exit Commands: - up Create and start containers + kill Kill containers logs View output from containers ps List containers + rm Remove stopped containers run Run a one-off command start Start services stop Stop services - kill Kill containers - rm Remove stopped containers + up Create and start containers """ def docopt_options(self): @@ -86,6 +86,24 @@ class TopLevelCommand(Command): options['version'] = "fig %s" % __version__ return options + def kill(self, options): + """ + Kill containers. + + Usage: kill [SERVICE...] + """ + self.project.kill(service_names=options['SERVICE']) + + def logs(self, options): + """ + View output from containers. + + Usage: logs [SERVICE...] + """ + containers = self.project.containers(service_names=options['SERVICE'], stopped=False) + print "Attaching to", list_containers(containers) + LogPrinter(containers, attach_params={'logs': True}).run() + def ps(self, options): """ List containers. @@ -117,6 +135,22 @@ class TopLevelCommand(Command): ]) print Formatter().table(headers, rows) + def rm(self, options): + """ + Remove stopped containers + + Usage: rm [SERVICE...] + """ + all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True) + stopped_containers = [c for c in all_containers if not c.is_running] + + if len(stopped_containers) > 0: + print "Going to remove", list_containers(stopped_containers) + if yesno("Are you sure? [yN] ", default=False): + self.project.remove_stopped(service_names=options['SERVICE']) + else: + print "No stopped containers" + def run(self, options): """ Run a one-off command. @@ -146,6 +180,22 @@ class TopLevelCommand(Command): service.start_container(container, ports=None) c.run() + def start(self, options): + """ + Start existing containers. + + Usage: start [SERVICE...] + """ + self.project.start(service_names=options['SERVICE']) + + def stop(self, options): + """ + Stop running containers. + + Usage: stop [SERVICE...] + """ + self.project.stop(service_names=options['SERVICE']) + def up(self, options): """ Create and start containers. @@ -172,56 +222,6 @@ class TopLevelCommand(Command): finally: self.project.kill(service_names=options['SERVICE']) - def start(self, options): - """ - Start existing containers. - - Usage: start [SERVICE...] - """ - self.project.start(service_names=options['SERVICE']) - - def stop(self, options): - """ - Stop running containers. - - Usage: stop [SERVICE...] - """ - self.project.stop(service_names=options['SERVICE']) - - def kill(self, options): - """ - Kill containers. - - Usage: kill [SERVICE...] - """ - self.project.kill(service_names=options['SERVICE']) - - def rm(self, options): - """ - Remove stopped containers - - Usage: rm [SERVICE...] - """ - all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True) - stopped_containers = [c for c in all_containers if not c.is_running] - - if len(stopped_containers) > 0: - print "Going to remove", list_containers(stopped_containers) - if yesno("Are you sure? [yN] ", default=False): - self.project.remove_stopped(service_names=options['SERVICE']) - else: - print "No stopped containers" - - def logs(self, options): - """ - View output from containers. - - Usage: logs [SERVICE...] - """ - containers = self.project.containers(service_names=options['SERVICE'], stopped=False) - print "Attaching to", list_containers(containers) - LogPrinter(containers, attach_params={'logs': True}).run() - def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False): stdio = self.client.attach_socket( container_id, From 17c6ae067ae949ddcdd8af55ff4e39374c35b677 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 14:55:48 +0000 Subject: [PATCH 0118/2154] Rewrite readme intro --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d4d539a0..cf661cef 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,15 @@ db: image: orchardup/postgresql ``` -Then type `fig up`, and Fig will start and run your entire app. +Then type `fig up`, and Fig will start and run your entire app: + + $ fig up + Pulling image orchardup/postgresql... + Building web... + Starting example_db_1... + Starting example_web_1... + example_db_1 | 2014-01-02 14:47:18 UTC LOG: database system is ready to accept connections + example_web_1 | * Running on http://0.0.0.0:5000/ There are commands to: @@ -25,6 +33,9 @@ There are commands to: - tail running services' log output - run a one-off command on a service +Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news. + + Installing ---------- From 853d8ad28078159fcd74fcac3b6868408a9fca25 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 15:27:51 +0000 Subject: [PATCH 0119/2154] Namespace tests inside a project So it doesn't delete all your containers for every test. Cool. --- tests/project_test.py | 6 +++--- tests/service_test.py | 22 ++++++++++++---------- tests/testcases.py | 6 ++++-- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/tests/project_test.py b/tests/project_test.py index 5d0d1e72..82b9be9d 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -4,7 +4,7 @@ from .testcases import DockerClientTestCase class ProjectTest(DockerClientTestCase): def test_from_dict(self): - project = Project.from_dicts('test', [ + project = Project.from_dicts('figtest', [ { 'name': 'web', 'image': 'ubuntu' @@ -21,7 +21,7 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(project.get_service('db').options['image'], 'ubuntu') def test_from_dict_sorts_in_dependency_order(self): - project = Project.from_dicts('test', [ + project = Project.from_dicts('figtest', [ { 'name': 'web', 'image': 'ubuntu', @@ -57,7 +57,7 @@ class ProjectTest(DockerClientTestCase): def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') - project = Project('test', [web, db], self.client) + project = Project('figtest', [web, db], self.client) project.start() diff --git a/tests/service_test.py b/tests/service_test.py index 02b96ab8..f946ab38 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -29,7 +29,7 @@ class ServiceTest(DockerClientTestCase): foo.start_container() self.assertEqual(len(foo.containers()), 1) - self.assertEqual(foo.containers()[0].name, 'default_foo_1') + self.assertEqual(foo.containers()[0].name, 'figtest_foo_1') self.assertEqual(len(bar.containers()), 0) bar.start_container() @@ -39,8 +39,8 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(bar.containers()), 2) names = [c.name for c in bar.containers()] - self.assertIn('default_bar_1', names) - self.assertIn('default_bar_2', names) + self.assertIn('figtest_bar_1', names) + self.assertIn('figtest_bar_2', names) def test_containers_one_off(self): db = self.create_service('db') @@ -49,9 +49,9 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(db.containers(one_off=True, stopped=True), [container]) def test_project_is_added_to_container_name(self): - service = self.create_service('web', project='myproject') + service = self.create_service('web') service.start_container() - self.assertEqual(service.containers()[0].name, 'myproject_web_1') + self.assertEqual(service.containers()[0].name, 'figtest_web_1') def test_start_stop(self): service = self.create_service('scalingtest') @@ -92,13 +92,13 @@ class ServiceTest(DockerClientTestCase): def test_create_container_with_one_off(self): db = self.create_service('db') container = db.create_container(one_off=True) - self.assertEqual(container.name, 'default_db_run_1') + self.assertEqual(container.name, 'figtest_db_run_1') def test_create_container_with_one_off_when_existing_container_is_running(self): db = self.create_service('db') db.start() container = db.create_container(one_off=True) - self.assertEqual(container.name, 'default_db_run_1') + self.assertEqual(container.name, 'figtest_db_run_1') def test_start_container_passes_through_options(self): db = self.create_service('db') @@ -115,7 +115,7 @@ class ServiceTest(DockerClientTestCase): web = self.create_service('web', links=[db]) db.start_container() web.start_container() - self.assertIn('default_db_1', web.containers()[0].links()) + self.assertIn('figtest_db_1', web.containers()[0].links()) db.stop(timeout=1) web.stop(timeout=1) @@ -124,18 +124,20 @@ class ServiceTest(DockerClientTestCase): name='test', client=self.client, build='tests/fixtures/simple-dockerfile', + project='figtest', ) container = service.start_container() container.wait() self.assertIn('success', container.logs()) - self.assertEqual(len(self.client.images(name='default_test')), 1) + self.assertEqual(len(self.client.images(name='figtest_test')), 1) def test_start_container_uses_tagged_image_if_it_exists(self): - self.client.build('tests/fixtures/simple-dockerfile', tag='default_test') + self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test') service = Service( name='test', client=self.client, build='this/does/not/exist/and/will/throw/error', + project='figtest', ) container = service.start_container() container.wait() diff --git a/tests/testcases.py b/tests/testcases.py index b363bc00..d43ab394 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -12,11 +12,13 @@ class DockerClientTestCase(TestCase): def setUp(self): for c in self.client.containers(all=True): - self.client.kill(c['Id']) - self.client.remove_container(c['Id']) + if c['Names'] and 'figtest' in c['Names'][0]: + self.client.kill(c['Id']) + self.client.remove_container(c['Id']) def create_service(self, name, **kwargs): return Service( + project='figtest', name=name, client=self.client, image="ubuntu", From a39db86651a62f9abda12f4272d10b2e4c5cff3b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 15:28:33 +0000 Subject: [PATCH 0120/2154] Add "fig build" command --- fig/cli/main.py | 9 +++++++++ fig/project.py | 7 +++++++ fig/service.py | 5 ++++- 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index f5b4f166..2f014a36 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -71,6 +71,7 @@ class TopLevelCommand(Command): --version Print version and exit Commands: + build Build or rebuild services kill Kill containers logs View output from containers ps List containers @@ -86,6 +87,14 @@ class TopLevelCommand(Command): options['version'] = "fig %s" % __version__ return options + def build(self, options): + """ + Build or rebuild services. + + Usage: build [SERVICE...] + """ + self.project.build(service_names=options['SERVICE']) + def kill(self, options): """ Kill containers. diff --git a/fig/project.py b/fig/project.py index eb9e8d43..9180dace 100644 --- a/fig/project.py +++ b/fig/project.py @@ -93,6 +93,13 @@ class Project(object): for service in self.get_services(service_names): service.kill(**options) + def build(self, service_names=None, **options): + for service in self.get_services(service_names): + if service.can_be_built(): + service.build(**options) + else: + log.info('%s uses an image, skipping') + def remove_stopped(self, service_names=None, **options): for service in self.get_services(service_names): service.remove_stopped(**options) diff --git a/fig/service.py b/fig/service.py index 537842db..3fb0e428 100644 --- a/fig/service.py +++ b/fig/service.py @@ -137,7 +137,7 @@ class Service(object): if 'volumes' in container_options: container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes']) - if 'build' in self.options: + if self.can_be_built(): if len(self.client.images(name=self._build_tag_name())) == 0: self.build() container_options['image'] = self._build_tag_name() @@ -167,6 +167,9 @@ class Service(object): return image_id + def can_be_built(self): + return 'build' in self.options + def _build_tag_name(self): """ The tag to give to images built for this service. From 38478ea50477db649b27d9bed9aba7b5b06f9237 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 2 Jan 2014 17:00:49 +0000 Subject: [PATCH 0121/2154] Full fig.yml and environment variable reference --- README.md | 97 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index cf661cef..4b053b53 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ web: ### Communicating between containers -Your web app will probably need to talk to your database. You can use [Docker links](http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container) to enable containers to communicate, pass in the right IP address and port via environment variables: +Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicate, pass in the right IP address and port via environment variables: ```yaml db: @@ -135,39 +135,17 @@ web: - db ``` -This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container, whose value will look like `tcp://172.17.0.4:45678`. Your web app's code can use that to connect to the database. To see all of the environment variables available, run `env` inside a container: +This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container (where MYAPP is the name of the current directory). Your web app's code can use that to connect to the database. ```bash $ fig up -d db $ fig run web env +... +MYAPP_DB_1_PORT=tcp://172.17.0.5:5432 +... ``` - -### Container configuration options - -You can pass extra configuration options to a container, much like with `docker run`: - -```yaml -web: - build: . - - -- override the default command - command: bundle exec thin -p 3000 - - -- expose ports, optionally specifying both host and container ports (a random host port will be chosen otherwise) - ports: - - 3000 - - 8000:8000 - - -- map volumes - volumes: - - cache/:/tmp/cache - - -- add environment variables - environment: - RACK_ENV: development -``` - +The full set of environment variables is documented in the Reference section. Running a one-off command ------------------------- @@ -180,4 +158,67 @@ $ fig run web rake db:migrate $ fig run web bash ``` +Reference +--------- +### fig.yml + +Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts. + +As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`. + +```yaml +-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally. +image: ubuntu +image: orchardup/postgresql +image: a4bc65fd + +-- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter. +build: /path/to/build/dir + +-- Override the default command. +command: bundle exec thin -p 3000 + +-- Link to containers in another service (see "Communicating between containers"). +links: + - db + - redis + +-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen). +ports: + - 3000 + - 8000:8000 + +-- Map volumes from the host machine (HOST:CONTAINER). +volumes: + - cache/:/tmp/cache + +-- Add environment variables. +environment: + RACK_ENV: development +``` + +### Environment variables + +Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. + +name\_PORT
+Full URL, e.g. `MYAPP_DB_1_PORT=tcp://172.17.0.5:5432` + +name\_PORT\_num\_protocol
+Full URL, e.g. `MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432` + +name\_PORT\_num\_protocol\_ADDR
+Container's IP address, e.g. `MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5` + +name\_PORT\_num\_protocol\_PORT
+Exposed port number, e.g. `MYAPP_DB_1_PORT_5432_TCP_PORT=5432` + +name\_PORT\_num\_protocol\_PROTO
+Protocol (tcp or udp), e.g. `MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp` + +name\_NAME
+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 From 6d0702e6075e8bdc9d4094fbd063214870f407cc Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 18:30:47 +0000 Subject: [PATCH 0122/2154] Send log output to stderr --- fig/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 2f014a36..a8c89e57 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -20,7 +20,7 @@ log = logging.getLogger(__name__) def main(): - console_handler = logging.StreamHandler() + console_handler = logging.StreamHandler(stream=sys.stderr) console_handler.setFormatter(logging.Formatter()) console_handler.setLevel(logging.INFO) root_logger = logging.getLogger() From 9b289b6f3bf5420022147ef0cdff8a1a1f13be63 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 19:18:08 +0000 Subject: [PATCH 0123/2154] Stop "fig up" containers gracefully With double ctrl-c force. --- fig/cli/main.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index a8c89e57..d4ac576b 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -1,6 +1,8 @@ import logging import sys import re +import signal +import sys from inspect import getdoc @@ -229,7 +231,13 @@ class TopLevelCommand(Command): try: log_printer.run() finally: - self.project.kill(service_names=options['SERVICE']) + def handler(signal, frame): + self.project.kill(service_names=options['SERVICE']) + sys.exit(0) + signal.signal(signal.SIGINT, handler) + + print "Gracefully stopping... (press Ctrl+C again to force)" + self.project.stop(service_names=options['SERVICE']) def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False): stdio = self.client.attach_socket( From 5b1fd647084fdcef8f52dd8919ca47b5ac194569 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 20:04:14 +0000 Subject: [PATCH 0124/2154] Add getting started guide to readme --- README.md | 189 ++++++++++++++++++++++++++---------------------------- 1 file changed, 90 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 4b053b53..ec925c61 100644 --- a/README.md +++ b/README.md @@ -36,128 +36,119 @@ There are commands to: Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news. -Installing ----------- +Getting started +--------------- -```bash -$ sudo pip install fig -``` +Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it. -Defining your app ------------------ +First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): -Put a `fig.yml` in your app's directory. Each top-level key defines a service, such as a web app, database or cache. For each service, Fig will start a Docker container, so at minimum it needs to know what image to use. + $ curl https://raw.github.com/noplay/docker-osx/master/docker > /usr/local/bin/docker + $ chmod +x /usr/local/bin/docker + $ docker version -The simplest way to get started is to just give it an image name: +Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation. -```yaml -db: - image: orchardup/postgresql -``` +Next, install Fig: -You've now given Fig the minimal amount of configuration it needs to run: + $ sudo pip install fig -```bash -$ fig up -Pulling image orchardup/postgresql... -Starting myapp_db_1... -myapp_db_1 is running at 127.0.0.1:45678 -<...output from postgresql server...> -``` +You'll want to make a directory for the project: -For each service you've defined, Fig will start a Docker container with the specified image, building or pulling it if necessary. You now have a PostgreSQL server running at `127.0.0.1:45678`. + $ mkdir figtest + $ cd figtest -By default, `fig up` will run until each container has shut down, and relay their output to the terminal. To run in the background instead, pass the `-d` flag: - -```bash -$ fig up -d -Starting myapp_db_1... done -myapp_db_1 is running at 127.0.0.1:45678 - -$ fig ps -Name State Ports ------------------------------------- -myapp_db_1 Up 5432->45678/tcp -``` - -### Building services - -Fig can automatically build images for you if your service specifies a directory with a `Dockerfile` in it (or a Git URL, as per the `docker build` command). - -This example will build an image with `app.py` inside it: - -#### app.py +Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis: ```python -print "Hello world!" -``` +from flask import Flask +from redis import Redis +import os +app = Flask(__name__) +redis = Redis( + host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'), + port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT')) +) -#### fig.yml +@app.route('/') +def hello(): + redis.incr('hits') + return 'Hello World! I have been seen %s times.' % redis.get('hits') -```yaml -web: - build: . -``` +if __name__ == "__main__": + app.run(host="0.0.0.0", debug=True)``` -#### Dockerfile +We define our Python dependencies in a file called `requirements.txt`: - FROM ubuntu:12.04 - RUN apt-get install python - ADD . /opt - WORKDIR /opt + flask + redis + +And we define how to build this into a Docker image using a file called `Dockerfile`: + + FROM stackbrew/ubuntu:13.10 + RUN apt-get -qq update + RUN apt-get install -y python python-pip + ADD . /code + WORKDIR /code + RUN pip install -r requirements.txt + EXPOSE 5000 CMD python app.py +That tells Docker to create an image with Python and Flask installed on it, run the command `python app.py`, and open port 5000 (the port that Flask listens on). + +We then define a set of services using `fig.yml`: + + web: + build: . + ports: + - 5000:5000 + volumes: + - .:/code + links: + - redis + redis: + image: orchardup/redis + +This defines two services: + + - `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the `redis` service, and mount the current directory inside the container so we can work on code without having to rebuild the image. + - `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/). + +Now if we run `fig up`, it'll pull a Redis image, build an image for our own app, and start everything up: + + $ fig up + Pulling image orchardup/redis... + Building web... + Starting figtest_redis_1... + Starting figtest_web_1... + figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 + figtest_web_1 | * Running on http://0.0.0.0:5000/ + +Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)). That's it! + +You can also pass the `-d` flag to `fig up` to run your services in the background, and use `fig ps` to see what is currently running: + + $ fig up -d + Starting figtest_redis_1... + Starting figtest_web_1... + $ fig ps + Name Command State Ports + ------------------------------------------------------------------- + figtest_redis_1 /usr/local/bin/run Up + figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp + +`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service: + + $ fig run web env -### Getting your code in +Run `fig --help` to see what other commands are available. You'll probably want to stop them when you've finished: -If you want to work on an application being run by Fig, you probably don't want to have to rebuild your image every time you make a change. To solve this, you can share the directory with the container using a volume so the changes are reflected immediately: + $ fig stop -```yaml -web: - build: . - volumes: - - .:/opt -``` +That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. -### Communicating between containers - -Your web app will probably need to talk to your database. You can use [Docker links] to enable containers to communicate, pass in the right IP address and port via environment variables: - -```yaml -db: - image: orchardup/postgresql - -web: - build: . - links: - - db -``` - -This will pass an environment variable called `MYAPP_DB_1_PORT` into the web container (where MYAPP is the name of the current directory). Your web app's code can use that to connect to the database. - -```bash -$ fig up -d db -$ fig run web env -... -MYAPP_DB_1_PORT=tcp://172.17.0.5:5432 -... -``` - -The full set of environment variables is documented in the Reference section. - -Running a one-off command -------------------------- - -If you want to run a management command, use `fig run` to start a one-off container: - -```bash -$ fig run db createdb myapp_development -$ fig run web rake db:migrate -$ fig run web bash -``` - Reference --------- From 0a92cbfa4d63b18098bac10f01c0e15de9f76599 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 20:11:59 +0000 Subject: [PATCH 0125/2154] Fix readme formatting --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec925c61..3e5bbb23 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,8 @@ def hello(): return 'Hello World! I have been seen %s times.' % redis.get('hits') if __name__ == "__main__": - app.run(host="0.0.0.0", debug=True)``` + app.run(host="0.0.0.0", debug=True) +``` We define our Python dependencies in a file called `requirements.txt`: From 36002f95edcb4f7664d03c42e7f96bcf56232903 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 20:51:35 +0000 Subject: [PATCH 0126/2154] Try connecting to localdocker:4243 See https://github.com/noplay/docker-osx/pull/22 --- fig/cli/utils.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/fig/cli/utils.py b/fig/cli/utils.py index 1094b5e7..a1ff0c00 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -83,21 +83,29 @@ def docker_url(): return os.environ['DOCKER_URL'] socket_path = '/var/run/docker.sock' + tcp_hosts = [ + ('localdocker', 4243), + ('127.0.0.1', 4243), + ] tcp_host = '127.0.0.1' tcp_port = 4243 if os.path.exists(socket_path): return 'unix://%s' % socket_path - try: - s = socket.socket() - s.connect((tcp_host, tcp_port)) - s.close() - return 'http://%s:%s' % (tcp_host, tcp_port) - except: - pass + for host, port in tcp_hosts: + try: + s = socket.create_connection((host, port), timeout=1) + s.close() + return 'http://%s:%s' % (host, port) + except: + pass raise UserError(""" - Couldn't find Docker daemon - tried %s and %s:%s. - If it's running elsewhere, specify a url with DOCKER_URL. - """ % (socket_path, tcp_host, tcp_port)) +Couldn't find Docker daemon - tried: + +unix://%s +%s + +If it's running elsewhere, specify a url with DOCKER_URL. + """ % (socket_path, '\n'.join('tcp://%s:%s' % h for h in tcp_hosts))) From 45b7bd43613c31f02e480425f783ae5752d842c9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 21:23:47 +0000 Subject: [PATCH 0127/2154] Readme tweaks --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3e5bbb23..db44d3bb 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,8 @@ Next, install Fig: $ sudo pip install fig +(If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.) + You'll want to make a directory for the project: $ mkdir figtest @@ -112,10 +114,10 @@ We then define a set of services using `fig.yml`: This defines two services: - - `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the `redis` service, and mount the current directory inside the container so we can work on code without having to rebuild the image. + - `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image. - `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/). -Now if we run `fig up`, it'll pull a Redis image, build an image for our own app, and start everything up: +Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up: $ fig up Pulling image orchardup/redis... @@ -125,9 +127,9 @@ Now if we run `fig up`, it'll pull a Redis image, build an image for our own app figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 figtest_web_1 | * Running on http://0.0.0.0:5000/ -Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)). That's it! +Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running! -You can also pass the `-d` flag to `fig up` to run your services in the background, and use `fig ps` to see what is currently running: +If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running: $ fig up -d Starting figtest_redis_1... @@ -143,11 +145,13 @@ You can also pass the `-d` flag to `fig up` to run your services in the backgrou $ fig run web env -Run `fig --help` to see what other commands are available. You'll probably want to stop them when you've finished: +See `fig --help` other commands that are available. + +You'll probably want to stop your services when you've finished with them: $ fig stop -That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. +That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com). Reference From e0a21e3df48bf7f4b564d7b4802c2b647fb7f8e8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 21:30:28 +0000 Subject: [PATCH 0128/2154] Bump version to 0.0.2 --- CHANGES.md | 9 +++++++++ fig/__init__.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d360853d..8d5001eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,15 @@ Change log ========== +0.0.2 (2014-01-02) +------------------ + + - Improve documentation + - Try to connect to Docker on `tcp://localdocker:4243` and a UNIX socket in addition to `localhost`. + - Improve `fig up` behaviour + - Add confirmation prompt to `fig rm` + - Add `fig build` command + 0.0.1 (2013-12-20) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 5282dce9..7d6d5a30 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,3 +1,3 @@ from .service import Service -__version__ = '0.0.1' +__version__ = '0.0.2' From 0fb915e57e63866ce6bb8c514aa3ffa840d0ee60 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 22:20:54 +0000 Subject: [PATCH 0129/2154] Add commands section to readme --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index db44d3bb..51cc9761 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,52 @@ environment: RACK_ENV: development ``` +### Commands + +Most commands are run against one or more services. If the service is omitted, it will apply to all services. + +#### `build [SERVICE...]` + +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. + +#### `kill [SERVICE...]` + +Force stop service containers. + +#### `logs [SERVICE...]` + +View output from services. + +#### `ps` + +List running containers. + +#### `rm [SERVICE...]` + +Remove stopped service containers. + + +#### `run SERVICE COMMAND [ARGS...]` + +Run a one-off command for a service. + +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. + +#### `start [SERVICE...]` + +Start existing containers for a service. + +#### `stop [SERVICE...]` + +Stop running containers without removing them. They can be started again with `fig start`. + +#### `up [SERVICE...]` + +Build, create, start and attach to containers for a service. + + ### Environment variables Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. From 88c74d67f6cb84a1b1c66d52f90663bdaff25753 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 22:31:14 +0000 Subject: [PATCH 0130/2154] Update tag line --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51cc9761..11389253 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Fig ==== -Punctual, lightweight development environments using Docker. +Punctual, isolated development environments using Docker. Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this: From febcbcddb98c1cdf8377c7f845e430afb40238d3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 22:36:04 +0000 Subject: [PATCH 0131/2154] Revert "Update tag line" This reverts commit 88c74d67f6cb84a1b1c66d52f90663bdaff25753. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 11389253..51cc9761 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Fig ==== -Punctual, isolated development environments using Docker. +Punctual, lightweight development environments using Docker. Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this: From dd1f8934ad3c56a68877fa93c46e25246920965f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 22:48:51 +0000 Subject: [PATCH 0132/2154] Fix markdown formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 51cc9761..58ae7457 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ Most commands are run against one or more services. If the service is omitted, i 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. +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. #### `kill [SERVICE...]` From 3d411ed0bb172b01706a953291fb857c0e22fa12 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 23:05:58 +0000 Subject: [PATCH 0133/2154] Remove monospace font from command headings --- README.md | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 58ae7457..817e25bc 100644 --- a/README.md +++ b/README.md @@ -198,44 +198,48 @@ environment: Most commands are run against one or more services. If the service is omitted, it will apply to all services. -#### `build [SERVICE...]` +Run `fig [COMMAND] --help` for full usage. + +#### build 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. -#### `kill [SERVICE...]` +#### kill Force stop service containers. -#### `logs [SERVICE...]` +#### logs View output from services. -#### `ps` +#### ps List running containers. -#### `rm [SERVICE...]` +#### rm Remove stopped service containers. -#### `run SERVICE COMMAND [ARGS...]` +#### run -Run a one-off command for a service. +Run a one-off command for a service. E.g.: + + $ 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. -#### `start [SERVICE...]` +#### start Start existing containers for a service. -#### `stop [SERVICE...]` +#### stop Stop running containers without removing them. They can be started again with `fig start`. -#### `up [SERVICE...]` +#### up Build, create, start and attach to containers for a service. From 5ba7040df279724a98a90448854f680bd5d36fad Mon Sep 17 00:00:00 2001 From: Tom Stuart Date: Thu, 2 Jan 2014 23:27:47 +0000 Subject: [PATCH 0134/2154] Make logger available in project.py --- fig/project.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fig/project.py b/fig/project.py index 9180dace..24d7e7e5 100644 --- a/fig/project.py +++ b/fig/project.py @@ -1,5 +1,8 @@ +import logging from .service import Service +log = logging.getLogger(__name__) + def sort_service_dicts(services): # Sort in dependency order def cmp(x, y): From aaf90639a065d189a8d8ebbdb5a10d40c5f4b8c7 Mon Sep 17 00:00:00 2001 From: Tom Stuart Date: Thu, 2 Jan 2014 23:28:18 +0000 Subject: [PATCH 0135/2154] Include service name in log message --- fig/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/project.py b/fig/project.py index 24d7e7e5..97ff319d 100644 --- a/fig/project.py +++ b/fig/project.py @@ -101,7 +101,7 @@ class Project(object): if service.can_be_built(): service.build(**options) else: - log.info('%s uses an image, skipping') + log.info('%s uses an image, skipping' % service.name) def remove_stopped(self, service_names=None, **options): for service in self.get_services(service_names): From 3fa80cd9745c3d6ac638c67ba62725f099d0891b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 2 Jan 2014 23:47:04 +0000 Subject: [PATCH 0136/2154] Add note about fig rm/build dance This needs more thought. Ref #2 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 817e25bc..4e046038 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,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. +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. #### kill @@ -241,8 +241,9 @@ 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, 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`. ### Environment variables From 490742b89246d503acb81cd833b954efb3c7656b Mon Sep 17 00:00:00 2001 From: Tom Stuart Date: Fri, 3 Jan 2014 11:58:49 +0000 Subject: [PATCH 0137/2154] Emit a friendly error when fig.yml is missing I keep doing this by accident, so I'd rather not see the stack trace. --- fig/cli/command.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index 36d4c0c6..813ea4db 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -1,4 +1,5 @@ from docker import Client +import errno import logging import os import re @@ -18,7 +19,16 @@ class Command(DocoptCommand): @cached_property def project(self): - config = yaml.load(open('fig.yml')) + try: + config = yaml.load(open('fig.yml')) + except IOError, e: + if e.errno == errno.ENOENT: + log.error("Can't find %s. Are you in the right directory?", e.filename) + else: + log.error(e) + + exit(1) + return Project.from_config(self.project_name, config, self.client) @cached_property From 17b9cc430c8f0ffe0d263e2afb08e7daf44f350f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 10:12:37 +0000 Subject: [PATCH 0138/2154] Add Travis CI --- .travis.yml | 16 +++++++++++++--- script/travis | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100755 script/travis diff --git a/.travis.yml b/.travis.yml index da53766e..7ef225a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,20 @@ language: python python: - "2.6" - "2.7" - - "3.2" - - "3.3" + install: + - sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" + - sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" + - sudo apt-get update + - echo exit 101 | sudo tee /usr/sbin/policy-rc.d + - sudo chmod +x /usr/sbin/policy-rc.d + - sudo apt-get install -qy slirp lxc lxc-docker=0.7.3 + - git clone git://github.com/jpetazzo/sekexe - python setup.py install - pip install nose==1.3.0 -script: nosetests + +script: + - pwd + - env + - sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION" diff --git a/script/travis b/script/travis new file mode 100755 index 00000000..650c9d39 --- /dev/null +++ b/script/travis @@ -0,0 +1,18 @@ +#!/bin/bash + +# Exit on first error +set -e + +TRAVIS_PYTHON_VERSION=$1 +source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate +env + +# Kill background processes on exit +trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT + +# Start docker daemon +docker -d -H 0.0.0.0:4243 -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & +sleep 2 + +# $init is set by sekexe +cd $(dirname $init)/.. && nosetests From ff9fa5661dd6f7f9e0572e7711db96586081a468 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 11:22:46 +0000 Subject: [PATCH 0139/2154] Fix Python 2.6 --- .travis.yml | 2 +- requirements-dev.txt | 3 ++- tests/testcases.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7ef225a8..2a7cad5b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - sudo apt-get install -qy slirp lxc lxc-docker=0.7.3 - git clone git://github.com/jpetazzo/sekexe - python setup.py install - - pip install nose==1.3.0 + - pip install -r requirements-dev.txt script: - pwd diff --git a/requirements-dev.txt b/requirements-dev.txt index f3c7e8e6..4ef6576c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,2 @@ -nose +nose==1.3.0 +unittest2==0.5.1 diff --git a/tests/testcases.py b/tests/testcases.py index d43ab394..c1da5389 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,7 +1,7 @@ from docker import Client from fig.service import Service from fig.cli.utils import docker_url -from unittest import TestCase +from unittest2 import TestCase class DockerClientTestCase(TestCase): From 8de07ccf65ae5fc3b70919464ae3d01b6d874100 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 11:34:44 +0000 Subject: [PATCH 0140/2154] Add Travis badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4e046038..71d87483 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Fig +Fig [![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) ==== Punctual, lightweight development environments using Docker. From f96a1a0b35b37c4da711f060ad6337a70d4aba48 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 11:22:46 +0000 Subject: [PATCH 0141/2154] Fix Python 2.6 --- tests/__init__.py | 7 +++++++ tests/testcases.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..b4d38ccc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,7 @@ +import sys + +if sys.version_info >= (2,7): + import unittest +else: + import unittest2 as unittest + diff --git a/tests/testcases.py b/tests/testcases.py index c1da5389..4ac61d9d 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,10 +1,10 @@ from docker import Client from fig.service import Service from fig.cli.utils import docker_url -from unittest2 import TestCase +from . import unittest -class DockerClientTestCase(TestCase): +class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.client = Client(docker_url()) From 93b9b6fd9fe23388682df75cc3b0212d4f6bb69f Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 18:26:32 -0800 Subject: [PATCH 0142/2154] First version with python3 support. * Moved requirements*.txt files to proper spec definitions in setup.py * Added a new fig.compat module to store some compatibility code --- MANIFEST.in | 4 +++- fig/__init__.py | 1 + fig/cli/colors.py | 1 + fig/cli/command.py | 4 +++- fig/cli/docopt_command.py | 2 ++ fig/cli/errors.py | 1 + fig/cli/formatter.py | 2 ++ fig/cli/log_printer.py | 4 +++- fig/cli/multiplexer.py | 1 + fig/cli/utils.py | 4 +++- fig/compat.py | 23 +++++++++++++++++++++++ fig/container.py | 4 +++- fig/project.py | 7 +++++-- fig/service.py | 10 ++++++---- requirements-dev.txt | 2 -- requirements.txt | 4 ---- setup.py | 30 ++++++++++++++++++++---------- tests/container_test.py | 1 + tests/project_test.py | 1 + tests/service_test.py | 2 ++ tests/testcases.py | 2 ++ 21 files changed, 83 insertions(+), 27 deletions(-) create mode 100644 fig/compat.py delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index a929a01c..95e97bf3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,5 @@ include LICENSE include *.md -include requirements.txt +recursive-include tests * +global-exclude *.pyc +global-exclode *.pyo diff --git a/fig/__init__.py b/fig/__init__.py index 7d6d5a30..baa08f1e 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from .service import Service __version__ = '0.0.2' diff --git a/fig/cli/colors.py b/fig/cli/colors.py index 09ec84bd..af4a32ab 100644 --- a/fig/cli/colors.py +++ b/fig/cli/colors.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals NAMES = [ 'grey', 'red', diff --git a/fig/cli/command.py b/fig/cli/command.py index 813ea4db..fb0adaf0 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import from docker import Client import errno import logging @@ -21,7 +23,7 @@ class Command(DocoptCommand): def project(self): try: config = yaml.load(open('fig.yml')) - except IOError, e: + except IOError as e: if e.errno == errno.ENOENT: log.error("Can't find %s. Are you in the right directory?", e.filename) else: diff --git a/fig/cli/docopt_command.py b/fig/cli/docopt_command.py index d2aeb035..cbb8e530 100644 --- a/fig/cli/docopt_command.py +++ b/fig/cli/docopt_command.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import import sys from inspect import getdoc diff --git a/fig/cli/errors.py b/fig/cli/errors.py index 038a7ea1..bb1702fd 100644 --- a/fig/cli/errors.py +++ b/fig/cli/errors.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from textwrap import dedent diff --git a/fig/cli/formatter.py b/fig/cli/formatter.py index 55a967f9..b57c1895 100644 --- a/fig/cli/formatter.py +++ b/fig/cli/formatter.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import import texttable import os diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index 480fc5eb..f31fb9b8 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import import sys from itertools import cycle @@ -41,7 +43,7 @@ class LogPrinter(object): 'stream': True, } params.update(self.attach_params) - params = dict((name, 1 if value else 0) for (name, value) in params.items()) + params = dict((name, 1 if value else 0) for (name, value) in list(params.items())) return container.attach_socket(params=params, ws=True) def read_websocket(websocket): diff --git a/fig/cli/multiplexer.py b/fig/cli/multiplexer.py index cfcc3b6c..579f3bca 100644 --- a/fig/cli/multiplexer.py +++ b/fig/cli/multiplexer.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from threading import Thread try: diff --git a/fig/cli/utils.py b/fig/cli/utils.py index a1ff0c00..8e8ab376 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import import datetime import os import socket @@ -69,7 +71,7 @@ def prettydate(d): return '{0} hours ago'.format(s/3600) -def mkdir(path, permissions=0700): +def mkdir(path, permissions=0o700): if not os.path.exists(path): os.mkdir(path) diff --git a/fig/compat.py b/fig/compat.py new file mode 100644 index 00000000..5b01f6b3 --- /dev/null +++ b/fig/compat.py @@ -0,0 +1,23 @@ + + +# Taken from python2.7/3.3 functools +def cmp_to_key(mycmp): + """Convert a cmp= function into a key= function""" + class K(object): + __slots__ = ['obj'] + def __init__(self, obj): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + __hash__ = None + return K diff --git a/fig/container.py b/fig/container.py index 454ec91f..9556ec1f 100644 --- a/fig/container.py +++ b/fig/container.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import import logging log = logging.getLogger(__name__) @@ -53,7 +55,7 @@ class Container(object): if not self.dictionary['NetworkSettings']['Ports']: return '' ports = [] - for private, public in self.dictionary['NetworkSettings']['Ports'].items(): + for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()): if public: ports.append('%s->%s' % (public[0]['HostPort'], private)) return ', '.join(ports) diff --git a/fig/project.py b/fig/project.py index 97ff319d..3c536ab6 100644 --- a/fig/project.py +++ b/fig/project.py @@ -1,5 +1,8 @@ +from __future__ import unicode_literals +from __future__ import absolute_import import logging from .service import Service +from .compat import cmp_to_key log = logging.getLogger(__name__) @@ -13,7 +16,7 @@ def sort_service_dicts(services): elif y_deps_x and not x_deps_y: return -1 return 0 - return sorted(services, cmp=cmp) + return sorted(services, key=cmp_to_key(cmp)) class Project(object): """ @@ -43,7 +46,7 @@ class Project(object): @classmethod def from_config(cls, name, config, client): dicts = [] - for service_name, service in config.items(): + for service_name, service in list(config.items()): service['name'] = service_name dicts.append(service) return cls.from_dicts(name, dicts, client) diff --git a/fig/service.py b/fig/service.py index 3fb0e428..4571f819 100644 --- a/fig/service.py +++ b/fig/service.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import from docker.client import APIError import logging import re @@ -64,7 +66,7 @@ class Service(object): container_options = self._get_container_options(override_options, one_off=one_off) try: return Container.create(self.client, **container_options) - except APIError, e: + except APIError as e: if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation: log.info('Pulling image %s...' % container_options['image']) self.client.pull(container_options['image']) @@ -82,7 +84,7 @@ class Service(object): if options.get('ports', None) is not None: for port in options['ports']: - port = unicode(port) + port = str(port) if ':' in port: internal_port, external_port = port.split(':', 1) port_bindings[int(internal_port)] = int(external_port) @@ -107,7 +109,7 @@ class Service(object): bits = [self.project, self.name] if one_off: bits.append('run') - return '_'.join(bits + [unicode(self.next_container_number(one_off=one_off))]) + return '_'.join(bits + [str(self.next_container_number(one_off=one_off))]) def next_container_number(self, one_off=False): numbers = [parse_name(c.name)[2] for c in self.containers(stopped=True, one_off=one_off)] @@ -132,7 +134,7 @@ class Service(object): container_options['name'] = self.next_container_name(one_off) if 'ports' in container_options: - container_options['ports'] = [unicode(p).split(':')[0] for p in container_options['ports']] + container_options['ports'] = [str(p).split(':')[0] for p in container_options['ports']] if 'volumes' in container_options: container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes']) diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 4ef6576c..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -nose==1.3.0 -unittest2==0.5.1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index dccc0aa1..00000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -docker-py==0.2.3 -docopt==0.6.1 -PyYAML==3.10 -texttable==0.8.1 diff --git a/setup.py b/setup.py index 2f7131f6..60b01a43 100644 --- a/setup.py +++ b/setup.py @@ -1,16 +1,17 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - -from setuptools import setup +from __future__ import unicode_literals +from __future__ import absolute_import +from setuptools import setup, find_packages import re import os import codecs -# Borrowed from -# https://github.com/jezdez/django_compressor/blob/develop/setup.py def read(*parts): - return codecs.open(os.path.join(os.path.dirname(__file__), *parts)).read() + path = os.path.join(os.path.dirname(__file__), *parts) + with codecs.open(path, encoding='utf-8') as fobj: + return fobj.read() def find_version(*file_paths): @@ -21,8 +22,6 @@ def find_version(*file_paths): return version_match.group(1) raise RuntimeError("Unable to find version string.") -with open('requirements.txt') as f: - install_requires = f.read().splitlines() setup( name='fig', @@ -31,10 +30,21 @@ setup( url='https://github.com/orchardup/fig', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', - packages=['fig', 'fig.cli'], - package_data={}, + packages=find_packages(), include_package_data=True, - install_requires=install_requires, + test_suite='nose.collector', + install_requires=[ + 'docker-py==0.2.3', + 'docopt==0.6.1', + 'PyYAML==3.10', + 'texttable==0.8.1', + # unfortunately `docker` requires six ==1.3.0 + 'six==1.3.0', + ], + tests_require=[ + 'nose==1.3.0', + 'unittest2==0.5.1' + ], entry_points=""" [console_scripts] fig=fig.cli.main:main diff --git a/tests/container_test.py b/tests/container_test.py index 0d6c5f0f..3666b3e4 100644 --- a/tests/container_test.py +++ b/tests/container_test.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from .testcases import DockerClientTestCase from fig.container import Container diff --git a/tests/project_test.py b/tests/project_test.py index 82b9be9d..09792fab 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -1,3 +1,4 @@ +from __future__ import unicode_literals from fig.project import Project from .testcases import DockerClientTestCase diff --git a/tests/service_test.py b/tests/service_test.py index f946ab38..bc5c6ffe 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import from fig import Service from .testcases import DockerClientTestCase diff --git a/tests/testcases.py b/tests/testcases.py index 4ac61d9d..671e091b 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals +from __future__ import absolute_import from docker import Client from fig.service import Service from fig.cli.utils import docker_url From bf8875d93075e34712b753c90b2affadba7db8d2 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 18:53:26 -0800 Subject: [PATCH 0143/2154] Added tox file --- tox.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 tox.ini diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..a4a195d6 --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py26,py27,py32,py33,pypy + +[testenv] +commands = + pip install -e {toxinidir} + pip install -e {toxinidir}[test] + python setup.py test From 3c91315426fe7e614560668ce2684361bc8deb39 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:06:12 -0800 Subject: [PATCH 0144/2154] Fix exception alias syntax --- fig/cli/main.py | 8 ++++---- fig/cli/socketclient.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index d4ac576b..3ce9e69c 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -38,18 +38,18 @@ def main(): except KeyboardInterrupt: log.error("\nAborting.") exit(1) - except UserError, e: + except UserError as e: log.error(e.msg) exit(1) - except NoSuchService, e: + except NoSuchService as e: log.error(e.msg) exit(1) - except NoSuchCommand, e: + except NoSuchCommand as e: log.error("No such command: %s", e.command) log.error("") log.error("\n".join(parse_doc_section("commands:", getdoc(e.supercommand)))) exit(1) - except APIError, e: + except APIError as e: log.error(e.explanation) exit(1) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 90ed8b58..550c3090 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -85,7 +85,7 @@ class SocketClient: stream.flush() else: break - except Exception, e: + except Exception as e: log.debug(e) def send_ws(self, socket, stream): @@ -101,7 +101,7 @@ class SocketClient: else: try: socket.send(chunk) - except Exception, e: + except Exception as e: if hasattr(e, 'errno') and e.errno == errno.EPIPE: break else: From 30ea4508c308bfb28af67520e62192b825ed4958 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:07:21 -0800 Subject: [PATCH 0145/2154] Use print function --- fig/cli/main.py | 16 ++++++++-------- fig/cli/socketclient.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 3ce9e69c..88ec1d47 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -112,7 +112,7 @@ class TopLevelCommand(Command): Usage: logs [SERVICE...] """ containers = self.project.containers(service_names=options['SERVICE'], stopped=False) - print "Attaching to", list_containers(containers) + print("Attaching to", list_containers(containers)) LogPrinter(containers, attach_params={'logs': True}).run() def ps(self, options): @@ -128,7 +128,7 @@ class TopLevelCommand(Command): if options['-q']: for container in containers: - print container.id + print(container.id) else: headers = [ 'Name', @@ -144,7 +144,7 @@ class TopLevelCommand(Command): container.human_readable_state, container.human_readable_ports, ]) - print Formatter().table(headers, rows) + print(Formatter().table(headers, rows)) def rm(self, options): """ @@ -156,11 +156,11 @@ class TopLevelCommand(Command): stopped_containers = [c for c in all_containers if not c.is_running] if len(stopped_containers) > 0: - print "Going to remove", list_containers(stopped_containers) + print("Going to remove", list_containers(stopped_containers)) if yesno("Are you sure? [yN] ", default=False): self.project.remove_stopped(service_names=options['SERVICE']) else: - print "No stopped containers" + print("No stopped containers") def run(self, options): """ @@ -180,7 +180,7 @@ class TopLevelCommand(Command): container = service.create_container(one_off=True, **container_options) if options['-d']: service.start_container(container, ports=None) - print container.name + print(container.name) else: with self._attach_to_container( container.id, @@ -222,7 +222,7 @@ class TopLevelCommand(Command): containers = self.project.containers(service_names=options['SERVICE'], stopped=True) if not detached: - print "Attaching to", list_containers(containers) + print("Attaching to", list_containers(containers)) log_printer = LogPrinter(containers) self.project.start(service_names=options['SERVICE']) @@ -236,7 +236,7 @@ class TopLevelCommand(Command): sys.exit(0) signal.signal(signal.SIGINT, handler) - print "Gracefully stopping... (press Ctrl+C again to force)" + print("Gracefully stopping... (press Ctrl+C again to force)") self.project.stop(service_names=options['SERVICE']) def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False): diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 550c3090..b20ae394 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -123,7 +123,7 @@ if __name__ == '__main__': url = sys.argv[1] socket = websocket.create_connection(url) - print "connected\r" + print("connected\r") with SocketClient(socket, interactive=True) as client: client.run() From b101118d1e9fd414909b9954552a1fa5bcd7cf15 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:07:44 -0800 Subject: [PATCH 0146/2154] Add future import for print function --- fig/cli/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fig/cli/main.py b/fig/cli/main.py index 88ec1d47..e061ab25 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -1,3 +1,4 @@ +from __future__ import print_function import logging import sys import re From c6e91db32fae225df8908f6d649f2f24d95e6223 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:13:41 -0800 Subject: [PATCH 0147/2154] Add texttable compat module that is py3k compatible --- fig/cli/formatter.py | 3 +- fig/{compat.py => compat/__init__.py} | 0 fig/compat/texttable.py | 579 ++++++++++++++++++++++++++ setup.py | 1 - 4 files changed, 581 insertions(+), 2 deletions(-) rename fig/{compat.py => compat/__init__.py} (100%) create mode 100644 fig/compat/texttable.py diff --git a/fig/cli/formatter.py b/fig/cli/formatter.py index b57c1895..47bf680c 100644 --- a/fig/cli/formatter.py +++ b/fig/cli/formatter.py @@ -1,8 +1,9 @@ from __future__ import unicode_literals from __future__ import absolute_import -import texttable import os +from fig.compat import texttable + class Formatter(object): def table(self, headers, rows): diff --git a/fig/compat.py b/fig/compat/__init__.py similarity index 100% rename from fig/compat.py rename to fig/compat/__init__.py diff --git a/fig/compat/texttable.py b/fig/compat/texttable.py new file mode 100644 index 00000000..a60be2ba --- /dev/null +++ b/fig/compat/texttable.py @@ -0,0 +1,579 @@ +#!/usr/bin/env python +# +# texttable - module for creating simple ASCII tables +# Copyright (C) 2003-2011 Gerome Fournier +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +"""module for creating simple ASCII tables + + +Example: + + table = Texttable() + table.set_cols_align(["l", "r", "c"]) + table.set_cols_valign(["t", "m", "b"]) + table.add_rows([ ["Name", "Age", "Nickname"], + ["Mr\\nXavier\\nHuon", 32, "Xav'"], + ["Mr\\nBaptiste\\nClement", 1, "Baby"] ]) + print(table.draw() + "\\n") + + table = Texttable() + table.set_deco(Texttable.HEADER) + table.set_cols_dtype(['t', # text + 'f', # float (decimal) + 'e', # float (exponent) + 'i', # integer + 'a']) # automatic + table.set_cols_align(["l", "r", "r", "r", "l"]) + table.add_rows([["text", "float", "exp", "int", "auto"], + ["abcd", "67", 654, 89, 128.001], + ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], + ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], + ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) + print(table.draw()) + +Result: + + +----------+-----+----------+ + | Name | Age | Nickname | + +==========+=====+==========+ + | Mr | | | + | Xavier | 32 | | + | Huon | | Xav' | + +----------+-----+----------+ + | Mr | | | + | Baptiste | 1 | | + | Clement | | Baby | + +----------+-----+----------+ + + text float exp int auto + =========================================== + abcd 67.000 6.540e+02 89 128.001 + efgh 67.543 6.540e-01 90 1.280e+22 + ijkl 0.000 5.000e-78 89 0.000 + mnop 0.023 5.000e+78 92 1.280e+22 +""" + +__all__ = ["Texttable", "ArraySizeError"] + +__author__ = 'Gerome Fournier ' +__license__ = 'LGPL' +__version__ = '0.8.1' +__credits__ = """\ +Jeff Kowalczyk: + - textwrap improved import + - comment concerning header output + +Anonymous: + - add_rows method, for adding rows in one go + +Sergey Simonenko: + - redefined len() function to deal with non-ASCII characters + +Roger Lew: + - columns datatype specifications + +Brian Peterson: + - better handling of unicode errors +""" + +# Modified version of `texttable` for python3 support. + +import sys +import string + + +def len(iterable): + """Redefining len here so it will be able to work with non-ASCII characters + """ + if not isinstance(iterable, str): + return iterable.__len__() + + try: + return len(unicode(iterable, 'utf')) + except: + return iterable.__len__() + + +class ArraySizeError(Exception): + """Exception raised when specified rows don't fit the required size + """ + + def __init__(self, msg): + self.msg = msg + Exception.__init__(self, msg, '') + + def __str__(self): + return self.msg + +class Texttable: + + BORDER = 1 + HEADER = 1 << 1 + HLINES = 1 << 2 + VLINES = 1 << 3 + + def __init__(self, max_width=80): + """Constructor + + - max_width is an integer, specifying the maximum width of the table + - if set to 0, size is unlimited, therefore cells won't be wrapped + """ + + if max_width <= 0: + max_width = False + self._max_width = max_width + self._precision = 3 + + self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \ + Texttable.HEADER + self.set_chars(['-', '|', '+', '=']) + self.reset() + + def reset(self): + """Reset the instance + + - reset rows and header + """ + + self._hline_string = None + self._row_size = None + self._header = [] + self._rows = [] + + def set_chars(self, array): + """Set the characters used to draw lines between rows and columns + + - the array should contain 4 fields: + + [horizontal, vertical, corner, header] + + - default is set to: + + ['-', '|', '+', '='] + """ + + if len(array) != 4: + raise ArraySizeError("array should contain 4 characters") + array = [ x[:1] for x in [ str(s) for s in array ] ] + (self._char_horiz, self._char_vert, + self._char_corner, self._char_header) = array + + def set_deco(self, deco): + """Set the table decoration + + - 'deco' can be a combinaison of: + + Texttable.BORDER: Border around the table + Texttable.HEADER: Horizontal line below the header + Texttable.HLINES: Horizontal lines between rows + Texttable.VLINES: Vertical lines between columns + + All of them are enabled by default + + - example: + + Texttable.BORDER | Texttable.HEADER + """ + + self._deco = deco + + def set_cols_align(self, array): + """Set the desired columns alignment + + - the elements of the array should be either "l", "c" or "r": + + * "l": column flushed left + * "c": column centered + * "r": column flushed right + """ + + self._check_row_size(array) + self._align = array + + def set_cols_valign(self, array): + """Set the desired columns vertical alignment + + - the elements of the array should be either "t", "m" or "b": + + * "t": column aligned on the top of the cell + * "m": column aligned on the middle of the cell + * "b": column aligned on the bottom of the cell + """ + + self._check_row_size(array) + self._valign = array + + def set_cols_dtype(self, array): + """Set the desired columns datatype for the cols. + + - the elements of the array should be either "a", "t", "f", "e" or "i": + + * "a": automatic (try to use the most appropriate datatype) + * "t": treat as text + * "f": treat as float in decimal format + * "e": treat as float in exponential format + * "i": treat as int + + - by default, automatic datatyping is used for each column + """ + + self._check_row_size(array) + self._dtype = array + + def set_cols_width(self, array): + """Set the desired columns width + + - the elements of the array should be integers, specifying the + width of each column. For example: + + [10, 20, 5] + """ + + self._check_row_size(array) + try: + array = map(int, array) + if reduce(min, array) <= 0: + raise ValueError + except ValueError: + sys.stderr.write("Wrong argument in column width specification\n") + raise + self._width = array + + def set_precision(self, width): + """Set the desired precision for float/exponential formats + + - width must be an integer >= 0 + + - default value is set to 3 + """ + + if not type(width) is int or width < 0: + raise ValueError('width must be an integer greater then 0') + self._precision = width + + def header(self, array): + """Specify the header of the table + """ + + self._check_row_size(array) + self._header = map(str, array) + + def add_row(self, array): + """Add a row in the rows stack + + - cells can contain newlines and tabs + """ + + self._check_row_size(array) + + if not hasattr(self, "_dtype"): + self._dtype = ["a"] * self._row_size + + cells = [] + for i,x in enumerate(array): + cells.append(self._str(i,x)) + self._rows.append(cells) + + def add_rows(self, rows, header=True): + """Add several rows in the rows stack + + - The 'rows' argument can be either an iterator returning arrays, + or a by-dimensional array + - 'header' specifies if the first row should be used as the header + of the table + """ + + # nb: don't use 'iter' on by-dimensional arrays, to get a + # usable code for python 2.1 + if header: + if hasattr(rows, '__iter__') and hasattr(rows, 'next'): + self.header(rows.next()) + else: + self.header(rows[0]) + rows = rows[1:] + for row in rows: + self.add_row(row) + + def draw(self): + """Draw the table + + - the table is returned as a whole string + """ + + if not self._header and not self._rows: + return + self._compute_cols_width() + self._check_align() + out = "" + if self._has_border(): + out += self._hline() + if self._header: + out += self._draw_line(self._header, isheader=True) + if self._has_header(): + out += self._hline_header() + length = 0 + for row in self._rows: + length += 1 + out += self._draw_line(row) + if self._has_hlines() and length < len(self._rows): + out += self._hline() + if self._has_border(): + out += self._hline() + return out[:-1] + + def _str(self, i, x): + """Handles string formatting of cell data + + i - index of the cell datatype in self._dtype + x - cell data to format + """ + try: + f = float(x) + except: + return str(x) + + n = self._precision + dtype = self._dtype[i] + + if dtype == 'i': + return str(int(round(f))) + elif dtype == 'f': + return '%.*f' % (n, f) + elif dtype == 'e': + return '%.*e' % (n, f) + elif dtype == 't': + return str(x) + else: + if f - round(f) == 0: + if abs(f) > 1e8: + return '%.*e' % (n, f) + else: + return str(int(round(f))) + else: + if abs(f) > 1e8: + return '%.*e' % (n, f) + else: + return '%.*f' % (n, f) + + def _check_row_size(self, array): + """Check that the specified array fits the previous rows size + """ + + if not self._row_size: + self._row_size = len(array) + elif self._row_size != len(array): + raise ArraySizeError("array should contain %d elements" % self._row_size) + + def _has_vlines(self): + """Return a boolean, if vlines are required or not + """ + + return self._deco & Texttable.VLINES > 0 + + def _has_hlines(self): + """Return a boolean, if hlines are required or not + """ + + return self._deco & Texttable.HLINES > 0 + + def _has_border(self): + """Return a boolean, if border is required or not + """ + + return self._deco & Texttable.BORDER > 0 + + def _has_header(self): + """Return a boolean, if header line is required or not + """ + + return self._deco & Texttable.HEADER > 0 + + def _hline_header(self): + """Print header's horizontal line + """ + + return self._build_hline(True) + + def _hline(self): + """Print an horizontal line + """ + + if not self._hline_string: + self._hline_string = self._build_hline() + return self._hline_string + + def _build_hline(self, is_header=False): + """Return a string used to separated rows or separate header from + rows + """ + horiz = self._char_horiz + if (is_header): + horiz = self._char_header + # compute cell separator + s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()], + horiz) + # build the line + l = string.join([horiz * n for n in self._width], s) + # add border if needed + if self._has_border(): + l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz, + self._char_corner) + else: + l += "\n" + return l + + def _len_cell(self, cell): + """Return the width of the cell + + Special characters are taken into account to return the width of the + cell, such like newlines and tabs + """ + + cell_lines = cell.split('\n') + maxi = 0 + for line in cell_lines: + length = 0 + parts = line.split('\t') + for part, i in zip(parts, range(1, len(parts) + 1)): + length = length + len(part) + if i < len(parts): + length = (length/8 + 1) * 8 + maxi = max(maxi, length) + return maxi + + def _compute_cols_width(self): + """Return an array with the width of each column + + If a specific width has been specified, exit. If the total of the + columns width exceed the table desired width, another width will be + computed to fit, and cells will be wrapped. + """ + + if hasattr(self, "_width"): + return + maxi = [] + if self._header: + maxi = [ self._len_cell(x) for x in self._header ] + for row in self._rows: + for cell,i in zip(row, range(len(row))): + try: + maxi[i] = max(maxi[i], self._len_cell(cell)) + except (TypeError, IndexError): + maxi.append(self._len_cell(cell)) + items = len(maxi) + length = reduce(lambda x,y: x+y, maxi) + if self._max_width and length + items * 3 + 1 > self._max_width: + maxi = [(self._max_width - items * 3 -1) / items \ + for n in range(items)] + self._width = maxi + + def _check_align(self): + """Check if alignment has been specified, set default one if not + """ + + if not hasattr(self, "_align"): + self._align = ["l"] * self._row_size + if not hasattr(self, "_valign"): + self._valign = ["t"] * self._row_size + + def _draw_line(self, line, isheader=False): + """Draw a line + + Loop over a single cell length, over all the cells + """ + + line = self._splitit(line, isheader) + space = " " + out = "" + for i in range(len(line[0])): + if self._has_border(): + out += "%s " % self._char_vert + length = 0 + for cell, width, align in zip(line, self._width, self._align): + length += 1 + cell_line = cell[i] + fill = width - len(cell_line) + if isheader: + align = "c" + if align == "r": + out += "%s " % (fill * space + cell_line) + elif align == "c": + out += "%s " % (fill/2 * space + cell_line \ + + (fill/2 + fill%2) * space) + else: + out += "%s " % (cell_line + fill * space) + if length < len(line): + out += "%s " % [space, self._char_vert][self._has_vlines()] + out += "%s\n" % ['', self._char_vert][self._has_border()] + return out + + def _splitit(self, line, isheader): + """Split each element of line to fit the column width + + Each element is turned into a list, result of the wrapping of the + string to the desired width + """ + + line_wrapped = [] + for cell, width in zip(line, self._width): + array = [] + for c in cell.split('\n'): + try: + c = unicode(c, 'utf') + except UnicodeDecodeError as strerror: + sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (c, strerror)) + c = unicode(c, 'utf', 'replace') + array.extend(textwrap.wrap(c, width)) + line_wrapped.append(array) + max_cell_lines = reduce(max, map(len, line_wrapped)) + for cell, valign in zip(line_wrapped, self._valign): + if isheader: + valign = "t" + if valign == "m": + missing = max_cell_lines - len(cell) + cell[:0] = [""] * (missing / 2) + cell.extend([""] * (missing / 2 + missing % 2)) + elif valign == "b": + cell[:0] = [""] * (max_cell_lines - len(cell)) + else: + cell.extend([""] * (max_cell_lines - len(cell))) + return line_wrapped + +if __name__ == '__main__': + table = Texttable() + table.set_cols_align(["l", "r", "c"]) + table.set_cols_valign(["t", "m", "b"]) + table.add_rows([ ["Name", "Age", "Nickname"], + ["Mr\nXavier\nHuon", 32, "Xav'"], + ["Mr\nBaptiste\nClement", 1, "Baby"] ]) + print(table.draw() + "\n") + + table = Texttable() + table.set_deco(Texttable.HEADER) + table.set_cols_dtype(['t', # text + 'f', # float (decimal) + 'e', # float (exponent) + 'i', # integer + 'a']) # automatic + table.set_cols_align(["l", "r", "r", "r", "l"]) + table.add_rows([["text", "float", "exp", "int", "auto"], + ["abcd", "67", 654, 89, 128.001], + ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], + ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], + ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) + print(table.draw()) + diff --git a/setup.py b/setup.py index 60b01a43..a114bdf9 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,6 @@ setup( 'docker-py==0.2.3', 'docopt==0.6.1', 'PyYAML==3.10', - 'texttable==0.8.1', # unfortunately `docker` requires six ==1.3.0 'six==1.3.0', ], From f600fa8bf3ec62b77e9dc036b88d0f96bd766aa8 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:15:09 -0800 Subject: [PATCH 0148/2154] More future imports for safety --- fig/cli/socketclient.py | 1 + fig/cli/utils.py | 1 + fig/compat/texttable.py | 3 +++ 3 files changed, 5 insertions(+) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index b20ae394..bbae60e6 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -1,3 +1,4 @@ +from __future__ import print_function # Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py from select import select diff --git a/fig/cli/utils.py b/fig/cli/utils.py index 8e8ab376..249baba7 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import +from __future__ import division import datetime import os import socket diff --git a/fig/compat/texttable.py b/fig/compat/texttable.py index a60be2ba..a9c91a50 100644 --- a/fig/compat/texttable.py +++ b/fig/compat/texttable.py @@ -66,6 +66,9 @@ Result: ijkl 0.000 5.000e-78 89 0.000 mnop 0.023 5.000e+78 92 1.280e+22 """ +from __future__ import division +from __future__ import print_function +from functools import reduce __all__ = ["Texttable", "ArraySizeError"] From 9bec059cc78e3992ff97c3b356367daba434ec0d Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:32:06 -0800 Subject: [PATCH 0149/2154] e.explanation a 'str' object --- fig/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index 4571f819..a2492e7f 100644 --- a/fig/service.py +++ b/fig/service.py @@ -67,7 +67,7 @@ class Service(object): try: return Container.create(self.client, **container_options) except APIError as e: - if e.response.status_code == 404 and e.explanation and 'No such image' in e.explanation: + if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation): log.info('Pulling image %s...' % container_options['image']) self.client.pull(container_options['image']) return Container.create(self.client, **container_options) From 31f09077324aa1f7611deb6d9bd2335c02a1e283 Mon Sep 17 00:00:00 2001 From: Christopher Grebs Date: Sun, 5 Jan 2014 19:52:56 -0800 Subject: [PATCH 0150/2154] Add unicode_literals to main module --- fig/cli/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fig/cli/main.py b/fig/cli/main.py index e061ab25..6b112f41 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -1,4 +1,5 @@ from __future__ import print_function +from __future__ import unicode_literals import logging import sys import re From 0760ea1b006cfb8dc3b9dcad7043b6d2866ee197 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 13:06:00 +0000 Subject: [PATCH 0151/2154] Add Python 3 and PyPy to .travis.yml --- .travis.yml | 13 +++---------- script/travis-install | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) create mode 100644 script/travis-install diff --git a/.travis.yml b/.travis.yml index 2a7cad5b..51d44bf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,10 @@ language: python python: - "2.6" - "2.7" + - "3.2" + - "3.3" -install: - - sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" - - sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" - - sudo apt-get update - - echo exit 101 | sudo tee /usr/sbin/policy-rc.d - - sudo chmod +x /usr/sbin/policy-rc.d - - sudo apt-get install -qy slirp lxc lxc-docker=0.7.3 - - git clone git://github.com/jpetazzo/sekexe - - python setup.py install - - pip install -r requirements-dev.txt +install: script/travis-install script: - pwd diff --git a/script/travis-install b/script/travis-install new file mode 100644 index 00000000..e68423da --- /dev/null +++ b/script/travis-install @@ -0,0 +1,16 @@ +#!/bin/bash + +sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" +sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" +sudo apt-get update +echo exit 101 | sudo tee /usr/sbin/policy-rc.d +sudo chmod +x /usr/sbin/policy-rc.d +sudo apt-get install -qy slirp lxc lxc-docker=0.7.3 +git clone git://github.com/jpetazzo/sekexe +python setup.py install +pip install -r requirements-dev.txt + +if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then + pip install unittest2 +fi + From 7888027425262f9c16c91ed0d71ab8683a9496fd Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 15:09:56 +0000 Subject: [PATCH 0152/2154] Put requirements back in .txt files Read-only FS in travis --- requirements-dev.txt | 2 ++ requirements.txt | 5 +++++ script/travis-install | 0 setup.py | 18 +++++++----------- 4 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 requirements-dev.txt create mode 100644 requirements.txt mode change 100644 => 100755 script/travis-install diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..4ef6576c --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +nose==1.3.0 +unittest2==0.5.1 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..bfc91093 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +docker-py==0.2.3 +docopt==0.6.1 +PyYAML==3.10 +# docker requires six==1.3.0 +six==1.3.0 diff --git a/script/travis-install b/script/travis-install old mode 100644 new mode 100755 diff --git a/setup.py b/setup.py index a114bdf9..f0ffceb7 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,11 @@ def find_version(*file_paths): return version_match.group(1) raise RuntimeError("Unable to find version string.") +with open('requirements.txt') as f: + install_requires = f.read().splitlines() + +with open('requirements-dev.txt') as f: + tests_require = f.read().splitlines() setup( name='fig', @@ -33,17 +38,8 @@ setup( packages=find_packages(), include_package_data=True, test_suite='nose.collector', - install_requires=[ - 'docker-py==0.2.3', - 'docopt==0.6.1', - 'PyYAML==3.10', - # unfortunately `docker` requires six ==1.3.0 - 'six==1.3.0', - ], - tests_require=[ - 'nose==1.3.0', - 'unittest2==0.5.1' - ], + install_requires=install_requires, + tests_require=tests_require, entry_points=""" [console_scripts] fig=fig.cli.main:main From 00a1835faeb10b3436309ea3f82158963bd7fe8c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 6 Jan 2014 17:34:08 +0000 Subject: [PATCH 0153/2154] Allow Python 3 to fail docker-py is broken --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 51d44bf3..d00b2228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,11 @@ python: - "3.2" - "3.3" +matrix: + allow_failures: + - python: "3.2" + - python: "3.3" + install: script/travis-install script: From 892677a9d3dc3d6fb3b425d2ebd7c19e7f729879 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 9 Jan 2014 15:30:36 +0000 Subject: [PATCH 0154/2154] Very basic CLI smoke test See #8. --- fig/cli/command.py | 7 +++++-- tests/cli_test.py | 12 ++++++++++++ tests/fixtures/simple-figfile/fig.yml | 2 ++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 tests/cli_test.py create mode 100644 tests/fixtures/simple-figfile/fig.yml diff --git a/fig/cli/command.py b/fig/cli/command.py index fb0adaf0..4e0705a9 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -15,6 +15,8 @@ from .utils import cached_property, docker_url log = logging.getLogger(__name__) class Command(DocoptCommand): + base_dir = '.' + @cached_property def client(self): return Client(docker_url()) @@ -22,10 +24,11 @@ class Command(DocoptCommand): @cached_property def project(self): try: - config = yaml.load(open('fig.yml')) + yaml_path = os.path.join(self.base_dir, 'fig.yml') + config = yaml.load(open(yaml_path)) except IOError as e: if e.errno == errno.ENOENT: - log.error("Can't find %s. Are you in the right directory?", e.filename) + log.error("Can't find %s. Are you in the right directory?", os.path.basename(e.filename)) else: log.error(e) diff --git a/tests/cli_test.py b/tests/cli_test.py new file mode 100644 index 00000000..ffd7fd41 --- /dev/null +++ b/tests/cli_test.py @@ -0,0 +1,12 @@ +from __future__ import unicode_literals +from __future__ import absolute_import +from . import unittest +from fig.cli.main import TopLevelCommand + +class CLITestCase(unittest.TestCase): + def setUp(self): + self.command = TopLevelCommand() + self.command.base_dir = 'tests/fixtures/simple-figfile' + + def test_help(self): + self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None)) diff --git a/tests/fixtures/simple-figfile/fig.yml b/tests/fixtures/simple-figfile/fig.yml new file mode 100644 index 00000000..aef2d39b --- /dev/null +++ b/tests/fixtures/simple-figfile/fig.yml @@ -0,0 +1,2 @@ +simple: + image: ubuntu From 8cab05feb4565eca43874c11ad8fcc74f1e748a9 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 9 Jan 2014 15:31:37 +0000 Subject: [PATCH 0155/2154] Failing (on 2.7, at least) smoke test for 'fig ps' See #8. --- tests/cli_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/cli_test.py b/tests/cli_test.py index ffd7fd41..2146a906 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -10,3 +10,6 @@ class CLITestCase(unittest.TestCase): def test_help(self): self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None)) + + def test_ps(self): + self.command.dispatch(['ps'], None) From 7a4b69edc032d5c09d1f4da50009ee99b699f1f0 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 9 Jan 2014 15:32:06 +0000 Subject: [PATCH 0156/2154] Remove compat texttable module - breaks on Python 2.7 --- fig/cli/formatter.py | 3 +- fig/compat/__init__.py | 23 -- fig/compat/texttable.py | 582 ---------------------------------------- requirements.txt | 1 + 4 files changed, 2 insertions(+), 607 deletions(-) delete mode 100644 fig/compat/__init__.py delete mode 100644 fig/compat/texttable.py diff --git a/fig/cli/formatter.py b/fig/cli/formatter.py index 47bf680c..3d7e2d51 100644 --- a/fig/cli/formatter.py +++ b/fig/cli/formatter.py @@ -1,8 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import os - -from fig.compat import texttable +import texttable class Formatter(object): diff --git a/fig/compat/__init__.py b/fig/compat/__init__.py deleted file mode 100644 index 5b01f6b3..00000000 --- a/fig/compat/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ - - -# Taken from python2.7/3.3 functools -def cmp_to_key(mycmp): - """Convert a cmp= function into a key= function""" - class K(object): - __slots__ = ['obj'] - def __init__(self, obj): - self.obj = obj - def __lt__(self, other): - return mycmp(self.obj, other.obj) < 0 - def __gt__(self, other): - return mycmp(self.obj, other.obj) > 0 - def __eq__(self, other): - return mycmp(self.obj, other.obj) == 0 - def __le__(self, other): - return mycmp(self.obj, other.obj) <= 0 - def __ge__(self, other): - return mycmp(self.obj, other.obj) >= 0 - def __ne__(self, other): - return mycmp(self.obj, other.obj) != 0 - __hash__ = None - return K diff --git a/fig/compat/texttable.py b/fig/compat/texttable.py deleted file mode 100644 index a9c91a50..00000000 --- a/fig/compat/texttable.py +++ /dev/null @@ -1,582 +0,0 @@ -#!/usr/bin/env python -# -# texttable - module for creating simple ASCII tables -# Copyright (C) 2003-2011 Gerome Fournier -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -"""module for creating simple ASCII tables - - -Example: - - table = Texttable() - table.set_cols_align(["l", "r", "c"]) - table.set_cols_valign(["t", "m", "b"]) - table.add_rows([ ["Name", "Age", "Nickname"], - ["Mr\\nXavier\\nHuon", 32, "Xav'"], - ["Mr\\nBaptiste\\nClement", 1, "Baby"] ]) - print(table.draw() + "\\n") - - table = Texttable() - table.set_deco(Texttable.HEADER) - table.set_cols_dtype(['t', # text - 'f', # float (decimal) - 'e', # float (exponent) - 'i', # integer - 'a']) # automatic - table.set_cols_align(["l", "r", "r", "r", "l"]) - table.add_rows([["text", "float", "exp", "int", "auto"], - ["abcd", "67", 654, 89, 128.001], - ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], - ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], - ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) - print(table.draw()) - -Result: - - +----------+-----+----------+ - | Name | Age | Nickname | - +==========+=====+==========+ - | Mr | | | - | Xavier | 32 | | - | Huon | | Xav' | - +----------+-----+----------+ - | Mr | | | - | Baptiste | 1 | | - | Clement | | Baby | - +----------+-----+----------+ - - text float exp int auto - =========================================== - abcd 67.000 6.540e+02 89 128.001 - efgh 67.543 6.540e-01 90 1.280e+22 - ijkl 0.000 5.000e-78 89 0.000 - mnop 0.023 5.000e+78 92 1.280e+22 -""" -from __future__ import division -from __future__ import print_function -from functools import reduce - -__all__ = ["Texttable", "ArraySizeError"] - -__author__ = 'Gerome Fournier ' -__license__ = 'LGPL' -__version__ = '0.8.1' -__credits__ = """\ -Jeff Kowalczyk: - - textwrap improved import - - comment concerning header output - -Anonymous: - - add_rows method, for adding rows in one go - -Sergey Simonenko: - - redefined len() function to deal with non-ASCII characters - -Roger Lew: - - columns datatype specifications - -Brian Peterson: - - better handling of unicode errors -""" - -# Modified version of `texttable` for python3 support. - -import sys -import string - - -def len(iterable): - """Redefining len here so it will be able to work with non-ASCII characters - """ - if not isinstance(iterable, str): - return iterable.__len__() - - try: - return len(unicode(iterable, 'utf')) - except: - return iterable.__len__() - - -class ArraySizeError(Exception): - """Exception raised when specified rows don't fit the required size - """ - - def __init__(self, msg): - self.msg = msg - Exception.__init__(self, msg, '') - - def __str__(self): - return self.msg - -class Texttable: - - BORDER = 1 - HEADER = 1 << 1 - HLINES = 1 << 2 - VLINES = 1 << 3 - - def __init__(self, max_width=80): - """Constructor - - - max_width is an integer, specifying the maximum width of the table - - if set to 0, size is unlimited, therefore cells won't be wrapped - """ - - if max_width <= 0: - max_width = False - self._max_width = max_width - self._precision = 3 - - self._deco = Texttable.VLINES | Texttable.HLINES | Texttable.BORDER | \ - Texttable.HEADER - self.set_chars(['-', '|', '+', '=']) - self.reset() - - def reset(self): - """Reset the instance - - - reset rows and header - """ - - self._hline_string = None - self._row_size = None - self._header = [] - self._rows = [] - - def set_chars(self, array): - """Set the characters used to draw lines between rows and columns - - - the array should contain 4 fields: - - [horizontal, vertical, corner, header] - - - default is set to: - - ['-', '|', '+', '='] - """ - - if len(array) != 4: - raise ArraySizeError("array should contain 4 characters") - array = [ x[:1] for x in [ str(s) for s in array ] ] - (self._char_horiz, self._char_vert, - self._char_corner, self._char_header) = array - - def set_deco(self, deco): - """Set the table decoration - - - 'deco' can be a combinaison of: - - Texttable.BORDER: Border around the table - Texttable.HEADER: Horizontal line below the header - Texttable.HLINES: Horizontal lines between rows - Texttable.VLINES: Vertical lines between columns - - All of them are enabled by default - - - example: - - Texttable.BORDER | Texttable.HEADER - """ - - self._deco = deco - - def set_cols_align(self, array): - """Set the desired columns alignment - - - the elements of the array should be either "l", "c" or "r": - - * "l": column flushed left - * "c": column centered - * "r": column flushed right - """ - - self._check_row_size(array) - self._align = array - - def set_cols_valign(self, array): - """Set the desired columns vertical alignment - - - the elements of the array should be either "t", "m" or "b": - - * "t": column aligned on the top of the cell - * "m": column aligned on the middle of the cell - * "b": column aligned on the bottom of the cell - """ - - self._check_row_size(array) - self._valign = array - - def set_cols_dtype(self, array): - """Set the desired columns datatype for the cols. - - - the elements of the array should be either "a", "t", "f", "e" or "i": - - * "a": automatic (try to use the most appropriate datatype) - * "t": treat as text - * "f": treat as float in decimal format - * "e": treat as float in exponential format - * "i": treat as int - - - by default, automatic datatyping is used for each column - """ - - self._check_row_size(array) - self._dtype = array - - def set_cols_width(self, array): - """Set the desired columns width - - - the elements of the array should be integers, specifying the - width of each column. For example: - - [10, 20, 5] - """ - - self._check_row_size(array) - try: - array = map(int, array) - if reduce(min, array) <= 0: - raise ValueError - except ValueError: - sys.stderr.write("Wrong argument in column width specification\n") - raise - self._width = array - - def set_precision(self, width): - """Set the desired precision for float/exponential formats - - - width must be an integer >= 0 - - - default value is set to 3 - """ - - if not type(width) is int or width < 0: - raise ValueError('width must be an integer greater then 0') - self._precision = width - - def header(self, array): - """Specify the header of the table - """ - - self._check_row_size(array) - self._header = map(str, array) - - def add_row(self, array): - """Add a row in the rows stack - - - cells can contain newlines and tabs - """ - - self._check_row_size(array) - - if not hasattr(self, "_dtype"): - self._dtype = ["a"] * self._row_size - - cells = [] - for i,x in enumerate(array): - cells.append(self._str(i,x)) - self._rows.append(cells) - - def add_rows(self, rows, header=True): - """Add several rows in the rows stack - - - The 'rows' argument can be either an iterator returning arrays, - or a by-dimensional array - - 'header' specifies if the first row should be used as the header - of the table - """ - - # nb: don't use 'iter' on by-dimensional arrays, to get a - # usable code for python 2.1 - if header: - if hasattr(rows, '__iter__') and hasattr(rows, 'next'): - self.header(rows.next()) - else: - self.header(rows[0]) - rows = rows[1:] - for row in rows: - self.add_row(row) - - def draw(self): - """Draw the table - - - the table is returned as a whole string - """ - - if not self._header and not self._rows: - return - self._compute_cols_width() - self._check_align() - out = "" - if self._has_border(): - out += self._hline() - if self._header: - out += self._draw_line(self._header, isheader=True) - if self._has_header(): - out += self._hline_header() - length = 0 - for row in self._rows: - length += 1 - out += self._draw_line(row) - if self._has_hlines() and length < len(self._rows): - out += self._hline() - if self._has_border(): - out += self._hline() - return out[:-1] - - def _str(self, i, x): - """Handles string formatting of cell data - - i - index of the cell datatype in self._dtype - x - cell data to format - """ - try: - f = float(x) - except: - return str(x) - - n = self._precision - dtype = self._dtype[i] - - if dtype == 'i': - return str(int(round(f))) - elif dtype == 'f': - return '%.*f' % (n, f) - elif dtype == 'e': - return '%.*e' % (n, f) - elif dtype == 't': - return str(x) - else: - if f - round(f) == 0: - if abs(f) > 1e8: - return '%.*e' % (n, f) - else: - return str(int(round(f))) - else: - if abs(f) > 1e8: - return '%.*e' % (n, f) - else: - return '%.*f' % (n, f) - - def _check_row_size(self, array): - """Check that the specified array fits the previous rows size - """ - - if not self._row_size: - self._row_size = len(array) - elif self._row_size != len(array): - raise ArraySizeError("array should contain %d elements" % self._row_size) - - def _has_vlines(self): - """Return a boolean, if vlines are required or not - """ - - return self._deco & Texttable.VLINES > 0 - - def _has_hlines(self): - """Return a boolean, if hlines are required or not - """ - - return self._deco & Texttable.HLINES > 0 - - def _has_border(self): - """Return a boolean, if border is required or not - """ - - return self._deco & Texttable.BORDER > 0 - - def _has_header(self): - """Return a boolean, if header line is required or not - """ - - return self._deco & Texttable.HEADER > 0 - - def _hline_header(self): - """Print header's horizontal line - """ - - return self._build_hline(True) - - def _hline(self): - """Print an horizontal line - """ - - if not self._hline_string: - self._hline_string = self._build_hline() - return self._hline_string - - def _build_hline(self, is_header=False): - """Return a string used to separated rows or separate header from - rows - """ - horiz = self._char_horiz - if (is_header): - horiz = self._char_header - # compute cell separator - s = "%s%s%s" % (horiz, [horiz, self._char_corner][self._has_vlines()], - horiz) - # build the line - l = string.join([horiz * n for n in self._width], s) - # add border if needed - if self._has_border(): - l = "%s%s%s%s%s\n" % (self._char_corner, horiz, l, horiz, - self._char_corner) - else: - l += "\n" - return l - - def _len_cell(self, cell): - """Return the width of the cell - - Special characters are taken into account to return the width of the - cell, such like newlines and tabs - """ - - cell_lines = cell.split('\n') - maxi = 0 - for line in cell_lines: - length = 0 - parts = line.split('\t') - for part, i in zip(parts, range(1, len(parts) + 1)): - length = length + len(part) - if i < len(parts): - length = (length/8 + 1) * 8 - maxi = max(maxi, length) - return maxi - - def _compute_cols_width(self): - """Return an array with the width of each column - - If a specific width has been specified, exit. If the total of the - columns width exceed the table desired width, another width will be - computed to fit, and cells will be wrapped. - """ - - if hasattr(self, "_width"): - return - maxi = [] - if self._header: - maxi = [ self._len_cell(x) for x in self._header ] - for row in self._rows: - for cell,i in zip(row, range(len(row))): - try: - maxi[i] = max(maxi[i], self._len_cell(cell)) - except (TypeError, IndexError): - maxi.append(self._len_cell(cell)) - items = len(maxi) - length = reduce(lambda x,y: x+y, maxi) - if self._max_width and length + items * 3 + 1 > self._max_width: - maxi = [(self._max_width - items * 3 -1) / items \ - for n in range(items)] - self._width = maxi - - def _check_align(self): - """Check if alignment has been specified, set default one if not - """ - - if not hasattr(self, "_align"): - self._align = ["l"] * self._row_size - if not hasattr(self, "_valign"): - self._valign = ["t"] * self._row_size - - def _draw_line(self, line, isheader=False): - """Draw a line - - Loop over a single cell length, over all the cells - """ - - line = self._splitit(line, isheader) - space = " " - out = "" - for i in range(len(line[0])): - if self._has_border(): - out += "%s " % self._char_vert - length = 0 - for cell, width, align in zip(line, self._width, self._align): - length += 1 - cell_line = cell[i] - fill = width - len(cell_line) - if isheader: - align = "c" - if align == "r": - out += "%s " % (fill * space + cell_line) - elif align == "c": - out += "%s " % (fill/2 * space + cell_line \ - + (fill/2 + fill%2) * space) - else: - out += "%s " % (cell_line + fill * space) - if length < len(line): - out += "%s " % [space, self._char_vert][self._has_vlines()] - out += "%s\n" % ['', self._char_vert][self._has_border()] - return out - - def _splitit(self, line, isheader): - """Split each element of line to fit the column width - - Each element is turned into a list, result of the wrapping of the - string to the desired width - """ - - line_wrapped = [] - for cell, width in zip(line, self._width): - array = [] - for c in cell.split('\n'): - try: - c = unicode(c, 'utf') - except UnicodeDecodeError as strerror: - sys.stderr.write("UnicodeDecodeError exception for string '%s': %s\n" % (c, strerror)) - c = unicode(c, 'utf', 'replace') - array.extend(textwrap.wrap(c, width)) - line_wrapped.append(array) - max_cell_lines = reduce(max, map(len, line_wrapped)) - for cell, valign in zip(line_wrapped, self._valign): - if isheader: - valign = "t" - if valign == "m": - missing = max_cell_lines - len(cell) - cell[:0] = [""] * (missing / 2) - cell.extend([""] * (missing / 2 + missing % 2)) - elif valign == "b": - cell[:0] = [""] * (max_cell_lines - len(cell)) - else: - cell.extend([""] * (max_cell_lines - len(cell))) - return line_wrapped - -if __name__ == '__main__': - table = Texttable() - table.set_cols_align(["l", "r", "c"]) - table.set_cols_valign(["t", "m", "b"]) - table.add_rows([ ["Name", "Age", "Nickname"], - ["Mr\nXavier\nHuon", 32, "Xav'"], - ["Mr\nBaptiste\nClement", 1, "Baby"] ]) - print(table.draw() + "\n") - - table = Texttable() - table.set_deco(Texttable.HEADER) - table.set_cols_dtype(['t', # text - 'f', # float (decimal) - 'e', # float (exponent) - 'i', # integer - 'a']) # automatic - table.set_cols_align(["l", "r", "r", "r", "l"]) - table.add_rows([["text", "float", "exp", "int", "auto"], - ["abcd", "67", 654, 89, 128.001], - ["efghijk", 67.5434, .654, 89.6, 12800000000000000000000.00023], - ["lmn", 5e-78, 5e-78, 89.4, .000000000000128], - ["opqrstu", .023, 5e+78, 92., 12800000000000000000000]]) - print(table.draw()) - diff --git a/requirements.txt b/requirements.txt index bfc91093..7eedd09c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ docker-py==0.2.3 docopt==0.6.1 PyYAML==3.10 +texttable==0.8.1 # docker requires six==1.3.0 six==1.3.0 From 059d240824c78326c9bd92c52a1e3d9e118b6985 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 9 Jan 2014 16:19:22 +0000 Subject: [PATCH 0157/2154] Fix line buffering when there's UTF-8 in a container's output --- fig/cli/log_printer.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index f31fb9b8..dce47d2e 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -55,10 +55,15 @@ def read_websocket(websocket): break def split_buffer(reader, separator): + """ + Given a generator which yields strings and a separator string, + joins all input, splits on the separator and yields each chunk. + Requires that each input string is decodable as UTF-8. + """ buffered = '' for data in reader: - lines = (buffered + data).split(separator) + lines = (buffered + data.decode('utf-8')).split(separator) for line in lines[:-1]: yield line + separator if len(lines) > 1: From 38008a87e848aed4e19f3c8c11cd6398ec6b5f80 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 10 Jan 2014 20:42:00 +0000 Subject: [PATCH 0158/2154] Gif. --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 71d87483..19a9bb0f 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,7 @@ db: Then type `fig up`, and Fig will start and run your entire app: - $ fig up - Pulling image orchardup/postgresql... - Building web... - Starting example_db_1... - Starting example_web_1... - example_db_1 | 2014-01-02 14:47:18 UTC LOG: database system is ready to accept connections - example_web_1 | * Running on http://0.0.0.0:5000/ +![example fig run](https://orchardup.com/static/images/fig-example.5807d0d2dbe6.gif) There are commands to: From c6efb455856c4c71ca4deceed323d463a1b2ecf1 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:13:38 +0000 Subject: [PATCH 0159/2154] Exit travis-install script on error --- script/travis-install | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/travis-install b/script/travis-install index e68423da..0b4889e0 100755 --- a/script/travis-install +++ b/script/travis-install @@ -1,5 +1,7 @@ #!/bin/bash +set -e + sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" sudo apt-get update From 544cd884ee095935c2e0377a1dac4f2df2ca3c5f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:14:02 +0000 Subject: [PATCH 0160/2154] Use Docker 0.7.4 on Travis Also use a package that doesn't disappear and break the tests. --- script/travis-install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/travis-install b/script/travis-install index 0b4889e0..75b4790c 100755 --- a/script/travis-install +++ b/script/travis-install @@ -7,7 +7,7 @@ sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources. sudo apt-get update echo exit 101 | sudo tee /usr/sbin/policy-rc.d sudo chmod +x /usr/sbin/policy-rc.d -sudo apt-get install -qy slirp lxc lxc-docker=0.7.3 +sudo apt-get install -qy slirp lxc lxc-docker-0.7.4 git clone git://github.com/jpetazzo/sekexe python setup.py install pip install -r requirements-dev.txt From 431b3dc2b2d4a931849488b68cd8a34af9d9ae4a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:17:00 +0000 Subject: [PATCH 0161/2154] Move Travis badge out of heading --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19a9bb0f..a069c633 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ -Fig [![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) -==== +Fig +=== + +[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) Punctual, lightweight development environments using Docker. From 0614e2c5905c35c551d5bc558288663e35b5f16c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:31:47 +0000 Subject: [PATCH 0162/2154] Use Docker 0.7.5 on Travis --- script/travis-install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/travis-install b/script/travis-install index 75b4790c..77f5df50 100755 --- a/script/travis-install +++ b/script/travis-install @@ -7,7 +7,7 @@ sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources. sudo apt-get update echo exit 101 | sudo tee /usr/sbin/policy-rc.d sudo chmod +x /usr/sbin/policy-rc.d -sudo apt-get install -qy slirp lxc lxc-docker-0.7.4 +sudo apt-get install -qy slirp lxc lxc-docker-0.7.5 git clone git://github.com/jpetazzo/sekexe python setup.py install pip install -r requirements-dev.txt From d063f0e00c28331f2397ea80eea42681594cedc9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:31:56 +0000 Subject: [PATCH 0163/2154] Add back missing compat module --- fig/compat/__init__.py | 0 fig/compat/functools.py | 23 +++++++++++++++++++++++ fig/project.py | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 fig/compat/__init__.py create mode 100644 fig/compat/functools.py diff --git a/fig/compat/__init__.py b/fig/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fig/compat/functools.py b/fig/compat/functools.py new file mode 100644 index 00000000..38a48c33 --- /dev/null +++ b/fig/compat/functools.py @@ -0,0 +1,23 @@ + +# Taken from python2.7/3.3 functools +def cmp_to_key(mycmp): + """Convert a cmp= function into a key= function""" + class K(object): + __slots__ = ['obj'] + def __init__(self, obj): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + __hash__ = None + return K + diff --git a/fig/project.py b/fig/project.py index 3c536ab6..7c05b2c7 100644 --- a/fig/project.py +++ b/fig/project.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import logging from .service import Service -from .compat import cmp_to_key +from .compat.functools import cmp_to_key log = logging.getLogger(__name__) From 342f187318c4d964dfa9e33d3f0096ee1826bf8e Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:52:37 +0000 Subject: [PATCH 0164/2154] Put python egg cache in a writeable dir --- script/travis | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/travis b/script/travis index 650c9d39..b77a3213 100755 --- a/script/travis +++ b/script/travis @@ -3,6 +3,8 @@ # Exit on first error set -e +export PYTHON_EGG_CACHE="/tmp/.python-eggs" + TRAVIS_PYTHON_VERSION=$1 source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate env From c9c844c27943af674539940405ef4289564e2113 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sat, 11 Jan 2014 14:53:07 +0000 Subject: [PATCH 0165/2154] Print commands travis scripts are running --- script/travis | 2 +- script/travis-install | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/script/travis b/script/travis index b77a3213..3cdacfa5 100755 --- a/script/travis +++ b/script/travis @@ -1,7 +1,7 @@ #!/bin/bash # Exit on first error -set -e +set -ex export PYTHON_EGG_CACHE="/tmp/.python-eggs" diff --git a/script/travis-install b/script/travis-install index 77f5df50..84b1e656 100755 --- a/script/travis-install +++ b/script/travis-install @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -ex sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" From f448a841c563e413488c81ee90f6150ba66e0e62 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 12 Jan 2014 16:58:50 +0000 Subject: [PATCH 0166/2154] New docker-osx installation instructions --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a069c633..9075e0f5 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ Let's get a basic Python web app running on Fig. It assumes a little knowledge o First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.github.com/noplay/docker-osx/master/docker > /usr/local/bin/docker - $ chmod +x /usr/local/bin/docker - $ docker version + $ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx + $ chmod +x /usr/local/bin/docker-osx + $ docker-osx shell Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation. From b92e998929826e806d560414b2e0dc6722e647dd Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 14 Jan 2014 12:39:13 +0000 Subject: [PATCH 0167/2154] 'fig logs' shows output for stopped containers --- fig/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 6b112f41..d2d0af15 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -113,7 +113,7 @@ class TopLevelCommand(Command): Usage: logs [SERVICE...] """ - containers = self.project.containers(service_names=options['SERVICE'], stopped=False) + containers = self.project.containers(service_names=options['SERVICE'], stopped=True) print("Attaching to", list_containers(containers)) LogPrinter(containers, attach_params={'logs': True}).run() From a3d024e11dee584828587c94d98bf958a170afb8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 14 Jan 2014 19:19:15 +0000 Subject: [PATCH 0168/2154] Larger gif in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9075e0f5..e9fd500e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ db: Then type `fig up`, and Fig will start and run your entire app: -![example fig run](https://orchardup.com/static/images/fig-example.5807d0d2dbe6.gif) +![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif) There are commands to: From d4000e07a99954df02f2de1f6bb6aa4ad56edda8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 00:58:46 +0000 Subject: [PATCH 0169/2154] Switch order of connection logic so TCP is tried first --- fig/cli/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fig/cli/utils.py b/fig/cli/utils.py index 249baba7..e9e43785 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -93,9 +93,6 @@ def docker_url(): tcp_host = '127.0.0.1' tcp_port = 4243 - if os.path.exists(socket_path): - return 'unix://%s' % socket_path - for host, port in tcp_hosts: try: s = socket.create_connection((host, port), timeout=1) @@ -104,6 +101,9 @@ def docker_url(): except: pass + if os.path.exists(socket_path): + return 'unix://%s' % socket_path + raise UserError(""" Couldn't find Docker daemon - tried: From 7a1fb3a8d271224c9846d32e3f5ac85dcee4de3e Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 01:54:05 +0000 Subject: [PATCH 0170/2154] Fix ordering of port mapping --- fig/service.py | 10 ++++++++-- tests/service_test.py | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/fig/service.py b/fig/service.py index a2492e7f..5b7b7663 100644 --- a/fig/service.py +++ b/fig/service.py @@ -86,7 +86,7 @@ class Service(object): for port in options['ports']: port = str(port) if ':' in port: - internal_port, external_port = port.split(':', 1) + external_port, internal_port = port.split(':', 1) port_bindings[int(internal_port)] = int(external_port) else: port_bindings[int(port)] = None @@ -134,7 +134,13 @@ class Service(object): container_options['name'] = self.next_container_name(one_off) if 'ports' in container_options: - container_options['ports'] = [str(p).split(':')[0] for p in container_options['ports']] + ports = [] + for port in container_options['ports']: + port = str(port) + if ':' in port: + port = port.split(':')[-1] + ports.append(port) + container_options['ports'] = ports if 'volumes' in container_options: container_options['volumes'] = dict((v.split(':')[1], {}) for v in container_options['volumes']) diff --git a/tests/service_test.py b/tests/service_test.py index bc5c6ffe..d2078414 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -157,4 +157,9 @@ class ServiceTest(DockerClientTestCase): self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') + def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self): + service = self.create_service('web', ports=['8001:8000']) + container = service.start_container().inspect() + self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) + self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8001') From 887a30e327135ce31fb2bba36ed740cd517529e3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 12:07:18 +0000 Subject: [PATCH 0171/2154] Clarify when 'fig stop' is necessary in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9fd500e..e2e87f75 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ If you want to run your services in the background, you can pass the `-d` flag t See `fig --help` other commands that are available. -You'll probably want to stop your services when you've finished with them: +If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them: $ fig stop From a8e275a4321c4bf743a88793ed9f14cd905df053 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 12:01:17 +0000 Subject: [PATCH 0172/2154] Implement UserError __unicode__ method --- fig/cli/errors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fig/cli/errors.py b/fig/cli/errors.py index bb1702fd..874d3591 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 From 3c5e334d9d0c9c20f825ee8a16385d4718cfabb9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 3 Jan 2014 11:18:59 +0000 Subject: [PATCH 0173/2154] Add recreate_containers method to service --- fig/project.py | 7 +++++++ fig/service.py | 19 +++++++++++++++++++ tests/service_test.py | 7 +++++++ 3 files changed, 33 insertions(+) diff --git a/fig/project.py b/fig/project.py index 7c05b2c7..95d13ee9 100644 --- a/fig/project.py +++ b/fig/project.py @@ -87,6 +87,13 @@ class Project(object): if len(service.containers(stopped=True)) == 0: service.create_container() + def recreate_containers(self, service_names): + """ + For each service, create or recreate their containers. + """ + for service in self.get_services(service_names): + service.recreate_containers() + def start(self, service_names=None, **options): for service in self.get_services(service_names): service.start(**options) diff --git a/fig/service.py b/fig/service.py index 5b7b7663..9ae47a36 100644 --- a/fig/service.py +++ b/fig/service.py @@ -73,6 +73,25 @@ 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, remove and recreate them. + """ + containers = self.containers(stopped=True) + if len(containers) == 0: + return [self.create_container(**override_options)] + else: + new_containers = [] + for old_container in containers: + if old_container.is_running: + old_container.stop() + options = dict(override_options) + options['volumes_from'] = old_container.id + new_containers.append(self.create_container(**options)) + old_container.remove() + return new_containers + def start_container(self, container=None, **override_options): if container is None: container = self.create_container(**override_options) diff --git a/tests/service_test.py b/tests/service_test.py index d2078414..202c5e26 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -102,6 +102,13 @@ class ServiceTest(DockerClientTestCase): container = db.create_container(one_off=True) self.assertEqual(container.name, 'figtest_db_run_1') + def test_recreate_containers(self): + service = self.create_service('db') + container = service.create_container() + new_container = service.recreate_containers()[0] + self.assertEqual(len(service.containers(stopped=True)), 1) + self.assertNotEqual(container.id, new_container.id) + def test_start_container_passes_through_options(self): db = self.create_service('db') db.start_container(environment={'FOO': 'BAR'}) From 207e83ac2f5345e3ecc7e4ff8f282284cb2f7d83 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 12:22:55 +0000 Subject: [PATCH 0174/2154] Be sure to test that recreate_containers updates config --- tests/service_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/service_test.py b/tests/service_test.py index 202c5e26..ea7db0ca 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -103,9 +103,14 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(container.name, 'figtest_db_run_1') def test_recreate_containers(self): - service = self.create_service('db') + service = self.create_service('db', environment={'FOO': '1'}) container = service.create_container() + self.assertEqual(container.dictionary['Config']['Env'], ['FOO=1']) + + service.options['environment']['FOO'] = '2' new_container = service.recreate_containers()[0] + self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2']) + self.assertEqual(len(service.containers(stopped=True)), 1) self.assertNotEqual(container.id, new_container.id) From 3669236aa18b234b20b622c818662eb6f52592a5 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 12:43:40 +0000 Subject: [PATCH 0175/2154] Support volumes in config with an unspecified host path --- fig/service.py | 18 +++++++++++++++--- tests/service_test.py | 6 ++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/fig/service.py b/fig/service.py index 9ae47a36..d16ab956 100644 --- a/fig/service.py +++ b/fig/service.py @@ -114,8 +114,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(), @@ -162,7 +163,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: @@ -233,3 +234,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/service_test.py b/tests/service_test.py index ea7db0ca..c59b4ebb 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -102,6 +102,12 @@ 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'}) container = service.create_container() From bdc6b47e1f8a0d9aafef7cb3a0b261b7f0f8bf4f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 13:06:25 +0000 Subject: [PATCH 0176/2154] service.recreate_containers() no longer removes the old containers We need to keep them around until the new ones have been started. --- fig/service.py | 11 +++++------ tests/service_test.py | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/fig/service.py b/fig/service.py index d16ab956..c6396d8c 100644 --- a/fig/service.py +++ b/fig/service.py @@ -76,21 +76,20 @@ class Service(object): def recreate_containers(self, **override_options): """ If a container for this service doesn't exist, create one. If there are - any, stop, remove and recreate them. + any, stop them and create new ones. Does not remove the old containers. """ - containers = self.containers(stopped=True) - if len(containers) == 0: + old_containers = self.containers(stopped=True) + if len(old_containers) == 0: return [self.create_container(**override_options)] else: new_containers = [] - for old_container in containers: + for old_container in old_containers: if old_container.is_running: old_container.stop() options = dict(override_options) options['volumes_from'] = old_container.id new_containers.append(self.create_container(**options)) - old_container.remove() - return new_containers + return (old_containers, new_containers) def start_container(self, container=None, **override_options): if container is None: diff --git a/tests/service_test.py b/tests/service_test.py index c59b4ebb..ee2e9093 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -109,16 +109,24 @@ class ServiceTest(DockerClientTestCase): self.assertIn('/var/db', container.inspect()['Volumes']) def test_recreate_containers(self): - service = self.create_service('db', environment={'FOO': '1'}) - container = service.create_container() - self.assertEqual(container.dictionary['Config']['Env'], ['FOO=1']) + 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']) + service.start_container(old_container) + volume_path = old_container.inspect()['Volumes']['/var/db'] service.options['environment']['FOO'] = '2' - new_container = service.recreate_containers()[0] - self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2']) + (old, new) = service.recreate_containers() + self.assertEqual(old, [old_container]) + self.assertEqual(len(new), 1) - self.assertEqual(len(service.containers(stopped=True)), 1) - self.assertNotEqual(container.id, new_container.id) + new_container = new[0] + self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2']) + service.start_container(new_container) + self.assertEqual(new_container.inspect()['Volumes']['/var/db'], volume_path) + + self.assertEqual(len(service.containers(stopped=True)), 2) + self.assertNotEqual(old_container.id, new_container.id) def test_start_container_passes_through_options(self): db = self.create_service('db') From f5f93577366ac1cfdbb7ea803c157959aa9a7009 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 13:17:39 +0000 Subject: [PATCH 0177/2154] Remove project.create_containers(), revamp project.recreate_containers() `recreate_containers` now returns two lists of old+new containers, along with their services. --- fig/project.py | 22 ++++++++++++---------- fig/service.py | 2 +- tests/project_test.py | 13 +++++++++---- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/fig/project.py b/fig/project.py index 95d13ee9..f77da5f7 100644 --- a/fig/project.py +++ b/fig/project.py @@ -79,20 +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): - """ - For each service, creates a container if there are none. - """ - for service in self.get_services(service_names): - if len(service.containers(stopped=True)) == 0: - service.create_container() - - def recreate_containers(self, service_names): + def recreate_containers(self, service_names=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): - service.recreate_containers() + (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 c6396d8c..91a89791 100644 --- a/fig/service.py +++ b/fig/service.py @@ -80,7 +80,7 @@ class Service(object): """ old_containers = self.containers(stopped=True) if len(old_containers) == 0: - return [self.create_container(**override_options)] + return ([], [self.create_container(**override_options)]) else: new_containers = [] for old_container in old_containers: diff --git a/tests/project_test.py b/tests/project_test.py index 09792fab..bad9f612 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -42,17 +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() - self.assertEqual(len(web.containers(stopped=True)), 1) + (old, new) = project.recreate_containers() + self.assertEqual(old, [(web, old_web_container)]) + self.assertEqual(len(new), 2) + self.assertEqual(new[0][0], web) + self.assertEqual(new[1][0], db) + + self.assertEqual(len(web.containers(stopped=True)), 2) self.assertEqual(len(db.containers(stopped=True)), 1) def test_start_stop_kill_remove(self): From 5db6c9f51ba045220ec1c8f3e12e6fbe620cf3d4 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 13:25:40 +0000 Subject: [PATCH 0178/2154] Rework 'fig up' to use recreate_containers() --- fig/cli/main.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index d2d0af15..51c4d27f 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: From 8a0071d9c150a3b11ac8ee5f8cc05601963bce9f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 15:51:39 +0000 Subject: [PATCH 0179/2154] Reduce stop() timeout when recreating containers --- fig/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index 91a89791..e81ec810 100644 --- a/fig/service.py +++ b/fig/service.py @@ -85,7 +85,7 @@ class Service(object): new_containers = [] for old_container in old_containers: if old_container.is_running: - old_container.stop() + old_container.stop(timeout=1) options = dict(override_options) options['volumes_from'] = old_container.id new_containers.append(self.create_container(**options)) From 3956d85a8c2a8ce0ab8edfd70dc474fda6b88b4b Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 16:15:46 +0000 Subject: [PATCH 0180/2154] Refactor recreate_containers() in preparation for smart name-preserving logic --- fig/service.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/fig/service.py b/fig/service.py index e81ec810..d7e2fff3 100644 --- a/fig/service.py +++ b/fig/service.py @@ -78,19 +78,30 @@ class Service(object): 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. """ - old_containers = self.containers(stopped=True) - if len(old_containers) == 0: + containers = self.containers(stopped=True) + + if len(containers) == 0: return ([], [self.create_container(**override_options)]) else: + old_containers = [] new_containers = [] - for old_container in old_containers: - if old_container.is_running: - old_container.stop(timeout=1) - options = dict(override_options) - options['volumes_from'] = old_container.id - new_containers.append(self.create_container(**options)) + + for c in containers: + (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) + + options = dict(override_options) + options['volumes_from'] = container.id + + return (container, self.create_container(**options)) + def start_container(self, container=None, **override_options): if container is None: container = self.create_container(**override_options) From ea4753c49a95c9f2cedb9c00081499b17898d6b8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 17:06:16 +0000 Subject: [PATCH 0181/2154] Use an anonymous intermediate container so that when recreating containers, suffixes always start from 1 --- fig/service.py | 17 ++++++++++++++--- tests/service_test.py | 8 ++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/fig/service.py b/fig/service.py index d7e2fff3..c9a36499 100644 --- a/fig/service.py +++ b/fig/service.py @@ -97,10 +97,21 @@ class Service(object): if container.is_running: container.stop(timeout=1) - options = dict(override_options) - options['volumes_from'] = container.id + intermediate_container = Container.create( + self.client, + image='ubuntu', + command='echo', + volumes_from=container.id, + ) + intermediate_container.start() + intermediate_container.wait() + container.remove() - return (container, self.create_container(**options)) + 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: diff --git a/tests/service_test.py b/tests/service_test.py index ee2e9093..2ccf1755 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -112,20 +112,24 @@ class ServiceTest(DockerClientTestCase): 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(old, [old_container]) + 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(service.containers(stopped=True)), 2) + 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): From 8c583d1bb2f53f336437bb89821093c258cc21e6 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 15 Jan 2014 18:06:49 +0000 Subject: [PATCH 0182/2154] Quieter log output when recreating Moved log stuff to Service, which I think makes more sense anyway. Maybe. --- fig/container.py | 7 ------- fig/service.py | 6 ++++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/fig/container.py b/fig/container.py index 9556ec1f..24b239ac 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/service.py b/fig/service.py index c9a36499..e7300410 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): @@ -81,12 +85,14 @@ class Service(object): 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) From ee0c4bf690a721d205849b780fb48b191da39dd2 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 11:56:02 +0000 Subject: [PATCH 0183/2154] Fix test regression --- tests/project_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/project_test.py b/tests/project_test.py index bad9f612..a73aa252 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -52,12 +52,13 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(db.containers(stopped=True)), 0) (old, new) = project.recreate_containers() - self.assertEqual(old, [(web, old_web_container)]) + 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)), 2) + self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 1) def test_start_stop_kill_remove(self): From e38b403b148b5b985d7dfc6965cd1adce566adae Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 12:06:50 +0000 Subject: [PATCH 0184/2154] Update README for new 'fig up' behaviour --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e2e87f75..f9139497 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, so that changes in `fig.yml` are picked up. ### Environment variables From 7b31fdf6f60de481c9d1458fefc2e0e206ea1279 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 12:41:18 +0000 Subject: [PATCH 0185/2154] Clarify that volumes are preserved when recreating containers --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f9139497..2f9599a8 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,7 @@ Build, (re)create, start and attach to containers for a service. 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, so that changes in `fig.yml` are picked up. +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 @@ -267,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/ From cdcea98290e01a90fa4ebfbd4528b3dfd890d1d6 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 13:17:00 +0000 Subject: [PATCH 0186/2154] Copy readme commands docs to CLI docstrings --- README.md | 6 ++++-- fig/cli/main.py | 35 +++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 2f9599a8..9499fc86 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ View output from services. #### ps -List running containers. +List containers. #### rm @@ -221,7 +221,9 @@ Remove stopped service containers. #### run -Run a one-off command for a service. E.g.: +Run a one-off command on a service. + +For example: $ fig run web python manage.py shell diff --git a/fig/cli/main.py b/fig/cli/main.py index 51c4d27f..b7a84fa3 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -95,13 +95,17 @@ class TopLevelCommand(Command): """ Build or rebuild services. + 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. + Usage: build [SERVICE...] """ self.project.build(service_names=options['SERVICE']) def kill(self, options): """ - Kill containers. + Force stop service containers. Usage: kill [SERVICE...] """ @@ -150,7 +154,7 @@ class TopLevelCommand(Command): def rm(self, options): """ - Remove stopped containers + Remove stopped service containers. Usage: rm [SERVICE...] """ @@ -166,7 +170,15 @@ class TopLevelCommand(Command): def run(self, options): """ - Run a one-off command. + Run a one-off command on a service. + + 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. Usage: run [options] SERVICE COMMAND [ARGS...] @@ -203,7 +215,9 @@ class TopLevelCommand(Command): def stop(self, options): """ - Stop running containers. + Stop running containers without removing them. + + They can be started again with `fig start`. Usage: stop [SERVICE...] """ @@ -211,12 +225,21 @@ class TopLevelCommand(Command): def up(self, options): """ - Create and start containers. + Build, (re)create, start and attach to containers for a service. + + 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. 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 """ detached = options['-d'] From b1e7f548f4058baf0050f804a0d928b128bcf191 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 13:24:43 +0000 Subject: [PATCH 0187/2154] Add help command --- README.md | 4 ++++ fig/cli/main.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/README.md b/README.md index 2f9599a8..c290e126 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,10 @@ Build or rebuild services. 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. +#### help + +Get help on a command. + #### kill Force stop service containers. diff --git a/fig/cli/main.py b/fig/cli/main.py index 51c4d27f..0eaf7874 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -76,6 +76,7 @@ class TopLevelCommand(Command): Commands: build Build or rebuild services + help Get help on a command kill Kill containers logs View output from containers ps List containers @@ -99,6 +100,17 @@ class TopLevelCommand(Command): """ self.project.build(service_names=options['SERVICE']) + def help(self, options): + """ + Get help on a command. + + Usage: help COMMAND + """ + command = options['COMMAND'] + if not hasattr(self, command): + raise NoSuchCommand(command, self) + raise SystemExit(getdoc(getattr(self, command))) + def kill(self, options): """ Kill containers. From 21528f08d4ff2987e59b9ea888893120352dbb6a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 12:56:36 +0000 Subject: [PATCH 0188/2154] Vendor docker-py From https://github.com/aanand/docker-py/commit/9dc03c57373a92076a7ea323f00d9717f97cd35e --- fig/cli/command.py | 2 +- fig/cli/main.py | 2 +- fig/packages/__init__.py | 0 fig/packages/docker/__init__.py | 15 + fig/packages/docker/auth/__init__.py | 7 + fig/packages/docker/auth/auth.py | 153 +++++ fig/packages/docker/client.py | 746 +++++++++++++++++++++++ fig/packages/docker/unixconn/__init__.py | 1 + fig/packages/docker/unixconn/unixconn.py | 71 +++ fig/packages/docker/utils/__init__.py | 3 + fig/packages/docker/utils/utils.py | 96 +++ fig/service.py | 2 +- requirements.txt | 3 +- tests/testcases.py | 2 +- 14 files changed, 1098 insertions(+), 5 deletions(-) create mode 100644 fig/packages/__init__.py create mode 100644 fig/packages/docker/__init__.py create mode 100644 fig/packages/docker/auth/__init__.py create mode 100644 fig/packages/docker/auth/auth.py create mode 100644 fig/packages/docker/client.py create mode 100644 fig/packages/docker/unixconn/__init__.py create mode 100644 fig/packages/docker/unixconn/unixconn.py create mode 100644 fig/packages/docker/utils/__init__.py create mode 100644 fig/packages/docker/utils/utils.py diff --git a/fig/cli/command.py b/fig/cli/command.py index 4e0705a9..00452dd3 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import -from docker import Client +from ..packages.docker import Client import errno import logging import os diff --git a/fig/cli/main.py b/fig/cli/main.py index 51c4d27f..c0e42143 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -15,7 +15,7 @@ from .formatter import Formatter from .log_printer import LogPrinter from .utils import yesno -from docker.client import APIError +from ..packages.docker.client import APIError from .errors import UserError from .docopt_command import NoSuchCommand from .socketclient import SocketClient diff --git a/fig/packages/__init__.py b/fig/packages/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/fig/packages/docker/__init__.py b/fig/packages/docker/__init__.py new file mode 100644 index 00000000..5f642a85 --- /dev/null +++ b/fig/packages/docker/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2013 dotCloud inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from .client import Client, APIError # flake8: noqa diff --git a/fig/packages/docker/auth/__init__.py b/fig/packages/docker/auth/__init__.py new file mode 100644 index 00000000..66acdb36 --- /dev/null +++ b/fig/packages/docker/auth/__init__.py @@ -0,0 +1,7 @@ +from .auth import ( + INDEX_URL, + encode_header, + load_config, + resolve_authconfig, + resolve_repository_name +) # flake8: noqa \ No newline at end of file diff --git a/fig/packages/docker/auth/auth.py b/fig/packages/docker/auth/auth.py new file mode 100644 index 00000000..bef010f2 --- /dev/null +++ b/fig/packages/docker/auth/auth.py @@ -0,0 +1,153 @@ +# Copyright 2013 dotCloud inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import fileinput +import json +import os + +import six + +from ..utils import utils + +INDEX_URL = 'https://index.docker.io/v1/' +DOCKER_CONFIG_FILENAME = '.dockercfg' + + +def swap_protocol(url): + if url.startswith('http://'): + return url.replace('http://', 'https://', 1) + if url.startswith('https://'): + return url.replace('https://', 'http://', 1) + return url + + +def expand_registry_url(hostname): + if hostname.startswith('http:') or hostname.startswith('https:'): + if '/' not in hostname[9:]: + hostname = hostname + '/v1/' + return hostname + if utils.ping('https://' + hostname + '/v1/_ping'): + return 'https://' + hostname + '/v1/' + return 'http://' + hostname + '/v1/' + + +def resolve_repository_name(repo_name): + if '://' in repo_name: + raise ValueError('Repository name cannot contain a ' + 'scheme ({0})'.format(repo_name)) + parts = repo_name.split('/', 1) + if not '.' in parts[0] and not ':' in parts[0] and parts[0] != 'localhost': + # This is a docker index repo (ex: foo/bar or ubuntu) + return INDEX_URL, repo_name + if len(parts) < 2: + raise ValueError('Invalid repository name ({0})'.format(repo_name)) + + if 'index.docker.io' in parts[0]: + raise ValueError('Invalid repository name,' + 'try "{0}" instead'.format(parts[1])) + + return expand_registry_url(parts[0]), parts[1] + + +def resolve_authconfig(authconfig, registry=None): + """Return the authentication data from the given auth configuration for a + specific registry. We'll do our best to infer the correct URL for the + registry, trying both http and https schemes. Returns an empty dictionnary + if no data exists.""" + # Default to the public index server + registry = registry or INDEX_URL + + # Ff its not the index server there are three cases: + # + # 1. this is a full config url -> it should be used as is + # 2. it could be a full url, but with the wrong protocol + # 3. it can be the hostname optionally with a port + # + # as there is only one auth entry which is fully qualified we need to start + # parsing and matching + if '/' not in registry: + registry = registry + '/v1/' + if not registry.startswith('http:') and not registry.startswith('https:'): + registry = 'https://' + registry + + if registry in authconfig: + return authconfig[registry] + return authconfig.get(swap_protocol(registry), None) + + +def decode_auth(auth): + if isinstance(auth, six.string_types): + auth = auth.encode('ascii') + s = base64.b64decode(auth) + login, pwd = s.split(b':') + return login.decode('ascii'), pwd.decode('ascii') + + +def encode_header(auth): + auth_json = json.dumps(auth).encode('ascii') + return base64.b64encode(auth_json) + + +def load_config(root=None): + """Loads authentication data from a Docker configuration file in the given + root directory.""" + conf = {} + data = None + + config_file = os.path.join(root or os.environ.get('HOME', '.'), + DOCKER_CONFIG_FILENAME) + + # First try as JSON + try: + with open(config_file) as f: + conf = {} + for registry, entry in six.iteritems(json.load(f)): + username, password = decode_auth(entry['auth']) + conf[registry] = { + 'username': username, + 'password': password, + 'email': entry['email'], + 'serveraddress': registry, + } + return conf + except: + pass + + # If that fails, we assume the configuration file contains a single + # authentication token for the public registry in the following format: + # + # auth = AUTH_TOKEN + # email = email@domain.com + try: + data = [] + for line in fileinput.input(config_file): + data.append(line.strip().split(' = ')[1]) + if len(data) < 2: + # Not enough data + raise Exception('Invalid or empty configuration file!') + + username, password = decode_auth(data[0]) + conf[INDEX_URL] = { + 'username': username, + 'password': password, + 'email': data[1], + 'serveraddress': INDEX_URL, + } + return conf + except: + pass + + # If all fails, return an empty config + return {} diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py new file mode 100644 index 00000000..e3cd976c --- /dev/null +++ b/fig/packages/docker/client.py @@ -0,0 +1,746 @@ +# Copyright 2013 dotCloud inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re +import shlex +import struct + +import requests +import requests.exceptions +import six + +from .auth import auth +from .unixconn import unixconn +from .utils import utils + +if not six.PY3: + import websocket + +DEFAULT_TIMEOUT_SECONDS = 60 +STREAM_HEADER_SIZE_BYTES = 8 + + +class APIError(requests.exceptions.HTTPError): + def __init__(self, message, response, explanation=None): + super(APIError, self).__init__(message, response=response) + + self.explanation = explanation + + if self.explanation is None and response.content: + self.explanation = response.content.strip() + + def __str__(self): + message = super(APIError, self).__str__() + + if self.is_client_error(): + message = '%s Client Error: %s' % ( + self.response.status_code, self.response.reason) + + elif self.is_server_error(): + message = '%s Server Error: %s' % ( + self.response.status_code, self.response.reason) + + if self.explanation: + message = '%s ("%s")' % (message, self.explanation) + + return message + + def is_client_error(self): + return 400 <= self.response.status_code < 500 + + def is_server_error(self): + return 500 <= self.response.status_code < 600 + + +class Client(requests.Session): + def __init__(self, base_url=None, version="1.6", + timeout=DEFAULT_TIMEOUT_SECONDS): + super(Client, self).__init__() + if base_url is None: + base_url = "unix://var/run/docker.sock" + if base_url.startswith('unix:///'): + base_url = base_url.replace('unix:/', 'unix:') + if base_url.startswith('tcp:'): + base_url = base_url.replace('tcp:', 'http:') + if base_url.endswith('/'): + base_url = base_url[:-1] + self.base_url = base_url + self._version = version + self._timeout = timeout + self._auth_configs = auth.load_config() + + self.mount('unix://', unixconn.UnixAdapter(base_url, timeout)) + + def _set_request_timeout(self, kwargs): + """Prepare the kwargs for an HTTP request by inserting the timeout + parameter, if not already present.""" + kwargs.setdefault('timeout', self._timeout) + return kwargs + + def _post(self, url, **kwargs): + return self.post(url, **self._set_request_timeout(kwargs)) + + def _get(self, url, **kwargs): + return self.get(url, **self._set_request_timeout(kwargs)) + + def _delete(self, url, **kwargs): + return self.delete(url, **self._set_request_timeout(kwargs)) + + def _url(self, path): + return '{0}/v{1}{2}'.format(self.base_url, self._version, path) + + def _raise_for_status(self, response, explanation=None): + """Raises stored :class:`APIError`, if one occurred.""" + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + raise APIError(e, response, explanation=explanation) + + def _result(self, response, json=False, binary=False): + assert not (json and binary) + self._raise_for_status(response) + + if json: + return response.json() + if binary: + return response.content + return response.text + + def _container_config(self, image, command, hostname=None, user=None, + detach=False, stdin_open=False, tty=False, + mem_limit=0, ports=None, environment=None, dns=None, + volumes=None, volumes_from=None, + network_disabled=False): + if isinstance(command, six.string_types): + command = shlex.split(str(command)) + if isinstance(environment, dict): + environment = [ + '{0}={1}'.format(k, v) for k, v in environment.items() + ] + + if ports and isinstance(ports, list): + exposed_ports = {} + for port_definition in ports: + port = port_definition + proto = None + if isinstance(port_definition, tuple): + if len(port_definition) == 2: + proto = port_definition[1] + port = port_definition[0] + exposed_ports['{0}{1}'.format( + port, + '/' + proto if proto else '' + )] = {} + ports = exposed_ports + + if volumes and isinstance(volumes, list): + volumes_dict = {} + for vol in volumes: + volumes_dict[vol] = {} + volumes = volumes_dict + + attach_stdin = False + attach_stdout = False + attach_stderr = False + + if not detach: + attach_stdout = True + attach_stderr = True + + if stdin_open: + attach_stdin = True + + return { + 'Hostname': hostname, + 'ExposedPorts': ports, + 'User': user, + 'Tty': tty, + 'OpenStdin': stdin_open, + 'Memory': mem_limit, + 'AttachStdin': attach_stdin, + 'AttachStdout': attach_stdout, + 'AttachStderr': attach_stderr, + 'Env': environment, + 'Cmd': command, + 'Dns': dns, + 'Image': image, + 'Volumes': volumes, + 'VolumesFrom': volumes_from, + 'NetworkDisabled': network_disabled + } + + def _post_json(self, url, data, **kwargs): + # Go <1.1 can't unserialize null to a string + # so we do this disgusting thing here. + data2 = {} + if data is not None: + for k, v in six.iteritems(data): + if v is not None: + data2[k] = v + + if 'headers' not in kwargs: + kwargs['headers'] = {} + kwargs['headers']['Content-Type'] = 'application/json' + return self._post(url, data=json.dumps(data2), **kwargs) + + def _attach_params(self, override=None): + return override or { + 'stdout': 1, + 'stderr': 1, + 'stream': 1 + } + + def _attach_websocket(self, container, params=None): + if six.PY3: + raise NotImplementedError("This method is not currently supported " + "under python 3") + url = self._url("/containers/{0}/attach/ws".format(container)) + req = requests.Request("POST", url, params=self._attach_params(params)) + full_url = req.prepare().url + full_url = full_url.replace("http://", "ws://", 1) + full_url = full_url.replace("https://", "wss://", 1) + return self._create_websocket_connection(full_url) + + def _create_websocket_connection(self, url): + return websocket.create_connection(url) + + def _stream_result(self, response): + """Generator for straight-out, non chunked-encoded HTTP responses.""" + self._raise_for_status(response) + for line in response.iter_lines(chunk_size=1): + # filter out keep-alive new lines + if line: + yield line + '\n' + + def _stream_result_socket(self, response): + self._raise_for_status(response) + return response.raw._fp.fp._sock + + def _stream_helper(self, response): + """Generator for data coming from a chunked-encoded HTTP response.""" + socket_fp = self._stream_result_socket(response) + socket_fp.setblocking(1) + socket = socket_fp.makefile() + while True: + size = int(socket.readline(), 16) + if size <= 0: + break + data = socket.readline() + if not data: + break + yield data + + def _multiplexed_buffer_helper(self, response): + """A generator of multiplexed data blocks read from a buffered + response.""" + buf = self._result(response, binary=True) + walker = 0 + while True: + if len(buf[walker:]) < 8: + break + _, length = struct.unpack_from('>BxxxL', buf[walker:]) + start = walker + STREAM_HEADER_SIZE_BYTES + end = start + length + walker = end + yield str(buf[start:end]) + + def _multiplexed_socket_stream_helper(self, response): + """A generator of multiplexed data blocks coming from a response + socket.""" + socket = self._stream_result_socket(response) + + def recvall(socket, size): + data = '' + while size > 0: + block = socket.recv(size) + if not block: + return None + + data += block + size -= len(block) + return data + + while True: + socket.settimeout(None) + header = recvall(socket, STREAM_HEADER_SIZE_BYTES) + if not header: + break + _, length = struct.unpack('>BxxxL', header) + if not length: + break + data = recvall(socket, length) + if not data: + break + yield data + + def attach(self, container, stdout=True, stderr=True, + stream=False, logs=False): + if isinstance(container, dict): + container = container.get('Id') + params = { + 'logs': logs and 1 or 0, + 'stdout': stdout and 1 or 0, + 'stderr': stderr and 1 or 0, + 'stream': stream and 1 or 0, + } + u = self._url("/containers/{0}/attach".format(container)) + response = self._post(u, params=params, stream=stream) + + # Stream multi-plexing was introduced in API v1.6. + if utils.compare_version('1.6', self._version) < 0: + return stream and self._stream_result(response) or \ + self._result(response, binary=True) + + return stream and self._multiplexed_socket_stream_helper(response) or \ + ''.join([x for x in self._multiplexed_buffer_helper(response)]) + + def attach_socket(self, container, params=None, ws=False): + if params is None: + params = { + 'stdout': 1, + 'stderr': 1, + 'stream': 1 + } + if ws: + return self._attach_websocket(container, params) + + if isinstance(container, dict): + container = container.get('Id') + u = self._url("/containers/{0}/attach".format(container)) + return self._stream_result_socket(self.post( + u, None, params=self._attach_params(params), stream=True)) + + def build(self, path=None, tag=None, quiet=False, fileobj=None, + nocache=False, rm=False, stream=False, timeout=None): + remote = context = headers = None + if path is None and fileobj is None: + raise Exception("Either path or fileobj needs to be provided.") + + if fileobj is not None: + context = utils.mkbuildcontext(fileobj) + elif path.startswith(('http://', 'https://', 'git://', 'github.com/')): + remote = path + else: + context = utils.tar(path) + + u = self._url('/build') + params = { + 't': tag, + 'remote': remote, + 'q': quiet, + 'nocache': nocache, + 'rm': rm + } + if context is not None: + headers = {'Content-Type': 'application/tar'} + + response = self._post( + u, + data=context, + params=params, + headers=headers, + stream=stream, + timeout=timeout, + ) + + if context is not None: + context.close() + if stream: + return self._stream_result(response) + else: + output = self._result(response) + srch = r'Successfully built ([0-9a-f]+)' + match = re.search(srch, output) + if not match: + return None, output + return match.group(1), output + + def commit(self, container, repository=None, tag=None, message=None, + author=None, conf=None): + params = { + 'container': container, + 'repo': repository, + 'tag': tag, + 'comment': message, + 'author': author + } + u = self._url("/commit") + return self._result(self._post_json(u, data=conf, params=params), + json=True) + + def containers(self, quiet=False, all=False, trunc=True, latest=False, + since=None, before=None, limit=-1): + params = { + 'limit': 1 if latest else limit, + 'all': 1 if all else 0, + 'trunc_cmd': 1 if trunc else 0, + 'since': since, + 'before': before + } + u = self._url("/containers/json") + res = self._result(self._get(u, params=params), True) + + if quiet: + return [{'Id': x['Id']} for x in res] + return res + + def copy(self, container, resource): + res = self._post_json( + self._url("/containers/{0}/copy".format(container)), + data={"Resource": resource}, + stream=True + ) + self._raise_for_status(res) + return res.raw + + def create_container(self, image, command=None, hostname=None, user=None, + detach=False, stdin_open=False, tty=False, + mem_limit=0, ports=None, environment=None, dns=None, + volumes=None, volumes_from=None, + network_disabled=False, name=None): + + config = self._container_config( + image, command, hostname, user, detach, stdin_open, tty, mem_limit, + ports, environment, dns, volumes, volumes_from, network_disabled + ) + return self.create_container_from_config(config, name) + + def create_container_from_config(self, config, name=None): + u = self._url("/containers/create") + params = { + 'name': name + } + res = self._post_json(u, data=config, params=params) + return self._result(res, True) + + def diff(self, container): + if isinstance(container, dict): + container = container.get('Id') + return self._result(self._get(self._url("/containers/{0}/changes". + format(container))), True) + + def events(self): + u = self._url("/events") + + socket = self._stream_result_socket(self.get(u, stream=True)) + + while True: + chunk = socket.recv(4096) + if chunk: + # Messages come in the format of length, data, newline. + length, data = chunk.split("\n", 1) + length = int(length, 16) + if length > len(data): + data += socket.recv(length - len(data)) + yield json.loads(data) + else: + break + + def export(self, container): + if isinstance(container, dict): + container = container.get('Id') + res = self._get(self._url("/containers/{0}/export".format(container)), + stream=True) + self._raise_for_status(res) + return res.raw + + def history(self, image): + res = self._get(self._url("/images/{0}/history".format(image))) + self._raise_for_status(res) + return self._result(res) + + def images(self, name=None, quiet=False, all=False, viz=False): + if viz: + return self._result(self._get(self._url("images/viz"))) + params = { + 'filter': name, + 'only_ids': 1 if quiet else 0, + 'all': 1 if all else 0, + } + res = self._result(self._get(self._url("/images/json"), params=params), + True) + if quiet: + return [x['Id'] for x in res] + return res + + def import_image(self, src, data=None, repository=None, tag=None): + u = self._url("/images/create") + params = { + 'repo': repository, + 'tag': tag + } + try: + # XXX: this is ways not optimal but the only way + # for now to import tarballs through the API + fic = open(src) + data = fic.read() + fic.close() + src = "-" + except IOError: + # file does not exists or not a file (URL) + data = None + if isinstance(src, six.string_types): + params['fromSrc'] = src + return self._result(self._post(u, data=data, params=params)) + + return self._result(self._post(u, data=src, params=params)) + + def info(self): + return self._result(self._get(self._url("/info")), + True) + + def insert(self, image, url, path): + api_url = self._url("/images/" + image + "/insert") + params = { + 'url': url, + 'path': path + } + return self._result(self._post(api_url, params=params)) + + def inspect_container(self, container): + if isinstance(container, dict): + container = container.get('Id') + return self._result( + self._get(self._url("/containers/{0}/json".format(container))), + True) + + def inspect_image(self, image_id): + return self._result( + self._get(self._url("/images/{0}/json".format(image_id))), + True + ) + + def kill(self, container, signal=None): + if isinstance(container, dict): + container = container.get('Id') + url = self._url("/containers/{0}/kill".format(container)) + params = {} + if signal is not None: + params['signal'] = signal + res = self._post(url, params=params) + + self._raise_for_status(res) + + def login(self, username, password=None, email=None, registry=None, + reauth=False): + # If we don't have any auth data so far, try reloading the config file + # one more time in case anything showed up in there. + if not self._auth_configs: + self._auth_configs = auth.load_config() + + registry = registry or auth.INDEX_URL + + authcfg = auth.resolve_authconfig(self._auth_configs, registry) + # If we found an existing auth config for this registry and username + # combination, we can return it immediately unless reauth is requested. + if authcfg and authcfg.get('username', None) == username \ + and not reauth: + return authcfg + + req_data = { + 'username': username, + 'password': password, + 'email': email, + 'serveraddress': registry, + } + + response = self._post_json(self._url('/auth'), data=req_data) + if response.status_code == 200: + self._auth_configs[registry] = req_data + return self._result(response, json=True) + + def logs(self, container, stdout=True, stderr=True, stream=False): + return self.attach( + container, + stdout=stdout, + stderr=stderr, + stream=stream, + logs=True + ) + + def port(self, container, private_port): + if isinstance(container, dict): + container = container.get('Id') + res = self._get(self._url("/containers/{0}/json".format(container))) + self._raise_for_status(res) + json_ = res.json() + s_port = str(private_port) + f_port = None + if s_port in json_['NetworkSettings']['PortMapping']['Udp']: + f_port = json_['NetworkSettings']['PortMapping']['Udp'][s_port] + elif s_port in json_['NetworkSettings']['PortMapping']['Tcp']: + f_port = json_['NetworkSettings']['PortMapping']['Tcp'][s_port] + + return f_port + + def pull(self, repository, tag=None, stream=False): + registry, repo_name = auth.resolve_repository_name(repository) + if repo_name.count(":") == 1: + repository, tag = repository.rsplit(":", 1) + + params = { + 'tag': tag, + 'fromImage': repository + } + headers = {} + + if utils.compare_version('1.5', self._version) >= 0: + # If we don't have any auth data so far, try reloading the config + # file one more time in case anything showed up in there. + if not self._auth_configs: + self._auth_configs = auth.load_config() + authcfg = auth.resolve_authconfig(self._auth_configs, registry) + + # Do not fail here if no atuhentication exists for this specific + # registry as we can have a readonly pull. Just put the header if + # we can. + if authcfg: + headers['X-Registry-Auth'] = auth.encode_header(authcfg) + + response = self._post(self._url('/images/create'), params=params, + headers=headers, stream=stream, timeout=None) + + if stream: + return self._stream_helper(response) + else: + return self._result(response) + + def push(self, repository, stream=False): + registry, repo_name = auth.resolve_repository_name(repository) + u = self._url("/images/{0}/push".format(repository)) + headers = {} + + if utils.compare_version('1.5', self._version) >= 0: + # If we don't have any auth data so far, try reloading the config + # file one more time in case anything showed up in there. + if not self._auth_configs: + self._auth_configs = auth.load_config() + authcfg = auth.resolve_authconfig(self._auth_configs, registry) + + # Do not fail here if no atuhentication exists for this specific + # registry as we can have a readonly pull. Just put the header if + # we can. + if authcfg: + headers['X-Registry-Auth'] = auth.encode_header(authcfg) + + response = self._post_json(u, None, headers=headers, stream=stream) + else: + response = self._post_json(u, authcfg, stream=stream) + + return stream and self._stream_helper(response) \ + or self._result(response) + + def remove_container(self, container, v=False, link=False): + if isinstance(container, dict): + container = container.get('Id') + params = {'v': v, 'link': link} + res = self._delete(self._url("/containers/" + container), + params=params) + self._raise_for_status(res) + + def remove_image(self, image): + res = self._delete(self._url("/images/" + image)) + self._raise_for_status(res) + + def restart(self, container, timeout=10): + if isinstance(container, dict): + container = container.get('Id') + params = {'t': timeout} + url = self._url("/containers/{0}/restart".format(container)) + res = self._post(url, params=params) + self._raise_for_status(res) + + def search(self, term): + return self._result(self._get(self._url("/images/search"), + params={'term': term}), + True) + + def start(self, container, binds=None, port_bindings=None, lxc_conf=None, + publish_all_ports=False, links=None, privileged=False): + if isinstance(container, dict): + container = container.get('Id') + + if isinstance(lxc_conf, dict): + formatted = [] + for k, v in six.iteritems(lxc_conf): + formatted.append({'Key': k, 'Value': str(v)}) + lxc_conf = formatted + + start_config = { + 'LxcConf': lxc_conf + } + if binds: + bind_pairs = [ + '{0}:{1}'.format(host, dest) for host, dest in binds.items() + ] + start_config['Binds'] = bind_pairs + + if port_bindings: + start_config['PortBindings'] = utils.convert_port_bindings( + port_bindings + ) + + start_config['PublishAllPorts'] = publish_all_ports + + if links: + formatted_links = [ + '{0}:{1}'.format(k, v) for k, v in sorted(six.iteritems(links)) + ] + + start_config['Links'] = formatted_links + + start_config['Privileged'] = privileged + + url = self._url("/containers/{0}/start".format(container)) + res = self._post_json(url, data=start_config) + self._raise_for_status(res) + + def stop(self, container, timeout=10): + if isinstance(container, dict): + container = container.get('Id') + params = {'t': timeout} + url = self._url("/containers/{0}/stop".format(container)) + res = self._post(url, params=params, + timeout=max(timeout, self._timeout)) + self._raise_for_status(res) + + def tag(self, image, repository, tag=None, force=False): + params = { + 'tag': tag, + 'repo': repository, + 'force': 1 if force else 0 + } + url = self._url("/images/{0}/tag".format(image)) + res = self._post(url, params=params) + self._raise_for_status(res) + return res.status_code == 201 + + def top(self, container): + u = self._url("/containers/{0}/top".format(container)) + return self._result(self._get(u), True) + + def version(self): + return self._result(self._get(self._url("/version")), True) + + def wait(self, container): + if isinstance(container, dict): + container = container.get('Id') + url = self._url("/containers/{0}/wait".format(container)) + res = self._post(url, timeout=None) + self._raise_for_status(res) + json_ = res.json() + if 'StatusCode' in json_: + return json_['StatusCode'] + return -1 diff --git a/fig/packages/docker/unixconn/__init__.py b/fig/packages/docker/unixconn/__init__.py new file mode 100644 index 00000000..53711fc6 --- /dev/null +++ b/fig/packages/docker/unixconn/__init__.py @@ -0,0 +1 @@ +from .unixconn import UnixAdapter # flake8: noqa diff --git a/fig/packages/docker/unixconn/unixconn.py b/fig/packages/docker/unixconn/unixconn.py new file mode 100644 index 00000000..c9565a25 --- /dev/null +++ b/fig/packages/docker/unixconn/unixconn.py @@ -0,0 +1,71 @@ +# Copyright 2013 dotCloud inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import six + +if six.PY3: + import http.client as httplib +else: + import httplib +import requests.adapters +import socket + +try: + import requests.packages.urllib3.connectionpool as connectionpool +except ImportError: + import urllib3.connectionpool as connectionpool + + +class UnixHTTPConnection(httplib.HTTPConnection, object): + def __init__(self, base_url, unix_socket, timeout=60): + httplib.HTTPConnection.__init__(self, 'localhost', timeout=timeout) + self.base_url = base_url + self.unix_socket = unix_socket + self.timeout = timeout + + def connect(self): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(self.timeout) + sock.connect(self.base_url.replace("unix:/", "")) + self.sock = sock + + def _extract_path(self, url): + #remove the base_url entirely.. + return url.replace(self.base_url, "") + + def request(self, method, url, **kwargs): + url = self._extract_path(self.unix_socket) + super(UnixHTTPConnection, self).request(method, url, **kwargs) + + +class UnixHTTPConnectionPool(connectionpool.HTTPConnectionPool): + def __init__(self, base_url, socket_path, timeout=60): + connectionpool.HTTPConnectionPool.__init__(self, 'localhost', + timeout=timeout) + self.base_url = base_url + self.socket_path = socket_path + self.timeout = timeout + + def _new_conn(self): + return UnixHTTPConnection(self.base_url, self.socket_path, + self.timeout) + + +class UnixAdapter(requests.adapters.HTTPAdapter): + def __init__(self, base_url, timeout=60): + self.base_url = base_url + self.timeout = timeout + super(UnixAdapter, self).__init__() + + def get_connection(self, socket_path, proxies=None): + return UnixHTTPConnectionPool(self.base_url, socket_path, self.timeout) diff --git a/fig/packages/docker/utils/__init__.py b/fig/packages/docker/utils/__init__.py new file mode 100644 index 00000000..386a01af --- /dev/null +++ b/fig/packages/docker/utils/__init__.py @@ -0,0 +1,3 @@ +from .utils import ( + compare_version, convert_port_bindings, mkbuildcontext, ping, tar +) # flake8: noqa diff --git a/fig/packages/docker/utils/utils.py b/fig/packages/docker/utils/utils.py new file mode 100644 index 00000000..8fd9e947 --- /dev/null +++ b/fig/packages/docker/utils/utils.py @@ -0,0 +1,96 @@ +# Copyright 2013 dotCloud inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import io +import tarfile +import tempfile + +import requests +import six + + +def mkbuildcontext(dockerfile): + f = tempfile.NamedTemporaryFile() + t = tarfile.open(mode='w', fileobj=f) + if isinstance(dockerfile, io.StringIO): + dfinfo = tarfile.TarInfo('Dockerfile') + if six.PY3: + raise TypeError('Please use io.BytesIO to create in-memory ' + 'Dockerfiles with Python 3') + else: + dfinfo.size = len(dockerfile.getvalue()) + elif isinstance(dockerfile, io.BytesIO): + dfinfo = tarfile.TarInfo('Dockerfile') + dfinfo.size = len(dockerfile.getvalue()) + else: + dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile') + t.addfile(dfinfo, dockerfile) + t.close() + f.seek(0) + return f + + +def tar(path): + f = tempfile.NamedTemporaryFile() + t = tarfile.open(mode='w', fileobj=f) + t.add(path, arcname='.') + t.close() + f.seek(0) + return f + + +def compare_version(v1, v2): + return float(v2) - float(v1) + + +def ping(url): + try: + res = requests.get(url) + return res.status >= 400 + except Exception: + return False + + +def _convert_port_binding(binding): + result = {'HostIp': '', 'HostPort': ''} + if isinstance(binding, tuple): + if len(binding) == 2: + result['HostPort'] = binding[1] + result['HostIp'] = binding[0] + elif isinstance(binding[0], six.string_types): + result['HostIp'] = binding[0] + else: + result['HostPort'] = binding[0] + else: + result['HostPort'] = binding + + if result['HostPort'] is None: + result['HostPort'] = '' + else: + result['HostPort'] = str(result['HostPort']) + + return result + + +def convert_port_bindings(port_bindings): + result = {} + for k, v in six.iteritems(port_bindings): + key = str(k) + if '/' not in key: + key = key + '/tcp' + if isinstance(v, list): + result[key] = [_convert_port_binding(binding) for binding in v] + else: + result[key] = [_convert_port_binding(v)] + return result diff --git a/fig/service.py b/fig/service.py index e7300410..29e867e9 100644 --- a/fig/service.py +++ b/fig/service.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import -from docker.client import APIError +from .packages.docker.client import APIError import logging import re import os diff --git a/requirements.txt b/requirements.txt index 7eedd09c..a4de170c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -docker-py==0.2.3 +requests==1.2.3 +websocket-client==0.11.0 docopt==0.6.1 PyYAML==3.10 texttable==0.8.1 diff --git a/tests/testcases.py b/tests/testcases.py index 671e091b..8cc1ab35 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import -from docker import Client +from fig.packages.docker import Client from fig.service import Service from fig.cli.utils import docker_url from . import unittest From c4f5ed839fb44b2d057389efa599d2f30938bd3e Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 14:02:52 +0000 Subject: [PATCH 0189/2154] Shorten long commands in ps --- fig/cli/main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index cd4476f4..0993510d 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -156,9 +156,12 @@ class TopLevelCommand(Command): ] rows = [] for container in containers: + command = container.human_readable_command + if len(command) > 30: + command = '%s ...' % command[:30] rows.append([ container.name, - container.human_readable_command, + command, container.human_readable_state, container.human_readable_ports, ]) From feafea2c6d992d2f1f455b83971e25671e539736 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 13 Jan 2014 16:23:10 +0000 Subject: [PATCH 0190/2154] LogPrinter uses regular `attach()`, not websocket Fixes #7. --- fig/cli/log_printer.py | 16 +++------------- fig/container.py | 3 +++ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index dce47d2e..f20ad88d 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -31,28 +31,18 @@ class LogPrinter(object): def _make_log_generator(self, container, color_fn): prefix = color_fn(container.name + " | ") - websocket = self._attach(container) - return (prefix + line for line in split_buffer(read_websocket(websocket), '\n')) + for line in split_buffer(self._attach(container), '\n'): + yield prefix + line def _attach(self, container): params = { - 'stdin': False, 'stdout': True, 'stderr': True, - 'logs': False, 'stream': True, } params.update(self.attach_params) params = dict((name, 1 if value else 0) for (name, value) in list(params.items())) - return container.attach_socket(params=params, ws=True) - -def read_websocket(websocket): - while True: - data = websocket.recv() - if data: - yield data - else: - break + return container.attach(**params) def split_buffer(reader, separator): """ diff --git a/fig/container.py b/fig/container.py index 24b239ac..f8abe83d 100644 --- a/fig/container.py +++ b/fig/container.py @@ -122,6 +122,9 @@ class Container(object): links.append(bits[2]) return links + def attach(self, *args, **kwargs): + return self.client.attach(self.id, *args, **kwargs) + def attach_socket(self, **kwargs): return self.client.attach_socket(self.id, **kwargs) From af1b0ed08895fa13ca46ef417c975e07f8391b08 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 14:06:48 +0000 Subject: [PATCH 0191/2154] Account for length of the ellipsis string when truncating commands --- fig/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 0993510d..24a0180d 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -158,7 +158,7 @@ class TopLevelCommand(Command): for container in containers: command = container.human_readable_command if len(command) > 30: - command = '%s ...' % command[:30] + command = '%s ...' % command[:26] rows.append([ container.name, command, From 3e2fd6a2a1dd9b8d68e1f44ba33cda5315a3b990 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 16:36:01 +0000 Subject: [PATCH 0192/2154] Use DOCKER_HOST environment variable to find Docker daemon Removed all "smart" connection logic. Fig either uses the DOCKER_HOST environment variable if it's present, or passes `None` to docker-py, which does the "right thing" (i.e. falls back to the Unix socket). This means we no longer know at URL-deciding time whether we can connect to the Docker daemon, so we wrap `dispatch` in a `try/except` which catches `requests.exceptions.ConnectionError`. --- fig/cli/command.py | 12 ++++++++++++ fig/cli/utils.py | 31 +------------------------------ 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index 00452dd3..2ccc8c1b 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from ..packages.docker import Client +from requests.exceptions import ConnectionError import errno import logging import os @@ -11,12 +12,23 @@ from ..project import Project from .docopt_command import DocoptCommand from .formatter import Formatter from .utils import cached_property, docker_url +from .errors import UserError log = logging.getLogger(__name__) class Command(DocoptCommand): base_dir = '.' + def dispatch(self, *args, **kwargs): + try: + super(Command, self).dispatch(*args, **kwargs) + except ConnectionError: + raise UserError(""" +Couldn't connect to Docker daemon at %s - is it running? + +If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable. +""" % self.client.base_url) + @cached_property def client(self): return Client(docker_url()) diff --git a/fig/cli/utils.py b/fig/cli/utils.py index e9e43785..2b0eb42d 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -82,33 +82,4 @@ def mkdir(path, permissions=0o700): def docker_url(): - if os.environ.get('DOCKER_URL'): - return os.environ['DOCKER_URL'] - - socket_path = '/var/run/docker.sock' - tcp_hosts = [ - ('localdocker', 4243), - ('127.0.0.1', 4243), - ] - tcp_host = '127.0.0.1' - tcp_port = 4243 - - for host, port in tcp_hosts: - try: - s = socket.create_connection((host, port), timeout=1) - s.close() - return 'http://%s:%s' % (host, port) - except: - pass - - if os.path.exists(socket_path): - return 'unix://%s' % socket_path - - raise UserError(""" -Couldn't find Docker daemon - tried: - -unix://%s -%s - -If it's running elsewhere, specify a url with DOCKER_URL. - """ % (socket_path, '\n'.join('tcp://%s:%s' % h for h in tcp_hosts))) + return os.environ.get('DOCKER_HOST') From bb7613f37bc0c8917b402d9610eef0cfc7d7c462 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 16:50:52 +0000 Subject: [PATCH 0193/2154] Travis runs Docker just on the Unix socket, not on TCP --- script/travis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/travis b/script/travis index 3cdacfa5..e85af550 100755 --- a/script/travis +++ b/script/travis @@ -13,7 +13,7 @@ env trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT # Start docker daemon -docker -d -H 0.0.0.0:4243 -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & +docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & sleep 2 # $init is set by sekexe From b20190da980b77c6243e8fc59ef23cc70acd93d5 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 17:28:47 +0000 Subject: [PATCH 0194/2154] Add basic Dockerfile Because. --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..4d31a5a0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM stackbrew/ubuntu:12.04 +RUN apt-get update -qq +RUN apt-get install -y python python-pip +ADD requirements.txt /code/ +WORKDIR /code/ +RUN pip install -r requirements.txt +ADD requirements-dev.txt /code/ +RUN pip install -r requirements-dev.txt +ADD . /code/ From 8ed86ed551f57a53e70e86e08f346d4659b71580 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:05:45 +0000 Subject: [PATCH 0195/2154] Add number to container --- fig/container.py | 7 +++++++ tests/container_test.py | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/fig/container.py b/fig/container.py index f8abe83d..da10ec7a 100644 --- a/fig/container.py +++ b/fig/container.py @@ -46,6 +46,13 @@ class Container(object): def name(self): return self.dictionary['Name'][1:] + @property + def number(self): + try: + return int(self.name.split('_')[-1]) + except ValueError: + return None + @property def human_readable_ports(self): self.inspect_if_not_inspected() diff --git a/tests/container_test.py b/tests/container_test.py index 3666b3e4..351a807a 100644 --- a/tests/container_test.py +++ b/tests/container_test.py @@ -35,3 +35,17 @@ class ContainerTest(DockerClientTestCase): 'FOO': 'BAR', 'BAZ': 'DOGE', }) + + def test_number(self): + container = Container.from_ps(self.client, { + "Id":"abc", + "Image":"ubuntu:12.04", + "Command":"sleep 300", + "Created":1387384730, + "Status":"Up 8 seconds", + "Ports":None, + "SizeRw":0, + "SizeRootFs":0, + "Names":["/db_1"] + }, has_been_inspected=True) + self.assertEqual(container.number, 1) From 56c6efdfcebfb2fab6a8281e9e7cccd29168fe8a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 17:58:53 +0000 Subject: [PATCH 0196/2154] Add scale command Closes #9 --- README.md | 9 ++++++ fig/cli/main.py | 27 ++++++++++++++++ fig/service.py | 44 +++++++++++++++++++++++++++ tests/cli_test.py | 23 ++++++++++++++ tests/fixtures/simple-figfile/fig.yml | 4 +++ tests/service_test.py | 18 +++++++++++ 6 files changed, 125 insertions(+) diff --git a/README.md b/README.md index 585cdd89..327ac232 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,15 @@ For example: 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. +#### scale + +Set number of containers to run for a service. + +Numbers are specified in the form `service=num` as arguments. +For example: + + $ fig scale web=2 worker=3 + #### start Start existing containers for a service. diff --git a/fig/cli/main.py b/fig/cli/main.py index 24a0180d..44422777 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -10,6 +10,7 @@ from inspect import getdoc from .. import __version__ from ..project import NoSuchService +from ..service import CannotBeScaledError from .command import Command from .formatter import Formatter from .log_printer import LogPrinter @@ -82,6 +83,7 @@ class TopLevelCommand(Command): ps List containers rm Remove stopped containers run Run a one-off command + scale Set number of containers for a service start Start services stop Stop services up Create and start containers @@ -220,6 +222,31 @@ class TopLevelCommand(Command): service.start_container(container, ports=None) c.run() + def scale(self, options): + """ + Set number of containers to run for a service. + + Numbers are specified in the form `service=num` as arguments. + For example: + + $ fig scale web=2 worker=3 + + Usage: scale [SERVICE=NUM...] + """ + for s in options['SERVICE=NUM']: + if '=' not in s: + raise UserError('Arguments to scale should be in the form service=num') + service_name, num = s.split('=', 1) + try: + num = int(num) + except ValueError: + raise UserError('Number of containers for service "%s" is not a number' % service) + try: + self.project.get_service(service_name).scale(num) + except CannotBeScaledError: + raise UserError('Service "%s" cannot be scaled because it specifies a port on the host. If multiple containers for this service were created, the port would clash.\n\nRemove the ":" from the port definition in fig.yml so Docker can choose a random port for each container.' % service_name) + + def start(self, options): """ Start existing containers. diff --git a/fig/service.py b/fig/service.py index 29e867e9..90194fdd 100644 --- a/fig/service.py +++ b/fig/service.py @@ -14,6 +14,10 @@ class BuildError(Exception): pass +class CannotBeScaledError(Exception): + pass + + class Service(object): def __init__(self, name, client=None, project='default', links=[], **options): if not re.match('^[a-zA-Z0-9]+$', name): @@ -56,6 +60,40 @@ class Service(object): log.info("Killing %s..." % c.name) c.kill(**options) + def scale(self, desired_num): + if not self.can_be_scaled(): + raise CannotBeScaledError() + + # Create enough containers + containers = self.containers(stopped=True) + while len(containers) < desired_num: + containers.append(self.create_container()) + + running_containers = [] + stopped_containers = [] + for c in containers: + if c.is_running: + running_containers.append(c) + else: + stopped_containers.append(c) + running_containers.sort(key=lambda c: c.number) + stopped_containers.sort(key=lambda c: c.number) + + # Stop containers + while len(running_containers) > desired_num: + c = running_containers.pop() + log.info("Stopping %s..." % c.name) + c.stop(timeout=1) + stopped_containers.append(c) + + # Start containers + while len(running_containers) < desired_num: + c = stopped_containers.pop(0) + log.info("Starting %s..." % c.name) + c.start() + running_containers.append(c) + + def remove_stopped(self, **options): for c in self.containers(stopped=True): if not c.is_running: @@ -231,6 +269,12 @@ class Service(object): """ return '%s_%s' % (self.project, self.name) + def can_be_scaled(self): + for port in self.options.get('ports', []): + if ':' in str(port): + return False + return True + NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') diff --git a/tests/cli_test.py b/tests/cli_test.py index 2146a906..0d9a2f54 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -13,3 +13,26 @@ class CLITestCase(unittest.TestCase): def test_ps(self): self.command.dispatch(['ps'], None) + + def test_scale(self): + project = self.command.project + + self.command.scale({'SERVICE=NUM': ['simple=1']}) + self.assertEqual(len(project.get_service('simple').containers()), 1) + + self.command.scale({'SERVICE=NUM': ['simple=3', 'another=2']}) + self.assertEqual(len(project.get_service('simple').containers()), 3) + self.assertEqual(len(project.get_service('another').containers()), 2) + + self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']}) + self.assertEqual(len(project.get_service('simple').containers()), 1) + self.assertEqual(len(project.get_service('another').containers()), 1) + + self.command.scale({'SERVICE=NUM': ['simple=1', 'another=1']}) + self.assertEqual(len(project.get_service('simple').containers()), 1) + self.assertEqual(len(project.get_service('another').containers()), 1) + + self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']}) + self.assertEqual(len(project.get_service('simple').containers()), 0) + self.assertEqual(len(project.get_service('another').containers()), 0) + diff --git a/tests/fixtures/simple-figfile/fig.yml b/tests/fixtures/simple-figfile/fig.yml index aef2d39b..22532375 100644 --- a/tests/fixtures/simple-figfile/fig.yml +++ b/tests/fixtures/simple-figfile/fig.yml @@ -1,2 +1,6 @@ simple: image: ubuntu + command: /bin/sleep 300 +another: + image: ubuntu + command: /bin/sleep 300 diff --git a/tests/service_test.py b/tests/service_test.py index 2ccf1755..ff7b2416 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from fig import Service +from fig.service import CannotBeScaledError from .testcases import DockerClientTestCase @@ -193,3 +194,20 @@ class ServiceTest(DockerClientTestCase): self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8001') + def test_scale(self): + service = self.create_service('web') + service.scale(1) + self.assertEqual(len(service.containers()), 1) + service.scale(3) + self.assertEqual(len(service.containers()), 3) + service.scale(1) + self.assertEqual(len(service.containers()), 1) + service.scale(0) + self.assertEqual(len(service.containers()), 0) + + def test_scale_on_service_that_cannot_be_scaled(self): + service = self.create_service('web', ports=['8000:8000']) + self.assertRaises(CannotBeScaledError, lambda: service.scale(1)) + + + From 65f23583ae340aa711e60d77983fbdfc40041be4 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:16:26 +0000 Subject: [PATCH 0197/2154] Ignore vim undo files --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 95e97bf3..02bdc73b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,3 +3,4 @@ include *.md recursive-include tests * global-exclude *.pyc global-exclode *.pyo +global-exclode *.un~ From a07b00606bc92e68002907b9114df62199368d8c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:17:16 +0000 Subject: [PATCH 0198/2154] Fix typos in manifest --- MANIFEST.in | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 02bdc73b..aae1042d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,5 @@ include LICENSE include *.md recursive-include tests * global-exclude *.pyc -global-exclode *.pyo -global-exclode *.un~ +global-exclude *.pyo +global-exclude *.un~ From 74fb400fefee51fa6fba2decfa3c41a995b2e345 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 17:28:43 +0000 Subject: [PATCH 0199/2154] Version 0.1.0 --- CHANGES.md | 14 ++++++++++++++ fig/__init__.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 8d5001eb..bfae75d0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,20 @@ Change log ========== +0.1.0 (2014-01-16) +------------------ + + - Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2) + - Add `fig scale` command (#9) + - Use DOCKER_HOST environment variable to find Docker daemon (#19) + - Truncate long commands in `fig ps` (#18) + - Fill out CLI help banners for commands (#15, #16) + - Show a friendlier error when `fig.yml` is missing (#4) + + - Fix bug with `fig build` logging (#3) + - Fix bug where builds would time out if a step took a long time without generating output (#6) + - Fix bug where streaming container output over the Unix socket raised an error (#7) + 0.0.2 (2014-01-02) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index baa08f1e..0cc7b145 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.0.2' +__version__ = '0.1.0' From 573fae089f665130d056f78f50a98235aeb959ee Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:21:56 +0000 Subject: [PATCH 0200/2154] Add missing files to manifest --- MANIFEST.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index aae1042d..9d1c2ae0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,8 @@ +include Dockerfile include LICENSE +include requirements.txt +include requirements-dev.txt +tox.ini include *.md recursive-include tests * global-exclude *.pyc From 720cc192bbfe8fb0656b38c8d6b28016fc086d22 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:27:06 +0000 Subject: [PATCH 0201/2154] Add upgrade instructions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 327ac232..dd76fe92 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun Next, install Fig: - $ sudo pip install fig + $ sudo pip install -U fig -(If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.) +(If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`. This command also upgrades Fig when we release a new version.) You'll want to make a directory for the project: From caccf96d3fb3825e7a1708e24d85cfc0ede04fcf Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 18:32:35 +0000 Subject: [PATCH 0202/2154] Fix markdown --- CHANGES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index bfae75d0..67f93847 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,7 +10,6 @@ Change log - Truncate long commands in `fig ps` (#18) - Fill out CLI help banners for commands (#15, #16) - Show a friendlier error when `fig.yml` is missing (#4) - - Fix bug with `fig build` logging (#3) - Fix bug where builds would time out if a step took a long time without generating output (#6) - Fix bug where streaming container output over the Unix socket raised an error (#7) From 15e8c9ffbbe893b80e73322a59c3f1456401d075 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:35:30 +0000 Subject: [PATCH 0203/2154] Fix ambiguous order of upgrade instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dd76fe92..6cb64078 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Next, install Fig: $ sudo pip install -U fig -(If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`. This command also upgrades Fig when we release a new version.) +(This command also upgrades Fig when we release a new version. If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.) You'll want to make a directory for the project: From 5b9c228cf852772eb0508f264aa759aaf63aeaa4 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 18:42:03 +0000 Subject: [PATCH 0204/2154] Add note to CHANGES.md about old/new env vars --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 67f93847..82a33c2b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,7 +6,7 @@ Change log - Containers are recreated on each `fig up`, ensuring config is up-to-date with `fig.yml` (#2) - Add `fig scale` command (#9) - - Use DOCKER_HOST environment variable to find Docker daemon (#19) + - Use `DOCKER_HOST` environment variable to find Docker daemon, for consistency with the official Docker client (was previously `DOCKER_URL`) (#19) - Truncate long commands in `fig ps` (#18) - Fill out CLI help banners for commands (#15, #16) - Show a friendlier error when `fig.yml` is missing (#4) From 7070e06ac6992217c9c1c0f213bcde7e9285afc1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 16 Jan 2014 18:45:38 +0000 Subject: [PATCH 0205/2154] Thank some folks --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 82a33c2b..644ff495 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -14,6 +14,8 @@ Change log - Fix bug where builds would time out if a step took a long time without generating output (#6) - Fix bug where streaming container output over the Unix socket raised an error (#7) +Big thanks to @tomstuart, @EnTeQuAk, @schickling, @aronasorman and @GeoffreyPlitt. + 0.0.2 (2014-01-02) ------------------ From 8b77b51c1586cca031b07003dbcecd6650318742 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:49:15 +0000 Subject: [PATCH 0206/2154] Fix travis formatting To use formatting travis command line tool uses --- .travis.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index d00b2228..8e93cb86 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,15 @@ language: python python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - +- '2.6' +- '2.7' +- '3.2' +- '3.3' matrix: allow_failures: - - python: "3.2" - - python: "3.3" - + - python: '3.2' + - python: '3.3' install: script/travis-install - script: - - pwd - - env - - sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION" - +- pwd +- env +- sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION" From 7c1ec74cf62172ec2454cae77f1d96e823bbe59b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:51:32 +0000 Subject: [PATCH 0207/2154] Add Travis PyPi deployment --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8e93cb86..c0e4cfc1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,11 @@ script: - pwd - env - sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION" +deploy: + provider: pypi + user: orchard + password: + secure: M8UMupCLSsB1hV00Zn6ra8Vg81SCFBpbcRsa0nUw9kgXn9hOCESWYVHTqQ1ksWZOa8z6WMaqYtoosPKXGJQNf0wF/kEVDsMUeaZWOF/PqDkx1EwQ1diVfwlbN4/k0iX+Se7SrZfiWnJiAqiIPqToQipvLlJohqf8WwfPcVvILVE= + on: + tags: true + repo: orchardup/fig From 4bec39535fe50055879c1da70cc0553f6d536103 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 16 Jan 2014 18:52:15 +0000 Subject: [PATCH 0208/2154] Remove unnecessary release script --- script/release | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100755 script/release diff --git a/script/release b/script/release deleted file mode 100755 index fdd4a960..00000000 --- a/script/release +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -set -xe - -if [ -z "$1" ]; then - echo 'pass a version as first argument' - exit 1 -fi - -git tag $1 -git push --tags -python setup.py sdist upload - - From c6e19e34f721699a5a7f1f730a5d252122e8896d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 17 Jan 2014 18:00:22 +0000 Subject: [PATCH 0209/2154] Fix external port config When exposing a port externally, it seems Docker only actually exposes it if you specify the *internal* port as `xxxx/tcp`. So add that on if it's not there. --- fig/service.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fig/service.py b/fig/service.py index 90194fdd..f9679426 100644 --- a/fig/service.py +++ b/fig/service.py @@ -224,6 +224,8 @@ class Service(object): port = str(port) if ':' in port: port = port.split(':')[-1] + if '/' not in port: + port = "%s/tcp" % port ports.append(port) container_options['ports'] = ports From b428988ef659b7c51db25267d9fd03b50d407adc Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 17 Jan 2014 19:14:48 +0000 Subject: [PATCH 0210/2154] Bump to version 0.1.1 --- CHANGES.md | 5 +++++ fig/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 644ff495..f94c0205 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change log ========== +0.1.1 (2014-01-17) +------------------ + + - Fix bug where ports were not exposed correctly (#29). Thanks @dustinlacewell! + 0.1.0 (2014-01-16) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 0cc7b145..415b4a31 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.1.0' +__version__ = '0.1.1' From 07f3c783699799129d83b518ac7705630db3939f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 19 Jan 2014 16:50:08 +0000 Subject: [PATCH 0211/2154] Update docker-py From https://github.com/bfirsh/docker-py/commit/4bc5d27e51cc9afecfbc1d566b413f4d45203823 --- fig/packages/docker/client.py | 70 ++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index e3cd976c..3fc2b084 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -122,7 +122,8 @@ class Client(requests.Session): detach=False, stdin_open=False, tty=False, mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, - network_disabled=False): + network_disabled=False, entrypoint=None, + cpu_shares=None, working_dir=None): if isinstance(command, six.string_types): command = shlex.split(str(command)) if isinstance(environment, dict): @@ -134,15 +135,12 @@ class Client(requests.Session): exposed_ports = {} for port_definition in ports: port = port_definition - proto = None + proto = 'tcp' if isinstance(port_definition, tuple): if len(port_definition) == 2: proto = port_definition[1] port = port_definition[0] - exposed_ports['{0}{1}'.format( - port, - '/' + proto if proto else '' - )] = {} + exposed_ports['{0}/{1}'.format(port, proto)] = {} ports = exposed_ports if volumes and isinstance(volumes, list): @@ -178,7 +176,10 @@ class Client(requests.Session): 'Image': image, 'Volumes': volumes, 'VolumesFrom': volumes_from, - 'NetworkDisabled': network_disabled + 'NetworkDisabled': network_disabled, + 'Entrypoint': entrypoint, + 'CpuShares': cpu_shares, + 'WorkingDir': working_dir } def _post_json(self, url, data, **kwargs): @@ -409,11 +410,13 @@ class Client(requests.Session): detach=False, stdin_open=False, tty=False, mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, - network_disabled=False, name=None): + network_disabled=False, name=None, entrypoint=None, + cpu_shares=None, working_dir=None): config = self._container_config( image, command, hostname, user, detach, stdin_open, tty, mem_limit, - ports, environment, dns, volumes, volumes_from, network_disabled + ports, environment, dns, volumes, volumes_from, network_disabled, + entrypoint, cpu_shares, working_dir ) return self.create_container_from_config(config, name) @@ -475,27 +478,34 @@ class Client(requests.Session): return [x['Id'] for x in res] return res - def import_image(self, src, data=None, repository=None, tag=None): + def import_image(self, src=None, repository=None, tag=None, image=None): u = self._url("/images/create") params = { 'repo': repository, 'tag': tag } - try: - # XXX: this is ways not optimal but the only way - # for now to import tarballs through the API - fic = open(src) - data = fic.read() - fic.close() - src = "-" - except IOError: - # file does not exists or not a file (URL) - data = None - if isinstance(src, six.string_types): - params['fromSrc'] = src - return self._result(self._post(u, data=data, params=params)) - return self._result(self._post(u, data=src, params=params)) + if src: + try: + # XXX: this is ways not optimal but the only way + # for now to import tarballs through the API + fic = open(src) + data = fic.read() + fic.close() + src = "-" + except IOError: + # file does not exists or not a file (URL) + data = None + if isinstance(src, six.string_types): + params['fromSrc'] = src + return self._result(self._post(u, data=data, params=params)) + return self._result(self._post(u, data=src, params=params)) + + if image: + params['fromImage'] = image + return self._result(self._post(u, data=None, params=params)) + + raise Exception("Must specify a src or image") def info(self): return self._result(self._get(self._url("/info")), @@ -577,13 +587,13 @@ class Client(requests.Session): self._raise_for_status(res) json_ = res.json() s_port = str(private_port) - f_port = None - if s_port in json_['NetworkSettings']['PortMapping']['Udp']: - f_port = json_['NetworkSettings']['PortMapping']['Udp'][s_port] - elif s_port in json_['NetworkSettings']['PortMapping']['Tcp']: - f_port = json_['NetworkSettings']['PortMapping']['Tcp'][s_port] + h_ports = None - return f_port + h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/udp') + if h_ports is None: + h_ports = json_['NetworkSettings']['Ports'].get(s_port + '/tcp') + + return h_ports def pull(self, repository, tag=None, stream=False): registry, repo_name = auth.resolve_repository_name(repository) From 62bba1684b91397f5deec3dad37a60eb37c1646e Mon Sep 17 00:00:00 2001 From: Cameron Maske Date: Sat, 18 Jan 2014 14:11:25 +0000 Subject: [PATCH 0212/2154] Updated recreate_containers to attempt to base intermediate container's the previous container's image. Added in additional functionality to reset any entrypoints for the intermediate container and pull/retry handling if the image does not exist. Updated test coverage to check if an container is recreated with an entrypoint it is handled correctly. --- fig/container.py | 6 +++++- fig/service.py | 16 +++++++++------- tests/service_test.py | 11 ++++++++--- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/fig/container.py b/fig/container.py index da10ec7a..76f2d29e 100644 --- a/fig/container.py +++ b/fig/container.py @@ -3,7 +3,7 @@ from __future__ import absolute_import class Container(object): """ - Represents a Docker container, constructed from the output of + Represents a Docker container, constructed from the output of GET /containers/:id:/json. """ def __init__(self, client, dictionary, has_been_inspected=False): @@ -38,6 +38,10 @@ class Container(object): def id(self): return self.dictionary['ID'] + @property + def image(self): + return self.dictionary['Image'] + @property def short_id(self): return self.id[:10] diff --git a/fig/service.py b/fig/service.py index f9679426..730e3a1c 100644 --- a/fig/service.py +++ b/fig/service.py @@ -141,12 +141,14 @@ class Service(object): if container.is_running: container.stop(timeout=1) - intermediate_container = Container.create( - self.client, - image='ubuntu', - command='echo', - volumes_from=container.id, - ) + intermediate_container_options = { + 'image': container.image, + 'command': 'echo', + 'volumes_from': container.id, + 'entrypoint': None + } + intermediate_container = self.create_container( + one_off=True, **intermediate_container_options) intermediate_container.start() intermediate_container.wait() container.remove() @@ -212,7 +214,7 @@ class Service(object): return links def _get_container_options(self, override_options, one_off=False): - keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from'] + keys = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint'] container_options = dict((k, self.options[k]) for k in keys if k in self.options) container_options.update(override_options) diff --git a/tests/service_test.py b/tests/service_test.py index ff7b2416..e4183a02 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -110,8 +110,9 @@ class ServiceTest(DockerClientTestCase): self.assertIn('/var/db', container.inspect()['Volumes']) def test_recreate_containers(self): - service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db']) + service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'], entrypoint=['ps']) old_container = service.create_container() + self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps']) self.assertEqual(old_container.dictionary['Config']['Env'], ['FOO=1']) self.assertEqual(old_container.name, 'figtest_db_1') service.start_container(old_container) @@ -120,11 +121,15 @@ class ServiceTest(DockerClientTestCase): num_containers_before = len(self.client.containers(all=True)) service.options['environment']['FOO'] = '2' - (old, new) = service.recreate_containers() - self.assertEqual(len(old), 1) + (intermediate, new) = service.recreate_containers() + self.assertEqual(len(intermediate), 1) self.assertEqual(len(new), 1) new_container = new[0] + intermediate_container = intermediate[0] + self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], None) + + self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps']) self.assertEqual(new_container.dictionary['Config']['Env'], ['FOO=2']) self.assertEqual(new_container.name, 'figtest_db_1') service.start_container(new_container) From f3d273864d0fbeec91a61fae467a82d784da7c86 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 19 Jan 2014 19:55:28 +0000 Subject: [PATCH 0213/2154] Add comments to script/travis --- script/travis | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/travis b/script/travis index e85af550..12ad673a 100755 --- a/script/travis +++ b/script/travis @@ -3,10 +3,13 @@ # Exit on first error set -ex +# Put Python eggs in a writeable directory export PYTHON_EGG_CACHE="/tmp/.python-eggs" +# Activate correct virtualenv TRAVIS_PYTHON_VERSION=$1 source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate + env # Kill background processes on exit From 24a6d1d836e918ba5534012727766f74778d147b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 19 Jan 2014 19:56:29 +0000 Subject: [PATCH 0214/2154] Forcibly kill Docker when Travis ends May fix tests timing out. --- script/travis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/travis b/script/travis index 12ad673a..69bc3043 100755 --- a/script/travis +++ b/script/travis @@ -13,7 +13,7 @@ source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate env # Kill background processes on exit -trap 'kill $(jobs -p)' SIGINT SIGTERM EXIT +trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT # Start docker daemon docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & From fc1bbb45b1a9a72d8f4d3fa00eeb1f8a3ebb3eb8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 19 Jan 2014 20:33:06 +0000 Subject: [PATCH 0215/2154] Add option to disable pseudo-tty on fig run Also disable tty if stdin is not a tty. --- fig/cli/main.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 44422777..1696a7c9 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -4,7 +4,6 @@ import logging import sys import re import signal -import sys from inspect import getdoc @@ -200,12 +199,20 @@ class TopLevelCommand(Command): Usage: run [options] SERVICE COMMAND [ARGS...] Options: - -d Detached mode: Run container in the background, print new container name + -d Detached mode: Run container in the background, print new + container name + -T Disable pseudo-tty allocation. By default `fig run` + allocates a TTY. """ service = self.project.get_service(options['SERVICE']) + + tty = True + if options['-d'] or options['-T'] or not sys.stdin.isatty(): + tty = False + container_options = { 'command': [options['COMMAND']] + options['ARGS'], - 'tty': not options['-d'], + 'tty': tty, 'stdin_open': not options['-d'], } container = service.create_container(one_off=True, **container_options) @@ -217,7 +224,7 @@ class TopLevelCommand(Command): container.id, interactive=True, logs=True, - raw=True + raw=tty ) as c: service.start_container(container, ports=None) c.run() From 405079f744fb6901a97a1910218ee008a03607de Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 20 Jan 2014 15:52:07 +0000 Subject: [PATCH 0216/2154] Use raw socket in 'fig run', simplify _attach_to_container --- fig/cli/main.py | 41 ++++++++--------------------------------- fig/cli/socketclient.py | 14 +++++++------- 2 files changed, 15 insertions(+), 40 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 1696a7c9..66d2bdf2 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -220,12 +220,7 @@ class TopLevelCommand(Command): service.start_container(container, ports=None) print(container.name) else: - with self._attach_to_container( - container.id, - interactive=True, - logs=True, - raw=tty - ) as c: + with self._attach_to_container(container.id, raw=tty) as c: service.start_container(container, ports=None) c.run() @@ -317,35 +312,15 @@ class TopLevelCommand(Command): print("Gracefully stopping... (press Ctrl+C again to force)") self.project.stop(service_names=options['SERVICE']) - def _attach_to_container(self, container_id, interactive, logs=False, stream=True, raw=False): - stdio = self.client.attach_socket( - container_id, - params={ - 'stdin': 1 if interactive else 0, - 'stdout': 1, - 'stderr': 0, - 'logs': 1 if logs else 0, - 'stream': 1 if stream else 0 - }, - ws=True, - ) - - stderr = self.client.attach_socket( - container_id, - params={ - 'stdin': 0, - 'stdout': 0, - 'stderr': 1, - 'logs': 1 if logs else 0, - 'stream': 1 if stream else 0 - }, - ws=True, - ) + def _attach_to_container(self, container_id, raw=False): + socket_in = self.client.attach_socket(container_id, params={'stdin': 1, 'stream': 1}) + socket_out = self.client.attach_socket(container_id, params={'stdout': 1, 'logs': 1, 'stream': 1}) + socket_err = self.client.attach_socket(container_id, params={'stderr': 1, 'logs': 1, 'stream': 1}) return SocketClient( - socket_in=stdio, - socket_out=stdio, - socket_err=stderr, + socket_in=socket_in, + socket_out=socket_out, + socket_err=socket_err, raw=raw, ) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index bbae60e6..842c9c6a 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -57,15 +57,15 @@ class SocketClient: def run(self): if self.socket_in is not None: - self.start_background_thread(target=self.send_ws, args=(self.socket_in, sys.stdin)) + self.start_background_thread(target=self.send, args=(self.socket_in, sys.stdin)) recv_threads = [] if self.socket_out is not None: - recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_out, sys.stdout))) + recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_out, sys.stdout))) if self.socket_err is not None: - recv_threads.append(self.start_background_thread(target=self.recv_ws, args=(self.socket_err, sys.stderr))) + recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_err, sys.stderr))) for t in recv_threads: t.join() @@ -76,10 +76,10 @@ class SocketClient: thread.start() return thread - def recv_ws(self, socket, stream): + def recv(self, socket, stream): try: while True: - chunk = socket.recv() + chunk = socket.recv(4096) if chunk: stream.write(chunk) @@ -89,7 +89,7 @@ class SocketClient: except Exception as e: log.debug(e) - def send_ws(self, socket, stream): + def send(self, socket, stream): while True: r, w, e = select([stream.fileno()], [], []) @@ -97,7 +97,7 @@ class SocketClient: chunk = stream.read(1) if chunk == '': - socket.send_close() + socket.close() break else: try: From 7e2d86c5109c5fc4ea337dbb16264dad3f3822a5 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 20 Jan 2014 16:10:54 +0000 Subject: [PATCH 0217/2154] Use Container.create to recreate containers self.create_container might do unexpected things. --- fig/service.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/fig/service.py b/fig/service.py index 730e3a1c..a60bf23f 100644 --- a/fig/service.py +++ b/fig/service.py @@ -141,14 +141,13 @@ class Service(object): if container.is_running: container.stop(timeout=1) - intermediate_container_options = { - 'image': container.image, - 'command': 'echo', - 'volumes_from': container.id, - 'entrypoint': None - } - intermediate_container = self.create_container( - one_off=True, **intermediate_container_options) + intermediate_container = Container.create( + self.client, + image=container.image, + command='echo', + volumes_from=container.id, + entrypoint=None + ) intermediate_container.start() intermediate_container.wait() container.remove() From 855a9c623c7b52bcded4d741fee2ffe8abb66ee6 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 20 Jan 2014 16:47:58 +0000 Subject: [PATCH 0218/2154] Remove containers after running CLI tests --- tests/cli_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/cli_test.py b/tests/cli_test.py index 0d9a2f54..197046c1 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -8,6 +8,10 @@ class CLITestCase(unittest.TestCase): self.command = TopLevelCommand() self.command.base_dir = 'tests/fixtures/simple-figfile' + def tearDown(self): + self.command.project.kill() + self.command.project.remove_stopped() + def test_help(self): self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None)) From 7abc4fbf3a6ca92701c019c6464d0d771dcb2611 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 20 Jan 2014 16:50:41 +0000 Subject: [PATCH 0219/2154] Improve ps CLI test --- requirements.txt | 1 + tests/cli_test.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a4de170c..ba6741dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ PyYAML==3.10 texttable==0.8.1 # docker requires six==1.3.0 six==1.3.0 +mock==1.0.1 diff --git a/tests/cli_test.py b/tests/cli_test.py index 197046c1..f57553c5 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals from __future__ import absolute_import from . import unittest +from mock import patch +from six import StringIO from fig.cli.main import TopLevelCommand class CLITestCase(unittest.TestCase): @@ -15,8 +17,11 @@ class CLITestCase(unittest.TestCase): def test_help(self): self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None)) - def test_ps(self): + @patch('sys.stdout', new_callable=StringIO) + def test_ps(self, mock_stdout): + self.command.project.get_service('simple').create_container() self.command.dispatch(['ps'], None) + self.assertIn('fig_simple_1', mock_stdout.getvalue()) def test_scale(self): project = self.command.project From 084db337a00ad10224c12ab6aebf21a3bdff8476 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 20 Jan 2014 18:09:25 +0000 Subject: [PATCH 0220/2154] Update docker-py Brought in changes from https://github.com/dotcloud/docker-py/pull/145 --- fig/packages/docker/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 3fc2b084..7bc46aac 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -152,6 +152,7 @@ class Client(requests.Session): attach_stdin = False attach_stdout = False attach_stderr = False + stdin_once = False if not detach: attach_stdout = True @@ -159,6 +160,7 @@ class Client(requests.Session): if stdin_open: attach_stdin = True + stdin_once = True return { 'Hostname': hostname, @@ -166,6 +168,7 @@ class Client(requests.Session): 'User': user, 'Tty': tty, 'OpenStdin': stdin_open, + 'StdinOnce': stdin_once, 'Memory': mem_limit, 'AttachStdin': attach_stdin, 'AttachStdout': attach_stdout, From 4646ac85b0f6aef9ab8de9dec92e12bcc1a32481 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 20 Jan 2014 18:41:04 +0000 Subject: [PATCH 0221/2154] Add PyPi badge --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6cb64078..bb5ebeec 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Fig === [![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) +[![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig) Punctual, lightweight development environments using Docker. From 40d04a076c84e7a59398e288ca70fa379c097a10 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 20 Jan 2014 19:22:05 +0000 Subject: [PATCH 0222/2154] Fix lag when using cursor keys in an interactive 'fig run' --- fig/cli/socketclient.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 842c9c6a..99333af8 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -91,22 +91,19 @@ class SocketClient: def send(self, socket, stream): while True: - r, w, e = select([stream.fileno()], [], []) + chunk = stream.read(1) - if r: - chunk = stream.read(1) - - if chunk == '': - socket.close() - break - else: - try: - socket.send(chunk) - except Exception as e: - if hasattr(e, 'errno') and e.errno == errno.EPIPE: - break - else: - raise e + if chunk == '': + socket.close() + break + else: + try: + socket.send(chunk) + except Exception as e: + if hasattr(e, 'errno') and e.errno == errno.EPIPE: + break + else: + raise e def destroy(self): if self.settings is not None: From 977ec7c94156b08d01ff9259b6b2a262f33bba73 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 20 Jan 2014 19:25:28 +0000 Subject: [PATCH 0223/2154] Remove unused import --- fig/cli/socketclient.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 99333af8..b0bb087b 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -1,7 +1,6 @@ from __future__ import print_function # Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py -from select import select import sys import tty import fcntl From 65071aafb0acad0603b5926e9b3d25d685252519 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 21 Jan 2014 17:58:04 +0000 Subject: [PATCH 0224/2154] Make sure attach() is called as soon as LogPrinter is initialized Fixes #35. --- fig/cli/log_printer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index f20ad88d..a3ad15eb 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -31,8 +31,9 @@ class LogPrinter(object): def _make_log_generator(self, container, color_fn): prefix = color_fn(container.name + " | ") - for line in split_buffer(self._attach(container), '\n'): - yield prefix + line + # Attach to container before log printer starts running + line_generator = split_buffer(self._attach(container), '\n') + return (prefix + line for line in line_generator) def _attach(self, container): params = { From deb7f3c5b602d49e36e18bba120ad554de9b44f2 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 22 Jan 2014 13:37:00 +0000 Subject: [PATCH 0225/2154] Bump to version 0.1.2 --- CHANGES.md | 7 +++++++ fig/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f94c0205..941a9e95 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,13 @@ Change log ========== +0.1.2 (2014-01-22) +------------------ + + - Add `-T` option to `fig run` to disable pseudo-TTY. (#34) + - Fix `fig up` requiring the ubuntu image to be pulled to recreate containers. (#33) Thanks @cameronmaske! + - Improve reliability, fix arrow keys and fix a race condition in `fig run`. (#34, #39, #40) + 0.1.1 (2014-01-17) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 415b4a31..ca028a4e 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.1.1' +__version__ = '0.1.2' From 64513e8d6f55223eb6555e2f4e81fb39d12050ec Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 19 Jan 2014 20:39:21 +0000 Subject: [PATCH 0226/2154] Verbose nosetests --- script/travis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/travis b/script/travis index 69bc3043..878b86e0 100755 --- a/script/travis +++ b/script/travis @@ -20,4 +20,4 @@ docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & sleep 2 # $init is set by sekexe -cd $(dirname $init)/.. && nosetests +cd $(dirname $init)/.. && nosetests -v From 18525554ede6aaa26a6a97ad8cb9053882a7feb8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 22 Jan 2014 14:15:17 +0000 Subject: [PATCH 0227/2154] Add license to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index f0ffceb7..da520a1c 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ setup( url='https://github.com/orchardup/fig', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', + license='BSD', packages=find_packages(), include_package_data=True, test_suite='nose.collector', From ae67d55bf2ff59946d4d41e2fe9069851055689f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 22 Jan 2014 16:52:42 +0000 Subject: [PATCH 0228/2154] Fix bug where too many '/tcp' suffixes were added to port config --- fig/service.py | 2 -- tests/service_test.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/fig/service.py b/fig/service.py index a60bf23f..adaf940e 100644 --- a/fig/service.py +++ b/fig/service.py @@ -225,8 +225,6 @@ class Service(object): port = str(port) if ':' in port: port = port.split(':')[-1] - if '/' not in port: - port = "%s/tcp" % port ports.append(port) container_options['ports'] = ports diff --git a/tests/service_test.py b/tests/service_test.py index e4183a02..dcbc4627 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -184,7 +184,7 @@ class ServiceTest(DockerClientTestCase): def test_start_container_creates_ports(self): service = self.create_service('web', ports=[8000]) container = service.start_container().inspect() - self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) + self.assertEqual(container['HostConfig']['PortBindings'].keys(), ['8000/tcp']) self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_creates_fixed_external_ports(self): From df9f66d437fb2b4e21595f312f8f63e6cc63b3d7 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 22 Jan 2014 17:01:10 +0000 Subject: [PATCH 0229/2154] Allow ports to be specified in '1234/tcp' format --- fig/service.py | 7 +++++-- tests/service_test.py | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/fig/service.py b/fig/service.py index adaf940e..aaaa97ab 100644 --- a/fig/service.py +++ b/fig/service.py @@ -172,9 +172,10 @@ class Service(object): port = str(port) if ':' in port: external_port, internal_port = port.split(':', 1) - port_bindings[int(internal_port)] = int(external_port) else: - port_bindings[int(port)] = None + external_port, internal_port = (None, port) + + port_bindings[internal_port] = external_port volume_bindings = {} @@ -225,6 +226,8 @@ class Service(object): port = str(port) if ':' in port: port = port.split(':')[-1] + if '/' in port: + port = tuple(port.split('/')) ports.append(port) container_options['ports'] = ports diff --git a/tests/service_test.py b/tests/service_test.py index dcbc4627..ca9a0021 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -187,6 +187,11 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(container['HostConfig']['PortBindings'].keys(), ['8000/tcp']) self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') + def test_start_container_creates_port_with_explicit_protocol(self): + service = self.create_service('web', ports=['8000/udp']) + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['PortBindings'].keys(), ['8000/udp']) + def test_start_container_creates_fixed_external_ports(self): service = self.create_service('web', ports=['8000:8000']) container = service.start_container().inspect() From e8472be6d5b21246303f2a78eb1cdfeb62ea202a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 22 Jan 2014 17:44:04 +0000 Subject: [PATCH 0230/2154] Fig bug in split_buffer where input was being discarded Also, write some tests for it. --- fig/cli/log_printer.py | 21 ++------------------- fig/cli/utils.py | 25 +++++++++++++++++++++++++ tests/split_buffer_test.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 tests/split_buffer_test.py diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index a3ad15eb..0fe3215e 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -6,6 +6,7 @@ from itertools import cycle from .multiplexer import Multiplexer from . import colors +from .utils import split_buffer class LogPrinter(object): @@ -33,7 +34,7 @@ class LogPrinter(object): prefix = color_fn(container.name + " | ") # Attach to container before log printer starts running line_generator = split_buffer(self._attach(container), '\n') - return (prefix + line for line in line_generator) + return (prefix + line.decode('utf-8') for line in line_generator) def _attach(self, container): params = { @@ -44,21 +45,3 @@ class LogPrinter(object): params.update(self.attach_params) params = dict((name, 1 if value else 0) for (name, value) in list(params.items())) return container.attach(**params) - -def split_buffer(reader, separator): - """ - Given a generator which yields strings and a separator string, - joins all input, splits on the separator and yields each chunk. - Requires that each input string is decodable as UTF-8. - """ - buffered = '' - - for data in reader: - lines = (buffered + data.decode('utf-8')).split(separator) - for line in lines[:-1]: - yield line + separator - if len(lines) > 1: - buffered = lines[-1] - - if len(buffered) > 0: - yield buffered diff --git a/fig/cli/utils.py b/fig/cli/utils.py index 2b0eb42d..3116df98 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -83,3 +83,28 @@ def mkdir(path, permissions=0o700): def docker_url(): return os.environ.get('DOCKER_HOST') + + +def split_buffer(reader, separator): + """ + Given a generator which yields strings and a separator string, + joins all input, splits on the separator and yields each chunk. + + Unlike string.split(), each chunk includes the trailing + separator, except for the last one if none was found on the end + of the input. + """ + buffered = str('') + separator = str(separator) + + for data in reader: + buffered += data + while True: + index = buffered.find(separator) + if index == -1: + break + yield buffered[:index+1] + buffered = buffered[index+1:] + + if len(buffered) > 0: + yield buffered diff --git a/tests/split_buffer_test.py b/tests/split_buffer_test.py new file mode 100644 index 00000000..b90463c0 --- /dev/null +++ b/tests/split_buffer_test.py @@ -0,0 +1,37 @@ +from __future__ import unicode_literals +from __future__ import absolute_import +from fig.cli.utils import split_buffer +from . import unittest + +class SplitBufferTest(unittest.TestCase): + def test_single_line_chunks(self): + def reader(): + yield "abc\n" + yield "def\n" + yield "ghi\n" + + self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi\n"]) + + def test_no_end_separator(self): + def reader(): + yield "abc\n" + yield "def\n" + yield "ghi" + + self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi"]) + + def test_multiple_line_chunk(self): + def reader(): + yield "abc\ndef\nghi" + + self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi"]) + + def test_chunked_line(self): + def reader(): + yield "a" + yield "b" + yield "c" + yield "\n" + yield "d" + + self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "d"]) From 33aada05a47358e260588b8d998963af0a8f748d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 23 Jan 2014 11:58:48 +0000 Subject: [PATCH 0231/2154] Bump version to 0.1.3 --- CHANGES.md | 6 ++++++ fig/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 941a9e95..c68184b0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Change log ========== +0.1.3 (2014-01-23) +------------------ + + - Fix ports sometimes being configured incorrectly. (#46) + - Fix log output sometimes not displaying. (#47) + 0.1.2 (2014-01-22) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index ca028a4e..35b931c9 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.1.2' +__version__ = '0.1.3' From 2ebec048119ec84064f9d1b5851a84f6c46d1439 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 23 Jan 2014 12:14:12 +0000 Subject: [PATCH 0232/2154] Hide build/PyPi badges on homepage --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bb5ebeec..e9056583 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,10 @@ Fig === + [![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) [![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig) + Punctual, lightweight development environments using Docker. From cf18a3141f6b9d618cd35adc2f574965fba29c92 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 26 Jan 2014 19:37:35 +0000 Subject: [PATCH 0233/2154] Remove images created by tests --- tests/testcases.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/testcases.py b/tests/testcases.py index 8cc1ab35..7f602bb5 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -17,6 +17,9 @@ class DockerClientTestCase(unittest.TestCase): if c['Names'] and 'figtest' in c['Names'][0]: self.client.kill(c['Id']) self.client.remove_container(c['Id']) + for i in self.client.images(): + if 'figtest' in i['Tag']: + self.client.remove_image(i) def create_service(self, name, **kwargs): return Service( From ea93c01dfb0c8de5945dad49440dc49282210f09 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 26 Jan 2014 19:47:43 +0000 Subject: [PATCH 0234/2154] Remove intermediate containers in recreate test --- tests/project_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/project_test.py b/tests/project_test.py index a73aa252..13afe533 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -61,6 +61,10 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(len(web.containers(stopped=True)), 1) self.assertEqual(len(db.containers(stopped=True)), 1) + # remove intermediate containers + for (service, container) in old: + container.remove() + def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') From ac90e0e9398be7447d6a300c3c77c7dba295df48 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 26 Jan 2014 19:54:00 +0000 Subject: [PATCH 0235/2154] Remove Travis badge Travis is running out of disk space. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9056583..b00160c4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Fig === -[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) + [![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig) From ee49e7055b836f05f87d979528d20c0078653237 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 26 Jan 2014 20:28:37 +0000 Subject: [PATCH 0236/2154] Pull ubuntu image for CLI tests --- tests/cli_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/cli_test.py b/tests/cli_test.py index f57553c5..f6d37268 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -1,12 +1,13 @@ from __future__ import unicode_literals from __future__ import absolute_import -from . import unittest +from .testcases import DockerClientTestCase from mock import patch from six import StringIO from fig.cli.main import TopLevelCommand -class CLITestCase(unittest.TestCase): +class CLITestCase(DockerClientTestCase): def setUp(self): + super(CLITestCase, self).setUp() self.command = TopLevelCommand() self.command.base_dir = 'tests/fixtures/simple-figfile' From ddf6819a75f4e39352b4a85f212b301f2f46243d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 26 Jan 2014 20:37:30 +0000 Subject: [PATCH 0237/2154] Only pull ubuntu:latest in tests Might stop Travis running out of disk space. --- tests/testcases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testcases.py b/tests/testcases.py index 7f602bb5..13c24a92 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -10,7 +10,7 @@ class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.client = Client(docker_url()) - cls.client.pull('ubuntu') + cls.client.pull('ubuntu', tag='latest') def setUp(self): for c in self.client.containers(all=True): From 8f8b0bbd16c58ad7305002fdf367d496cb2c4543 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 26 Jan 2014 21:29:59 +0000 Subject: [PATCH 0238/2154] Revert "Remove Travis badge" This reverts commit ac90e0e9398be7447d6a300c3c77c7dba295df48. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b00160c4..e9056583 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Fig === - +[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) [![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig) From 7c9c55785da9c7181743357a3ff719d6388c74d4 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 27 Jan 2014 10:00:36 +0000 Subject: [PATCH 0239/2154] Move mock to dev requirements --- requirements-dev.txt | 1 + requirements.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4ef6576c..5fd088d4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ +mock==1.0.1 nose==1.3.0 unittest2==0.5.1 diff --git a/requirements.txt b/requirements.txt index ba6741dc..a4de170c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,3 @@ PyYAML==3.10 texttable==0.8.1 # docker requires six==1.3.0 six==1.3.0 -mock==1.0.1 From f60621ee1bbed066bb36fb3e78f7b9ed32bc3c4c Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 27 Jan 2014 11:42:38 +0000 Subject: [PATCH 0240/2154] Move docs to master branch - build with script/build-docs - deploy with script/deploy-docs --- .gitignore | 3 +- docs/.gitignore-gh-pages | 1 + docs/Dockerfile | 10 ++ docs/Gemfile | 3 + docs/Gemfile.lock | 62 ++++++++ docs/_config.yml | 1 + docs/_layouts/default.html | 43 ++++++ docs/css/bootstrap.min.css | 7 + docs/css/fig.css | 124 ++++++++++++++++ docs/fig.yml | 8 + docs/img/logo.png | Bin 0 -> 133640 bytes docs/index.md | 293 +++++++++++++++++++++++++++++++++++++ script/build-docs | 5 + script/deploy-docs | 29 ++++ 14 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 docs/.gitignore-gh-pages create mode 100644 docs/Dockerfile create mode 100644 docs/Gemfile create mode 100644 docs/Gemfile.lock create mode 100644 docs/_config.yml create mode 100644 docs/_layouts/default.html create mode 100644 docs/css/bootstrap.min.css create mode 100644 docs/css/fig.css create mode 100644 docs/fig.yml create mode 100644 docs/img/logo.png create mode 100644 docs/index.md create mode 100755 script/build-docs create mode 100755 script/deploy-docs diff --git a/.gitignore b/.gitignore index c6e51dbc..5aad33fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.egg-info *.pyc /dist -/_site +/docs/_site +/docs/.git-gh-pages diff --git a/docs/.gitignore-gh-pages b/docs/.gitignore-gh-pages new file mode 100644 index 00000000..0baf0152 --- /dev/null +++ b/docs/.gitignore-gh-pages @@ -0,0 +1 @@ +/_site diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..3102b418 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,10 @@ +FROM stackbrew/ubuntu:13.10 +RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python +RUN locale-gen en_US.UTF-8 +ADD Gemfile /code/ +ADD Gemfile.lock /code/ +WORKDIR /code +RUN bundle install +ADD . /code +EXPOSE 4000 +CMD bundle exec jekyll build diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000..97355ea7 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'github-pages' diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 00000000..447ae05c --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,62 @@ +GEM + remote: https://rubygems.org/ + specs: + RedCloth (4.2.9) + blankslate (2.1.2.4) + classifier (1.3.3) + fast-stemmer (>= 1.0.0) + colorator (0.1) + commander (4.1.5) + highline (~> 1.6.11) + fast-stemmer (1.0.2) + ffi (1.9.3) + github-pages (12) + RedCloth (= 4.2.9) + jekyll (= 1.4.2) + kramdown (= 1.2.0) + liquid (= 2.5.4) + maruku (= 0.7.0) + rdiscount (= 2.1.7) + redcarpet (= 2.3.0) + highline (1.6.20) + jekyll (1.4.2) + classifier (~> 1.3) + colorator (~> 0.1) + commander (~> 4.1.3) + liquid (~> 2.5.2) + listen (~> 1.3) + maruku (~> 0.7.0) + pygments.rb (~> 0.5.0) + redcarpet (~> 2.3.0) + safe_yaml (~> 0.9.7) + toml (~> 0.1.0) + kramdown (1.2.0) + liquid (2.5.4) + listen (1.3.1) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + rb-kqueue (>= 0.2) + maruku (0.7.0) + parslet (1.5.0) + blankslate (~> 2.0) + posix-spawn (0.3.8) + pygments.rb (0.5.4) + posix-spawn (~> 0.3.6) + yajl-ruby (~> 1.1.0) + rb-fsevent (0.9.4) + rb-inotify (0.9.3) + ffi (>= 0.5.0) + rb-kqueue (0.2.0) + ffi (>= 0.5.0) + rdiscount (2.1.7) + redcarpet (2.3.0) + safe_yaml (0.9.7) + toml (0.1.0) + parslet (~> 1.5.0) + yajl-ruby (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + github-pages diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 00000000..956da86d --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +markdown: redcarpet diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 00000000..796d0d4e --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,43 @@ + + + + + {{ page.title }} + + + + + + + + +
+

+ + Fig +

+ + + + +
{{ content }}
+
+ + + + + + diff --git a/docs/css/bootstrap.min.css b/docs/css/bootstrap.min.css new file mode 100644 index 00000000..c547283b --- /dev/null +++ b/docs/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.0.3 (http://getbootstrap.com) + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + */ + +/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}} \ No newline at end of file diff --git a/docs/css/fig.css b/docs/css/fig.css new file mode 100644 index 00000000..48fef5e2 --- /dev/null +++ b/docs/css/fig.css @@ -0,0 +1,124 @@ +body { + padding-top: 20px; + padding-bottom: 60px; + font-family: 'Lato', sans-serif; + font-weight: 300; + font-size: 18px; + color: #362; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Lato', sans-serif; + font-weight: 400; + color: #25594D; +} + +h2, h3, h4, h5, h6 { + margin-top: 1.5em; +} + +p { + margin: 20px 0; +} + +a, a:hover, a:visited { + color: #4D9900; + text-decoration: underline; +} + +pre, code { + border: none; + background: #D5E1B4; +} + +code, pre code { + color: #484F40; +} + +pre { + border-bottom: 2px solid #bec9a1; + font-size: 14px; +} + +code { + font-size: 0.84em; +} + +pre code { + background: none; +} + +img { + max-width: 100%; +} + +/* Customize container */ +@media (min-width: 768px) { + .container { + max-width: 730px; + } +} + +@media (min-width: 481px) { + .github-top { + position: absolute; + top: 0; + right: 0; + } +} + +.content h1 { + display: none; +} + +.logo { + text-align: center; + font-family: 'Lilita One', sans-serif; + font-size: 80px; + color: #a41211; + margin: 20px 0 40px 0; +} + +.logo img { + width: 100px; + vertical-align: -17px; +} + +@media (min-width: 481px) { + .logo { + font-size: 96px; + margin-top: 40px; + } + .logo img { + vertical-align: -40px; + width: 150px; + margin-right: 20px; + } +} + +.github-top, +.github-bottom { + text-align: center; +} + +.github-top { + margin: 30px; +} + +.github-bottom { + margin: 60px 0; +} + +a.btn { + background: #25594D; + color: white; + text-transform: uppercase; + text-decoration: none; +} + +a.btn:hover { + color: white; +} + + + diff --git a/docs/fig.yml b/docs/fig.yml new file mode 100644 index 00000000..45edcb3d --- /dev/null +++ b/docs/fig.yml @@ -0,0 +1,8 @@ +jekyll: + build: . + ports: + - 4000:4000 + volumes: + - .:/code + environment: + - LANG=en_US.UTF-8 diff --git a/docs/img/logo.png b/docs/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..13ca9bc78b18f3bb7195dec87662195508cf5a51 GIT binary patch literal 133640 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≺_r~!Y{H%?R0D5ge&@j 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 z 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 + +Punctual, lightweight development environments using Docker. + +Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this: + +```yaml +web: + build: . + links: + - db + ports: + - 8000:8000 +db: + image: orchardup/postgresql +``` + +Then type `fig up`, and Fig will start and run your entire app: + +![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif) + +There are commands to: + + - start, stop and rebuild services + - view the status of running services + - tail running services' log output + - run a one-off command on a service + +Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news. + + +Getting started +--------------- + +Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it. + +First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): + + $ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx + $ chmod +x /usr/local/bin/docker-osx + $ docker-osx shell + +Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation. + +Next, install Fig: + + $ sudo pip install -U fig + +(This command also upgrades Fig when we release a new version. If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.) + +You'll want to make a directory for the project: + + $ mkdir figtest + $ cd figtest + +Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis: + +```python +from flask import Flask +from redis import Redis +import os +app = Flask(__name__) +redis = Redis( + host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'), + port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT')) +) + +@app.route('/') +def hello(): + redis.incr('hits') + return 'Hello World! I have been seen %s times.' % redis.get('hits') + +if __name__ == "__main__": + app.run(host="0.0.0.0", debug=True) +``` + +We define our Python dependencies in a file called `requirements.txt`: + + flask + redis + +And we define how to build this into a Docker image using a file called `Dockerfile`: + + FROM stackbrew/ubuntu:13.10 + RUN apt-get -qq update + RUN apt-get install -y python python-pip + ADD . /code + WORKDIR /code + RUN pip install -r requirements.txt + EXPOSE 5000 + CMD python app.py + +That tells Docker to create an image with Python and Flask installed on it, run the command `python app.py`, and open port 5000 (the port that Flask listens on). + +We then define a set of services using `fig.yml`: + + web: + build: . + ports: + - 5000:5000 + volumes: + - .:/code + links: + - redis + redis: + image: orchardup/redis + +This defines two services: + + - `web`, which is built from `Dockerfile` in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image. + - `redis`, which uses the public image [orchardup/redis](https://index.docker.io/u/orchardup/redis/). + +Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up: + + $ fig up + Pulling image orchardup/redis... + Building web... + Starting figtest_redis_1... + Starting figtest_web_1... + figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 + figtest_web_1 | * Running on http://0.0.0.0:5000/ + +Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running! + +If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running: + + $ fig up -d + Starting figtest_redis_1... + Starting figtest_web_1... + $ fig ps + Name Command State Ports + ------------------------------------------------------------------- + figtest_redis_1 /usr/local/bin/run Up + figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp + +`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service: + + $ fig run web env + + +See `fig --help` other commands that are available. + +If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them: + + $ fig stop + +That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/orchardup/fig) or [email us](mailto:hello@orchardup.com). + + +Reference +--------- + +### fig.yml + +Each service defined in `fig.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts. + +As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`. + +```yaml +-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally. +image: ubuntu +image: orchardup/postgresql +image: a4bc65fd + +-- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter. +build: /path/to/build/dir + +-- Override the default command. +command: bundle exec thin -p 3000 + +-- Link to containers in another service (see "Communicating between containers"). +links: + - db + - redis + +-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen). +ports: + - 3000 + - 8000:8000 + +-- Map volumes from the host machine (HOST:CONTAINER). +volumes: + - cache/:/tmp/cache + +-- Add environment variables. +environment: + RACK_ENV: development +``` + +### Commands + +Most commands are run against one or more services. If the service is omitted, it will apply to all services. + +Run `fig [COMMAND] --help` for full usage. + +#### build + +Build or rebuild services. + +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. + +#### help + +Get help on a command. + +#### kill + +Force stop service containers. + +#### logs + +View output from services. + +#### ps + +List containers. + +#### rm + +Remove stopped service containers. + + +#### run + +Run a one-off command on a service. + +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. + +#### scale + +Set number of containers to run for a service. + +Numbers are specified in the form `service=num` as arguments. +For example: + + $ fig scale web=2 worker=3 + +#### start + +Start existing containers for a service. + +#### stop + +Stop running containers without removing them. They can be started again with `fig start`. + +#### up + +Build, (re)create, start and attach to containers for a service. + +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 + +Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. + +name\_PORT
+Full URL, e.g. `MYAPP_DB_1_PORT=tcp://172.17.0.5:5432` + +name\_PORT\_num\_protocol
+Full URL, e.g. `MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432` + +name\_PORT\_num\_protocol\_ADDR
+Container's IP address, e.g. `MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5` + +name\_PORT\_num\_protocol\_PORT
+Exposed port number, e.g. `MYAPP_DB_1_PORT_5432_TCP_PORT=5432` + +name\_PORT\_num\_protocol\_PROTO
+Protocol (tcp or udp), e.g. `MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp` + +name\_NAME
+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/script/build-docs b/script/build-docs new file mode 100755 index 00000000..c4e64943 --- /dev/null +++ b/script/build-docs @@ -0,0 +1,5 @@ +#!/bin/bash + +pushd docs +fig run jekyll jekyll build +popd diff --git a/script/deploy-docs b/script/deploy-docs new file mode 100755 index 00000000..447751ee --- /dev/null +++ b/script/deploy-docs @@ -0,0 +1,29 @@ +#!/bin/bash + +set -ex + +pushd docs + +export GIT_DIR=.git-gh-pages +export GIT_WORK_TREE=. + +if [ ! -d "$GIT_DIR" ]; then + git init +fi + +if !(git remote | grep origin); then + git remote add origin git@github.com:orchardup/fig.git +fi + +cp .gitignore-gh-pages .gitignore +echo ".git-gh-pages" >> .gitignore + +git add -u +git add . + +git commit -m "update" || echo "didn't commit" +git push -f origin master:gh-pages + +rm .gitignore + +popd From db396b81ef29c31be179624a7fee5dfcb370a1ae Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 27 Jan 2014 12:26:33 +0000 Subject: [PATCH 0241/2154] Just deploy the _site directory to gh-pages --- script/deploy-docs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/script/deploy-docs b/script/deploy-docs index 447751ee..4bb6b7b0 100755 --- a/script/deploy-docs +++ b/script/deploy-docs @@ -2,7 +2,7 @@ set -ex -pushd docs +pushd docs/_site export GIT_DIR=.git-gh-pages export GIT_WORK_TREE=. @@ -15,8 +15,7 @@ if !(git remote | grep origin); then git remote add origin git@github.com:orchardup/fig.git fi -cp .gitignore-gh-pages .gitignore -echo ".git-gh-pages" >> .gitignore +echo ".git-gh-pages" > .gitignore git add -u git add . @@ -24,6 +23,4 @@ git add . git commit -m "update" || echo "didn't commit" git push -f origin master:gh-pages -rm .gitignore - popd From 1bab14213dde464dd44c468461674f7035b492d5 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 27 Jan 2014 15:29:31 +0000 Subject: [PATCH 0242/2154] Update docker-py From https://github.com/bfirsh/docker-py/commit/0a9512d008b4b79d625aed360d9dc323ccc342d5 --- fig/packages/docker/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 7bc46aac..bc04c40d 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -708,8 +708,11 @@ class Client(requests.Session): start_config['PublishAllPorts'] = publish_all_ports if links: + if isinstance(links, dict): + links = six.iteritems(links) + formatted_links = [ - '{0}:{1}'.format(k, v) for k, v in sorted(six.iteritems(links)) + '{0}:{1}'.format(k, v) for k, v in sorted(links) ] start_config['Links'] = formatted_links From 3e7e6e76563afbbd244803769be4e3e84f4d8a81 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 27 Jan 2014 15:29:58 +0000 Subject: [PATCH 0243/2154] Add link alias without project name REDIS_1_PORT_6379_TCP_ADDR instead of FIGTEST_REDIS_1_PORT_6379_TCP_ADDR. Ref #37 --- README.md | 16 ++++++++-------- fig/container.py | 4 ++++ fig/service.py | 5 +++-- tests/service_test.py | 1 + 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e9056583..b2df3502 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,8 @@ from redis import Redis import os app = Flask(__name__) redis = Redis( - host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'), - port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT')) + host=os.environ.get('REDIS_1_PORT_6379_TCP_ADDR'), + port=int(os.environ.get('REDIS_1_PORT_6379_TCP_PORT')) ) @app.route('/') @@ -266,22 +266,22 @@ If there are existing containers for a service, `fig up` will stop and recreate Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. name\_PORT
-Full URL, e.g. `MYAPP_DB_1_PORT=tcp://172.17.0.5:5432` +Full URL, e.g. `DB_1_PORT=tcp://172.17.0.5:5432` name\_PORT\_num\_protocol
-Full URL, e.g. `MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432` +Full URL, e.g. `DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432` name\_PORT\_num\_protocol\_ADDR
-Container's IP address, e.g. `MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5` +Container's IP address, e.g. `DB_1_PORT_5432_TCP_ADDR=172.17.0.5` name\_PORT\_num\_protocol\_PORT
-Exposed port number, e.g. `MYAPP_DB_1_PORT_5432_TCP_PORT=5432` +Exposed port number, e.g. `DB_1_PORT_5432_TCP_PORT=5432` name\_PORT\_num\_protocol\_PROTO
-Protocol (tcp or udp), e.g. `MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp` +Protocol (tcp or udp), e.g. `DB_1_PORT_5432_TCP_PROTO=tcp` name\_NAME
-Fully qualified container name, e.g. `MYAPP_DB_1_NAME=/myapp_web_1/myapp_db_1` +Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1` [Docker links]: http://docs.docker.io/en/latest/use/port_redirection/#linking-a-container diff --git a/fig/container.py b/fig/container.py index 76f2d29e..c9417d0f 100644 --- a/fig/container.py +++ b/fig/container.py @@ -50,6 +50,10 @@ class Container(object): def name(self): return self.dictionary['Name'][1:] + @property + def name_without_project(self): + return '_'.join(self.dictionary['Name'].split('_')[1:]) + @property def number(self): try: diff --git a/fig/service.py b/fig/service.py index aaaa97ab..2e7a89d0 100644 --- a/fig/service.py +++ b/fig/service.py @@ -207,10 +207,11 @@ class Service(object): return max(numbers) + 1 def _get_links(self): - links = {} + links = [] for service in self.links: for container in service.containers(): - links[container.name] = container.name + links.append((container.name, container.name)) + links.append((container.name, container.name_without_project)) return links def _get_container_options(self, override_options, one_off=False): diff --git a/tests/service_test.py b/tests/service_test.py index ca9a0021..1357b761 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -154,6 +154,7 @@ class ServiceTest(DockerClientTestCase): db.start_container() web.start_container() self.assertIn('figtest_db_1', web.containers()[0].links()) + self.assertIn('db_1', web.containers()[0].links()) db.stop(timeout=1) web.stop(timeout=1) From 9a5a021f913a1d9570ef6357f354bbee217818b6 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 27 Jan 2014 15:54:43 +0000 Subject: [PATCH 0244/2154] Rewrite introduction --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e9056583..23732d0c 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,17 @@ Fig [![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig) -Punctual, lightweight development environments using Docker. +Fast, isolated development environments using Docker. -Fig is a tool for defining and running isolated application environments. You define the services which comprise your app in a simple, version-controllable YAML configuration file that looks like this: +Define your app's environment with Docker so it can be reproduced anywhere. + + FROM orchardup/python:2.7 + ADD . /code + RUN pip install -r requirements.txt + WORKDIR /code + CMD python app.py + +Define your app's services so they can be run alongside in an isolated environment. (No more installing Postgres on your laptop!) ```yaml web: @@ -32,7 +40,7 @@ There are commands to: - tail running services' log output - run a one-off command on a service -Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news. +Fig is a project from [Orchard](https://orchardup.com). [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news. Getting started From 5d71c33cd79215385e9340fe6b03b2a1d8e89f7f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 23 Jan 2014 11:47:51 +0000 Subject: [PATCH 0245/2154] Only install unittest2 on Python 2.6 --- requirements-dev.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 5fd088d4..115e826e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,2 @@ mock==1.0.1 nose==1.3.0 -unittest2==0.5.1 From 15319e86ebd4c7d36f38ae4dfe14b012596b08c0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 27 Jan 2014 17:38:47 +0000 Subject: [PATCH 0246/2154] update --- .gitignore | 1 + Dockerfile | 10 +++ Gemfile | 3 + Gemfile.lock | 62 +++++++++++++ cli.html | 127 +++++++++++++++++++++++++++ css/bootstrap.min.css | 7 ++ css/fig.css | 167 +++++++++++++++++++++++++++++++++++ django.html | 138 +++++++++++++++++++++++++++++ env.html | 84 ++++++++++++++++++ fig.yml | 8 ++ img/logo.png | Bin 0 -> 133640 bytes index.html | 200 ++++++++++++++++++++++++++++++++++++++++++ install.html | 78 ++++++++++++++++ rails.html | 144 ++++++++++++++++++++++++++++++ yml.html | 96 ++++++++++++++++++++ 15 files changed, 1125 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 cli.html create mode 100644 css/bootstrap.min.css create mode 100644 css/fig.css create mode 100644 django.html create mode 100644 env.html create mode 100644 fig.yml create mode 100644 img/logo.png create mode 100644 index.html create mode 100644 install.html create mode 100644 rails.html create mode 100644 yml.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..32995cbb --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.git-gh-pages diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3102b418 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM stackbrew/ubuntu:13.10 +RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python +RUN locale-gen en_US.UTF-8 +ADD Gemfile /code/ +ADD Gemfile.lock /code/ +WORKDIR /code +RUN bundle install +ADD . /code +EXPOSE 4000 +CMD bundle exec jekyll build diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000..97355ea7 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'github-pages' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 00000000..447ae05c --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,62 @@ +GEM + remote: https://rubygems.org/ + specs: + RedCloth (4.2.9) + blankslate (2.1.2.4) + classifier (1.3.3) + fast-stemmer (>= 1.0.0) + colorator (0.1) + commander (4.1.5) + highline (~> 1.6.11) + fast-stemmer (1.0.2) + ffi (1.9.3) + github-pages (12) + RedCloth (= 4.2.9) + jekyll (= 1.4.2) + kramdown (= 1.2.0) + liquid (= 2.5.4) + maruku (= 0.7.0) + rdiscount (= 2.1.7) + redcarpet (= 2.3.0) + highline (1.6.20) + jekyll (1.4.2) + classifier (~> 1.3) + colorator (~> 0.1) + commander (~> 4.1.3) + liquid (~> 2.5.2) + listen (~> 1.3) + maruku (~> 0.7.0) + pygments.rb (~> 0.5.0) + redcarpet (~> 2.3.0) + safe_yaml (~> 0.9.7) + toml (~> 0.1.0) + kramdown (1.2.0) + liquid (2.5.4) + listen (1.3.1) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + rb-kqueue (>= 0.2) + maruku (0.7.0) + parslet (1.5.0) + blankslate (~> 2.0) + posix-spawn (0.3.8) + pygments.rb (0.5.4) + posix-spawn (~> 0.3.6) + yajl-ruby (~> 1.1.0) + rb-fsevent (0.9.4) + rb-inotify (0.9.3) + ffi (>= 0.5.0) + rb-kqueue (0.2.0) + ffi (>= 0.5.0) + rdiscount (2.1.7) + redcarpet (2.3.0) + safe_yaml (0.9.7) + toml (0.1.0) + parslet (~> 1.5.0) + yajl-ruby (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + github-pages diff --git a/cli.html b/cli.html new file mode 100644 index 00000000..b33abbba --- /dev/null +++ b/cli.html @@ -0,0 +1,127 @@ + + + + + Fig CLI reference + + + + + + +
+ + +

CLI reference

+ +

Most commands are run against one or more services. If the service is omitted, it will apply to all services.

+ +

Run fig [COMMAND] --help for full usage.

+ +

build

+ +

Build or rebuild services.

+ +

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.

+ +

help

+ +

Get help on a command.

+ +

kill

+ +

Force stop service containers.

+ +

logs

+ +

View output from services.

+ +

ps

+ +

List containers.

+ +

rm

+ +

Remove stopped service containers.

+ +

run

+ +

Run a one-off command on a service.

+ +

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.

+ +

scale

+ +

Set number of containers to run for a service.

+ +

Numbers are specified in the form service=num as arguments. +For example:

+
$ fig scale web=2 worker=3
+
+

start

+ +

Start existing containers for a service.

+ +

stop

+ +

Stop running containers without removing them. They can be started again with fig start.

+ +

up

+ +

Build, (re)create, start and attach to containers for a service.

+ +

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.

+
+ + +
+ + + + diff --git a/css/bootstrap.min.css b/css/bootstrap.min.css new file mode 100644 index 00000000..c547283b --- /dev/null +++ b/css/bootstrap.min.css @@ -0,0 +1,7 @@ +/*! + * Bootstrap v3.0.3 (http://getbootstrap.com) + * Copyright 2013 Twitter, Inc. + * Licensed under http://www.apache.org/licenses/LICENSE-2.0 + */ + +/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}} \ No newline at end of file diff --git a/css/fig.css b/css/fig.css new file mode 100644 index 00000000..345a82cc --- /dev/null +++ b/css/fig.css @@ -0,0 +1,167 @@ +body { + padding-top: 20px; + padding-bottom: 60px; + font-family: 'Lato', sans-serif; + font-weight: 300; + font-size: 18px; + color: #362; +} + +h1, h2, h3, h4, h5, h6 { + font-family: 'Lato', sans-serif; + font-weight: 400; + color: #25594D; +} + +h2, h3, h4, h5, h6 { + margin-top: 1.5em; +} + +p { + margin: 20px 0; +} + +a, a:hover, a:visited { + color: #4D9900; + text-decoration: underline; +} + +pre, code { + border: none; + background: #D5E1B4; +} + +code, pre code { + color: #484F40; +} + +pre { + border-bottom: 2px solid #bec9a1; + font-size: 14px; +} + +code { + font-size: 0.84em; +} + +pre code { + background: none; +} + +img { + max-width: 100%; +} + +.container { + margin-left: 0; +} + +.logo { + font-family: 'Lilita One', sans-serif; + font-size: 80px; + margin: 20px 0 40px 0; + color: #a41211; +} + +.logo img { + width: 80px; + vertical-align: -17px; +} + +.mobile-logo { + text-align: center; +} + +.sidebar { + font-size: 16px; +} + +.sidebar a { + color: #a41211; +} + +@media (max-width: 767px) { + .sidebar { + text-align: center; + margin-top: 40px; + } + + .sidebar .logo { + display: none; + } +} + +@media (min-width: 768px) { + .mobile-logo { + display: none; + } + + .logo { + margin-top: 40px; + } + + .content h1 { + margin: 60px 0 55px 0; + } + + .sidebar { + position: fixed; + top: 0; + left: 0; + bottom: 0; + width: 280px; + overflow-y: auto; + padding-left: 40px; + border-right: 1px solid #ccc; + } + + .content { + margin-left: 320px; + max-width: 650px; + } +} + +.nav { + margin: 20px 0; +} + +.nav li a { + display: block; + padding: 8px 0; + line-height: 1.2; + text-decoration: none; +} + +.nav li a:hover, .nav li a:focus { + text-decoration: underline; + background: none; +} + +.nav ul { + padding-left: 20px; + list-style: none; +} + +.badges { + margin: 40px 0; +} + +a.btn { + background: #25594D; + color: white; + text-transform: uppercase; + text-decoration: none; +} + +a.btn:hover { + color: white; +} + +.strapline { + font-size: 40px; + display: block; + line-height: 1.2; + margin-top: 25px; + margin-bottom: 35px; +} + diff --git a/django.html b/django.html new file mode 100644 index 00000000..70dd0c14 --- /dev/null +++ b/django.html @@ -0,0 +1,138 @@ + + + + + Getting started with Fig and Django + + + + + + +
+ + +

Getting started with Fig and Django

+ +

Let's use Fig to set up and run a Django/PostgreSQL app. Before starting, you'll need to have Fig installed.

+ +

Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called Dockerfile. It'll contain this to start with:

+
FROM orchardup/python:2.7
+RUN mkdir /code
+WORKDIR /code
+ADD requirements.txt /code/
+RUN pip install -r requirements.txt
+ADD . /code/
+
+

That'll install our application inside an image with Python installed alongside all of our Python dependencies.

+ +

Second, we define our Python dependencies in a file called requirements.txt:

+
Django
+
+

Simple enough. Finally, this is all tied together with a file called fig.yml. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.

+
db:
+  image: orchardup/postgresql
+  ports:
+    - 5432
+web:
+  build: .
+  command: python manage.py runserver 0.0.0.0:8000
+  volumes:
+    - .:/code
+  ports:
+    - 8000:8000
+  links:
+    - db
+
+

See the fig.yml reference for more information on how it works.

+ +

We can now start a Django project using fig run:

+
$ fig run web django-admin.py startproject figexample .
+
+

First, Fig will build an image for the web service using the Dockerfile. It will then run django-admin.py startproject figexample . inside a container using that image.

+ +

This will generate a Django app inside the current directory:

+
$ ls
+Dockerfile       fig.yml          figexample       manage.py        requirements.txt
+
+

First thing we need to do is set up the database connection. Replace the DATABASES = ... definition in figexample/settings.py to read:

+
DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.postgresql_psycopg2',
+        'NAME': 'docker',
+        'USER': 'docker',
+        'PASSWORD': 'docker',
+        'HOST': os.environ.get('MYAPP_DB_1_PORT_5432_TCP_ADDR'),
+        'PORT': os.environ.get('MYAPP_DB_1_PORT_5432_TCP_PORT'),
+    }
+}
+
+

Then, run fig up:

+
Recreating myapp_db_1...
+Recreating myapp_web_1...
+Attaching to myapp_db_1, myapp_web_1
+myapp_db_1 |
+myapp_db_1 | PostgreSQL stand-alone backend 9.1.11
+myapp_db_1 | 2014-01-27 12:17:03 UTC LOG:  database system is ready to accept connections
+myapp_db_1 | 2014-01-27 12:17:03 UTC LOG:  autovacuum launcher started
+myapp_web_1 | Validating models...
+myapp_web_1 |
+myapp_web_1 | 0 errors found
+myapp_web_1 | January 27, 2014 - 12:12:40
+myapp_web_1 | Django version 1.6.1, using settings 'figexample.settings'
+myapp_web_1 | Starting development server at http://0.0.0.0:8000/
+myapp_web_1 | Quit the server with CONTROL-C.
+
+

And your Django app should be running at localhost:3000 (or localdocker:3000 if you're using docker-osx).

+ +

You can also run management commands with Docker. To set up your database, for example, run fig up and in other terminal run:

+
$ fig run web python manage.py syncdb
+
+ + +
+ + + + diff --git a/env.html b/env.html new file mode 100644 index 00000000..8dc16585 --- /dev/null +++ b/env.html @@ -0,0 +1,84 @@ + + + + + Fig environment variables reference + + + + + + +
+ + +

Environment variables reference

+ +

Fig uses Docker links to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container.

+ +

name_PORT
+Full URL, e.g. MYAPP_DB_1_PORT=tcp://172.17.0.5:5432

+ +

name_PORT_num_protocol
+Full URL, e.g. MYAPP_DB_1_PORT_5432_TCP=tcp://172.17.0.5:5432

+ +

name_PORT_num_protocol_ADDR
+Container's IP address, e.g. MYAPP_DB_1_PORT_5432_TCP_ADDR=172.17.0.5

+ +

name_PORT_num_protocol_PORT
+Exposed port number, e.g. MYAPP_DB_1_PORT_5432_TCP_PORT=5432

+ +

name_PORT_num_protocol_PROTO
+Protocol (tcp or udp), e.g. MYAPP_DB_1_PORT_5432_TCP_PROTO=tcp

+ +

name_NAME
+Fully qualified container name, e.g. MYAPP_DB_1_NAME=/myapp_web_1/myapp_db_1

+
+ + +
+ + + + diff --git a/fig.yml b/fig.yml new file mode 100644 index 00000000..45edcb3d --- /dev/null +++ b/fig.yml @@ -0,0 +1,8 @@ +jekyll: + build: . + ports: + - 4000:4000 + volumes: + - .:/code + environment: + - LANG=en_US.UTF-8 diff --git a/img/logo.png b/img/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..13ca9bc78b18f3bb7195dec87662195508cf5a51 GIT binary patch literal 133640 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≺_r~!Y{H%?R0D5ge&@j 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 z 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 + + + + Fig | Punctual, lightweight development environments using Docker + + + + + + +
+ + +

Fast, isolated development environments using Docker.

+ +

Define your app's environment with Docker so it can be reproduced anywhere.

+
FROM orchardup/python:2.7
+ADD . /code
+RUN pip install -r requirements.txt
+WORKDIR /code
+CMD python app.py
+
+

Define your app's services so they can be run alongside in an isolated environment. (No more installing Postgres on your laptop!)

+
web:
+  build: .
+  links:
+   - db
+  ports:
+   - 8000:8000
+db:
+  image: orchardup/postgresql
+
+

Then type fig up, and Fig will start and run your entire app:

+ +

example fig run

+ +

There are commands to:

+ +
    +
  • start, stop and rebuild services
  • +
  • view the status of running services
  • +
  • tail running services' log output
  • +
  • run a one-off command on a service
  • +
+ +

Fig is a project from Orchard. Follow us on Twitter to keep up to date with Fig and other Docker news.

+ +

Quick start

+ +

Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.

+ +

First, install Docker. If you're on OS X, you can use docker-osx:

+
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
+$ chmod +x /usr/local/bin/docker-osx
+$ docker-osx shell
+
+

Docker has guides for Ubuntu and other platforms in their documentation.

+ +

Next, install Fig:

+
$ sudo pip install -U fig
+
+

(This command also upgrades Fig when we release a new version. If you don’t have pip installed, try brew install python or apt-get install python-pip.)

+ +

You'll want to make a directory for the project:

+
$ mkdir figtest
+$ cd figtest
+
+

Inside this directory, create app.py, a simple web app that uses the Flask framework and increments a value in Redis:

+
from flask import Flask
+from redis import Redis
+import os
+app = Flask(__name__)
+redis = Redis(
+    host=os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_ADDR'),
+    port=int(os.environ.get('FIGTEST_REDIS_1_PORT_6379_TCP_PORT'))
+)
+
+@app.route('/')
+def hello():
+    redis.incr('hits')
+    return 'Hello World! I have been seen %s times.' % redis.get('hits')
+
+if __name__ == "__main__":
+    app.run(host="0.0.0.0", debug=True)
+
+

We define our Python dependencies in a file called requirements.txt:

+
flask
+redis
+
+

And we define how to build this into a Docker image using a file called Dockerfile:

+
FROM stackbrew/ubuntu:13.10
+RUN apt-get -qq update
+RUN apt-get install -y python python-pip
+ADD . /code
+WORKDIR /code
+RUN pip install -r requirements.txt
+EXPOSE 5000
+CMD python app.py
+
+

That tells Docker to create an image with Python and Flask installed on it, run the command python app.py, and open port 5000 (the port that Flask listens on).

+ +

We then define a set of services using fig.yml:

+
web:
+  build: .
+  ports:
+   - 5000:5000
+  volumes:
+   - .:/code
+  links:
+   - redis
+redis:
+  image: orchardup/redis
+
+

This defines two services:

+ +
    +
  • web, which is built from Dockerfile in the current directory. It also says to forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image.
  • +
  • redis, which uses the public image orchardup/redis.
  • +
+ +

Now if we run fig up, it'll pull a Redis image, build an image for our own code, and start everything up:

+
$ fig up
+Pulling image orchardup/redis...
+Building web...
+Starting figtest_redis_1...
+Starting figtest_web_1...
+figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3
+figtest_web_1 |  * Running on http://0.0.0.0:5000/
+
+

Open up http://localhost:5000 in your browser (or http://localdocker:5000 if you're using docker-osx) and you should see it running!

+ +

If you want to run your services in the background, you can pass the -d flag to fig up and use fig ps to see what is currently running:

+
$ fig up -d
+Starting figtest_redis_1...
+Starting figtest_web_1...
+$ fig ps
+        Name                 Command            State       Ports
+-------------------------------------------------------------------
+figtest_redis_1   /usr/local/bin/run         Up
+figtest_web_1     /bin/sh -c python app.py   Up      5000->5000/tcp
+
+

fig run allows you to run one-off commands for your services. For example, to see what environment variables are available to the web service:

+
$ fig run web env
+
+

See fig --help other commands that are available.

+ +

If you started Fig with fig up -d, you'll probably want to stop your services once you've finished with them:

+
$ fig stop
+
+

That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, open an issue on GitHub or email us.

+
+ + +
+ + + + diff --git a/install.html b/install.html new file mode 100644 index 00000000..42a26906 --- /dev/null +++ b/install.html @@ -0,0 +1,78 @@ + + + + + Installing Fig + + + + + + +
+ + +

Installing Fig

+ +

First, install Docker. If you're on OS X, you can use docker-osx:

+
$ curl https://raw.github.com/noplay/docker-osx/master/docker-osx > /usr/local/bin/docker-osx
+$ chmod +x /usr/local/bin/docker-osx
+$ docker-osx shell
+
+

Docker has guides for Ubuntu and other platforms in their documentation.

+ +

Next, install Fig:

+
$ sudo pip install -U fig
+
+

(This command also upgrades Fig when we release a new version. If you don’t have pip installed, try brew install python or apt-get install python-pip.)

+ +

That should be all you need! Run fig --version to see if it worked.

+
+ + +
+ + + + diff --git a/rails.html b/rails.html new file mode 100644 index 00000000..5be52253 --- /dev/null +++ b/rails.html @@ -0,0 +1,144 @@ + + + + + Getting started with Fig and Rails + + + + + + +
+ + +

Getting started with Fig and Rails

+ +

We're going to use Fig to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have Fig installed.

+ +

Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called Dockerfile. It'll contain this to start with:

+
FROM binaryphile/ruby:2.0.0-p247
+RUN mkdir /myapp
+WORKDIR /myapp
+ADD Gemfile /myapp/Gemfile
+RUN bundle install
+ADD . /myapp
+
+

That'll put our application code inside an image with Ruby, Bundler and all our dependencies.

+ +

Next, we have a bootstrap Gemfile which just loads Rails. It'll be overwritten in a moment by rails new.

+
source 'https://rubygems.org'
+gem 'rails', '4.0.2'
+
+

Finally, fig.yml is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration we need to link them together and expose the web app's port.

+
db:
+  image: orchardup/postgresql
+  ports:
+    - 5432
+web:
+  build: .
+  command: bundle exec rackup -p 3000
+  volumes:
+    - .:/myapp
+  ports:
+    - 3000:3000
+  links:
+    - db
+
+

With those files in place, we can now generate the Rails skeleton app using fig run:

+
$ fig run web rails new . --force --database=postgresql --skip-bundle
+
+

First, Fig will build the image for the web service using the Dockerfile. Then it'll run rails new inside a new container, using that image. Once it's done, you should have a fresh app generated:

+
$ ls
+Dockerfile   app          fig.yml      tmp
+Gemfile      bin          lib          vendor
+Gemfile.lock config       log
+README.rdoc  config.ru    public
+Rakefile     db           test
+
+

Uncomment the line in your new Gemfile which loads therubyracer, so we've got a Javascript runtime:

+
gem 'therubyracer', platforms: :ruby
+
+

Now that we've got a new Gemfile, we need to build the image again. (This, and changes to the Dockerfile itself, should be the only times you'll need to rebuild).

+
$ fig build
+
+

The app is now bootable, but we're not quite there yet. By default, Rails expects a database to be running on localhost - we need to point it at the db container instead. We also need to change the username and password to align with the defaults set by orchardup/postgresql.

+ +

Open up your newly-generated database.yml. Replace its contents with the following:

+
development: &default
+  adapter: postgresql
+  encoding: unicode
+  database: myapp_development
+  pool: 5
+  username: docker
+  password: docker
+  host: <%= ENV.fetch('MYAPP_DB_1_PORT_5432_TCP_ADDR', 'localhost') %>
+  port: <%= ENV.fetch('MYAPP_DB_1_PORT_5432_TCP_PORT', '5432') %>
+
+test:
+  <<: *default
+  database: myapp_test
+
+

We can now boot the app.

+
$ fig up
+
+

If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain:

+
myapp_web_1 | [2014-01-17 17:16:29] INFO  WEBrick 1.3.1
+myapp_web_1 | [2014-01-17 17:16:29] INFO  ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu]
+myapp_web_1 | [2014-01-17 17:16:29] INFO  WEBrick::HTTPServer#start: pid=1 port=3000
+
+

Finally, we just need to create the database. In another terminal, run:

+
$ fig run web rake db:create
+
+

And we're rolling—see for yourself at localhost:3000 (or localdocker:3000 if you're using docker-osx).

+ +

Screenshot of Rails' stock index.html

+
+ + +
+ + + + diff --git a/yml.html b/yml.html new file mode 100644 index 00000000..228b88f6 --- /dev/null +++ b/yml.html @@ -0,0 +1,96 @@ + + + + + fig.yml reference + + + + + + +
+ + +

fig.yml reference

+ +

Each service defined in fig.yml must specify exactly one of image or build. Other keys are optional, and are analogous to their docker run command-line counterparts.

+ +

As with docker run, options specified in the Dockerfile (e.g. CMD, EXPOSE, VOLUME, ENV) are respected by default - you don't need to specify them again in fig.yml.

+
-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
+image: ubuntu
+image: orchardup/postgresql
+image: a4bc65fd
+
+-- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
+build: /path/to/build/dir
+
+-- Override the default command.
+command: bundle exec thin -p 3000
+
+-- Link to containers in another service (see "Communicating between containers").
+links:
+ - db
+ - redis
+
+-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen).
+ports:
+ - 3000
+ - 8000:8000
+
+-- Map volumes from the host machine (HOST:CONTAINER).
+volumes:
+ - cache/:/tmp/cache
+
+-- Add environment variables.
+environment:
+  RACK_ENV: development
+
+ + +
+ + + + From 5035a10cbef1071ebf9dd25f07b3b09d4290715b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 27 Jan 2014 17:53:58 +0000 Subject: [PATCH 0247/2154] Ship 0.1.4 --- CHANGES.md | 5 +++++ fig/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c68184b0..097cb3c1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change log ========== +0.1.4 (2014-01-27) +------------------ + + - Add a link alias without the project name. This makes the environment variables a little shorter: `REDIS_1_PORT_6379_TCP_ADDR`. (#54) + 0.1.3 (2014-01-23) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 35b931c9..357eaa34 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.1.3' +__version__ = '0.1.4' From 01d0e49a1c180ed1fa1a648df3c6debc07053b66 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 27 Jan 2014 15:03:21 +0000 Subject: [PATCH 0248/2154] New website --- docs/_layouts/default.html | 46 ++++++++--- docs/cli.md | 75 ++++++++++++++++++ docs/css/fig.css | 124 ++++++++++++++++++++--------- docs/django.md | 90 +++++++++++++++++++++ docs/env.md | 29 +++++++ docs/index.md | 157 +++---------------------------------- docs/install.md | 23 ++++++ docs/rails.md | 98 +++++++++++++++++++++++ docs/yml.md | 43 ++++++++++ 9 files changed, 487 insertions(+), 198 deletions(-) create mode 100644 docs/cli.md create mode 100644 docs/django.md create mode 100644 docs/env.md create mode 100644 docs/install.md create mode 100644 docs/rails.md create mode 100644 docs/yml.md diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 796d0d4e..b76ed438 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -4,29 +4,49 @@ {{ page.title }} - + - -
-

+

- - - +
{{ content }}
- - + + + diff --git a/yml.html b/yml.html index dafcf879..9d863148 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
@@ -65,6 +65,7 @@
  • Install
  • Get started with Rails
  • Get started with Django
  • +
  • Get started with Wordpress
  • -

    Fig is a project from Orchard. Follow us on Twitter to keep up to date with Fig and other Docker news.

    +

    Fig is a project from Orchard, a Docker hosting service. Follow us on Twitter to keep up to date with Fig and other Docker news.

    Quick start

    diff --git a/install.html b/install.html index 58236e30..ccba6780 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index ddfb4af3..e661b044 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 1171951a..53bb2ca4 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index b962542c..b98fab96 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 75c430635b46de090165bcc5140050fe0f1dd56d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 28 Feb 2014 19:16:32 +0000 Subject: [PATCH 0339/2154] Vendorise six.py Because pyinstaller adds an old version to the path: http://www.pyinstaller.org/ticket/773 --- fig/cli/command.py | 2 +- fig/packages/docker/auth/auth.py | 2 +- fig/packages/docker/client.py | 2 +- fig/packages/docker/unixconn/unixconn.py | 2 +- fig/packages/docker/utils/utils.py | 2 +- fig/packages/six.py | 404 +++++++++++++++++++++++ 6 files changed, 409 insertions(+), 5 deletions(-) create mode 100644 fig/packages/six.py diff --git a/fig/cli/command.py b/fig/cli/command.py index 62a8a6f1..26825899 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -7,7 +7,7 @@ import logging import os import re import yaml -import six +from ..packages import six import sys from ..project import Project diff --git a/fig/packages/docker/auth/auth.py b/fig/packages/docker/auth/auth.py index bef010f2..8037dcbb 100644 --- a/fig/packages/docker/auth/auth.py +++ b/fig/packages/docker/auth/auth.py @@ -17,7 +17,7 @@ import fileinput import json import os -import six +from fig.packages import six from ..utils import utils diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index c81aa9e3..948a3a67 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -19,7 +19,7 @@ import struct import requests import requests.exceptions -import six +from fig.packages import six from .auth import auth from .unixconn import unixconn diff --git a/fig/packages/docker/unixconn/unixconn.py b/fig/packages/docker/unixconn/unixconn.py index 28068f3c..b5c65931 100644 --- a/fig/packages/docker/unixconn/unixconn.py +++ b/fig/packages/docker/unixconn/unixconn.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import six +from fig.packages import six if six.PY3: import http.client as httplib diff --git a/fig/packages/docker/utils/utils.py b/fig/packages/docker/utils/utils.py index 8fd9e947..1cb04f0c 100644 --- a/fig/packages/docker/utils/utils.py +++ b/fig/packages/docker/utils/utils.py @@ -17,7 +17,7 @@ import tarfile import tempfile import requests -import six +from fig.packages import six def mkbuildcontext(dockerfile): diff --git a/fig/packages/six.py b/fig/packages/six.py new file mode 100644 index 00000000..eae31454 --- /dev/null +++ b/fig/packages/six.py @@ -0,0 +1,404 @@ +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2013 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.3.0" + + +# True if we are running on Python 3. +PY3 = sys.version_info[0] == 3 + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) + # This is a bit ugly, but it avoids running this again. + delattr(tp, self.name) + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + + +class _MovedItems(types.ModuleType): + """Lazy loading of moved objects""" + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("reload_module", "__builtin__", "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("winreg", "_winreg"), +] +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) +del attr + +moves = sys.modules[__name__ + ".moves"] = _MovedItems("moves") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" + + _iterkeys = "keys" + _itervalues = "values" + _iteritems = "items" + _iterlists = "lists" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + _iterkeys = "iterkeys" + _itervalues = "itervalues" + _iteritems = "iteritems" + _iterlists = "iterlists" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +def iterkeys(d, **kw): + """Return an iterator over the keys of a dictionary.""" + return iter(getattr(d, _iterkeys)(**kw)) + +def itervalues(d, **kw): + """Return an iterator over the values of a dictionary.""" + return iter(getattr(d, _itervalues)(**kw)) + +def iteritems(d, **kw): + """Return an iterator over the (key, value) pairs of a dictionary.""" + return iter(getattr(d, _iteritems)(**kw)) + +def iterlists(d, **kw): + """Return an iterator over the (key, [values]) pairs of a dictionary.""" + return iter(getattr(d, _iterlists)(**kw)) + + +if PY3: + def b(s): + return s.encode("latin-1") + def u(s): + return s + if sys.version_info[1] <= 1: + def int2byte(i): + return bytes((i,)) + else: + # This is about 2x faster than the implementation above on 3.2+ + int2byte = operator.methodcaller("to_bytes", 1, "big") + import io + StringIO = io.StringIO + BytesIO = io.BytesIO +else: + def b(s): + return s + def u(s): + return unicode(s, "unicode_escape") + int2byte = chr + import StringIO + StringIO = BytesIO = StringIO.StringIO +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +if PY3: + import builtins + exec_ = getattr(builtins, "exec") + + + def reraise(tp, value, tb=None): + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + + print_ = getattr(builtins, "print") + del builtins + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + + def print_(*args, **kwargs): + """The new-style print function.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + def write(data): + if not isinstance(data, basestring): + data = str(data) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) + +_add_doc(reraise, """Reraise an exception.""") + + +def with_metaclass(meta, base=object): + """Create a base class with a metaclass.""" + return meta("NewBase", (base,), {}) From 5b09fe6af822718912a87b4e4b3a2ce5a17272a0 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 28 Feb 2014 16:08:06 -0800 Subject: [PATCH 0340/2154] Fixing link to Dockerfile reference I was browsing your docs and found the original Dockerfile reference link wasn't correct (i.e. it was/is going to a 404/search page). I corrected the link here. --- django.html | 2 +- index.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/django.html b/django.html index 8cff1ea4..c1965c6c 100644 --- a/django.html +++ b/django.html @@ -30,7 +30,7 @@ ADD requirements.txt /code/ RUN pip install -r requirements.txt ADD . /code/
    -

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Second, we define our Python dependencies in a file called requirements.txt:

    Django
    diff --git a/index.html b/index.html
    index 38f46fee..5cd4757a 100644
    --- a/index.html
    +++ b/index.html
    @@ -101,7 +101,7 @@ ADD . /code
     WORKDIR /code
     RUN pip install -r requirements.txt
     
    -

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    We then define a set of services using fig.yml:

    web:
    diff --git a/rails.html b/rails.html
    index e661b044..5ba47450 100644
    --- a/rails.html
    +++ b/rails.html
    @@ -30,7 +30,7 @@ ADD Gemfile /myapp/Gemfile
     RUN bundle install
     ADD . /myapp
     
    -

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Next, we have a bootstrap Gemfile which just loads Rails. It'll be overwritten in a moment by rails new.

    source 'https://rubygems.org'
    diff --git a/wordpress.html b/wordpress.html
    index 53bb2ca4..018b94a5 100644
    --- a/wordpress.html
    +++ b/wordpress.html
    @@ -26,7 +26,7 @@
     
    FROM orchardup/php5
     ADD . /code
     
    -

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Next up, fig.yml starts our web service and a separate MySQL instance:

    web:
    
    From c66e18c913cda2a0e2881d8089cecc12c0b6fb5c Mon Sep 17 00:00:00 2001
    From: Aanand Prasad 
    Date: Sat, 1 Mar 2014 11:28:31 +0000
    Subject: [PATCH 0341/2154] Fix Dockerfile reference URL in docs
    
    ---
     docs/django.md    | 2 +-
     docs/index.md     | 2 +-
     docs/rails.md     | 2 +-
     docs/wordpress.md | 2 +-
     4 files changed, 4 insertions(+), 4 deletions(-)
    
    diff --git a/docs/django.md b/docs/django.md
    index 9e927916..476c0c2b 100644
    --- a/docs/django.md
    +++ b/docs/django.md
    @@ -18,7 +18,7 @@ Let's set up the three files that'll get us started. First, our app is going to
         RUN pip install -r requirements.txt
         ADD . /code/
     
    -That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/use/builder/).
    +That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
     
     Second, we define our Python dependencies in a file called `requirements.txt`:
     
    diff --git a/docs/index.md b/docs/index.md
    index 5fd7274b..64713244 100644
    --- a/docs/index.md
    +++ b/docs/index.md
    @@ -99,7 +99,7 @@ Next, we want to create a Docker image containing all of our app's dependencies.
         WORKDIR /code
         RUN pip install -r requirements.txt
     
    -This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/use/builder/).
    +This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
     
     We then define a set of services using `fig.yml`:
     
    diff --git a/docs/rails.md b/docs/rails.md
    index 0ee56b65..dca4dbe7 100644
    --- a/docs/rails.md
    +++ b/docs/rails.md
    @@ -18,7 +18,7 @@ Let's set up the three files that'll get us started. First, our app is going to
         RUN bundle install
         ADD . /myapp
     
    -That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/use/builder/).
    +That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
     
     Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`.
     
    diff --git a/docs/wordpress.md b/docs/wordpress.md
    index 6ddfebd2..70ea9f62 100644
    --- a/docs/wordpress.md
    +++ b/docs/wordpress.md
    @@ -17,7 +17,7 @@ FROM orchardup/php5
     ADD . /code
     ```
     
    -This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/use/builder/).
    +This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/).
     
     Next up, `fig.yml` starts our web service and a separate MySQL instance:
     
    
    From afb4220837683f474e68bb5b3adf25df17df7772 Mon Sep 17 00:00:00 2001
    From: Aanand Prasad 
    Date: Sat, 1 Mar 2014 11:28:38 +0000
    Subject: [PATCH 0342/2154] update
    
    ---
     cli.html       | 2 +-
     django.html    | 2 +-
     env.html       | 2 +-
     index.html     | 2 +-
     install.html   | 2 +-
     rails.html     | 2 +-
     wordpress.html | 2 +-
     yml.html       | 2 +-
     8 files changed, 8 insertions(+), 8 deletions(-)
    
    diff --git a/cli.html b/cli.html
    index 02438197..8d6cb403 100644
    --- a/cli.html
    +++ b/cli.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/django.html b/django.html index c1965c6c..28eabe2d 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 42aa007a..2c1601c5 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 5cd4757a..01f7c282 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index ccba6780..60a60afb 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index 5ba47450..a0fd521c 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 018b94a5..c5879771 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index b98fab96..fa3d430f 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 9d1383ba269e2b04b93a7a5042d6589f7870b077 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 26 Feb 2014 15:45:14 +0000 Subject: [PATCH 0343/2154] Alternate fig file can be specified with -f --- fig/cli/command.py | 13 +++++++++++-- fig/cli/main.py | 1 + tests/cli_test.py | 22 ++++++++++++++++++++++ tests/fixtures/multiple-figfiles/fig.yml | 6 ++++++ tests/fixtures/multiple-figfiles/fig2.yml | 3 +++ 5 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/multiple-figfiles/fig.yml create mode 100644 tests/fixtures/multiple-figfiles/fig2.yml diff --git a/fig/cli/command.py b/fig/cli/command.py index 5ac0b48f..8a945a67 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -22,6 +22,9 @@ log = logging.getLogger(__name__) class Command(DocoptCommand): base_dir = '.' + def __init__(self): + self.yaml_path = os.environ.get('FIG_FILE', None) + def dispatch(self, *args, **kwargs): try: super(Command, self).dispatch(*args, **kwargs) @@ -54,6 +57,11 @@ Couldn't connect to Docker daemon at %s - is it running? If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable. """ % self.client.base_url) + def perform_command(self, options, *args, **kwargs): + if options['--file'] is not None: + self.yaml_path = os.path.join(self.base_dir, options['--file']) + return super(Command, self).perform_command(options, *args, **kwargs) + @cached_property def client(self): return Client(docker_url()) @@ -61,9 +69,10 @@ If it's at a non-standard location, specify the URL with the DOCKER_HOST environ @cached_property def project(self): try: - yaml_path = self.check_yaml_filename() + yaml_path = self.yaml_path + if yaml_path is None: + yaml_path = self.check_yaml_filename() config = yaml.load(open(yaml_path)) - except IOError as e: if e.errno == errno.ENOENT: log.error("Can't find %s. Are you in the right directory?", os.path.basename(e.filename)) diff --git a/fig/cli/main.py b/fig/cli/main.py index eee5563f..37c425f1 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -70,6 +70,7 @@ class TopLevelCommand(Command): Options: --verbose Show more output --version Print version and exit + -f, --file FILE Specify an alternate fig file (default: fig.yml) Commands: build Build or rebuild services diff --git a/tests/cli_test.py b/tests/cli_test.py index b6d02daa..38a9cd28 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -31,6 +31,28 @@ class CLITestCase(DockerClientTestCase): self.command.dispatch(['ps'], None) self.assertIn('fig_simple_1', mock_stdout.getvalue()) + @patch('sys.stdout', new_callable=StringIO) + def test_default_figfile(self, mock_stdout): + self.command.base_dir = 'tests/fixtures/multiple-figfiles' + self.command.dispatch(['up', '-d'], None) + self.command.dispatch(['ps'], None) + + output = mock_stdout.getvalue() + self.assertIn('fig_simple_1', output) + self.assertIn('fig_another_1', output) + self.assertNotIn('fig_yetanother_1', output) + + @patch('sys.stdout', new_callable=StringIO) + def test_alternate_figfile(self, mock_stdout): + self.command.base_dir = 'tests/fixtures/multiple-figfiles' + self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None) + self.command.dispatch(['-f', 'fig2.yml', 'ps'], None) + + output = mock_stdout.getvalue() + self.assertNotIn('fig_simple_1', output) + self.assertNotIn('fig_another_1', output) + self.assertIn('fig_yetanother_1', output) + def test_scale(self): project = self.command.project diff --git a/tests/fixtures/multiple-figfiles/fig.yml b/tests/fixtures/multiple-figfiles/fig.yml new file mode 100644 index 00000000..22532375 --- /dev/null +++ b/tests/fixtures/multiple-figfiles/fig.yml @@ -0,0 +1,6 @@ +simple: + image: ubuntu + command: /bin/sleep 300 +another: + image: ubuntu + command: /bin/sleep 300 diff --git a/tests/fixtures/multiple-figfiles/fig2.yml b/tests/fixtures/multiple-figfiles/fig2.yml new file mode 100644 index 00000000..330ed771 --- /dev/null +++ b/tests/fixtures/multiple-figfiles/fig2.yml @@ -0,0 +1,3 @@ +yetanother: + image: ubuntu + command: /bin/sleep 300 From c709251f211113d7729e4ed8e3cc8060c2dbd958 Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Sun, 2 Mar 2014 00:17:19 +0800 Subject: [PATCH 0344/2154] Add custom link names (Closes #72) --- fig/project.py | 14 ++++++++++---- fig/service.py | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/fig/project.py b/fig/project.py index 235826d2..1b4d2725 100644 --- a/fig/project.py +++ b/fig/project.py @@ -12,15 +12,17 @@ def sort_service_dicts(services): temporary_marked = set() sorted_services = [] + get_service_names = lambda links: [link.split(':')[0] for link in links] + def visit(n): if n['name'] in temporary_marked: - if n['name'] in n.get('links', []): + if n['name'] in get_service_names(n.get('links', [])): raise DependencyError('A service can not link to itself: %s' % n['name']) else: raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked)) if n in unmarked: temporary_marked.add(n['name']) - dependents = [m for m in services if n['name'] in m.get('links', [])] + dependents = [m for m in services if n['name'] in get_service_names(m.get('links', []))] for m in dependents: visit(m) temporary_marked.remove(n['name']) @@ -51,8 +53,12 @@ class Project(object): # Reference links by object links = [] if 'links' in service_dict: - for service_name in service_dict.get('links', []): - links.append(project.get_service(service_name)) + for link in service_dict.get('links', []): + if ':' in link: + service_name, link_name = link.split(':', 1) + else: + service_name, link_name = link, None + links.append((project.get_service(service_name), link_name)) del service_dict['links'] project.services.append(Service(client=client, project=name, links=links, **service_dict)) return project diff --git a/fig/service.py b/fig/service.py index e98441f8..c4ebd85e 100644 --- a/fig/service.py +++ b/fig/service.py @@ -229,8 +229,10 @@ class Service(object): def _get_links(self): links = [] - for service in self.links: + for service, link_name in self.links: for container in service.containers(): + if link_name: + links.append((container.name, link_name)) links.append((container.name, container.name)) links.append((container.name, container.name_without_project)) for container in self.containers(): From e38e8666268b89cf12c0a947be81bb367bdb88eb Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Sun, 2 Mar 2014 00:30:33 +0800 Subject: [PATCH 0345/2154] Fix links related test --- tests/service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/service_test.py b/tests/service_test.py index c2f42510..258d9261 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -154,7 +154,7 @@ class ServiceTest(DockerClientTestCase): def test_start_container_creates_links(self): db = self.create_service('db') - web = self.create_service('web', links=[db]) + web = self.create_service('web', links=[(db, None)]) db.start_container() web.start_container() self.assertIn('figtest_db_1', web.containers()[0].links()) From 193558a4bce1980676d9fd3d9d95dfb020f3c415 Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Mon, 3 Mar 2014 08:54:47 +0800 Subject: [PATCH 0346/2154] Add link names test --- tests/service_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/service_test.py b/tests/service_test.py index 258d9261..01c1ecab 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -160,6 +160,13 @@ class ServiceTest(DockerClientTestCase): self.assertIn('figtest_db_1', web.containers()[0].links()) self.assertIn('db_1', web.containers()[0].links()) + def test_start_container_creates_links_with_names(self): + db = self.create_service('db') + web = self.create_service('web', links=[(db, 'custom_link_name')]) + db.start_container() + web.start_container() + self.assertIn('custom_link_name', web.containers()[0].links()) + def test_start_container_creates_links_to_its_own_service(self): db1 = self.create_service('db') db2 = self.create_service('db') From 41bacae171ef021b7f85b79250bfcd2b3151b97a Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Mon, 3 Mar 2014 17:55:00 +0800 Subject: [PATCH 0347/2154] Provide option to remove volumes in `fig rm` --- fig/cli/main.py | 6 +++++- fig/container.py | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index eee5563f..dfd02d0b 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -170,6 +170,9 @@ class TopLevelCommand(Command): Remove stopped service containers. Usage: rm [SERVICE...] + + Options: + -v Remove volumes associated with containers """ all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True) stopped_containers = [c for c in all_containers if not c.is_running] @@ -177,7 +180,8 @@ class TopLevelCommand(Command): if len(stopped_containers) > 0: print("Going to remove", list_containers(stopped_containers)) if yesno("Are you sure? [yN] ", default=False): - self.project.remove_stopped(service_names=options['SERVICE']) + self.project.remove_stopped(service_names=options['SERVICE'], + remove_volumes=options['-v']) else: print("No stopped containers") diff --git a/fig/container.py b/fig/container.py index c9417d0f..a38828d0 100644 --- a/fig/container.py +++ b/fig/container.py @@ -111,8 +111,9 @@ class Container(object): def kill(self): return self.client.kill(self.id) - def remove(self): - return self.client.remove_container(self.id) + def remove(self, **options): + v = options.get('remove_volumes', False) + return self.client.remove_container(self.id, v=v) def inspect_if_not_inspected(self): if not self.has_been_inspected: From 8e42d6fbb3364aae6de16525bea6996a4f52f0b1 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 12:08:09 +0000 Subject: [PATCH 0348/2154] Remove six from requirements It was vendorised in 75c430635b46de090165bcc5140050fe0f1dd56d --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5dfd7d46..bec41811 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ docopt==0.6.1 PyYAML==3.10 requests==2.2.1 -six>=1.3.0 texttable==0.8.1 websocket-client==0.11.0 From bf47aa97b4687fb78abc0a42cd033fa8dff742c8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 12:25:38 +0000 Subject: [PATCH 0349/2154] Fix tests importing six --- tests/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/cli_test.py b/tests/cli_test.py index 38a9cd28..61e30b7a 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from .testcases import DockerClientTestCase from mock import patch -from six import StringIO +from fig.packages.six import StringIO from fig.cli.main import TopLevelCommand class CLITestCase(DockerClientTestCase): From b06d37f88525c5688dd4e2ac099a4fd0eadd8d3c Mon Sep 17 00:00:00 2001 From: Gary Rennie Date: Thu, 20 Feb 2014 12:53:43 +0000 Subject: [PATCH 0350/2154] Documentation: Include notes on mapping ports When mapping ports as strings there is an issue with the way YAML parses numbers in the format "xx:yy" where yy is less than 60 - this issue is now included in the documentation. This fixes #103 --- README.md | 5 ++++- docs/django.md | 4 ++-- docs/fig.yml | 2 +- docs/index.md | 4 ++-- docs/rails.md | 4 ++-- docs/wordpress.md | 4 ++-- docs/yml.md | 6 ++++-- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1918d9b3..bd7d4515 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,14 @@ web: links: - db ports: - - 8000:8000 + - "8000:8000" + - "49100:22" db: image: orchardup/postgresql ``` +**Note** When mapping ports in the format HOST:CONTAINER you may experience erroneous results when using a container port lower than 60. This is due to YAML parsing numbers in the format "xx:yy" as sexagesimal (base 60) for this reason it is recommended to define your port mappings as strings. + (No more installing Postgres on your laptop!) Then type `fig up`, and Fig will start and run your entire app: diff --git a/docs/django.md b/docs/django.md index 476c0c2b..8ecb3641 100644 --- a/docs/django.md +++ b/docs/django.md @@ -29,14 +29,14 @@ Simple enough. Finally, this is all tied together with a file called `fig.yml`. db: image: orchardup/postgresql ports: - - 5432 + - "5432" web: build: . command: python manage.py runserver 0.0.0.0:8000 volumes: - .:/code ports: - - 8000:8000 + - "8000:8000" links: - db diff --git a/docs/fig.yml b/docs/fig.yml index 45edcb3d..30e8f6d9 100644 --- a/docs/fig.yml +++ b/docs/fig.yml @@ -1,7 +1,7 @@ jekyll: build: . ports: - - 4000:4000 + - "4000:4000" volumes: - .:/code environment: diff --git a/docs/index.md b/docs/index.md index 64713244..29b2a653 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ web: links: - db ports: - - 8000:8000 + - "8000:8000" db: image: orchardup/postgresql ``` @@ -107,7 +107,7 @@ We then define a set of services using `fig.yml`: build: . command: python app.py ports: - - 5000:5000 + - "5000:5000" volumes: - .:/code links: diff --git a/docs/rails.md b/docs/rails.md index dca4dbe7..2dbf9003 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -30,14 +30,14 @@ Finally, `fig.yml` is where the magic happens. It describes what services our ap db: image: orchardup/postgresql ports: - - 5432 + - "5432" web: build: . command: bundle exec rackup -p 3000 volumes: - .:/myapp ports: - - 3000:3000 + - "3000:3000" links: - db diff --git a/docs/wordpress.md b/docs/wordpress.md index 70ea9f62..a2f1bcf4 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -26,7 +26,7 @@ web: build: . command: php -S 0.0.0.0:8000 -t /code ports: - - 8000:8000 + - "8000:8000" links: - db volumes: @@ -34,7 +34,7 @@ web: db: image: orchardup/mysql ports: - - 3306:3306 + - "3306:3306" environment: MYSQL_DATABASE: wordpress ``` diff --git a/docs/yml.md b/docs/yml.md index 2ae3c959..283d41fc 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -28,9 +28,11 @@ links: - redis -- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen). +-- Note When mapping ports in the format HOST:CONTAINER you may experience erroneous results when using a container port lower than 60. This is due to YAML parsing numbers in the format "xx:yy" as sexagesimal (base 60) for this reason it is recommended to define your port mappings as strings. ports: - - 3000 - - 8000:8000 + - "3000" + - "8000:8000" + - "49100:22" -- Map volumes from the host machine (HOST:CONTAINER). volumes: From 9550387e39012644e4f19a724225c17fb85a2bdf Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Feb 2014 16:02:35 -0800 Subject: [PATCH 0351/2154] Add script to build an OS X binary --- .gitignore | 1 + README.md | 6 ++++++ bin/fig | 3 +++ script/build-osx | 6 ++++++ 4 files changed, 16 insertions(+) create mode 100755 bin/fig create mode 100755 script/build-osx diff --git a/.gitignore b/.gitignore index bdd3ac14..9a8c8325 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /dist /docs/_site /docs/.git-gh-pages +fig.spec diff --git a/README.md b/README.md index 1918d9b3..de4bbc27 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,10 @@ Running the test suite $ script/test +Building OS X binaries +--------------------- + + $ script/build-osx + +Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807). diff --git a/bin/fig b/bin/fig new file mode 100755 index 00000000..550a5e24 --- /dev/null +++ b/bin/fig @@ -0,0 +1,3 @@ +#!/usr/bin/env python +from fig.cli.main import main +main() diff --git a/script/build-osx b/script/build-osx new file mode 100755 index 00000000..ee011d1b --- /dev/null +++ b/script/build-osx @@ -0,0 +1,6 @@ +#!/bin/bash +set -ex +virtualenv venv +venv/bin/pip install pyinstaller==2.1 +venv/bin/pip install . +venv/bin/pyinstaller -F bin/fig From ba66c849b5a6883792f6a426f5fd09b30d9ff93a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 15:08:51 +0000 Subject: [PATCH 0352/2154] Use Python base image and run as normal user --- Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4d31a5a0..961a5511 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ -FROM stackbrew/ubuntu:12.04 -RUN apt-get update -qq -RUN apt-get install -y python python-pip +FROM orchardup/python:2.7 ADD requirements.txt /code/ WORKDIR /code/ RUN pip install -r requirements.txt ADD requirements-dev.txt /code/ RUN pip install -r requirements-dev.txt ADD . /code/ +RUN useradd -d /home/user -m -s /bin/bash user +RUN chown -R user /code/ +USER user From 431ce67f858e81b7fafead384201fd142ee818a7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 15:09:26 +0000 Subject: [PATCH 0353/2154] Add script to build Linux binary --- requirements-dev.txt | 1 + script/build-linux | 3 +++ 2 files changed, 4 insertions(+) create mode 100755 script/build-linux diff --git a/requirements-dev.txt b/requirements-dev.txt index 115e826e..46d6a8aa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,3 @@ mock==1.0.1 nose==1.3.0 +pyinstaller==2.1 diff --git a/script/build-linux b/script/build-linux new file mode 100755 index 00000000..a4004611 --- /dev/null +++ b/script/build-linux @@ -0,0 +1,3 @@ +#!/bin/sh +docker build -t fig . +docker run -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig From 29f7b78deb100c682ee80b0bb1c6ae62c79b15bc Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 15:20:09 +0000 Subject: [PATCH 0354/2154] Add installation instructions for binaries --- docs/install.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/install.md b/docs/install.md index 35dc3767..a2d8ba4c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -14,10 +14,18 @@ First, install Docker. If you're on OS X, you can use [docker-osx](https://githu Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation. -Next, install Fig: +Next, install Fig. On OS X: + + $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/darwin > /usr/local/bin/fig + $ chmod +x /usr/local/bin/fig + +On 64-bit Linux: + + $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/linux > /usr/local/bin/fig + $ chmod +x /usr/local/bin/fig + +Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): $ sudo pip install -U fig -(This command also upgrades Fig when we release a new version. If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.) - That should be all you need! Run `fig --version` to see if it worked. From 17b9801ec940fe78c1b67eaddf869083c6cacfb9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 15:43:10 +0000 Subject: [PATCH 0355/2154] Document what version of Docker is supported --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index a2d8ba4c..27d66af2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,7 +6,7 @@ title: Installing Fig Installing Fig ============== -First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): +First, install Docker (version 0.8 or higher). If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): $ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx From 343d3bb5569f0edaccb9799b2ebe9fc2c6c9bb7c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 15:43:48 +0000 Subject: [PATCH 0356/2154] Remove unsupported Docker 0.7.6 from Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5e5fbbd7..d30066e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ python: - '3.2' - '3.3' env: -- DOCKER_VERSION=0.7.6 - DOCKER_VERSION=0.8.0 +- DOCKER_VERSION=0.8.1 matrix: allow_failures: - python: '3.2' From 3e7360c2c655a5397087c959b52ee0cbc2b4cf1b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 16:21:42 +0000 Subject: [PATCH 0357/2154] Improve error when service is not a dict Fixes #127 --- fig/cli/main.py | 4 ++-- fig/project.py | 8 +++++++- tests/project_test.py | 23 ++++++++++++++++++++++- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index e6a8f5ff..e64769f0 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -8,7 +8,7 @@ import signal from inspect import getdoc from .. import __version__ -from ..project import NoSuchService, DependencyError +from ..project import NoSuchService, ConfigurationError from ..service import CannotBeScaledError from .command import Command from .formatter import Formatter @@ -40,7 +40,7 @@ def main(): except KeyboardInterrupt: log.error("\nAborting.") sys.exit(1) - except (UserError, NoSuchService, DependencyError) as e: + except (UserError, NoSuchService, ConfigurationError) as e: log.error(e.msg) sys.exit(1) except NoSuchCommand as e: diff --git a/fig/project.py b/fig/project.py index 1b4d2725..12ec7069 100644 --- a/fig/project.py +++ b/fig/project.py @@ -67,6 +67,8 @@ class Project(object): def from_config(cls, name, config, client): dicts = [] for service_name, service in list(config.items()): + if not isinstance(service, dict): + raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.') service['name'] = service_name dicts.append(service) return cls.from_dicts(name, dicts, client) @@ -156,9 +158,13 @@ class NoSuchService(Exception): return self.msg -class DependencyError(Exception): +class ConfigurationError(Exception): def __init__(self, msg): self.msg = msg def __str__(self): return self.msg + +class DependencyError(ConfigurationError): + pass + diff --git a/tests/project_test.py b/tests/project_test.py index 13afe533..07f38340 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from fig.project import Project +from fig.project import Project, ConfigurationError from .testcases import DockerClientTestCase @@ -37,6 +37,27 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(project.services[0].name, 'db') self.assertEqual(project.services[1].name, 'web') + def test_from_config(self): + project = Project.from_config('figtest', { + 'web': { + 'image': 'ubuntu', + }, + 'db': { + 'image': 'ubuntu', + }, + }, self.client) + self.assertEqual(len(project.services), 2) + self.assertEqual(project.get_service('web').name, 'web') + self.assertEqual(project.get_service('web').options['image'], 'ubuntu') + self.assertEqual(project.get_service('db').name, 'db') + self.assertEqual(project.get_service('db').options['image'], 'ubuntu') + + def test_from_config_throws_error_when_not_dict(self): + with self.assertRaises(ConfigurationError): + project = Project.from_config('figtest', { + 'web': 'ubuntu', + }, self.client) + def test_get_service(self): web = self.create_service('web') project = Project('test', [web], self.client) From f79e0e588e45891f0aabcf6ff65e80bdd1e45ec4 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 3 Mar 2014 16:20:43 +0000 Subject: [PATCH 0358/2154] Reword port warning in YAML reference, remove it from README --- README.md | 2 -- docs/yml.md | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index da662a73..c5ff341f 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,6 @@ db: image: orchardup/postgresql ``` -**Note** When mapping ports in the format HOST:CONTAINER you may experience erroneous results when using a container port lower than 60. This is due to YAML parsing numbers in the format "xx:yy" as sexagesimal (base 60) for this reason it is recommended to define your port mappings as strings. - (No more installing Postgres on your laptop!) Then type `fig up`, and Fig will start and run your entire app: diff --git a/docs/yml.md b/docs/yml.md index 283d41fc..f2c093e3 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -28,7 +28,7 @@ links: - redis -- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen). --- Note When mapping ports in the format HOST:CONTAINER you may experience erroneous results when using a container port lower than 60. This is due to YAML parsing numbers in the format "xx:yy" as sexagesimal (base 60) for this reason it is recommended to define your port mappings as strings. +-- Note: When mapping ports in the HOST:CONTAINER format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format "xx:yy" as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings. ports: - "3000" - "8000:8000" From 348ba0818f69a009d6733bd128e33ae2daa7e5f1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 3 Mar 2014 16:23:52 +0000 Subject: [PATCH 0359/2154] Reformat comments in YAML reference for readability --- docs/yml.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index f2c093e3..50a8ec7e 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -11,24 +11,31 @@ Each service defined in `fig.yml` must specify exactly one of `image` or `build` As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`. ```yaml --- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally. +-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull +-- if it doesn't exist locally. image: ubuntu image: orchardup/postgresql image: a4bc65fd --- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter. +-- Path to a directory containing a Dockerfile. Fig will build and tag it with +-- a generated name, and use that image thereafter. build: /path/to/build/dir -- Override the default command. command: bundle exec thin -p 3000 --- Link to containers in another service (see "Communicating between containers"). +-- Link to containers in another service links: - db - redis --- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen). --- Note: When mapping ports in the HOST:CONTAINER format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format "xx:yy" as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings. +-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the +-- container port (a random host port will be chosen). +-- Note: When mapping ports in the HOST:CONTAINER format, you may experience +-- erroneous results when using a container port lower than 60, because YAML +-- will parse numbers in the format "xx:yy" as sexagesimal (base 60). For +-- this reason, we recommend always explicitly specifying your port mappings +-- as strings. ports: - "3000" - "8000:8000" From be1ba818e47b59915f6ffddd53308499839caee7 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 3 Mar 2014 17:58:35 +0000 Subject: [PATCH 0360/2154] Document link aliases --- docs/yml.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/yml.md b/docs/yml.md index 50a8ec7e..df462b17 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -24,9 +24,12 @@ build: /path/to/build/dir -- Override the default command. command: bundle exec thin -p 3000 --- Link to containers in another service +-- Link to containers in another service. Optionally specify an alternate name +-- for the link, which will determine how environment variables are prefixed, +-- e.g. "db" -> DB_1_PORT, "db:database" -> DATABASE_1_PORT links: - db + - db:database - redis -- Expose ports. Either specify both ports (HOST:CONTAINER), or just the From a00ec9d1f870703319dff1d9f96c831e8e56291d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 21 Feb 2014 18:12:51 +0000 Subject: [PATCH 0361/2154] Fix: image-defined entrypoint not overridden by intermediate container This was causing recreation to hang. --- fig/service.py | 4 ++-- tests/service_test.py | 12 ++++++++++-- tests/testcases.py | 3 ++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/fig/service.py b/fig/service.py index c4ebd85e..d07f80d1 100644 --- a/fig/service.py +++ b/fig/service.py @@ -165,9 +165,9 @@ class Service(object): intermediate_container = Container.create( self.client, image=container.image, - command='echo', volumes_from=container.id, - entrypoint=None + entrypoint=['echo'], + command=[], ) intermediate_container.start() intermediate_container.wait() diff --git a/tests/service_test.py b/tests/service_test.py index 01c1ecab..63a31df5 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -114,9 +114,16 @@ class ServiceTest(DockerClientTestCase): self.assertIn('/var/db', container.inspect()['Volumes']) def test_recreate_containers(self): - service = self.create_service('db', environment={'FOO': '1'}, volumes=['/var/db'], entrypoint=['ps']) + service = self.create_service( + 'db', + environment={'FOO': '1'}, + volumes=['/var/db'], + entrypoint=['ps'], + command=['ax'] + ) old_container = service.create_container() self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps']) + self.assertEqual(old_container.dictionary['Config']['Cmd'], ['ax']) self.assertIn('FOO=1', old_container.dictionary['Config']['Env']) self.assertEqual(old_container.name, 'figtest_db_1') service.start_container(old_container) @@ -131,9 +138,10 @@ class ServiceTest(DockerClientTestCase): new_container = new[0] intermediate_container = intermediate[0] - self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], None) + self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['echo']) self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps']) + self.assertEqual(new_container.dictionary['Config']['Cmd'], ['ax']) self.assertIn('FOO=2', new_container.dictionary['Config']['Env']) self.assertEqual(new_container.name, 'figtest_db_1') service.start_container(new_container) diff --git a/tests/testcases.py b/tests/testcases.py index 7a6c8e4a..6556311d 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -22,12 +22,13 @@ class DockerClientTestCase(unittest.TestCase): self.client.remove_image(i) def create_service(self, name, **kwargs): + if 'command' not in kwargs: + kwargs['command'] = ["/bin/sleep", "300"] return Service( project='figtest', name=name, client=self.client, image="ubuntu", - command=["/bin/sleep", "300"], **kwargs ) From 6e9983fc6af97e8487b1eb05e2734c83c5a5be86 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 17:35:51 +0000 Subject: [PATCH 0362/2154] Ship 0.3.0 --- CHANGES.md | 13 +++++++++++++ fig/__init__.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cf47558f..4cb87c18 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,19 @@ Change log ========== +0.3.0 (2014-03-03) +------------------ + + - We now ship binaries for OS X and Linux. No more having to install with Pip! + - Add `-f` flag to specify alternate `fig.yml` files + - Add support for custom link names + - Fix a bug where recreating would sometimes hang + - Update docker-py to support Docker 0.8.0. + - Various documentation improvements + - Various error message improvements + +Thanks @marksteve, @Gazler and @teozkr! + 0.2.2 (2014-02-17) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 1feb1795..bb994c35 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.2.2' +__version__ = '0.3.0' From 48e7b4b0a6d5d69c3932547b6e1eb7a4c7d5aee3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 19:21:27 +0000 Subject: [PATCH 0363/2154] Remove Python 3 from Travis Let's not kid ourselves. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index d30066e6..5d6cc863 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,8 +2,6 @@ language: python python: - '2.6' - '2.7' -- '3.2' -- '3.3' env: - DOCKER_VERSION=0.8.0 - DOCKER_VERSION=0.8.1 From 827811d6644d73e58a3483d36e6bea6ae4ee1fc3 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 19:31:14 +0000 Subject: [PATCH 0364/2154] update --- cli.html | 2 +- django.html | 4 ++-- env.html | 2 +- index.html | 4 ++-- install.html | 2 +- rails.html | 4 ++-- wordpress.html | 4 ++-- yml.html | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli.html b/cli.html index 8d6cb403..02438197 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 28eabe2d..8cff1ea4 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -30,7 +30,7 @@ ADD requirements.txt /code/ RUN pip install -r requirements.txt ADD . /code/
    -

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Second, we define our Python dependencies in a file called requirements.txt:

    Django
    diff --git a/env.html b/env.html
    index 2c1601c5..42aa007a 100644
    --- a/env.html
    +++ b/env.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/index.html b/index.html index 01f7c282..38f46fee 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    @@ -101,7 +101,7 @@ ADD . /code WORKDIR /code RUN pip install -r requirements.txt
    -

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    We then define a set of services using fig.yml:

    web:
    diff --git a/install.html b/install.html
    index 60a60afb..ccba6780 100644
    --- a/install.html
    +++ b/install.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/rails.html b/rails.html index a0fd521c..e661b044 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    @@ -30,7 +30,7 @@ ADD Gemfile /myapp/Gemfile RUN bundle install ADD . /myapp
    -

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Next, we have a bootstrap Gemfile which just loads Rails. It'll be overwritten in a moment by rails new.

    source 'https://rubygems.org'
    diff --git a/wordpress.html b/wordpress.html
    index c5879771..53bb2ca4 100644
    --- a/wordpress.html
    +++ b/wordpress.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    @@ -26,7 +26,7 @@
    FROM orchardup/php5
     ADD . /code
     
    -

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Next up, fig.yml starts our web service and a separate MySQL instance:

    web:
    diff --git a/yml.html b/yml.html
    index fa3d430f..b98fab96 100644
    --- a/yml.html
    +++ b/yml.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    From 8903a5508f3d1df59eba9d2b32f4934b7361ec56 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 3 Mar 2014 19:33:17 +0000 Subject: [PATCH 0365/2154] update --- cli.html | 2 +- django.html | 8 ++++---- env.html | 2 +- fig.yml | 2 +- index.html | 8 ++++---- install.html | 16 +++++++++++----- rails.html | 8 ++++---- wordpress.html | 8 ++++---- yml.html | 26 +++++++++++++++++++------- 9 files changed, 49 insertions(+), 31 deletions(-) diff --git a/cli.html b/cli.html index 02438197..cee66ac9 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 8cff1ea4..14f458c2 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -30,7 +30,7 @@ ADD requirements.txt /code/ RUN pip install -r requirements.txt ADD . /code/
    -

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Second, we define our Python dependencies in a file called requirements.txt:

    Django
    @@ -39,14 +39,14 @@ ADD . /code/
     
    db:
       image: orchardup/postgresql
       ports:
    -    - 5432
    +    - "5432"
     web:
       build: .
       command: python manage.py runserver 0.0.0.0:8000
       volumes:
         - .:/code
       ports:
    -    - 8000:8000
    +    - "8000:8000"
       links:
         - db
     
    diff --git a/env.html b/env.html index 42aa007a..395ce6ce 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/fig.yml b/fig.yml index 45edcb3d..30e8f6d9 100644 --- a/fig.yml +++ b/fig.yml @@ -1,7 +1,7 @@ jekyll: build: . ports: - - 4000:4000 + - "4000:4000" volumes: - .:/code environment: diff --git a/index.html b/index.html index 38f46fee..711bfaa1 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    @@ -32,7 +32,7 @@ RUN pip install -r requirements.txt links: - db ports: - - 8000:8000 + - "8000:8000" db: image: orchardup/postgresql
    @@ -101,14 +101,14 @@ ADD . /code WORKDIR /code RUN pip install -r requirements.txt
    -

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    We then define a set of services using fig.yml:

    web:
       build: .
       command: python app.py
       ports:
    -   - 5000:5000
    +   - "5000:5000"
       volumes:
        - .:/code
       links:
    diff --git a/install.html b/install.html
    index ccba6780..63535820 100644
    --- a/install.html
    +++ b/install.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    @@ -19,18 +19,24 @@

    Installing Fig

    -

    First, install Docker. If you're on OS X, you can use docker-osx:

    +

    First, install Docker (version 0.8 or higher). If you're on OS X, you can use docker-osx:

    $ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     

    Docker has guides for Ubuntu and other platforms in their documentation.

    -

    Next, install Fig:

    +

    Next, install Fig. On OS X:

    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/darwin > /usr/local/bin/fig
    +$ chmod +x /usr/local/bin/fig
    +
    +

    On 64-bit Linux:

    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/linux > /usr/local/bin/fig
    +$ chmod +x /usr/local/bin/fig
    +
    +

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    $ sudo pip install -U fig
     
    -

    (This command also upgrades Fig when we release a new version. If you don’t have pip installed, try brew install python or apt-get install python-pip.)

    -

    That should be all you need! Run fig --version to see if it worked.

    diff --git a/rails.html b/rails.html index e661b044..7075d145 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    @@ -30,7 +30,7 @@ ADD Gemfile /myapp/Gemfile RUN bundle install ADD . /myapp
    -

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Next, we have a bootstrap Gemfile which just loads Rails. It'll be overwritten in a moment by rails new.

    source 'https://rubygems.org'
    @@ -40,14 +40,14 @@ gem 'rails', '4.0.2'
     
    db:
       image: orchardup/postgresql
       ports:
    -    - 5432
    +    - "5432"
     web:
       build: .
       command: bundle exec rackup -p 3000
       volumes:
         - .:/myapp
       ports:
    -    - 3000:3000
    +    - "3000:3000"
       links:
         - db
     
    diff --git a/wordpress.html b/wordpress.html index 53bb2ca4..56749308 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    @@ -26,14 +26,14 @@
    FROM orchardup/php5
     ADD . /code
     
    -

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    Next up, fig.yml starts our web service and a separate MySQL instance:

    web:
       build: .
       command: php -S 0.0.0.0:8000 -t /code
       ports:
    -    - 8000:8000
    +    - "8000:8000"
       links:
         - db
       volumes:
    @@ -41,7 +41,7 @@ ADD . /code
     db:
       image: orchardup/mysql
       ports:
    -    - 3306:3306
    +    - "3306:3306"
       environment:
         MYSQL_DATABASE: wordpress
     
    diff --git a/yml.html b/yml.html index b98fab96..62a0f9bf 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    @@ -22,26 +22,38 @@

    Each service defined in fig.yml must specify exactly one of image or build. Other keys are optional, and are analogous to their docker run command-line counterparts.

    As with docker run, options specified in the Dockerfile (e.g. CMD, EXPOSE, VOLUME, ENV) are respected by default - you don't need to specify them again in fig.yml.

    -
    -- Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
    +
    -- Tag or partial image ID. Can be local or remote - Fig will attempt to pull
    +-- if it doesn't exist locally.
     image: ubuntu
     image: orchardup/postgresql
     image: a4bc65fd
     
    --- Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
    +-- Path to a directory containing a Dockerfile. Fig will build and tag it with
    +-- a generated name, and use that image thereafter.
     build: /path/to/build/dir
     
     -- Override the default command.
     command: bundle exec thin -p 3000
     
    --- Link to containers in another service (see "Communicating between containers").
    +-- Link to containers in another service. Optionally specify an alternate name
    +-- for the link, which will determine how environment variables are prefixed,
    +-- e.g. "db" -> DB_1_PORT, "db:database" -> DATABASE_1_PORT
     links:
      - db
    + - db:database
      - redis
     
    --- Expose ports. Either specify both ports (HOST:CONTAINER), or just the container port (a random host port will be chosen).
    +-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the
    +-- container port (a random host port will be chosen).
    +-- Note: When mapping ports in the HOST:CONTAINER format, you may experience
    +-- erroneous results when using a container port lower than 60, because YAML
    +-- will parse numbers in the format "xx:yy" as sexagesimal (base 60). For
    +-- this reason, we recommend always explicitly specifying your port mappings
    +-- as strings.
     ports:
    - - 3000
    - - 8000:8000
    + - "3000"
    + - "8000:8000"
    + - "49100:22"
     
     -- Map volumes from the host machine (HOST:CONTAINER).
     volumes:
    
    From 96a92a73f1585deb6c936592d4602bf210109eed Mon Sep 17 00:00:00 2001
    From: Mark Steve Samson 
    Date: Tue, 4 Mar 2014 13:13:23 +0800
    Subject: [PATCH 0366/2154] Fix KeyError when `-v` is not specified in `fig rm`
    
    ---
     fig/cli/main.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/fig/cli/main.py b/fig/cli/main.py
    index e64769f0..178a61d0 100644
    --- a/fig/cli/main.py
    +++ b/fig/cli/main.py
    @@ -182,7 +182,7 @@ class TopLevelCommand(Command):
                 print("Going to remove", list_containers(stopped_containers))
                 if yesno("Are you sure? [yN] ", default=False):
                     self.project.remove_stopped(service_names=options['SERVICE'],
    -                                            remove_volumes=options['-v'])
    +                                            remove_volumes=options.get('-v', False))
             else:
                 print("No stopped containers")
     
    
    From 2ca0e7954a9208dce1bb501dd15972fbf7a7910d Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 10:25:50 +0000
    Subject: [PATCH 0367/2154] Add --force option to fig rm
    
    ---
     fig/cli/main.py | 8 +++++---
     1 file changed, 5 insertions(+), 3 deletions(-)
    
    diff --git a/fig/cli/main.py b/fig/cli/main.py
    index 178a61d0..668ef9e8 100644
    --- a/fig/cli/main.py
    +++ b/fig/cli/main.py
    @@ -170,17 +170,19 @@ class TopLevelCommand(Command):
             """
             Remove stopped service containers.
     
    -        Usage: rm [SERVICE...]
    +        Usage: rm [options] [SERVICE...]
     
             Options:
    -            -v    Remove volumes associated with containers
    +            --force   Don't ask to confirm removal
    +            -v        Remove volumes associated with containers
             """
             all_containers = self.project.containers(service_names=options['SERVICE'], stopped=True)
             stopped_containers = [c for c in all_containers if not c.is_running]
     
             if len(stopped_containers) > 0:
                 print("Going to remove", list_containers(stopped_containers))
    -            if yesno("Are you sure? [yN] ", default=False):
    +            if options.get('--force') \
    +                    or yesno("Are you sure? [yN] ", default=False):
                     self.project.remove_stopped(service_names=options['SERVICE'],
                                                 remove_volumes=options.get('-v', False))
             else:
    
    From 465c7d569ca1b5fb3ef4ddf1767e6c04c0ba8109 Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 10:26:57 +0000
    Subject: [PATCH 0368/2154] Improve CLI test names
    
    ---
     tests/cli_test.py | 5 ++---
     1 file changed, 2 insertions(+), 3 deletions(-)
    
    diff --git a/tests/cli_test.py b/tests/cli_test.py
    index 61e30b7a..c2040e63 100644
    --- a/tests/cli_test.py
    +++ b/tests/cli_test.py
    @@ -32,7 +32,7 @@ class CLITestCase(DockerClientTestCase):
             self.assertIn('fig_simple_1', mock_stdout.getvalue())
     
         @patch('sys.stdout', new_callable=StringIO)
    -    def test_default_figfile(self, mock_stdout):
    +    def test_ps_default_figfile(self, mock_stdout):
             self.command.base_dir = 'tests/fixtures/multiple-figfiles'
             self.command.dispatch(['up', '-d'], None)
             self.command.dispatch(['ps'], None)
    @@ -43,7 +43,7 @@ class CLITestCase(DockerClientTestCase):
             self.assertNotIn('fig_yetanother_1', output)
     
         @patch('sys.stdout', new_callable=StringIO)
    -    def test_alternate_figfile(self, mock_stdout):
    +    def test_ps_alternate_figfile(self, mock_stdout):
             self.command.base_dir = 'tests/fixtures/multiple-figfiles'
             self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None)
             self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
    @@ -74,4 +74,3 @@ class CLITestCase(DockerClientTestCase):
             self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']})
             self.assertEqual(len(project.get_service('simple').containers()), 0)
             self.assertEqual(len(project.get_service('another').containers()), 0)
    -
    
    From 044c348fafa63457548e886581c919df68b75762 Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 10:49:07 +0000
    Subject: [PATCH 0369/2154] Add test for fig rm
    
    ---
     tests/cli_test.py | 8 ++++++++
     1 file changed, 8 insertions(+)
    
    diff --git a/tests/cli_test.py b/tests/cli_test.py
    index c2040e63..2b81e26b 100644
    --- a/tests/cli_test.py
    +++ b/tests/cli_test.py
    @@ -53,6 +53,14 @@ class CLITestCase(DockerClientTestCase):
             self.assertNotIn('fig_another_1', output)
             self.assertIn('fig_yetanother_1', output)
     
    +    def test_rm(self):
    +        service = self.command.project.get_service('simple')
    +        service.create_container()
    +        service.kill()
    +        self.assertEqual(len(service.containers(stopped=True)), 1)
    +        self.command.dispatch(['rm', '--force'], None)
    +        self.assertEqual(len(service.containers(stopped=True)), 0)
    +
         def test_scale(self):
             project = self.command.project
     
    
    From 5be8a37b7e14a0eaa37457751329ef4a7a0a2016 Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 10:50:09 +0000
    Subject: [PATCH 0370/2154] Pass through standard remove_container options
    
    ---
     fig/cli/main.py  | 6 ++++--
     fig/container.py | 3 +--
     2 files changed, 5 insertions(+), 4 deletions(-)
    
    diff --git a/fig/cli/main.py b/fig/cli/main.py
    index 668ef9e8..4d65ce9e 100644
    --- a/fig/cli/main.py
    +++ b/fig/cli/main.py
    @@ -183,8 +183,10 @@ class TopLevelCommand(Command):
                 print("Going to remove", list_containers(stopped_containers))
                 if options.get('--force') \
                         or yesno("Are you sure? [yN] ", default=False):
    -                self.project.remove_stopped(service_names=options['SERVICE'],
    -                                            remove_volumes=options.get('-v', False))
    +                self.project.remove_stopped(
    +                    service_names=options['SERVICE'],
    +                    v=options.get('-v', False)
    +                )
             else:
                 print("No stopped containers")
     
    diff --git a/fig/container.py b/fig/container.py
    index a38828d0..3b004b92 100644
    --- a/fig/container.py
    +++ b/fig/container.py
    @@ -112,8 +112,7 @@ class Container(object):
             return self.client.kill(self.id)
     
         def remove(self, **options):
    -        v = options.get('remove_volumes', False)
    -        return self.client.remove_container(self.id, v=v)
    +        return self.client.remove_container(self.id, **options)
     
         def inspect_if_not_inspected(self):
             if not self.has_been_inspected:
    
    From b92651070f3775e1d716d527aa86a2d4decd1c77 Mon Sep 17 00:00:00 2001
    From: Kevin van Zonneveld 
    Date: Tue, 4 Mar 2014 11:56:41 +0100
    Subject: [PATCH 0371/2154] Add steps for contributing to README
    
    ---
     README.md | 13 +++++++++++++
     1 file changed, 13 insertions(+)
    
    diff --git a/README.md b/README.md
    index c5ff341f..2749a741 100644
    --- a/README.md
    +++ b/README.md
    @@ -60,3 +60,16 @@ Building OS X binaries
     
     Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807).
     
    +Contributing to Fig
    +-------------------
    +
    +If you're looking contribute to [Fig](http://orchardup.github.io/fig/)
    +but you're new to the project or maybe even to Python, here are the steps
    +that should get you started.
    +
    +1. Fork [https://github.com/orchardup/fig](https://github.com/orchardup/fig) to your username. kvz in this example.
    +1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`.
    +1. Enter the local directory `cd fig`.
    +1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : )
    +1. Works for you? Run the test suite via `./scripts/test` to verify it won't break other usecases.
    +1. All good? Commit and push to GitHub, and submit a pull request.
    
    From f430b82b4349c92b2d2fd2d412bb8bfbc937707b Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 11:26:56 +0000
    Subject: [PATCH 0372/2154] Fix ps on Docker 0.8.1 when there is no command
    
    Fixes #138
    ---
     fig/container.py | 5 ++++-
     1 file changed, 4 insertions(+), 1 deletion(-)
    
    diff --git a/fig/container.py b/fig/container.py
    index 3b004b92..0a118af6 100644
    --- a/fig/container.py
    +++ b/fig/container.py
    @@ -86,7 +86,10 @@ class Container(object):
         @property
         def human_readable_command(self):
             self.inspect_if_not_inspected()
    -        return ' '.join(self.dictionary['Config']['Cmd'])
    +        if self.dictionary['Config']['Cmd']:
    +            return ' '.join(self.dictionary['Config']['Cmd'])
    +        else:
    +            return ''
     
         @property
         def environment(self):
    
    From 4fbad941cb3afe7c7259c3bf5a2c5298703f1966 Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 11:40:39 +0000
    Subject: [PATCH 0373/2154] Ship 0.3.1
    
    ---
     CHANGES.md      | 7 +++++++
     fig/__init__.py | 2 +-
     2 files changed, 8 insertions(+), 1 deletion(-)
    
    diff --git a/CHANGES.md b/CHANGES.md
    index 4cb87c18..40f5c11e 100644
    --- a/CHANGES.md
    +++ b/CHANGES.md
    @@ -1,6 +1,13 @@
     Change log
     ==========
     
    +0.3.1 (2014-03-04)
    +------------------
    +
    + - Added contribution instructions. (Thanks @kvz!)
    + - Fixed `fig rm` throwing an error.
    + - Fixed a bug in `fig ps` on Docker 0.8.1 when there is a container with no command.
    +
     0.3.0 (2014-03-03)
     ------------------
     
    diff --git a/fig/__init__.py b/fig/__init__.py
    index bb994c35..47ebe395 100644
    --- a/fig/__init__.py
    +++ b/fig/__init__.py
    @@ -1,4 +1,4 @@
     from __future__ import unicode_literals
     from .service import Service
     
    -__version__ = '0.3.0'
    +__version__ = '0.3.1'
    
    From f5cf9b60b2d1eef601ccd80efe5feb33bcf473f7 Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 11:51:12 +0000
    Subject: [PATCH 0374/2154] Remove venv before building on OS X
    
    ---
     script/build-osx | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/script/build-osx b/script/build-osx
    index ee011d1b..7cba8702 100755
    --- a/script/build-osx
    +++ b/script/build-osx
    @@ -1,5 +1,6 @@
     #!/bin/bash
     set -ex
    +rm -r venv
     virtualenv venv
     venv/bin/pip install pyinstaller==2.1
     venv/bin/pip install .
    
    From 6813cb86a2aff1edbc6092c0b5b1c7774a2a13f8 Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 11:53:16 +0000
    Subject: [PATCH 0375/2154] Update version in installation docs
    
    ---
     docs/install.md | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/docs/install.md b/docs/install.md
    index 27d66af2..907b2cb1 100644
    --- a/docs/install.md
    +++ b/docs/install.md
    @@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun
     
     Next, install Fig. On OS X:
     
    -    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/darwin > /usr/local/bin/fig
    +    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/darwin > /usr/local/bin/fig
         $ chmod +x /usr/local/bin/fig
     
     On 64-bit Linux:
     
    -    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/linux > /usr/local/bin/fig
    +    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/linux > /usr/local/bin/fig
         $ chmod +x /usr/local/bin/fig
     
     Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):
    
    From 2659567b01e398e12a4d7c4191e0bf38f492d77f Mon Sep 17 00:00:00 2001
    From: Ben Firshman 
    Date: Tue, 4 Mar 2014 11:57:13 +0000
    Subject: [PATCH 0376/2154] update
    
    ---
     cli.html       | 2 +-
     django.html    | 2 +-
     env.html       | 2 +-
     index.html     | 2 +-
     install.html   | 6 +++---
     rails.html     | 2 +-
     wordpress.html | 2 +-
     yml.html       | 2 +-
     8 files changed, 10 insertions(+), 10 deletions(-)
    
    diff --git a/cli.html b/cli.html
    index cee66ac9..3a78e82b 100644
    --- a/cli.html
    +++ b/cli.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/django.html b/django.html index 14f458c2..70f4bdbd 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 395ce6ce..9cce65fb 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 711bfaa1..fa0df7a8 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 63535820..5369838b 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -27,11 +27,11 @@ $ docker-osx shell

    Docker has guides for Ubuntu and other platforms in their documentation.

    Next, install Fig. On OS X:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/darwin > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/darwin > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    On 64-bit Linux:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.0/linux > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/linux > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    diff --git a/rails.html b/rails.html index 7075d145..a32ca23b 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 56749308..1492d698 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 62a0f9bf..e0e6b01e 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 6a3d1d06b5fc4eae7d09bf6c6511651281325a6d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 4 Mar 2014 17:58:42 +0000 Subject: [PATCH 0377/2154] Check NetworkSettings.Ports instead of HostConfig.PortBindings in tests The latter appears to be unreliable. --- tests/service_test.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/service_test.py b/tests/service_test.py index 63a31df5..458de676 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -209,25 +209,25 @@ class ServiceTest(DockerClientTestCase): def test_start_container_creates_ports(self): service = self.create_service('web', ports=[8000]) container = service.start_container().inspect() - self.assertEqual(list(container['HostConfig']['PortBindings'].keys()), ['8000/tcp']) - self.assertNotEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') + self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp']) + self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_creates_port_with_explicit_protocol(self): service = self.create_service('web', ports=['8000/udp']) container = service.start_container().inspect() - self.assertEqual(list(container['HostConfig']['PortBindings'].keys()), ['8000/udp']) + self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp']) def test_start_container_creates_fixed_external_ports(self): service = self.create_service('web', ports=['8000:8000']) container = service.start_container().inspect() - self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) - self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8000') + self.assertIn('8000/tcp', container['NetworkSettings']['Ports']) + self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self): service = self.create_service('web', ports=['8001:8000']) container = service.start_container().inspect() - self.assertIn('8000/tcp', container['HostConfig']['PortBindings']) - self.assertEqual(container['HostConfig']['PortBindings']['8000/tcp'][0]['HostPort'], '8001') + self.assertIn('8000/tcp', container['NetworkSettings']['Ports']) + self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001') def test_scale(self): service = self.create_service('web') From 2d98071e552da0c79bcfb843f9b6bfd08ae8e6b7 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 4 Mar 2014 17:59:42 +0000 Subject: [PATCH 0378/2154] Support 'expose' config option, like docker's --expose Exposes ports to linked services without publishing them to the world. --- docs/yml.md | 6 ++++++ fig/service.py | 7 ++++--- tests/service_test.py | 5 +++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index df462b17..24484d4f 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -44,6 +44,12 @@ ports: - "8000:8000" - "49100:22" +-- Expose ports without publishing them to the host machine - they'll only be +-- accessible to linked services. Only the internal port can be specified. +expose: + - "3000" + - "8000" + -- Map volumes from the host machine (HOST:CONTAINER). volumes: - cache/:/tmp/cache diff --git a/fig/service.py b/fig/service.py index d07f80d1..cfc39367 100644 --- a/fig/service.py +++ b/fig/service.py @@ -39,7 +39,7 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) - supported_options = DOCKER_CONFIG_KEYS + ['build'] + supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose'] for k in options: if k not in supported_options: @@ -246,9 +246,10 @@ class Service(object): container_options['name'] = self.next_container_name(one_off) - if 'ports' in container_options: + if 'ports' in container_options or 'expose' in self.options: ports = [] - for port in container_options['ports']: + all_ports = container_options.get('ports', []) + self.options.get('expose', []) + for port in all_ports: port = str(port) if ':' in port: port = port.split(':')[-1] diff --git a/tests/service_test.py b/tests/service_test.py index 458de676..3448596d 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -212,6 +212,11 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp']) self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000') + def test_expose_does_not_publish_ports(self): + service = self.create_service('web', expose=[8000]) + container = service.start_container().inspect() + self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None}) + def test_start_container_creates_port_with_explicit_protocol(self): service = self.create_service('web', ports=['8000/udp']) container = service.start_container().inspect() From 5f5fbb3ea4db38c379494598359ee24d674775fe Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 4 Mar 2014 18:00:46 +0000 Subject: [PATCH 0379/2154] Display unpublished ports in 'fig ps' --- fig/container.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fig/container.py b/fig/container.py index 0a118af6..c926d2a9 100644 --- a/fig/container.py +++ b/fig/container.py @@ -70,6 +70,8 @@ class Container(object): for private, public in list(self.dictionary['NetworkSettings']['Ports'].items()): if public: ports.append('%s->%s' % (public[0]['HostPort'], private)) + else: + ports.append(private) return ', '.join(ports) @property From d1a52d2b1a5d1e2bd62d9671b636079ff9765d79 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 4 Mar 2014 21:54:10 +0000 Subject: [PATCH 0380/2154] Remove unneeded 'ports' entries from WP and Django fig.ymls in docs --- docs/django.md | 2 -- docs/wordpress.md | 2 -- 2 files changed, 4 deletions(-) diff --git a/docs/django.md b/docs/django.md index 8ecb3641..c85eb507 100644 --- a/docs/django.md +++ b/docs/django.md @@ -28,8 +28,6 @@ Simple enough. Finally, this is all tied together with a file called `fig.yml`. db: image: orchardup/postgresql - ports: - - "5432" web: build: . command: python manage.py runserver 0.0.0.0:8000 diff --git a/docs/wordpress.md b/docs/wordpress.md index a2f1bcf4..5eff3487 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -33,8 +33,6 @@ web: - .:/code db: image: orchardup/mysql - ports: - - "3306:3306" environment: MYSQL_DATABASE: wordpress ``` From 59cc9c9b68c4ec3a957b67c9f4f776e7543eefb1 Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Wed, 5 Mar 2014 09:01:31 +0800 Subject: [PATCH 0381/2154] Add option to remove container in `docker run` (Closes #137) --- fig/cli/main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/fig/cli/main.py b/fig/cli/main.py index 4d65ce9e..4188d770 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -209,6 +209,7 @@ class TopLevelCommand(Command): container name -T Disable pseudo-tty allocation. By default `fig run` allocates a TTY. + --rm Remove container after run. Ignored in detached mode. """ service = self.project.get_service(options['SERVICE']) @@ -229,6 +230,10 @@ class TopLevelCommand(Command): with self._attach_to_container(container.id, raw=tty) as c: service.start_container(container, ports=None) c.run() + if options['--rm']: + container.wait() + log.info("Removing %s..." % container.name) + self.client.remove_container(container.id) def scale(self, options): """ From 2595f89519add8964f5e7af721153b15ddb4714c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Mar 2014 14:33:32 +0000 Subject: [PATCH 0382/2154] Ship 0.3.2 --- CHANGES.md | 6 ++++++ fig/__init__.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 40f5c11e..3c042b1b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,12 @@ Change log ========== +0.3.2 (2014-03-05) +------------------ + + - Added an `--rm` option to `fig run`. (Thanks @marksteve!) + - Added an `expose` option to `fig.yml`. + 0.3.1 (2014-03-04) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 47ebe395..3160f16d 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.3.1' +__version__ = '0.3.2' From 14e778b3de6d2557d4709fc50353117d0c96f5fd Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Mar 2014 14:57:24 +0000 Subject: [PATCH 0383/2154] Update version in install docs --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 907b2cb1..c5ad320c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun Next, install Fig. On OS X: - $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/darwin > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/darwin > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig On 64-bit Linux: - $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/linux > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/linux > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): From a6ae3ed144e4410eb365a380ed24541fea53bd77 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Mar 2014 14:58:19 +0000 Subject: [PATCH 0384/2154] update --- cli.html | 2 +- django.html | 4 +--- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 4 +--- yml.html | 8 +++++++- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cli.html b/cli.html index 3a78e82b..1362d1cd 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 70f4bdbd..f0861c98 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -38,8 +38,6 @@ ADD . /code/

    Simple enough. Finally, this is all tied together with a file called fig.yml. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose.

    db:
       image: orchardup/postgresql
    -  ports:
    -    - "5432"
     web:
       build: .
       command: python manage.py runserver 0.0.0.0:8000
    diff --git a/env.html b/env.html
    index 9cce65fb..215f2977 100644
    --- a/env.html
    +++ b/env.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/index.html b/index.html index fa0df7a8..259287f7 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 5369838b..45c3c49f 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -27,11 +27,11 @@ $ docker-osx shell

    Docker has guides for Ubuntu and other platforms in their documentation.

    Next, install Fig. On OS X:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/darwin > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/darwin > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    On 64-bit Linux:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.1/linux > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/linux > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    diff --git a/rails.html b/rails.html index a32ca23b..b276422b 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 1492d698..c679abf8 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    @@ -40,8 +40,6 @@ ADD . /code - .:/code db: image: orchardup/mysql - ports: - - "3306:3306" environment: MYSQL_DATABASE: wordpress
    diff --git a/yml.html b/yml.html index e0e6b01e..5a964cea 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    @@ -55,6 +55,12 @@ - "8000:8000" - "49100:22" +-- Expose ports without publishing them to the host machine - they'll only be +-- accessible to linked services. Only the internal port can be specified. +expose: + - "3000" + - "8000" + -- Map volumes from the host machine (HOST:CONTAINER). volumes: - cache/:/tmp/cache From 3a48172332fad628bf744331c4945b50812a162f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 6 Mar 2014 10:57:24 +0000 Subject: [PATCH 0385/2154] Create CONTRIBUTING.md --- CONTRIBUTING.md | 30 ++++++++++++++++++++++++++++++ README.md | 26 -------------------------- 2 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..432e7e1e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Contributing to Fig + +If you're looking contribute to [Fig](http://orchardup.github.io/fig/) +but you're new to the project or maybe even to Python, here are the steps +that should get you started. + +1. Fork [https://github.com/orchardup/fig](https://github.com/orchardup/fig) to your username. kvz in this example. +1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`. +1. Enter the local directory `cd fig`. +1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : ) +1. Works for you? Run the test suite via `./scripts/test` to verify it won't break other usecases. +1. All good? Commit and push to GitHub, and submit a pull request. + +## Running the test suite + + $ script/test + +## Building binaries + +Linux: + + $ script/build-linux + +OS X: + + $ script/build-osx + +Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807). + + diff --git a/README.md b/README.md index 2749a741..225125a8 100644 --- a/README.md +++ b/README.md @@ -47,29 +47,3 @@ Installation and documentation ------------------------------ Full documentation is available on [Fig's website](http://orchardup.github.io/fig/). - -Running the test suite ----------------------- - - $ script/test - -Building OS X binaries ---------------------- - - $ script/build-osx - -Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807). - -Contributing to Fig -------------------- - -If you're looking contribute to [Fig](http://orchardup.github.io/fig/) -but you're new to the project or maybe even to Python, here are the steps -that should get you started. - -1. Fork [https://github.com/orchardup/fig](https://github.com/orchardup/fig) to your username. kvz in this example. -1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`. -1. Enter the local directory `cd fig`. -1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : ) -1. Works for you? Run the test suite via `./scripts/test` to verify it won't break other usecases. -1. All good? Commit and push to GitHub, and submit a pull request. From 13c7380113b5029156a9a5717eec7b454b2a1140 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 6 Mar 2014 18:59:24 +0000 Subject: [PATCH 0386/2154] Only `fig run` containers link back to the service they're part of Fixes #107. --- fig/service.py | 11 ++++++----- tests/service_test.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/fig/service.py b/fig/service.py index cfc39367..0675a697 100644 --- a/fig/service.py +++ b/fig/service.py @@ -207,7 +207,7 @@ class Service(object): volume_bindings[os.path.abspath(external_dir)] = internal_dir container.start( - links=self._get_links(), + links=self._get_links(link_to_self=override_options.get('one_off', False)), port_bindings=port_bindings, binds=volume_bindings, ) @@ -227,7 +227,7 @@ class Service(object): else: return max(numbers) + 1 - def _get_links(self): + def _get_links(self, link_to_self): links = [] for service, link_name in self.links: for container in service.containers(): @@ -235,9 +235,10 @@ class Service(object): links.append((container.name, link_name)) links.append((container.name, container.name)) links.append((container.name, container.name_without_project)) - for container in self.containers(): - links.append((container.name, container.name)) - links.append((container.name, container.name_without_project)) + if link_to_self: + for container in self.containers(): + links.append((container.name, container.name)) + links.append((container.name, container.name_without_project)) return links def _get_container_options(self, override_options, one_off=False): diff --git a/tests/service_test.py b/tests/service_test.py index 3448596d..b19901a9 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -175,12 +175,17 @@ class ServiceTest(DockerClientTestCase): web.start_container() self.assertIn('custom_link_name', web.containers()[0].links()) - def test_start_container_creates_links_to_its_own_service(self): - db1 = self.create_service('db') - db2 = self.create_service('db') - db1.start_container() - db2.start_container() - self.assertIn('db_1', db2.containers()[0].links()) + def test_start_normal_container_does_not_create_links_to_its_own_service(self): + db = self.create_service('db') + c1 = db.start_container() + c2 = db.start_container() + self.assertNotIn(c1.name, c2.links()) + + def test_start_one_off_container_creates_links_to_its_own_service(self): + db = self.create_service('db') + c1 = db.start_container() + c2 = db.start_container(one_off=True) + self.assertIn(c1.name, c2.links()) def test_start_container_builds_images(self): service = Service( From 2360bf0eb9b297658634d726c51c1622dbef5f19 Mon Sep 17 00:00:00 2001 From: Andrew DeMaria Date: Sat, 8 Mar 2014 18:28:41 -0700 Subject: [PATCH 0387/2154] Use utf8 when building docs --- docs/_config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/_config.yml b/docs/_config.yml index 956da86d..b7abc223 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1 +1,3 @@ markdown: redcarpet +encoding: utf-8 + From b0ec54b6f790959d91f1aecd805b025115e8ac86 Mon Sep 17 00:00:00 2001 From: Aaditya Talwai Date: Wed, 12 Mar 2014 15:45:32 -0700 Subject: [PATCH 0388/2154] Update django.md: Fix link to fig.yml reference --- docs/django.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/django.md b/docs/django.md index c85eb507..1c961b6c 100644 --- a/docs/django.md +++ b/docs/django.md @@ -38,7 +38,7 @@ Simple enough. Finally, this is all tied together with a file called `fig.yml`. links: - db -See the [`fig.yml` reference]() for more information on how it works. +See the [`fig.yml` reference](http://orchardup.github.io/fig/yml.html) for more information on how it works. We can now start a Django project using `fig run`: From 0f5a56b3c20b477eeda99a24add2a4c54a1655f7 Mon Sep 17 00:00:00 2001 From: Kevin van Zonneveld Date: Tue, 4 Mar 2014 00:51:24 +0100 Subject: [PATCH 0389/2154] Add support for privileged containers #123 This is required for mounting external volumes and addresses errors such as `mount.nfs: Operation not permitted` Be gentle, I don't normally use Python :) --- fig/service.py | 22 ++++++++++++++++------ tests/service_test.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/fig/service.py b/fig/service.py index 0675a697..51ec004b 100644 --- a/fig/service.py +++ b/fig/service.py @@ -10,11 +10,14 @@ from .container import Container log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint', 'privileged'] DOCKER_CONFIG_HINTS = { - 'link': 'links', - 'port': 'ports', - 'volume': 'volumes', + 'link' : 'links', + 'port' : 'ports', + 'privilege' : 'privileged', + 'priviliged': 'privileged', + 'privilige' : 'privileged', + 'volume' : 'volumes', } @@ -126,7 +129,7 @@ class Service(object): Create a container for this service. If the image doesn't exist, attempt to pull it. """ - container_options = self._get_container_options(override_options, one_off=one_off) + container_options = self._get_container_create_options(override_options, one_off=one_off) try: return Container.create(self.client, **container_options) except APIError as e: @@ -206,10 +209,13 @@ class Service(object): external_dir, internal_dir = volume.split(':') volume_bindings[os.path.abspath(external_dir)] = internal_dir + privileged = options.get('privileged', False) + container.start( links=self._get_links(link_to_self=override_options.get('one_off', False)), port_bindings=port_bindings, binds=volume_bindings, + privileged=privileged, ) return container @@ -241,7 +247,7 @@ class Service(object): links.append((container.name, container.name_without_project)) return links - def _get_container_options(self, override_options, one_off=False): + def _get_container_create_options(self, override_options, one_off=False): container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options) container_options.update(override_options) @@ -267,6 +273,10 @@ class Service(object): self.build() container_options['image'] = self._build_tag_name() + # Priviliged is only required for starting containers, not for creating them + if 'privileged' in container_options: + del container_options['privileged'] + return container_options def build(self): diff --git a/tests/service_test.py b/tests/service_test.py index b19901a9..5e8fe3ba 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -217,6 +217,16 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp']) self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000') + def test_start_container_stays_unpriviliged(self): + service = self.create_service('web') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['Privileged'], False) + + def test_start_container_becomes_priviliged(self): + service = self.create_service('web', privileged = True) + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['Privileged'], True) + def test_expose_does_not_publish_ports(self): service = self.create_service('web', expose=[8000]) container = service.start_container().inspect() From 168b1909ae25f1fd653d1768647f5ed61dadc0bb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Mar 2014 12:19:42 +0000 Subject: [PATCH 0390/2154] Friendlier build error Hide the backtrace and show a comprehensible message. Closes #160. --- fig/cli/main.py | 5 ++++- fig/service.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 4188d770..d377e683 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -9,7 +9,7 @@ from inspect import getdoc from .. import __version__ from ..project import NoSuchService, ConfigurationError -from ..service import CannotBeScaledError +from ..service import BuildError, CannotBeScaledError from .command import Command from .formatter import Formatter from .log_printer import LogPrinter @@ -51,6 +51,9 @@ def main(): except APIError as e: log.error(e.explanation) sys.exit(1) + except BuildError as e: + log.error("Service '%s' failed to build." % e.service.name) + sys.exit(1) # stolen from docopt master diff --git a/fig/service.py b/fig/service.py index 51ec004b..f9a2b5c0 100644 --- a/fig/service.py +++ b/fig/service.py @@ -22,7 +22,8 @@ DOCKER_CONFIG_HINTS = { class BuildError(Exception): - pass + def __init__(self, service): + self.service = service class CannotBeScaledError(Exception): @@ -298,7 +299,7 @@ class Service(object): sys.stdout.write(line) if image_id is None: - raise BuildError() + raise BuildError(self) return image_id From a3374ac51d7a83d09ac83980264ffdd6e68d6e7e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Mar 2014 12:26:33 +0000 Subject: [PATCH 0391/2154] Update install instructions on homepage Closes #161 --- docs/index.md | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/docs/index.md b/docs/index.md index 29b2a653..590f2dfe 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,19 +47,7 @@ Quick start Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it. -First, install Docker. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - - $ curl https://raw.github.com/noplay/docker-osx/0.7.6/docker-osx > /usr/local/bin/docker-osx - $ chmod +x /usr/local/bin/docker-osx - $ docker-osx shell - -Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubuntulinux/) and [other platforms](http://docs.docker.io/en/latest/installation/) in their documentation. - -Next, install Fig: - - $ sudo pip install -U fig - -(This command also upgrades Fig when we release a new version. If you don’t have pip installed, try `brew install python` or `apt-get install python-pip`.) +First, [install Docker and Fig](install.html). You'll want to make a directory for the project: From 3d5190d6d4e2ab9dc7f92d3c1ce3b6b6a5f365a3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Mar 2014 12:27:02 +0000 Subject: [PATCH 0392/2154] update --- cli.html | 2 +- django.html | 4 ++-- env.html | 2 +- index.html | 14 ++------------ install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 20 deletions(-) diff --git a/cli.html b/cli.html index 1362d1cd..e5d0739d 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index f0861c98..b3b3576a 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -48,7 +48,7 @@ web: links: - db
    -

    See the fig.yml reference for more information on how it works.

    +

    See the fig.yml reference for more information on how it works.

    We can now start a Django project using fig run:

    $ fig run web django-admin.py startproject figexample .
    diff --git a/env.html b/env.html
    index 215f2977..8c248a4f 100644
    --- a/env.html
    +++ b/env.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/index.html b/index.html index 259287f7..821b6690 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    @@ -57,17 +57,7 @@ RUN pip install -r requirements.txt

    Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.

    -

    First, install Docker. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.github.com/noplay/docker-osx/0.7.6/docker-osx > /usr/local/bin/docker-osx
    -$ chmod +x /usr/local/bin/docker-osx
    -$ docker-osx shell
    -
    -

    Docker has guides for Ubuntu and other platforms in their documentation.

    - -

    Next, install Fig:

    -
    $ sudo pip install -U fig
    -
    -

    (This command also upgrades Fig when we release a new version. If you don’t have pip installed, try brew install python or apt-get install python-pip.)

    +

    First, install Docker and Fig.

    You'll want to make a directory for the project:

    $ mkdir figtest
    diff --git a/install.html b/install.html
    index 45c3c49f..d038116d 100644
    --- a/install.html
    +++ b/install.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/rails.html b/rails.html index b276422b..5778e7c6 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index c679abf8..95ac3f9b 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 5a964cea..6db64b55 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 859d4bb98b1d4d7217eff54f138c92d44a29863d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Mar 2014 13:19:12 +0000 Subject: [PATCH 0393/2154] Stop 'fig up' when a container exits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #1 ヽ(*・ω・)ノ --- fig/cli/log_printer.py | 10 ++++++++-- fig/cli/multiplexer.py | 11 ++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index 0fe3215e..96d10315 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -4,7 +4,7 @@ import sys from itertools import cycle -from .multiplexer import Multiplexer +from .multiplexer import Multiplexer, STOP from . import colors from .utils import split_buffer @@ -34,7 +34,13 @@ class LogPrinter(object): prefix = color_fn(container.name + " | ") # Attach to container before log printer starts running line_generator = split_buffer(self._attach(container), '\n') - return (prefix + line.decode('utf-8') for line in line_generator) + + for line in line_generator: + yield prefix + line.decode('utf-8') + + exit_code = container.wait() + yield color_fn("%s exited with code %s\n" % (container.name, exit_code)) + yield STOP def _attach(self, container): params = { diff --git a/fig/cli/multiplexer.py b/fig/cli/multiplexer.py index 579f3bca..849dbd66 100644 --- a/fig/cli/multiplexer.py +++ b/fig/cli/multiplexer.py @@ -7,6 +7,11 @@ except ImportError: from queue import Queue, Empty # Python 3.x +# Yield STOP from an input generator to stop the +# top-level loop without processing any more input. +STOP = object() + + class Multiplexer(object): def __init__(self, generators): self.generators = generators @@ -17,7 +22,11 @@ class Multiplexer(object): while True: try: - yield self.queue.get(timeout=0.1) + item = self.queue.get(timeout=0.1) + if item is STOP: + break + else: + yield item except Empty: pass From 710cd3859101806dfae9c623fe9c103fa3d3685b Mon Sep 17 00:00:00 2001 From: Maurits van Mastrigt Date: Mon, 3 Mar 2014 15:21:53 +0100 Subject: [PATCH 0394/2154] Fix UnicodeEncodeErrors in output of 'build', 'run' and 'up' Squashed version of #125. Closes #112. --- fig/cli/log_printer.py | 2 +- fig/cli/socketclient.py | 2 +- fig/service.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index 0fe3215e..5efce1d1 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -18,7 +18,7 @@ class LogPrinter(object): def run(self): mux = Multiplexer(self.generators) for line in mux.loop(): - sys.stdout.write(line) + sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf8')) def _make_log_generators(self): color_fns = cycle(colors.rainbow()) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 6cc1f2c5..53e3bf79 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -81,7 +81,7 @@ class SocketClient: chunk = socket.recv(4096) if chunk: - stream.write(chunk) + stream.write(chunk.encode(stream.encoding or 'utf8')) stream.flush() else: break diff --git a/fig/service.py b/fig/service.py index 51ec004b..cf600dc1 100644 --- a/fig/service.py +++ b/fig/service.py @@ -295,7 +295,7 @@ class Service(object): match = re.search(r'Successfully built ([0-9a-f]+)', line) if match: image_id = match.group(1) - sys.stdout.write(line) + sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf8')) if image_id is None: raise BuildError() From 352ad7a38c68c736725ea82177e5614a45bae5f1 Mon Sep 17 00:00:00 2001 From: sebastianneubauer Date: Fri, 21 Mar 2014 19:34:19 +0100 Subject: [PATCH 0395/2154] Scaling down removes containers Squashed version of #162. Closes #121. --- fig/cli/main.py | 9 +-------- fig/project.py | 11 +++++++++++ fig/service.py | 10 ++++++++++ tests/project_test.py | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 4188d770..2e6a748f 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -298,20 +298,13 @@ class TopLevelCommand(Command): """ detached = options['-d'] - (old, new) = self.project.recreate_containers(service_names=options['SERVICE']) + new = self.project.up(service_names=options['SERVICE']) if not detached: to_attach = [c for (s, c) in new] print("Attaching to", list_containers(to_attach)) log_printer = LogPrinter(to_attach, attach_params={"logs": True}) - for (service, container) in new: - service.start_container(container) - - for (service, container) in old: - container.remove() - - if not detached: try: log_printer.run() finally: diff --git a/fig/project.py b/fig/project.py index 12ec7069..f8920205 100644 --- a/fig/project.py +++ b/fig/project.py @@ -137,6 +137,17 @@ class Project(object): else: log.info('%s uses an image, skipping' % service.name) + def up(self, service_names=None): + (old, new) = self.recreate_containers(service_names=service_names) + + for (service, container) in new: + service.start_container(container) + + for (service, container) in old: + container.remove() + + return new + def remove_stopped(self, service_names=None, **options): for service in self.get_services(service_names): service.remove_stopped(**options) diff --git a/fig/service.py b/fig/service.py index cf600dc1..d0de2c2b 100644 --- a/fig/service.py +++ b/fig/service.py @@ -85,6 +85,14 @@ class Service(object): c.kill(**options) def scale(self, desired_num): + """ + Adjusts the number of containers to the specified number and ensures they are running. + + - creates containers until there are at least `desired_num` + - stops containers until there are at most `desired_num` running + - starts containers until there are at least `desired_num` running + - removes all stopped containers + """ if not self.can_be_scaled(): raise CannotBeScaledError() @@ -117,6 +125,8 @@ class Service(object): self.start_container(c) running_containers.append(c) + self.remove_stopped() + def remove_stopped(self, **options): for c in self.containers(stopped=True): diff --git a/tests/project_test.py b/tests/project_test.py index 07f38340..b8a5d682 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -118,3 +118,41 @@ class ProjectTest(DockerClientTestCase): project.remove_stopped() self.assertEqual(len(project.containers(stopped=True)), 0) + + def test_project_up(self): + web = self.create_service('web') + db = self.create_service('db') + project = Project('figtest', [web, db], self.client) + project.start() + self.assertEqual(len(project.containers()), 0) + project.up() + self.assertEqual(len(project.containers()), 2) + project.kill() + project.remove_stopped() + + def test_unscale_after_restart(self): + web = self.create_service('web') + project = Project('figtest', [web], self.client) + + project.start() + + service = project.get_service('web') + service.scale(1) + self.assertEqual(len(service.containers()), 1) + service.scale(3) + self.assertEqual(len(service.containers()), 3) + project.up() + service = project.get_service('web') + self.assertEqual(len(service.containers()), 3) + service.scale(1) + self.assertEqual(len(service.containers()), 1) + project.up() + service = project.get_service('web') + self.assertEqual(len(service.containers()), 1) + # does scale=0 ,makes any sense? after recreating at least 1 container is running + service.scale(0) + project.up() + service = project.get_service('web') + self.assertEqual(len(service.containers()), 1) + project.kill() + project.remove_stopped() From 97be5b4cfbefee00f8767bc6b056d7c1e2f42bdd Mon Sep 17 00:00:00 2001 From: Colin McCune Date: Thu, 27 Mar 2014 20:15:17 -0400 Subject: [PATCH 0396/2154] Updates the mac build script so it wont fail when the venv folder does not exist. --- script/build-osx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/build-osx b/script/build-osx index 7cba8702..aba215fd 100755 --- a/script/build-osx +++ b/script/build-osx @@ -1,6 +1,6 @@ #!/bin/bash set -ex -rm -r venv +rm -rf venv virtualenv venv venv/bin/pip install pyinstaller==2.1 venv/bin/pip install . From 050f81e37ccb4d454384fa9b932b5a46065c2764 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 4 Apr 2014 13:06:52 +0100 Subject: [PATCH 0397/2154] Improve error message when link does not exist --- fig/project.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fig/project.py b/fig/project.py index f8920205..38bbba22 100644 --- a/fig/project.py +++ b/fig/project.py @@ -58,7 +58,11 @@ class Project(object): service_name, link_name = link.split(':', 1) else: service_name, link_name = link, None - links.append((project.get_service(service_name), link_name)) + try: + links.append((project.get_service(service_name), link_name)) + except NoSuchService: + 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 From d9782b2dd1cca218e580d04a270af5fe0bfa1604 Mon Sep 17 00:00:00 2001 From: Shane Jonas Date: Thu, 10 Apr 2014 16:47:26 -0700 Subject: [PATCH 0398/2154] fix issue with utf8 encoding in logger stdout --- fig/cli/log_printer.py | 2 +- fig/cli/socketclient.py | 2 +- fig/service.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index 1bfc7bab..35d36282 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -18,7 +18,7 @@ class LogPrinter(object): def run(self): mux = Multiplexer(self.generators) for line in mux.loop(): - sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf8')) + sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf-8')) def _make_log_generators(self): color_fns = cycle(colors.rainbow()) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 53e3bf79..518b7af4 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -81,7 +81,7 @@ class SocketClient: chunk = socket.recv(4096) if chunk: - stream.write(chunk.encode(stream.encoding or 'utf8')) + stream.write(chunk.encode(stream.encoding or 'utf-8')) stream.flush() else: break diff --git a/fig/service.py b/fig/service.py index 2555967b..4059ead6 100644 --- a/fig/service.py +++ b/fig/service.py @@ -306,7 +306,7 @@ class Service(object): match = re.search(r'Successfully built ([0-9a-f]+)', line) if match: image_id = match.group(1) - sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf8')) + sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf-8')) if image_id is None: raise BuildError(self) From 94e15a998590175d961c8e9a2c06c27c6e6b3e22 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 14 Apr 2014 22:29:03 +0100 Subject: [PATCH 0399/2154] Fix one-off containers not linking to service Closes #185. Need to test this more thoroughly. We need a docker-py mock. --- fig/cli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 1f80e0c7..9585371d 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -227,11 +227,11 @@ class TopLevelCommand(Command): } container = service.create_container(one_off=True, **container_options) if options['-d']: - service.start_container(container, ports=None) + service.start_container(container, ports=None, one_off=True) print(container.name) else: with self._attach_to_container(container.id, raw=tty) as c: - service.start_container(container, ports=None) + service.start_container(container, ports=None, one_off=True) c.run() if options['--rm']: container.wait() From 4e20be9c6618152801fa21c9e466004bc364bd15 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 14 Apr 2014 22:39:49 +0100 Subject: [PATCH 0400/2154] Remove unused imports --- fig/cli/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/fig/cli/utils.py b/fig/cli/utils.py index 64b2ea38..fb7533b9 100644 --- a/fig/cli/utils.py +++ b/fig/cli/utils.py @@ -3,10 +3,8 @@ from __future__ import absolute_import from __future__ import division import datetime import os -import socket import subprocess import platform -from .errors import UserError def cached_property(f): From 2b245bdf9e254746061a4ce9499bf517e488c8f5 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 10 Mar 2014 13:57:13 +0000 Subject: [PATCH 0401/2154] Update to docker-py 0.3.1 From https://github.com/dotcloud/docker-py/commit/7f55a101f813f3e96413d1b577e98d9467b0bffc This now requires Docker 0.9 or greater. --- .travis.yml | 8 +- fig/packages/docker/__init__.py | 3 + fig/packages/docker/auth/auth.py | 13 +- fig/packages/docker/client.py | 154 +++++++++++++---------- fig/packages/docker/unixconn/unixconn.py | 2 +- fig/packages/docker/utils/__init__.py | 2 +- fig/packages/docker/utils/utils.py | 36 +++++- tests/testcases.py | 2 +- 8 files changed, 139 insertions(+), 81 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5d6cc863..ea561955 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,8 @@ python: - '2.6' - '2.7' env: -- DOCKER_VERSION=0.8.0 -- DOCKER_VERSION=0.8.1 -matrix: - allow_failures: - - python: '3.2' - - python: '3.3' +- DOCKER_VERSION=0.9.1 +- DOCKER_VERSION=0.10.0 install: script/travis-install script: - pwd diff --git a/fig/packages/docker/__init__.py b/fig/packages/docker/__init__.py index 5f642a85..5388e728 100644 --- a/fig/packages/docker/__init__.py +++ b/fig/packages/docker/__init__.py @@ -12,4 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +__title__ = 'docker-py' +__version__ = '0.3.0' + from .client import Client, APIError # flake8: noqa diff --git a/fig/packages/docker/auth/auth.py b/fig/packages/docker/auth/auth.py index 8037dcbb..69cfa89d 100644 --- a/fig/packages/docker/auth/auth.py +++ b/fig/packages/docker/auth/auth.py @@ -48,7 +48,7 @@ def resolve_repository_name(repo_name): raise ValueError('Repository name cannot contain a ' 'scheme ({0})'.format(repo_name)) parts = repo_name.split('/', 1) - if not '.' in parts[0] and not ':' in parts[0] and parts[0] != 'localhost': + if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost': # This is a docker index repo (ex: foo/bar or ubuntu) return INDEX_URL, repo_name if len(parts) < 2: @@ -87,6 +87,11 @@ def resolve_authconfig(authconfig, registry=None): return authconfig.get(swap_protocol(registry), None) +def encode_auth(auth_info): + return base64.b64encode(auth_info.get('username', '') + b':' + + auth_info.get('password', '')) + + def decode_auth(auth): if isinstance(auth, six.string_types): auth = auth.encode('ascii') @@ -100,6 +105,12 @@ def encode_header(auth): return base64.b64encode(auth_json) +def encode_full_header(auth): + """ Returns the given auth block encoded for the X-Registry-Config header. + """ + return encode_header({'configs': auth}) + + def load_config(root=None): """Loads authentication data from a Docker configuration file in the given root directory.""" diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 948a3a67..77bb962f 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -28,13 +28,17 @@ from .utils import utils if not six.PY3: import websocket +DEFAULT_DOCKER_API_VERSION = '1.9' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 class APIError(requests.exceptions.HTTPError): def __init__(self, message, response, explanation=None): - super(APIError, self).__init__(message, response=response) + # requests 1.2 supports response as a keyword argument, but + # requests 1.1 doesn't + super(APIError, self).__init__(message) + self.response = response self.explanation = explanation @@ -65,7 +69,7 @@ class APIError(requests.exceptions.HTTPError): class Client(requests.Session): - def __init__(self, base_url=None, version="1.6", + def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, timeout=DEFAULT_TIMEOUT_SECONDS): super(Client, self).__init__() if base_url is None: @@ -125,7 +129,7 @@ class Client(requests.Session): mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, network_disabled=False, entrypoint=None, - cpu_shares=None, working_dir=None): + cpu_shares=None, working_dir=None, domainname=None): if isinstance(command, six.string_types): command = shlex.split(str(command)) if isinstance(environment, dict): @@ -133,7 +137,7 @@ class Client(requests.Session): '{0}={1}'.format(k, v) for k, v in environment.items() ] - if ports and isinstance(ports, list): + if isinstance(ports, list): exposed_ports = {} for port_definition in ports: port = port_definition @@ -145,12 +149,15 @@ class Client(requests.Session): exposed_ports['{0}/{1}'.format(port, proto)] = {} ports = exposed_ports - if volumes and isinstance(volumes, list): + if isinstance(volumes, list): volumes_dict = {} for vol in volumes: volumes_dict[vol] = {} volumes = volumes_dict + if volumes_from and not isinstance(volumes_from, six.string_types): + volumes_from = ','.join(volumes_from) + attach_stdin = False attach_stdout = False attach_stderr = False @@ -165,26 +172,27 @@ class Client(requests.Session): stdin_once = True return { - 'Hostname': hostname, + 'Hostname': hostname, + 'Domainname': domainname, 'ExposedPorts': ports, - 'User': user, - 'Tty': tty, - 'OpenStdin': stdin_open, - 'StdinOnce': stdin_once, - 'Memory': mem_limit, - 'AttachStdin': attach_stdin, + 'User': user, + 'Tty': tty, + 'OpenStdin': stdin_open, + 'StdinOnce': stdin_once, + 'Memory': mem_limit, + 'AttachStdin': attach_stdin, 'AttachStdout': attach_stdout, 'AttachStderr': attach_stderr, - 'Env': environment, - 'Cmd': command, - 'Dns': dns, - 'Image': image, - 'Volumes': volumes, - 'VolumesFrom': volumes_from, + 'Env': environment, + 'Cmd': command, + 'Dns': dns, + 'Image': image, + 'Volumes': volumes, + 'VolumesFrom': volumes_from, 'NetworkDisabled': network_disabled, - 'Entrypoint': entrypoint, - 'CpuShares': cpu_shares, - 'WorkingDir': working_dir + 'Entrypoint': entrypoint, + 'CpuShares': cpu_shares, + 'WorkingDir': working_dir } def _post_json(self, url, data, **kwargs): @@ -222,31 +230,18 @@ class Client(requests.Session): def _create_websocket_connection(self, url): return websocket.create_connection(url) - def _stream_result(self, response): - """Generator for straight-out, non chunked-encoded HTTP responses.""" + def _get_raw_response_socket(self, response): self._raise_for_status(response) - for line in response.iter_lines(chunk_size=1, decode_unicode=True): - # filter out keep-alive new lines - if line: - yield line + '\n' - - def _stream_result_socket(self, response): - self._raise_for_status(response) - return response.raw._fp.fp._sock + if six.PY3: + return response.raw._fp.fp.raw._sock + else: + return response.raw._fp.fp._sock def _stream_helper(self, response): """Generator for data coming from a chunked-encoded HTTP response.""" - socket_fp = self._stream_result_socket(response) - socket_fp.setblocking(1) - socket = socket_fp.makefile() - while True: - size = int(socket.readline(), 16) - if size <= 0: - break - data = socket.readline() - if not data: - break - yield data + for line in response.iter_lines(chunk_size=32): + if line: + yield line def _multiplexed_buffer_helper(self, response): """A generator of multiplexed data blocks read from a buffered @@ -265,17 +260,20 @@ class Client(requests.Session): def _multiplexed_socket_stream_helper(self, response): """A generator of multiplexed data blocks coming from a response socket.""" - socket = self._stream_result_socket(response) + socket = self._get_raw_response_socket(response) def recvall(socket, size): - data = '' + blocks = [] while size > 0: block = socket.recv(size) if not block: return None - data += block + blocks.append(block) size -= len(block) + + sep = bytes() if six.PY3 else str() + data = sep.join(blocks) return data while True: @@ -304,9 +302,18 @@ class Client(requests.Session): u = self._url("/containers/{0}/attach".format(container)) response = self._post(u, params=params, stream=stream) - # Stream multi-plexing was introduced in API v1.6. + # Stream multi-plexing was only introduced in API v1.6. Anything before + # that needs old-style streaming. if utils.compare_version('1.6', self._version) < 0: - return stream and self._stream_result(response) or \ + def stream_result(): + self._raise_for_status(response) + for line in response.iter_lines(chunk_size=1, + decode_unicode=True): + # filter out keep-alive new lines + if line: + yield line + + return stream_result() if stream else \ self._result(response, binary=True) return stream and self._multiplexed_socket_stream_helper(response) or \ @@ -319,13 +326,15 @@ class Client(requests.Session): 'stderr': 1, 'stream': 1 } + if ws: return self._attach_websocket(container, params) if isinstance(container, dict): container = container.get('Id') + u = self._url("/containers/{0}/attach".format(container)) - return self._stream_result_socket(self.post( + return self._get_raw_response_socket(self.post( u, None, params=self._attach_params(params), stream=True)) def build(self, path=None, tag=None, quiet=False, fileobj=None, @@ -341,6 +350,9 @@ class Client(requests.Session): else: context = utils.tar(path) + if utils.compare_version('1.8', self._version) >= 0: + stream = True + u = self._url('/build') params = { 't': tag, @@ -352,6 +364,19 @@ class Client(requests.Session): if context is not None: headers = {'Content-Type': 'application/tar'} + if utils.compare_version('1.9', self._version) >= 0: + # If we don't have any auth data so far, try reloading the config + # file one more time in case anything showed up in there. + if not self._auth_configs: + self._auth_configs = auth.load_config() + + # Send the full auth configuration (if any exists), since the build + # could use any (or all) of the registries. + if self._auth_configs: + headers['X-Registry-Config'] = auth.encode_full_header( + self._auth_configs + ) + response = self._post( u, data=context, @@ -363,8 +388,9 @@ class Client(requests.Session): if context is not None: context.close() + if stream: - return self._stream_result(response) + return self._stream_helper(response) else: output = self._result(response) srch = r'Successfully built ([0-9a-f]+)' @@ -403,6 +429,8 @@ class Client(requests.Session): return res def copy(self, container, resource): + if isinstance(container, dict): + container = container.get('Id') res = self._post_json( self._url("/containers/{0}/copy".format(container)), data={"Resource": resource}, @@ -416,12 +444,12 @@ class Client(requests.Session): mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, network_disabled=False, name=None, entrypoint=None, - cpu_shares=None, working_dir=None): + cpu_shares=None, working_dir=None, domainname=None): config = self._container_config( image, command, hostname, user, detach, stdin_open, tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, - entrypoint, cpu_shares, working_dir + entrypoint, cpu_shares, working_dir, domainname ) return self.create_container_from_config(config, name) @@ -440,21 +468,7 @@ class Client(requests.Session): format(container))), True) def events(self): - u = self._url("/events") - - socket = self._stream_result_socket(self.get(u, stream=True)) - - while True: - chunk = socket.recv(4096) - if chunk: - # Messages come in the format of length, data, newline. - length, data = chunk.split("\n", 1) - length = int(length, 16) - if length > len(data): - data += socket.recv(length - len(data)) - yield json.loads(data) - else: - break + return self._stream_helper(self.get(self._url('/events'), stream=True)) def export(self, container): if isinstance(container, dict): @@ -471,6 +485,8 @@ class Client(requests.Session): def images(self, name=None, quiet=False, all=False, viz=False): if viz: + if utils.compare_version('1.7', self._version) >= 0: + raise Exception('Viz output is not supported in API >= 1.7!') return self._result(self._get(self._url("images/viz"))) params = { 'filter': name, @@ -618,7 +634,7 @@ class Client(requests.Session): self._auth_configs = auth.load_config() authcfg = auth.resolve_authconfig(self._auth_configs, registry) - # Do not fail here if no atuhentication exists for this specific + # Do not fail here if no authentication exists for this specific # registry as we can have a readonly pull. Just put the header if # we can. if authcfg: @@ -644,7 +660,7 @@ class Client(requests.Session): self._auth_configs = auth.load_config() authcfg = auth.resolve_authconfig(self._auth_configs, registry) - # Do not fail here if no atuhentication exists for this specific + # Do not fail here if no authentication exists for this specific # registry as we can have a readonly pull. Just put the header if # we can. if authcfg: @@ -652,7 +668,7 @@ class Client(requests.Session): response = self._post_json(u, None, headers=headers, stream=stream) else: - response = self._post_json(u, authcfg, stream=stream) + response = self._post_json(u, None, stream=stream) return stream and self._stream_helper(response) \ or self._result(response) diff --git a/fig/packages/docker/unixconn/unixconn.py b/fig/packages/docker/unixconn/unixconn.py index b5c65931..176659e7 100644 --- a/fig/packages/docker/unixconn/unixconn.py +++ b/fig/packages/docker/unixconn/unixconn.py @@ -40,7 +40,7 @@ class UnixHTTPConnection(httplib.HTTPConnection, object): self.sock = sock def _extract_path(self, url): - #remove the base_url entirely.. + # remove the base_url entirely.. return url.replace(self.base_url, "") def request(self, method, url, **kwargs): diff --git a/fig/packages/docker/utils/__init__.py b/fig/packages/docker/utils/__init__.py index 386a01af..8a85975d 100644 --- a/fig/packages/docker/utils/__init__.py +++ b/fig/packages/docker/utils/__init__.py @@ -1,3 +1,3 @@ from .utils import ( - compare_version, convert_port_bindings, mkbuildcontext, ping, tar + compare_version, convert_port_bindings, mkbuildcontext, ping, tar, parse_repository_tag ) # flake8: noqa diff --git a/fig/packages/docker/utils/utils.py b/fig/packages/docker/utils/utils.py index 1cb04f0c..0e4c1c1f 100644 --- a/fig/packages/docker/utils/utils.py +++ b/fig/packages/docker/utils/utils.py @@ -15,6 +15,7 @@ import io import tarfile import tempfile +from distutils.version import StrictVersion import requests from fig.packages import six @@ -51,15 +52,34 @@ def tar(path): def compare_version(v1, v2): - return float(v2) - float(v1) + """Compare docker versions + + >>> v1 = '1.9' + >>> v2 = '1.10' + >>> compare_version(v1, v2) + 1 + >>> compare_version(v2, v1) + -1 + >>> compare_version(v2, v2) + 0 + """ + s1 = StrictVersion(v1) + s2 = StrictVersion(v2) + if s1 == s2: + return 0 + elif s1 > s2: + return -1 + else: + return 1 def ping(url): try: res = requests.get(url) - return res.status >= 400 except Exception: return False + else: + return res.status_code < 400 def _convert_port_binding(binding): @@ -94,3 +114,15 @@ def convert_port_bindings(port_bindings): else: result[key] = [_convert_port_binding(v)] return result + + +def parse_repository_tag(repo): + column_index = repo.rfind(':') + if column_index < 0: + return repo, "" + tag = repo[column_index+1:] + slash_index = tag.find('/') + if slash_index < 0: + return repo[:column_index], tag + + return repo, "" diff --git a/tests/testcases.py b/tests/testcases.py index 6556311d..ac395040 100644 --- a/tests/testcases.py +++ b/tests/testcases.py @@ -18,7 +18,7 @@ class DockerClientTestCase(unittest.TestCase): self.client.kill(c['Id']) self.client.remove_container(c['Id']) for i in self.client.images(): - if isinstance(i['Tag'], basestring) and 'figtest' in i['Tag']: + if isinstance(i.get('Tag'), basestring) and 'figtest' in i['Tag']: self.client.remove_image(i) def create_service(self, name, **kwargs): From f8ee52ca2a618b15208d3d6cd2c1c05b4fc5025d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 25 Mar 2014 17:12:59 +0000 Subject: [PATCH 0402/2154] Fix build output docker-py now streams us the raw JSON events, so we have to replicate the Docker client's progress logic. On the bright side, we now have well-behaved progress bars when pulling an image during `fig build` (no more ski slopes) and `fig up` (no more silence). --- fig/service.py | 91 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 5 deletions(-) diff --git a/fig/service.py b/fig/service.py index 4059ead6..3e032fa8 100644 --- a/fig/service.py +++ b/fig/service.py @@ -5,6 +5,7 @@ import logging import re import os import sys +import json from .container import Container log = logging.getLogger(__name__) @@ -146,7 +147,8 @@ class Service(object): except APIError as e: if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation): log.info('Pulling image %s...' % container_options['image']) - self.client.pull(container_options['image']) + output = self.client.pull(container_options['image'], stream=True) + stream_output(output, sys.stdout) return Container.create(self.client, **container_options) raise @@ -299,14 +301,15 @@ class Service(object): stream=True ) + all_events = stream_output(build_output, sys.stdout) + image_id = None - for line in build_output: - if line: - match = re.search(r'Successfully built ([0-9a-f]+)', line) + for event in all_events: + if 'stream' in event: + match = re.search(r'Successfully built ([0-9a-f]+)', event.get('stream', '')) if match: image_id = match.group(1) - sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf-8')) if image_id is None: raise BuildError(self) @@ -329,6 +332,84 @@ class Service(object): return True +def stream_output(output, stream): + is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno()) + all_events = [] + lines = {} + diff = 0 + + for chunk in output: + event = json.loads(chunk) + all_events.append(event) + + if 'progress' in event or 'progressDetail' in event: + image_id = event['id'] + + if image_id in lines: + diff = len(lines) - lines[image_id] + else: + lines[image_id] = len(lines) + stream.write("\n") + diff = 0 + + if is_terminal: + # move cursor up `diff` rows + stream.write("%c[%dA" % (27, diff)) + + try: + print_output_event(event, stream, is_terminal) + except Exception: + stream.write(repr(event) + "\n") + raise + + if 'id' in event and is_terminal: + # move cursor back down + stream.write("%c[%dB" % (27, diff)) + + stream.flush() + + return all_events + +def print_output_event(event, stream, is_terminal): + if 'errorDetail' in event: + raise Exception(event['errorDetail']['message']) + + terminator = '' + + if is_terminal and 'stream' not in event: + # erase current line + stream.write("%c[2K\r" % 27) + terminator = "\r" + pass + elif 'progressDetail' in event: + return + + if 'time' in event: + stream.write("[%s] " % event['time']) + + if 'id' in event: + stream.write("%s: " % event['id']) + + if 'from' in event: + stream.write("(from %s) " % event['from']) + + status = event.get('status', '') + + if 'progress' in event: + stream.write("%s %s%s" % (status, event['progress'], terminator)) + elif 'progressDetail' in event: + detail = event['progressDetail'] + if 'current' in detail: + percentage = float(detail['current']) / float(detail['total']) * 100 + stream.write('%s (%.1f%%)%s' % (status, percentage, terminator)) + else: + stream.write('%s%s' % (status, terminator)) + elif 'stream' in event: + stream.write("%s%s" % (event['stream'], terminator)) + else: + stream.write("%s%s\n" % (status, terminator)) + + NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') From c2acceba4e94b47395c55eb978d5e90b401ced66 Mon Sep 17 00:00:00 2001 From: "Scott M. Likens" Date: Wed, 16 Apr 2014 00:15:25 +0000 Subject: [PATCH 0403/2154] Ensure `pwd`/dist exists always and is 777. Fixes #192 Signed-off-by: Scott M. Likens --- script/build-linux | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/build-linux b/script/build-linux index a4004611..c9348afc 100755 --- a/script/build-linux +++ b/script/build-linux @@ -1,3 +1,5 @@ #!/bin/sh +mkdir -p `pwd`/dist +chmod 777 `pwd`/dist docker build -t fig . docker run -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig From 80991f152124d2b951683f391fac74678b59b49f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 23 Apr 2014 15:46:26 +0100 Subject: [PATCH 0404/2154] Set "VolumesFrom" when starting containers This is necessary when working with Docker 0.10.0 and up. Fortunately, we can set it both when creating and starting, and retain compatibility with 0.8.x and 0.9.x. recreate_containers() is now responsible for starting containers, as well as creating them. This greatly simplifies usage of the Service class. --- fig/cli/main.py | 3 +-- fig/packages/docker/client.py | 9 +++++++-- fig/project.py | 29 +++++----------------------- fig/service.py | 25 +++++++++++++----------- tests/project_test.py | 36 ++++++++++++----------------------- tests/service_test.py | 14 +++++++------- 6 files changed, 46 insertions(+), 70 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 9585371d..4700803d 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -301,10 +301,9 @@ class TopLevelCommand(Command): """ detached = options['-d'] - new = self.project.up(service_names=options['SERVICE']) + to_attach = self.project.up(service_names=options['SERVICE']) if not detached: - to_attach = [c for (s, c) in new] print("Attaching to", list_containers(to_attach)) log_printer = LogPrinter(to_attach, attach_params={"logs": True}) diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 77bb962f..8b447d78 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -698,8 +698,8 @@ class Client(requests.Session): params={'term': term}), True) - def start(self, container, binds=None, port_bindings=None, lxc_conf=None, - publish_all_ports=False, links=None, privileged=False): + def start(self, container, binds=None, volumes_from=None, port_bindings=None, + lxc_conf=None, publish_all_ports=False, links=None, privileged=False): if isinstance(container, dict): container = container.get('Id') @@ -718,6 +718,11 @@ class Client(requests.Session): ] start_config['Binds'] = bind_pairs + if volumes_from and not isinstance(volumes_from, six.string_types): + volumes_from = ','.join(volumes_from) + + start_config['VolumesFrom'] = volumes_from + if port_bindings: start_config['PortBindings'] = utils.convert_port_bindings( port_bindings diff --git a/fig/project.py b/fig/project.py index 38bbba22..b271a810 100644 --- a/fig/project.py +++ b/fig/project.py @@ -105,23 +105,6 @@ 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 recreate_containers(self, service_names=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): - (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): service.start(**options) @@ -142,15 +125,13 @@ class Project(object): log.info('%s uses an image, skipping' % service.name) def up(self, service_names=None): - (old, new) = self.recreate_containers(service_names=service_names) + new_containers = [] - for (service, container) in new: - service.start_container(container) + for service in self.get_services(service_names): + for (_, new) in service.recreate_containers(): + new_containers.append(new) - for (service, container) in old: - container.remove() - - return new + return new_containers def remove_stopped(self, service_names=None, **options): for service in self.get_services(service_names): diff --git a/fig/service.py b/fig/service.py index 3e032fa8..20c4e120 100644 --- a/fig/service.py +++ b/fig/service.py @@ -154,25 +154,24 @@ class Service(object): 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. + If a container for this service doesn't exist, create and start one. If there are + any, stop them, create+start new ones, and 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)]) + container = self.create_container(**override_options) + self.start_container(container) + return [(None, container)] else: - old_containers = [] - new_containers = [] + tuples = [] 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) + tuples.append(self.recreate_container(c, **override_options)) - return (old_containers, new_containers) + return tuples def recreate_container(self, container, **override_options): if container.is_running: @@ -185,17 +184,20 @@ class Service(object): entrypoint=['echo'], command=[], ) - intermediate_container.start() + intermediate_container.start(volumes_from=container.id) intermediate_container.wait() container.remove() options = dict(override_options) options['volumes_from'] = intermediate_container.id new_container = self.create_container(**options) + self.start_container(new_container, volumes_from=intermediate_container.id) + + intermediate_container.remove() return (intermediate_container, new_container) - def start_container(self, container=None, **override_options): + def start_container(self, container=None, volumes_from=None, **override_options): if container is None: container = self.create_container(**override_options) @@ -228,6 +230,7 @@ class Service(object): links=self._get_links(link_to_self=override_options.get('one_off', False)), port_bindings=port_bindings, binds=volume_bindings, + volumes_from=volumes_from, privileged=privileged, ) return container diff --git a/tests/project_test.py b/tests/project_test.py index b8a5d682..bde40e89 100644 --- a/tests/project_test.py +++ b/tests/project_test.py @@ -63,29 +63,6 @@ class ProjectTest(DockerClientTestCase): project = Project('test', [web], self.client) self.assertEqual(project.get_service('web'), web) - def test_recreate_containers(self): - web = self.create_service('web') - db = self.create_service('db') - project = Project('test', [web, db], self.client) - - old_web_container = web.create_container() - self.assertEqual(len(web.containers(stopped=True)), 1) - self.assertEqual(len(db.containers(stopped=True)), 0) - - (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) - - # remove intermediate containers - for (service, container) in old: - container.remove() - def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') @@ -121,12 +98,23 @@ class ProjectTest(DockerClientTestCase): def test_project_up(self): web = self.create_service('web') - db = self.create_service('db') + 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() self.assertEqual(len(project.containers()), 2) + + db_container = [c for c in project.containers() if 'db' in c.name][0] + self.assertNotEqual(c.id, old_db_id) + self.assertEqual(c.inspect()['Volumes']['/var/db'], db_volume_path) + project.kill() project.remove_stopped() diff --git a/tests/service_test.py b/tests/service_test.py index 5e8fe3ba..a8ea017a 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from fig import Service from fig.service import CannotBeScaledError, ConfigError +from fig.packages.docker.client import APIError from .testcases import DockerClientTestCase @@ -132,23 +133,22 @@ class ServiceTest(DockerClientTestCase): num_containers_before = len(self.client.containers(all=True)) service.options['environment']['FOO'] = '2' - (intermediate, new) = service.recreate_containers() - self.assertEqual(len(intermediate), 1) - self.assertEqual(len(new), 1) + tuples = service.recreate_containers() + self.assertEqual(len(tuples), 1) - new_container = new[0] - intermediate_container = intermediate[0] + intermediate_container = tuples[0][0] + new_container = tuples[0][1] self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['echo']) self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps']) self.assertEqual(new_container.dictionary['Config']['Cmd'], ['ax']) self.assertIn('FOO=2', new_container.dictionary['Config']['Env']) 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.assertEqual(len(self.client.containers(all=True)), num_containers_before) self.assertNotEqual(old_container.id, new_container.id) + self.assertRaises(APIError, lambda: self.client.inspect_container(intermediate_container.id)) def test_start_container_passes_through_options(self): db = self.create_service('db') From 5166b2c1a87aa4f09fecbe48d33bffc93b478134 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 23 Apr 2014 18:16:35 +0100 Subject: [PATCH 0405/2154] Update docker-py Using commit: https://github.com/aanand/docker-py/commit/b31bb4d879c8ecc37491edb9f56369c513577918 --- fig/packages/docker/__init__.py | 2 +- fig/packages/docker/auth/auth.py | 15 +++++--- fig/packages/docker/client.py | 66 ++++++++++++-------------------- fig/packages/docker/errors.py | 61 +++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 48 deletions(-) create mode 100644 fig/packages/docker/errors.py diff --git a/fig/packages/docker/__init__.py b/fig/packages/docker/__init__.py index 5388e728..e10a5761 100644 --- a/fig/packages/docker/__init__.py +++ b/fig/packages/docker/__init__.py @@ -15,4 +15,4 @@ __title__ = 'docker-py' __version__ = '0.3.0' -from .client import Client, APIError # flake8: noqa +from .client import Client # flake8: noqa diff --git a/fig/packages/docker/auth/auth.py b/fig/packages/docker/auth/auth.py index 69cfa89d..36f3e4c9 100644 --- a/fig/packages/docker/auth/auth.py +++ b/fig/packages/docker/auth/auth.py @@ -20,6 +20,7 @@ import os from fig.packages import six from ..utils import utils +from .. import errors INDEX_URL = 'https://index.docker.io/v1/' DOCKER_CONFIG_FILENAME = '.dockercfg' @@ -45,18 +46,19 @@ def expand_registry_url(hostname): def resolve_repository_name(repo_name): if '://' in repo_name: - raise ValueError('Repository name cannot contain a ' - 'scheme ({0})'.format(repo_name)) + raise errors.InvalidRepository( + 'Repository name cannot contain a scheme ({0})'.format(repo_name)) parts = repo_name.split('/', 1) if '.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost': # This is a docker index repo (ex: foo/bar or ubuntu) return INDEX_URL, repo_name if len(parts) < 2: - raise ValueError('Invalid repository name ({0})'.format(repo_name)) + raise errors.InvalidRepository( + 'Invalid repository name ({0})'.format(repo_name)) if 'index.docker.io' in parts[0]: - raise ValueError('Invalid repository name,' - 'try "{0}" instead'.format(parts[1])) + raise errors.InvalidRepository( + 'Invalid repository name, try "{0}" instead'.format(parts[1])) return expand_registry_url(parts[0]), parts[1] @@ -147,7 +149,8 @@ def load_config(root=None): data.append(line.strip().split(' = ')[1]) if len(data) < 2: # Not enough data - raise Exception('Invalid or empty configuration file!') + raise errors.InvalidConfigFile( + 'Invalid or empty configuration file!') username, password = decode_auth(data[0]) conf[INDEX_URL] = { diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 8b447d78..7f00b4c4 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -24,6 +24,7 @@ from fig.packages import six from .auth import auth from .unixconn import unixconn from .utils import utils +from . import errors if not six.PY3: import websocket @@ -33,41 +34,6 @@ DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 -class APIError(requests.exceptions.HTTPError): - def __init__(self, message, response, explanation=None): - # requests 1.2 supports response as a keyword argument, but - # requests 1.1 doesn't - super(APIError, self).__init__(message) - self.response = response - - self.explanation = explanation - - if self.explanation is None and response.content: - self.explanation = response.content.strip() - - def __str__(self): - message = super(APIError, self).__str__() - - if self.is_client_error(): - message = '%s Client Error: %s' % ( - self.response.status_code, self.response.reason) - - elif self.is_server_error(): - message = '%s Server Error: %s' % ( - self.response.status_code, self.response.reason) - - if self.explanation: - message = '%s ("%s")' % (message, self.explanation) - - return message - - def is_client_error(self): - return 400 <= self.response.status_code < 500 - - def is_server_error(self): - return 500 <= self.response.status_code < 600 - - class Client(requests.Session): def __init__(self, base_url=None, version=DEFAULT_DOCKER_API_VERSION, timeout=DEFAULT_TIMEOUT_SECONDS): @@ -112,7 +78,7 @@ class Client(requests.Session): try: response.raise_for_status() except requests.exceptions.HTTPError as e: - raise APIError(e, response, explanation=explanation) + raise errors.APIError(e, response, explanation=explanation) def _result(self, response, json=False, binary=False): assert not (json and binary) @@ -239,9 +205,23 @@ class Client(requests.Session): def _stream_helper(self, response): """Generator for data coming from a chunked-encoded HTTP response.""" - for line in response.iter_lines(chunk_size=32): - if line: - yield line + socket_fp = self._get_raw_response_socket(response) + socket_fp.setblocking(1) + socket = socket_fp.makefile() + while True: + # Because Docker introduced newlines at the end of chunks in v0.9, + # and only on some API endpoints, we have to cater for both cases. + size_line = socket.readline() + if size_line == '\r\n': + size_line = socket.readline() + + size = int(size_line, 16) + if size <= 0: + break + data = socket.readline() + if not data: + break + yield data def _multiplexed_buffer_helper(self, response): """A generator of multiplexed data blocks read from a buffered @@ -341,7 +321,7 @@ class Client(requests.Session): nocache=False, rm=False, stream=False, timeout=None): remote = context = headers = None if path is None and fileobj is None: - raise Exception("Either path or fileobj needs to be provided.") + raise TypeError("Either path or fileobj needs to be provided.") if fileobj is not None: context = utils.mkbuildcontext(fileobj) @@ -714,8 +694,12 @@ class Client(requests.Session): } if binds: bind_pairs = [ - '{0}:{1}'.format(host, dest) for host, dest in binds.items() + '%s:%s:%s' % ( + h, d['bind'], + 'ro' if 'ro' in d and d['ro'] else 'rw' + ) for h, d in binds.items() ] + start_config['Binds'] = bind_pairs if volumes_from and not isinstance(volumes_from, six.string_types): diff --git a/fig/packages/docker/errors.py b/fig/packages/docker/errors.py new file mode 100644 index 00000000..9aad700d --- /dev/null +++ b/fig/packages/docker/errors.py @@ -0,0 +1,61 @@ +# Copyright 2014 dotCloud inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests + + +class APIError(requests.exceptions.HTTPError): + def __init__(self, message, response, explanation=None): + # requests 1.2 supports response as a keyword argument, but + # requests 1.1 doesn't + super(APIError, self).__init__(message) + self.response = response + + self.explanation = explanation + + if self.explanation is None and response.content: + self.explanation = response.content.strip() + + def __str__(self): + message = super(APIError, self).__str__() + + if self.is_client_error(): + message = '%s Client Error: %s' % ( + self.response.status_code, self.response.reason) + + elif self.is_server_error(): + message = '%s Server Error: %s' % ( + self.response.status_code, self.response.reason) + + if self.explanation: + message = '%s ("%s")' % (message, self.explanation) + + return message + + def is_client_error(self): + return 400 <= self.response.status_code < 500 + + def is_server_error(self): + return 500 <= self.response.status_code < 600 + + +class DockerException(Exception): + pass + + +class InvalidRepository(DockerException): + pass + + +class InvalidConfigFile(DockerException): + pass From 9e1dfcfb37be6fc34e1b89d68521e408d6cb14d8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 23 Apr 2014 18:20:27 +0100 Subject: [PATCH 0406/2154] Update docker-py APIError imports --- fig/cli/main.py | 2 +- fig/service.py | 2 +- tests/service_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 4700803d..39ed9f60 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -15,7 +15,7 @@ from .formatter import Formatter from .log_printer import LogPrinter from .utils import yesno -from ..packages.docker.client import APIError +from ..packages.docker.errors import APIError from .errors import UserError from .docopt_command import NoSuchCommand from .socketclient import SocketClient diff --git a/fig/service.py b/fig/service.py index 20c4e120..54c35d0c 100644 --- a/fig/service.py +++ b/fig/service.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import -from .packages.docker.client import APIError +from .packages.docker.errors import APIError import logging import re import os diff --git a/tests/service_test.py b/tests/service_test.py index a8ea017a..f1b1f9de 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from fig import Service from fig.service import CannotBeScaledError, ConfigError -from fig.packages.docker.client import APIError +from fig.packages.docker.errors import APIError from .testcases import DockerClientTestCase From 6e932794f71b99ac6bd85274190d285a156fcdcc Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 25 Apr 2014 12:28:00 +0100 Subject: [PATCH 0407/2154] Fix regression when mounting volumes Caused by https://github.com/dotcloud/docker-py/commit/77fec67c608d0839a795f6eb807c9df2fd5bfd45 --- fig/service.py | 5 ++++- tests/service_test.py | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index 54c35d0c..2c7cc1c8 100644 --- a/fig/service.py +++ b/fig/service.py @@ -222,7 +222,10 @@ class Service(object): for volume in options['volumes']: if ':' in volume: external_dir, internal_dir = volume.split(':') - volume_bindings[os.path.abspath(external_dir)] = internal_dir + volume_bindings[os.path.abspath(external_dir)] = { + 'bind': internal_dir, + 'ro': False, + } privileged = options.get('privileged', False) diff --git a/tests/service_test.py b/tests/service_test.py index f1b1f9de..78947e1f 100644 --- a/tests/service_test.py +++ b/tests/service_test.py @@ -114,6 +114,12 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) self.assertIn('/var/db', container.inspect()['Volumes']) + def test_create_container_with_specified_volume(self): + service = self.create_service('db', volumes=['/tmp:/host-tmp']) + container = service.create_container() + service.start_container(container) + self.assertIn('/host-tmp', container.inspect()['Volumes']) + def test_recreate_containers(self): service = self.create_service( 'db', From ca7151aeb1848770e6c0df4d49bdc4b70bc57dfa Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 25 Apr 2014 22:58:21 +0100 Subject: [PATCH 0408/2154] Split tests into unit and integration --- tests/integration/__init__.py | 0 tests/{ => integration}/cli_test.py | 12 +---- tests/{ => integration}/project_test.py | 59 --------------------- tests/{ => integration}/service_test.py | 28 +--------- tests/{ => integration}/testcases.py | 2 +- tests/unit/__init__.py | 0 tests/unit/cli_test.py | 16 ++++++ tests/{ => unit}/container_test.py | 10 ++-- tests/unit/project_test.py | 69 +++++++++++++++++++++++++ tests/unit/service_test.py | 29 +++++++++++ tests/{ => unit}/sort_service_test.py | 2 +- tests/{ => unit}/split_buffer_test.py | 2 +- 12 files changed, 124 insertions(+), 105 deletions(-) create mode 100644 tests/integration/__init__.py rename tests/{ => integration}/cli_test.py (88%) rename tests/{ => integration}/project_test.py (61%) rename tests/{ => integration}/service_test.py (90%) rename tests/{ => integration}/testcases.py (97%) create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/cli_test.py rename tests/{ => unit}/container_test.py (84%) create mode 100644 tests/unit/project_test.py create mode 100644 tests/unit/service_test.py rename tests/{ => unit}/sort_service_test.py (99%) rename tests/{ => unit}/split_buffer_test.py (97%) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/cli_test.py b/tests/integration/cli_test.py similarity index 88% rename from tests/cli_test.py rename to tests/integration/cli_test.py index 2b81e26b..125b018e 100644 --- a/tests/cli_test.py +++ b/tests/integration/cli_test.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals from __future__ import absolute_import from .testcases import DockerClientTestCase from mock import patch -from fig.packages.six import StringIO from fig.cli.main import TopLevelCommand +from fig.packages.six import StringIO class CLITestCase(DockerClientTestCase): def setUp(self): @@ -15,16 +15,6 @@ class CLITestCase(DockerClientTestCase): self.command.project.kill() self.command.project.remove_stopped() - def test_yaml_filename_check(self): - self.command.base_dir = 'tests/fixtures/longer-filename-figfile' - - project = self.command.project - - self.assertTrue( project.get_service('definedinyamlnotyml'), "Service: definedinyamlnotyml should have been loaded from .yaml file" ) - - def test_help(self): - self.assertRaises(SystemExit, lambda: self.command.dispatch(['-h'], None)) - @patch('sys.stdout', new_callable=StringIO) def test_ps(self, mock_stdout): self.command.project.get_service('simple').create_container() diff --git a/tests/project_test.py b/tests/integration/project_test.py similarity index 61% rename from tests/project_test.py rename to tests/integration/project_test.py index bde40e89..fa7e3858 100644 --- a/tests/project_test.py +++ b/tests/integration/project_test.py @@ -4,65 +4,6 @@ from .testcases import DockerClientTestCase class ProjectTest(DockerClientTestCase): - def test_from_dict(self): - project = Project.from_dicts('figtest', [ - { - 'name': 'web', - 'image': 'ubuntu' - }, - { - 'name': 'db', - 'image': 'ubuntu' - } - ], self.client) - self.assertEqual(len(project.services), 2) - self.assertEqual(project.get_service('web').name, 'web') - self.assertEqual(project.get_service('web').options['image'], 'ubuntu') - self.assertEqual(project.get_service('db').name, 'db') - self.assertEqual(project.get_service('db').options['image'], 'ubuntu') - - def test_from_dict_sorts_in_dependency_order(self): - project = Project.from_dicts('figtest', [ - { - 'name': 'web', - 'image': 'ubuntu', - 'links': ['db'], - }, - { - 'name': 'db', - 'image': 'ubuntu' - } - ], self.client) - - self.assertEqual(project.services[0].name, 'db') - self.assertEqual(project.services[1].name, 'web') - - def test_from_config(self): - project = Project.from_config('figtest', { - 'web': { - 'image': 'ubuntu', - }, - 'db': { - 'image': 'ubuntu', - }, - }, self.client) - self.assertEqual(len(project.services), 2) - self.assertEqual(project.get_service('web').name, 'web') - self.assertEqual(project.get_service('web').options['image'], 'ubuntu') - self.assertEqual(project.get_service('db').name, 'db') - self.assertEqual(project.get_service('db').options['image'], 'ubuntu') - - def test_from_config_throws_error_when_not_dict(self): - with self.assertRaises(ConfigurationError): - project = Project.from_config('figtest', { - 'web': 'ubuntu', - }, self.client) - - def test_get_service(self): - web = self.create_service('web') - project = Project('test', [web], self.client) - self.assertEqual(project.get_service('web'), web) - def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') diff --git a/tests/service_test.py b/tests/integration/service_test.py similarity index 90% rename from tests/service_test.py rename to tests/integration/service_test.py index 78947e1f..78ddbd85 100644 --- a/tests/service_test.py +++ b/tests/integration/service_test.py @@ -1,35 +1,11 @@ from __future__ import unicode_literals from __future__ import absolute_import from fig import Service -from fig.service import CannotBeScaledError, ConfigError +from fig.service import CannotBeScaledError from fig.packages.docker.errors import APIError from .testcases import DockerClientTestCase - class ServiceTest(DockerClientTestCase): - def test_name_validations(self): - self.assertRaises(ConfigError, lambda: Service(name='')) - - self.assertRaises(ConfigError, lambda: Service(name=' ')) - self.assertRaises(ConfigError, lambda: Service(name='/')) - self.assertRaises(ConfigError, lambda: Service(name='!')) - self.assertRaises(ConfigError, lambda: Service(name='\xe2')) - self.assertRaises(ConfigError, lambda: Service(name='_')) - self.assertRaises(ConfigError, lambda: Service(name='____')) - self.assertRaises(ConfigError, lambda: Service(name='foo_bar')) - self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__')) - - Service('a') - Service('foo') - - def test_project_validation(self): - self.assertRaises(ConfigError, lambda: Service(name='foo', project='_')) - Service(name='foo', project='bar') - - def test_config_validation(self): - self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000'])) - Service(name='foo', ports=['8000']) - def test_containers(self): foo = self.create_service('foo') bar = self.create_service('bar') @@ -277,5 +253,3 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(containers), 2) for container in containers: self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp']) - - diff --git a/tests/testcases.py b/tests/integration/testcases.py similarity index 97% rename from tests/testcases.py rename to tests/integration/testcases.py index ac395040..f913b7c3 100644 --- a/tests/testcases.py +++ b/tests/integration/testcases.py @@ -3,7 +3,7 @@ from __future__ import absolute_import from fig.packages.docker import Client from fig.service import Service from fig.cli.utils import docker_url -from . import unittest +from .. import unittest class DockerClientTestCase(unittest.TestCase): diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py new file mode 100644 index 00000000..a3f13804 --- /dev/null +++ b/tests/unit/cli_test.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from __future__ import absolute_import +from .. import unittest +from fig.cli.main import TopLevelCommand +from fig.packages.six import StringIO + +class CLITestCase(unittest.TestCase): + def test_yaml_filename_check(self): + command = TopLevelCommand() + command.base_dir = 'tests/fixtures/longer-filename-figfile' + self.assertTrue(command.project.get_service('definedinyamlnotyml')) + + def test_help(self): + command = TopLevelCommand() + with self.assertRaises(SystemExit): + command.dispatch(['-h'], None) diff --git a/tests/container_test.py b/tests/unit/container_test.py similarity index 84% rename from tests/container_test.py rename to tests/unit/container_test.py index 351a807a..b1d87f7c 100644 --- a/tests/container_test.py +++ b/tests/unit/container_test.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals -from .testcases import DockerClientTestCase +from .. import unittest from fig.container import Container -class ContainerTest(DockerClientTestCase): +class ContainerTest(unittest.TestCase): def test_from_ps(self): - container = Container.from_ps(self.client, { + container = Container.from_ps(None, { "Id":"abc", "Image":"ubuntu:12.04", "Command":"sleep 300", @@ -22,7 +22,7 @@ class ContainerTest(DockerClientTestCase): }) def test_environment(self): - container = Container(self.client, { + container = Container(None, { 'ID': 'abc', 'Config': { 'Env': [ @@ -37,7 +37,7 @@ class ContainerTest(DockerClientTestCase): }) def test_number(self): - container = Container.from_ps(self.client, { + container = Container.from_ps(None, { "Id":"abc", "Image":"ubuntu:12.04", "Command":"sleep 300", diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py new file mode 100644 index 00000000..4a2ad142 --- /dev/null +++ b/tests/unit/project_test.py @@ -0,0 +1,69 @@ +from __future__ import unicode_literals +from .. import unittest +from fig.service import Service +from fig.project import Project, ConfigurationError + +class ProjectTest(unittest.TestCase): + def test_from_dict(self): + project = Project.from_dicts('figtest', [ + { + 'name': 'web', + 'image': 'ubuntu' + }, + { + 'name': 'db', + 'image': 'ubuntu' + } + ], None) + self.assertEqual(len(project.services), 2) + self.assertEqual(project.get_service('web').name, 'web') + self.assertEqual(project.get_service('web').options['image'], 'ubuntu') + self.assertEqual(project.get_service('db').name, 'db') + self.assertEqual(project.get_service('db').options['image'], 'ubuntu') + + def test_from_dict_sorts_in_dependency_order(self): + project = Project.from_dicts('figtest', [ + { + 'name': 'web', + 'image': 'ubuntu', + 'links': ['db'], + }, + { + 'name': 'db', + 'image': 'ubuntu' + } + ], None) + + self.assertEqual(project.services[0].name, 'db') + self.assertEqual(project.services[1].name, 'web') + + def test_from_config(self): + project = Project.from_config('figtest', { + 'web': { + 'image': 'ubuntu', + }, + 'db': { + 'image': 'ubuntu', + }, + }, None) + self.assertEqual(len(project.services), 2) + self.assertEqual(project.get_service('web').name, 'web') + self.assertEqual(project.get_service('web').options['image'], 'ubuntu') + self.assertEqual(project.get_service('db').name, 'db') + self.assertEqual(project.get_service('db').options['image'], 'ubuntu') + + def test_from_config_throws_error_when_not_dict(self): + with self.assertRaises(ConfigurationError): + project = Project.from_config('figtest', { + 'web': 'ubuntu', + }, None) + + def test_get_service(self): + web = Service( + project='figtest', + name='web', + client=None, + image="ubuntu", + ) + project = Project('test', [web], None) + self.assertEqual(project.get_service('web'), web) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py new file mode 100644 index 00000000..490cb60d --- /dev/null +++ b/tests/unit/service_test.py @@ -0,0 +1,29 @@ +from __future__ import unicode_literals +from __future__ import absolute_import +from .. import unittest +from fig import Service +from fig.service import ConfigError + +class ServiceTest(unittest.TestCase): + def test_name_validations(self): + self.assertRaises(ConfigError, lambda: Service(name='')) + + self.assertRaises(ConfigError, lambda: Service(name=' ')) + self.assertRaises(ConfigError, lambda: Service(name='/')) + self.assertRaises(ConfigError, lambda: Service(name='!')) + self.assertRaises(ConfigError, lambda: Service(name='\xe2')) + self.assertRaises(ConfigError, lambda: Service(name='_')) + self.assertRaises(ConfigError, lambda: Service(name='____')) + self.assertRaises(ConfigError, lambda: Service(name='foo_bar')) + self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__')) + + Service('a') + Service('foo') + + def test_project_validation(self): + self.assertRaises(ConfigError, lambda: Service(name='foo', project='_')) + Service(name='foo', project='bar') + + def test_config_validation(self): + self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000'])) + Service(name='foo', ports=['8000']) diff --git a/tests/sort_service_test.py b/tests/unit/sort_service_test.py similarity index 99% rename from tests/sort_service_test.py rename to tests/unit/sort_service_test.py index 13cff89d..e2a7bdb3 100644 --- a/tests/sort_service_test.py +++ b/tests/unit/sort_service_test.py @@ -1,5 +1,5 @@ from fig.project import sort_service_dicts, DependencyError -from . import unittest +from .. import unittest class SortServiceTest(unittest.TestCase): diff --git a/tests/split_buffer_test.py b/tests/unit/split_buffer_test.py similarity index 97% rename from tests/split_buffer_test.py rename to tests/unit/split_buffer_test.py index b90463c0..a78e99a6 100644 --- a/tests/split_buffer_test.py +++ b/tests/unit/split_buffer_test.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from fig.cli.utils import split_buffer -from . import unittest +from .. import unittest class SplitBufferTest(unittest.TestCase): def test_single_line_chunks(self): From 29c9763feb8eac648323911627f60624fdd11559 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 18 Feb 2014 16:21:30 +0000 Subject: [PATCH 0409/2154] Use Orchard to run integration tests --- .travis.yml | 21 +++++++++++++++------ requirements-dev.txt | 1 + script/travis | 23 ----------------------- script/travis-install | 18 ------------------ script/travis-integration | 10 ++++++++++ 5 files changed, 26 insertions(+), 47 deletions(-) delete mode 100755 script/travis delete mode 100755 script/travis-install create mode 100755 script/travis-integration diff --git a/.travis.yml b/.travis.yml index ea561955..d1c437cd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,13 +3,22 @@ python: - '2.6' - '2.7' env: -- DOCKER_VERSION=0.9.1 -- DOCKER_VERSION=0.10.0 -install: script/travis-install + global: + - secure: exbot0LTV/0Wic6ElKCrOZmh2ZrieuGwEqfYKf5rVuwu1sLngYRihh+lBL/hTwc79NSu829pbwiWfsQZrXbk/yvaS7avGR0CLDoipyPxlYa2/rfs/o4OdTZqXv0LcFmmd54j5QBMpWU1S+CYOwNkwas57trrvIpPbzWjMtfYzOU= +install: +- pip install . +- pip install -r requirements.txt +- pip install -r requirements-dev.txt +- sudo curl -L -o /usr/local/bin/orchard https://github.com/orchardup/go-orchard/releases/download/2.0.5/linux +- sudo chmod +x /usr/local/bin/orchard +before_script: + - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts rm -f $TRAVIS_JOB_ID' + - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts create $TRAVIS_JOB_ID || false' script: -- pwd -- env -- sekexe/run "`pwd`/script/travis $TRAVIS_PYTHON_VERSION" + - nosetests tests/unit + - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && script/travis-integration || false' +after_script: + - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts rm -f $TRAVIS_JOB_ID || false' deploy: provider: pypi user: orchard diff --git a/requirements-dev.txt b/requirements-dev.txt index 46d6a8aa..8d419005 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ mock==1.0.1 nose==1.3.0 pyinstaller==2.1 +unittest2 diff --git a/script/travis b/script/travis deleted file mode 100755 index 878b86e0..00000000 --- a/script/travis +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -# Exit on first error -set -ex - -# Put Python eggs in a writeable directory -export PYTHON_EGG_CACHE="/tmp/.python-eggs" - -# Activate correct virtualenv -TRAVIS_PYTHON_VERSION=$1 -source /home/travis/virtualenv/python${TRAVIS_PYTHON_VERSION}/bin/activate - -env - -# Kill background processes on exit -trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT - -# Start docker daemon -docker -d -H unix:///var/run/docker.sock 2>> /dev/null >> /dev/null & -sleep 2 - -# $init is set by sekexe -cd $(dirname $init)/.. && nosetests -v diff --git a/script/travis-install b/script/travis-install deleted file mode 100755 index 44aa3532..00000000 --- a/script/travis-install +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -set -ex - -sudo sh -c "wget -qO- https://get.docker.io/gpg | apt-key add -" -sudo sh -c "echo deb http://get.docker.io/ubuntu docker main > /etc/apt/sources.list.d/docker.list" -sudo apt-get update -echo exit 101 | sudo tee /usr/sbin/policy-rc.d -sudo chmod +x /usr/sbin/policy-rc.d -sudo apt-get install -qy slirp lxc lxc-docker-$DOCKER_VERSION -git clone git://github.com/jpetazzo/sekexe -python setup.py install -pip install -r requirements-dev.txt - -if [[ $TRAVIS_PYTHON_VERSION == "2.6" ]]; then - pip install unittest2 -fi - diff --git a/script/travis-integration b/script/travis-integration new file mode 100755 index 00000000..c0ff5b4b --- /dev/null +++ b/script/travis-integration @@ -0,0 +1,10 @@ +#!/bin/bash +set -ex + +# Kill background processes on exit +trap 'kill -9 $(jobs -p)' SIGINT SIGTERM EXIT + +export DOCKER_HOST=tcp://localhost:4243 +orchard proxy -H $TRAVIS_JOB_ID $DOCKER_HOST & +sleep 2 +nosetests -v From 24959209cc24a9ec4ee788074da399afc50b116d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 25 Apr 2014 23:33:25 +0100 Subject: [PATCH 0410/2154] Update build status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 225125a8..27a5c5c7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Fig === -[![Build Status](https://travis-ci.org/orchardup/fig.png?branch=master)](https://travis-ci.org/orchardup/fig) +[![Build Status](https://travis-ci.org/orchardup/fig.svg?branch=master)](https://travis-ci.org/orchardup/fig) [![PyPI version](https://badge.fury.io/py/fig.png)](http://badge.fury.io/py/fig) Fast, isolated development environments using Docker. From fd85be2c9e248fee0e746df7e6e8455b652f20a5 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 28 Apr 2014 18:22:11 +0100 Subject: [PATCH 0411/2154] Update docker[-osx] version in install docs --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index c5ad320c..2269e76c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,9 +6,9 @@ title: Installing Fig Installing Fig ============== -First, install Docker (version 0.8 or higher). If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): +First, install Docker version 0.10.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.github.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From 12b4c5358dfecbc783f05c8a5074b51f9b323f13 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 28 Apr 2014 18:52:15 +0100 Subject: [PATCH 0412/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli.html b/cli.html index e5d0739d..38c304ce 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index b3b3576a..e3899f10 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 8c248a4f..a72b958b 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 821b6690..80704d02 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index d038116d..171dcd2a 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -19,8 +19,8 @@

    Installing Fig

    -

    First, install Docker (version 0.8 or higher). If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker version 0.10.0. If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.github.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index 5778e7c6..8515dee7 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 95ac3f9b..7364374f 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 6db64b55..dd3f6ea5 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From ab7a7ffd6cd1a9df181620ed395d212378297414 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 28 Apr 2014 18:53:05 +0100 Subject: [PATCH 0413/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli.html b/cli.html index 38c304ce..2069f4be 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index e3899f10..b558ce6a 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index a72b958b..572337ac 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 80704d02..714059ef 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 171dcd2a..0cbc034c 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -19,8 +19,8 @@

    Installing Fig

    -

    First, install Docker version 0.10.0. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.github.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker (version 0.8 or higher). If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index 8515dee7..baecb634 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 7364374f..8edc5c20 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index dd3f6ea5..97007d64 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 10725136d834eb678df7b2673cc8c1a39f3b54d8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 29 Apr 2014 09:20:29 +0100 Subject: [PATCH 0414/2154] Add tests for names on containers --- tests/unit/container_test.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index b1d87f7c..44589241 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -13,12 +13,12 @@ class ContainerTest(unittest.TestCase): "Ports":None, "SizeRw":0, "SizeRootFs":0, - "Names":["/db_1"] + "Names":["/figtest_db_1"] }, has_been_inspected=True) self.assertEqual(container.dictionary, { "ID": "abc", "Image":"ubuntu:12.04", - "Name": "/db_1", + "Name": "/figtest_db_1", }) def test_environment(self): @@ -46,6 +46,24 @@ class ContainerTest(unittest.TestCase): "Ports":None, "SizeRw":0, "SizeRootFs":0, - "Names":["/db_1"] + "Names":["/figtest_db_1"] }, has_been_inspected=True) self.assertEqual(container.number, 1) + + def test_name(self): + container = Container.from_ps(None, { + "Id":"abc", + "Image":"ubuntu:12.04", + "Command":"sleep 300", + "Names":["/figtest_db_1"] + }, has_been_inspected=True) + self.assertEqual(container.name, "figtest_db_1") + + def test_name_without_project(self): + container = Container.from_ps(None, { + "Id":"abc", + "Image":"ubuntu:12.04", + "Command":"sleep 300", + "Names":["/figtest_db_1"] + }, has_been_inspected=True) + self.assertEqual(container.name_without_project, "db_1") From a724aa5717183e08982a3e2f160fa2425664a574 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 29 Apr 2014 09:22:20 +0100 Subject: [PATCH 0415/2154] Use name without project for log printing --- fig/cli/log_printer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index 35d36282..c528a126 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -31,7 +31,7 @@ class LogPrinter(object): return generators def _make_log_generator(self, container, color_fn): - prefix = color_fn(container.name + " | ") + prefix = color_fn(container.name_without_project + " | ") # Attach to container before log printer starts running line_generator = split_buffer(self._attach(container), '\n') From fff5e51426f586562b8438d84ced8330ef3c6975 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 29 Apr 2014 09:31:57 +0100 Subject: [PATCH 0416/2154] Make log messages line up with each other --- fig/cli/log_printer.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index c528a126..e302aecb 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -13,6 +13,7 @@ class LogPrinter(object): def __init__(self, containers, attach_params=None): self.containers = containers self.attach_params = attach_params or {} + self.prefix_width = self._calculate_prefix_width(containers) self.generators = self._make_log_generators() def run(self): @@ -20,6 +21,19 @@ class LogPrinter(object): for line in mux.loop(): sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf-8')) + def _calculate_prefix_width(self, containers): + """ + Calculate the maximum width of container names so we can make the log + prefixes line up like so: + + db_1 | Listening + web_1 | Listening + """ + prefix_width = 0 + for container in containers: + prefix_width = max(prefix_width, len(container.name_without_project)) + return prefix_width + def _make_log_generators(self): color_fns = cycle(colors.rainbow()) generators = [] @@ -31,7 +45,7 @@ class LogPrinter(object): return generators def _make_log_generator(self, container, color_fn): - prefix = color_fn(container.name_without_project + " | ") + prefix = color_fn(self._generate_prefix(container)) # Attach to container before log printer starts running line_generator = split_buffer(self._attach(container), '\n') @@ -42,6 +56,14 @@ class LogPrinter(object): yield color_fn("%s exited with code %s\n" % (container.name, exit_code)) yield STOP + def _generate_prefix(self, container): + """ + Generate the prefix for a log line without colour + """ + name = container.name_without_project + padding = ' ' * (self.prefix_width - len(name)) + return ''.join([name, padding, ' | ']) + def _attach(self, container): params = { 'stdout': True, From 3abce4259fba8a3005272b4531795a1d0d3ca994 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 30 Apr 2014 11:53:23 +0100 Subject: [PATCH 0417/2154] Fix regression in handling of build errors --- fig/cli/main.py | 2 +- fig/service.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 39ed9f60..a5f88dbc 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -52,7 +52,7 @@ def main(): log.error(e.explanation) sys.exit(1) except BuildError as e: - log.error("Service '%s' failed to build." % e.service.name) + log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason)) sys.exit(1) diff --git a/fig/service.py b/fig/service.py index 2c7cc1c8..597c3731 100644 --- a/fig/service.py +++ b/fig/service.py @@ -23,8 +23,9 @@ DOCKER_CONFIG_HINTS = { class BuildError(Exception): - def __init__(self, service): + def __init__(self, service, reason): self.service = service + self.reason = reason class CannotBeScaledError(Exception): @@ -307,7 +308,10 @@ class Service(object): stream=True ) - all_events = stream_output(build_output, sys.stdout) + try: + all_events = stream_output(build_output, sys.stdout) + except StreamOutputError, e: + raise BuildError(self, unicode(e)) image_id = None @@ -338,6 +342,10 @@ class Service(object): return True +class StreamOutputError(Exception): + pass + + def stream_output(output, stream): is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno()) all_events = [] @@ -362,11 +370,7 @@ def stream_output(output, stream): # move cursor up `diff` rows stream.write("%c[%dA" % (27, diff)) - try: - print_output_event(event, stream, is_terminal) - except Exception: - stream.write(repr(event) + "\n") - raise + print_output_event(event, stream, is_terminal) if 'id' in event and is_terminal: # move cursor back down @@ -378,7 +382,7 @@ def stream_output(output, stream): def print_output_event(event, stream, is_terminal): if 'errorDetail' in event: - raise Exception(event['errorDetail']['message']) + raise StreamOutputError(event['errorDetail']['message']) terminator = '' From d1d4f477647b9672b28c3761360291a2e5b16ce1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 29 Apr 2014 12:40:10 +0100 Subject: [PATCH 0418/2154] Ship 0.4.0 --- CHANGES.md | 12 ++++++++++++ fig/__init__.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 3c042b1b..1c8e6bd3 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,18 @@ Change log ========== +0.4.0 (2014-04-29) +------------------ + + - Support Docker 0.9 and 0.10 + - Display progress bars correctly when pulling images (no more ski slopes) + - `fig up` now stops all services when any container exits + - Added support for the `privileged` config option in fig.yml (thanks @kvz!) + - Shortened and aligned log prefixes in `fig up` output + - Only containers started with `fig run` link back to their own service + - Handle UTF-8 correctly when streaming `fig build/run/up` output (thanks @mauvm and @shanejonas!) + - Error message improvements + 0.3.2 (2014-03-05) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 3160f16d..285dca9b 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.3.2' +__version__ = '0.4.0' From 5e6f175b5f89bdc4c30085e2c6b0cf9153daf30e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 30 Apr 2014 16:26:46 +0100 Subject: [PATCH 0419/2154] Update download URLs on install page --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 2269e76c..4ef58d65 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun Next, install Fig. On OS X: - $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/darwin > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/darwin > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig On 64-bit Linux: - $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/linux > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/linux > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): From 629fe771df890280008b8580010868bdb26b075a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 30 Apr 2014 16:37:20 +0100 Subject: [PATCH 0420/2154] Update domain in docker-osx download URL --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 4ef58d65..9308484e 100644 --- a/docs/install.md +++ b/docs/install.md @@ -8,7 +8,7 @@ Installing Fig First, install Docker version 0.10.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.github.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From 33b057bfaf9326fdef0ce60b6496451c3a5ee26f Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 1 May 2014 10:51:42 +0100 Subject: [PATCH 0421/2154] Update tag in docker-osx download URL --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 9308484e..3f68499d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -8,7 +8,7 @@ Installing Fig First, install Docker version 0.10.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-1/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From a96a7d2e268f64f09538222a6994e5338d7df5a1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 1 May 2014 10:56:32 +0100 Subject: [PATCH 0422/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 10 +++++----- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli.html b/cli.html index 2069f4be..54e7bff5 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index b558ce6a..5dab58b6 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 572337ac..0345c14b 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 714059ef..d07c1ee2 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 0cbc034c..0cc0c752 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -19,19 +19,19 @@

    Installing Fig

    -

    First, install Docker (version 0.8 or higher). If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.github.com/noplay/docker-osx/0.8.0/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker version 0.10.0. If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-1/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     

    Docker has guides for Ubuntu and other platforms in their documentation.

    Next, install Fig. On OS X:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/darwin > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/darwin > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    On 64-bit Linux:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.3.2/linux > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/linux > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    diff --git a/rails.html b/rails.html index baecb634..ed52ac08 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 8edc5c20..8278b6fb 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 97007d64..3266a0a6 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 3d4b5cfbfe77aa0227d24d48668fcb3acdd358d9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 15:34:21 +0100 Subject: [PATCH 0423/2154] Update version of docker-osx --- docs/install.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.md b/docs/install.md index 3f68499d..30b2d024 100644 --- a/docs/install.md +++ b/docs/install.md @@ -8,7 +8,7 @@ Installing Fig First, install Docker version 0.10.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-1/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-2/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From ee6d858023bb3cb9b18af194c1395f3d9da888f6 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 17:16:22 +0100 Subject: [PATCH 0424/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 4 ++-- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cli.html b/cli.html index 54e7bff5..3b14d4bc 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 5dab58b6..f5b5f606 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 0345c14b..6a37554b 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index d07c1ee2..321d1a82 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 0cc0c752..9f512360 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -20,7 +20,7 @@

    Installing Fig

    First, install Docker version 0.10.0. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-1/docker-osx > /usr/local/bin/docker-osx
    +
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-2/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index ed52ac08..743956bc 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 8278b6fb..19cef85f 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 3266a0a6..57da1fac 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 9a3aafd065131bd7c87c2e685d82f0d474394f16 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 17:16:46 +0100 Subject: [PATCH 0425/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli.html b/cli.html index 3b14d4bc..64456f41 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index f5b5f606..058aaa7c 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 6a37554b..a17f2834 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 321d1a82..0d012b45 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 9f512360..f6b9711b 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index 743956bc..3df8bdc0 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 19cef85f..1dd6d612 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 57da1fac..f2bb9b6f 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 52f994cf048661c4b5bcddf5f8ee1bfe9507a3f0 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 17:17:06 +0100 Subject: [PATCH 0426/2154] Remove /docs/.git-gh-pages from gitignore It's inside /docs/_site --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9a8c8325..1623c04f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ /build /dist /docs/_site -/docs/.git-gh-pages fig.spec From 8251dec58780a889934524659a4c50205e4e1feb Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 17:17:23 +0100 Subject: [PATCH 0427/2154] Add docs, build and dist to clean script --- script/clean | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/clean b/script/clean index f9f1b0da..0eeff0a1 100755 --- a/script/clean +++ b/script/clean @@ -1,3 +1,3 @@ #!/bin/sh find . -type f -name '*.pyc' -delete - +rm -rf docs/_site build dist From 983337401cb90c28be5793369b96207fb9a3052d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 18:10:38 +0100 Subject: [PATCH 0428/2154] Return correct exit code from fig run Closes #197 --- fig/cli/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index a5f88dbc..1a26432a 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -233,10 +233,11 @@ class TopLevelCommand(Command): with self._attach_to_container(container.id, raw=tty) as c: service.start_container(container, ports=None, one_off=True) c.run() + exit_code = container.wait() if options['--rm']: - container.wait() log.info("Removing %s..." % container.name) self.client.remove_container(container.id) + sys.exit(exit_code) def scale(self, options): """ From 5878fe383459e94ad706c441c7c12c0d4558e58a Mon Sep 17 00:00:00 2001 From: Jef Mathiot Date: Fri, 2 May 2014 18:00:58 +0200 Subject: [PATCH 0429/2154] Add the ability to configure the project name --- fig/cli/command.py | 5 +++++ fig/cli/main.py | 7 ++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index 0a224273..cb256983 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -24,6 +24,7 @@ class Command(DocoptCommand): def __init__(self): self.yaml_path = os.environ.get('FIG_FILE', None) + self.explicit_project_name = None def dispatch(self, *args, **kwargs): try: @@ -44,6 +45,8 @@ class Command(DocoptCommand): def perform_command(self, options, *args, **kwargs): if options['--file'] is not None: self.yaml_path = os.path.join(self.base_dir, options['--file']) + if options['--project-name'] is not None: + self.explicit_project_name = options['--project-name'] return super(Command, self).perform_command(options, *args, **kwargs) @cached_property @@ -70,6 +73,8 @@ class Command(DocoptCommand): @cached_property def project_name(self): project = os.path.basename(os.getcwd()) + if self.explicit_project_name is not None: + project = self.explicit_project_name project = re.sub(r'[^a-zA-Z0-9]', '', project) if not project: project = 'default' diff --git a/fig/cli/main.py b/fig/cli/main.py index 1a26432a..f5898645 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -71,9 +71,10 @@ class TopLevelCommand(Command): fig -h|--help Options: - --verbose Show more output - --version Print version and exit - -f, --file FILE Specify an alternate fig file (default: fig.yml) + --verbose Show more output + --version Print version and exit + -f, --file FILE Specify an alternate fig file (default: fig.yml) + -p, --project-name NAME Specify an alternate project name (default: directory name) Commands: build Build or rebuild services From ab145b5365b9fb07e3b5accf4a66c7bd8f203a58 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 5 May 2014 10:46:52 +0100 Subject: [PATCH 0430/2154] Fix pull requests failing on travis --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d1c437cd..fe0f4dfe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,13 +12,13 @@ install: - sudo curl -L -o /usr/local/bin/orchard https://github.com/orchardup/go-orchard/releases/download/2.0.5/linux - sudo chmod +x /usr/local/bin/orchard before_script: - - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts rm -f $TRAVIS_JOB_ID' - - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts create $TRAVIS_JOB_ID || false' + - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts rm -f $TRAVIS_JOB_ID || true; fi' + - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts create $TRAVIS_JOB_ID; fi' script: - nosetests tests/unit - - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && script/travis-integration || false' + - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then script/travis-integration; fi' after_script: - - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && orchard hosts rm -f $TRAVIS_JOB_ID || false' + - 'if [ "${TRAVIS_PULL_REQUEST}" = "false" ]; then orchard hosts rm -f $TRAVIS_JOB_ID; fi' deploy: provider: pypi user: orchard From dff9aa6f0cb147bb3720f891b12947e5337cfd70 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 5 May 2014 10:50:23 +0100 Subject: [PATCH 0431/2154] Add installation and entrypoint to dockerfile --- Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dockerfile b/Dockerfile index 961a5511..a4dd7cbd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,8 @@ RUN pip install -r requirements.txt ADD requirements-dev.txt /code/ RUN pip install -r requirements-dev.txt ADD . /code/ +RUN python setup.py develop RUN useradd -d /home/user -m -s /bin/bash user RUN chown -R user /code/ USER user +ENTRYPOINT /usr/local/bin/fig From a3d8f7d1136623e89aa7a4d79fe4a21ce6c41e8d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 6 May 2014 17:13:56 +0100 Subject: [PATCH 0432/2154] Remove hash from gif URL --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27a5c5c7..9e7e4fc4 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ db: Then type `fig up`, and Fig will start and run your entire app: -![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif) +![example fig run](https://orchardup.com/static/images/fig-example-large.gif) There are commands to: diff --git a/docs/index.md b/docs/index.md index 590f2dfe..4ded20ba 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,7 +30,7 @@ db: Then type `fig up`, and Fig will start and run your entire app: -![example fig run](https://orchardup.com/static/images/fig-example-large.f96065fc9e22.gif) +![example fig run](https://orchardup.com/static/images/fig-example-large.gif) There are commands to: From 1420d76f335aea3122d5a2b12a69924b50230538 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 6 May 2014 17:14:05 +0100 Subject: [PATCH 0433/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 4 ++-- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cli.html b/cli.html index 64456f41..5e8a55f5 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 058aaa7c..94a406f3 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index a17f2834..ba317152 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 0d012b45..3ed3178b 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    @@ -40,7 +40,7 @@ RUN pip install -r requirements.txt

    Then type fig up, and Fig will start and run your entire app:

    -

    example fig run

    +

    example fig run

    There are commands to:

    diff --git a/install.html b/install.html index f6b9711b..1161d831 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index 3df8bdc0..f26b939f 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 1dd6d612..3224aa8a 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index f2bb9b6f..d30fa2b1 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 38c008e52704bef5666cfa364e20d7e0a3e59dd3 Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Thu, 8 May 2014 12:39:15 +0800 Subject: [PATCH 0434/2154] Fix index error when getting ghost state of containers --- fig/container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/container.py b/fig/container.py index c926d2a9..4ac42a44 100644 --- a/fig/container.py +++ b/fig/container.py @@ -78,7 +78,7 @@ class Container(object): def human_readable_state(self): self.inspect_if_not_inspected() if self.dictionary['State']['Running']: - if self.dictionary['State']['Ghost']: + if self.dictionary['State'].get('Ghost'): return 'Ghost' else: return 'Up' From 3c5e818b49d8da5a653d2688db16d21178d63d5a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 12:21:46 +0100 Subject: [PATCH 0435/2154] Update install docs for Docker 0.11.0 --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 30b2d024..5fae29fd 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,9 +6,9 @@ title: Installing Fig Installing Fig ============== -First, install Docker version 0.10.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): +First, install Docker version 0.11.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-2/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.0/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From 257a171c0c4d47c7e9ca5b0ebadcdb7e6c4770a9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 12:43:09 +0100 Subject: [PATCH 0436/2154] Ship 0.4.1 --- CHANGES.md | 5 +++++ fig/__init__.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 1c8e6bd3..d700605b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change log ========== +0.4.1 (2014-05-08) +------------------ + + - Add support for Docker 0.11.0. + 0.4.0 (2014-04-29) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 285dca9b..39011dec 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.4.0' +__version__ = '0.4.1' From d600b3498bf6e09bc2e1ef4f7f9c0e48e9ebaaef Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 13:00:04 +0100 Subject: [PATCH 0437/2154] Remove entrypoint from dockerfile --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a4dd7cbd..e7e27260 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,4 +9,3 @@ RUN python setup.py develop RUN useradd -d /home/user -m -s /bin/bash user RUN chown -R user /code/ USER user -ENTRYPOINT /usr/local/bin/fig From 580affa5f316ab12cdc5b40477d18dd06fb85ddb Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 13:02:50 +0100 Subject: [PATCH 0438/2154] Add sanity check to linux build script --- script/build-linux | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/build-linux b/script/build-linux index c9348afc..c30dbfbd 100755 --- a/script/build-linux +++ b/script/build-linux @@ -1,5 +1,7 @@ #!/bin/sh +set -ex mkdir -p `pwd`/dist chmod 777 `pwd`/dist docker build -t fig . docker run -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig +docker run -v `pwd`/dist:/code/dist fig dist/fig --version From b99bb64487ed699d94d71fe1187635cd7c9494bd Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 13:03:21 +0100 Subject: [PATCH 0439/2154] Add sanity check to OS X build script --- script/build-osx | 1 + 1 file changed, 1 insertion(+) diff --git a/script/build-osx b/script/build-osx index aba215fd..383c1464 100755 --- a/script/build-osx +++ b/script/build-osx @@ -5,3 +5,4 @@ virtualenv venv venv/bin/pip install pyinstaller==2.1 venv/bin/pip install . venv/bin/pyinstaller -F bin/fig +dist/fig --version From 0f7840270b3459d6681f78457ba80446a3ac712b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 13:07:34 +0100 Subject: [PATCH 0440/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli.html b/cli.html index 5e8a55f5..439fde30 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 94a406f3..7b89414b 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index ba317152..a9c2fad7 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 3ed3178b..40a9cad4 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 1161d831..8046f398 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -19,8 +19,8 @@

    Installing Fig

    -

    First, install Docker version 0.10.0. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.10.0-2/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker version 0.11.0. If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.0/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index f26b939f..68ee0e8c 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 3224aa8a..3fde6ccf 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index d30fa2b1..4f17fb87 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 98ceb6220264dee18cdb50938b8913a13ee74991 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 8 May 2014 13:10:13 +0100 Subject: [PATCH 0441/2154] Update Fig version on install page --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 5fae29fd..72e2a834 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun Next, install Fig. On OS X: - $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/darwin > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/darwin > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig On 64-bit Linux: - $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/linux > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/linux > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): From 3f12f9b156172f5c35efda25d419ef5489265572 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 8 May 2014 13:10:24 +0100 Subject: [PATCH 0442/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli.html b/cli.html index 439fde30..cbeb2247 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 7b89414b..76044320 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index a9c2fad7..f7efb2ed 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 40a9cad4..a3b54ac0 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 8046f398..09b990ed 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -27,11 +27,11 @@ $ docker-osx shell

    Docker has guides for Ubuntu and other platforms in their documentation.

    Next, install Fig. On OS X:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/darwin > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/darwin > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    On 64-bit Linux:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.0/linux > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/linux > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    diff --git a/rails.html b/rails.html index 68ee0e8c..1b53f55a 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 3fde6ccf..08bbd5f6 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 4f17fb87..c3909c38 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From bfbd2b3803cd8a4d4465ede3cfa882cd278fe94f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 8 May 2014 13:12:24 +0100 Subject: [PATCH 0443/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli.html b/cli.html index cbeb2247..df0ce540 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 76044320..98e46ad3 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index f7efb2ed..18b315f5 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index a3b54ac0..188a5a4b 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 09b990ed..45f57589 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index 1b53f55a..ac2de966 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 08bbd5f6..6379607d 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index c3909c38..c45adeea 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 7a9228ad75dda2e5d1069d550374b04bf53b0554 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 1 May 2014 15:41:36 +0100 Subject: [PATCH 0444/2154] Remove intermediate build containers Docker does this by default now. --- fig/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index 597c3731..21a3ea40 100644 --- a/fig/service.py +++ b/fig/service.py @@ -305,7 +305,8 @@ class Service(object): build_output = self.client.build( self.options['build'], tag=self._build_tag_name(), - stream=True + stream=True, + rm=True ) try: From cd1c8b2f09843d9a2c1bdcb5b6a17ac5f0aae2c8 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 9 May 2014 10:53:12 +0100 Subject: [PATCH 0445/2154] Update docs to Docker 0.11.1 --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index 72e2a834..b8d79b9b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,9 +6,9 @@ title: Installing Fig Installing Fig ============== -First, install Docker version 0.11.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): +First, install Docker version 0.11.1. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.0/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.1/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From 629eac535cf7df914c7da4afca2f869ba24c3105 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 9 May 2014 10:59:18 +0100 Subject: [PATCH 0446/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli.html b/cli.html index df0ce540..1e8fcfcd 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 98e46ad3..c28f20b0 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 18b315f5..6122f516 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 188a5a4b..05f9939b 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 45f57589..4d43dfed 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -19,8 +19,8 @@

    Installing Fig

    -

    First, install Docker version 0.11.0. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.0/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker version 0.11.1. If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.1/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index ac2de966..30cb705e 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 6379607d..253a8a57 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index c45adeea..1fa68292 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 7879dfd3fd69dd5c003c49315727c1c5e2de791b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 9 May 2014 10:59:44 +0100 Subject: [PATCH 0447/2154] Fix deploy docs script --- script/deploy-docs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/script/deploy-docs b/script/deploy-docs index f5427ea5..518815dc 100755 --- a/script/deploy-docs +++ b/script/deploy-docs @@ -21,8 +21,7 @@ git reset --soft origin/gh-pages echo ".git-gh-pages" > .gitignore -git add -u -git add . +git add -A . git commit -m "update" || echo "didn't commit" git push origin master:gh-pages From c1a38d787d03273f44b7c680006aa289c1cc993c Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 12 May 2014 11:45:55 +0100 Subject: [PATCH 0448/2154] Fix 0.4.1 release notes --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index d700605b..91d84793 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,9 @@ Change log 0.4.1 (2014-05-08) ------------------ - - Add support for Docker 0.11.0. + - Add support for Docker 0.11.0. (Thanks @marksteve!) + - Make project name configurable. (Thanks @jefmathiot!) + - Return correct exit code from `fig run`. 0.4.0 (2014-04-29) ------------------ From adda3a7f79e1f136749be19775bb7197489a48bb Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 14 May 2014 13:05:40 +0100 Subject: [PATCH 0449/2154] Put Orchard in docs sidebar --- docs/_layouts/default.html | 4 +++- docs/index.md | 2 -- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 4ca2a92d..6ba5651c 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -44,10 +44,12 @@ +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/docs/index.md b/docs/index.md index 4ded20ba..46273308 100644 --- a/docs/index.md +++ b/docs/index.md @@ -39,8 +39,6 @@ There are commands to: - tail running services' log output - run a one-off command on a service -Fig is a project from [Orchard](https://orchardup.com), a Docker hosting service. [Follow us on Twitter](https://twitter.com/orchardup) to keep up to date with Fig and other Docker news. - Quick start ----------- From 8394e840995eee7184012f967537cfa424ea31a3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 14 May 2014 16:32:39 +0100 Subject: [PATCH 0450/2154] Compress the sidebar a bit --- docs/css/fig.css | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/css/fig.css b/docs/css/fig.css index fb9fb497..3dc990f1 100644 --- a/docs/css/fig.css +++ b/docs/css/fig.css @@ -58,7 +58,7 @@ img { .logo { font-family: 'Lilita One', sans-serif; - font-size: 80px; + font-size: 64px; margin: 20px 0 40px 0; } @@ -68,8 +68,8 @@ img { } .logo img { - width: 80px; - vertical-align: -17px; + width: 60px; + vertical-align: -8px; } .mobile-logo { @@ -77,13 +77,18 @@ img { } .sidebar { - font-size: 16px; + font-size: 15px; + color: #777; } .sidebar a { color: #a41211; } +.sidebar p { + margin: 10px 0; +} + @media (max-width: 767px) { .sidebar { text-align: center; @@ -101,7 +106,8 @@ img { } .logo { - margin-top: 40px; + margin-top: 30px; + margin-bottom: 30px; } .content h1 { @@ -116,6 +122,7 @@ img { width: 280px; overflow-y: auto; padding-left: 40px; + padding-right: 10px; border-right: 1px solid #ccc; } @@ -126,12 +133,12 @@ img { } .nav { - margin: 20px 0; + margin: 15px 0; } .nav li a { display: block; - padding: 8px 0; + padding: 5px 0; line-height: 1.2; text-decoration: none; } From 14667a37ea1479209464ba77df3bca88c66c4f6d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 14 May 2014 16:36:06 +0100 Subject: [PATCH 0451/2154] update --- cli.html | 6 ++++-- css/fig.css | 21 ++++++++++++++------- django.html | 6 ++++-- env.html | 6 ++++-- index.html | 8 ++++---- install.html | 6 ++++-- rails.html | 6 ++++-- wordpress.html | 6 ++++-- yml.html | 6 ++++-- 9 files changed, 46 insertions(+), 25 deletions(-) diff --git a/cli.html b/cli.html index 1e8fcfcd..6393f50a 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    @@ -113,10 +113,12 @@ For example:

    +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/css/fig.css b/css/fig.css index fb9fb497..3dc990f1 100644 --- a/css/fig.css +++ b/css/fig.css @@ -58,7 +58,7 @@ img { .logo { font-family: 'Lilita One', sans-serif; - font-size: 80px; + font-size: 64px; margin: 20px 0 40px 0; } @@ -68,8 +68,8 @@ img { } .logo img { - width: 80px; - vertical-align: -17px; + width: 60px; + vertical-align: -8px; } .mobile-logo { @@ -77,13 +77,18 @@ img { } .sidebar { - font-size: 16px; + font-size: 15px; + color: #777; } .sidebar a { color: #a41211; } +.sidebar p { + margin: 10px 0; +} + @media (max-width: 767px) { .sidebar { text-align: center; @@ -101,7 +106,8 @@ img { } .logo { - margin-top: 40px; + margin-top: 30px; + margin-bottom: 30px; } .content h1 { @@ -116,6 +122,7 @@ img { width: 280px; overflow-y: auto; padding-left: 40px; + padding-right: 10px; border-right: 1px solid #ccc; } @@ -126,12 +133,12 @@ img { } .nav { - margin: 20px 0; + margin: 15px 0; } .nav li a { display: block; - padding: 8px 0; + padding: 5px 0; line-height: 1.2; text-decoration: none; } diff --git a/django.html b/django.html index c28f20b0..55f1d6ac 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -120,10 +120,12 @@ myapp_web_1 | Quit the server with CONTROL-C. +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/env.html b/env.html index 6122f516..72ad7283 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    @@ -67,10 +67,12 @@ Fully qualified container name, e.g. DB_1_NAME=/myapp_web_1/myapp_db_1 +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/index.html b/index.html index 05f9939b..ff070308 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    @@ -51,8 +51,6 @@ RUN pip install -r requirements.txt
  • run a one-off command on a service
  • -

    Fig is a project from Orchard, a Docker hosting service. Follow us on Twitter to keep up to date with Fig and other Docker news.

    -

    Quick start

    Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it.

    @@ -170,10 +168,12 @@ figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/install.html b/install.html index 4d43dfed..983b118f 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -65,10 +65,12 @@ $ chmod +x /usr/local/bin/fig +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/rails.html b/rails.html index 30cb705e..bb920f14 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    @@ -126,10 +126,12 @@ myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port= +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/wordpress.html b/wordpress.html index 253a8a57..800130c0 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    @@ -116,10 +116,12 @@ if(file_exists($root.$path)) +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    diff --git a/yml.html b/yml.html index 1fa68292..fadc9125 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    @@ -95,10 +95,12 @@ +

    Fig is a project from Orchard, a Docker hosting service.

    +

    Follow us on Twitter to keep up to date with Fig and other Docker news.

    +
    From 1053eb94f7bf2e271d81447c1df429a651c07abe Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 14 May 2014 16:36:40 +0100 Subject: [PATCH 0452/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli.html b/cli.html index 6393f50a..f6b83886 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 55f1d6ac..7dd3b8f5 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 72ad7283..f9ad3bea 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index ff070308..5f2a38ca 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 983b118f..21e6314f 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index bb920f14..d7b43b17 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index 800130c0..cdd45474 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index fadc9125..fe02df3c 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 2ecd366905fb2e78fecb17bf325e0c4c70016cc9 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 22 May 2014 14:15:17 +0100 Subject: [PATCH 0453/2154] Add PYTHONUNBUFFERED=1 to Django tutorial --- docs/django.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/django.md b/docs/django.md index 1c961b6c..82b557ee 100644 --- a/docs/django.md +++ b/docs/django.md @@ -11,6 +11,7 @@ Let's use Fig to set up and run a Django/PostgreSQL app. Before starting, you'll Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with: FROM orchardup/python:2.7 + ENV PYTHONUNBUFFERED 1 RUN apt-get update -qq && apt-get install -y python-psycopg2 RUN mkdir /code WORKDIR /code From 859e85a6a4f3d5411812094e0d54973aedd1c8cb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 22 May 2014 17:15:34 +0100 Subject: [PATCH 0454/2154] update --- cli.html | 2 +- django.html | 3 ++- env.html | 2 +- index.html | 2 +- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cli.html b/cli.html index f6b83886..09075afb 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 7dd3b8f5..1ca13b9a 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -23,6 +23,7 @@

    Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called Dockerfile. It'll contain this to start with:

    FROM orchardup/python:2.7
    +ENV PYTHONUNBUFFERED 1
     RUN apt-get update -qq && apt-get install -y python-psycopg2
     RUN mkdir /code
     WORKDIR /code
    diff --git a/env.html b/env.html
    index f9ad3bea..69c0eb75 100644
    --- a/env.html
    +++ b/env.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/index.html b/index.html index 5f2a38ca..5673babf 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 21e6314f..d5aa3796 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    diff --git a/rails.html b/rails.html index d7b43b17..6763e322 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index cdd45474..fb241721 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index fe02df3c..a5cdcb0f 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 3a2c9c1016a3e0929f13b34ef69e0abe9494aa7d Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 28 May 2014 10:59:21 +0100 Subject: [PATCH 0455/2154] Switch to Apache License 2.0 Signed-off-by: Ben Firshman --- LICENSE | 215 ++++++++++++++++++++++++++++++++++++++++++++++++------- setup.py | 2 +- 2 files changed, 192 insertions(+), 25 deletions(-) diff --git a/LICENSE b/LICENSE index a75bd571..1d9a77f5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,24 +1,191 @@ -Copyright (c) 2013, Orchard Laboratories Ltd. -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * The names of its contributors may not be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2014 Orchard Laboratories Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/setup.py b/setup.py index c82f0ffa..1f196baf 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ setup( url='http://orchardup.github.io/fig/', author='Orchard Laboratories Ltd.', author_email='hello@orchardup.com', - license='BSD', + license='Apache License 2.0', packages=find_packages(), include_package_data=True, test_suite='nose.collector', From 1b5335f40947d7f9c9b3bf514c6bbb8be86b0d07 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 28 May 2014 11:08:46 +0100 Subject: [PATCH 0456/2154] Add developer certificate of origin docs Signed-off-by: Ben Firshman --- CONTRIBUTING.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 432e7e1e..b660f6a0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,7 @@ # Contributing to Fig +## Development environment + If you're looking contribute to [Fig](http://orchardup.github.io/fig/) but you're new to the project or maybe even to Python, here are the steps that should get you started. @@ -27,4 +29,47 @@ OS X: Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyInstaller](http://www.pyinstaller.org/ticket/807). +## Sign your work + +The sign-off is a simple line at the end of the explanation for the +patch, which certifies that you wrote it or otherwise have the right to +pass it on as an open-source patch. The rules are pretty simple: if you +can certify the below (from [developercertificate.org](http://developercertificate.org/)): + + Developer's Certificate of Origin 1.1 + + By making a contribution to this project, I certify that: + + (a) The contribution was created in whole or in part by me and I + have the right to submit it under the open source license + indicated in the file; or + + (b) The contribution is based upon previous work that, to the best + of my knowledge, is covered under an appropriate open source + license and I have the right under that license to submit that + work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am + permitted to submit under a different license), as indicated + in the file; or + + (c) The contribution was provided directly to me by some other + person who certified (a), (b) or (c) and I have not modified + it. + + (d) I understand and agree that this project and the contribution + are public and that a record of the contribution (including all + personal information I submit with it, including my sign-off) is + maintained indefinitely and may be redistributed consistent with + this project or the open source license(s) involved. + +then you just add a line saying + + Signed-off-by: Random J Developer + +using your real name (sorry, no pseudonyms or anonymous contributions.) + +The easiest way to do this is to use the `--signoff` flag when committing. E.g.: + + + $ git commit --signoff From 8b4ed0c1a8c5da8d1bd650698049491d702684d9 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sat, 7 Jun 2014 08:22:27 +0000 Subject: [PATCH 0457/2154] Spike: Add 'auto_start' option to fig.yml Signed-off-by: Chris Corbyn --- fig/project.py | 12 +++++++++--- fig/service.py | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/fig/project.py b/fig/project.py index b271a810..1f1571f0 100644 --- a/fig/project.py +++ b/fig/project.py @@ -64,7 +64,13 @@ 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)) + + auto_start = True + if 'auto_start' in service_dict: + auto_start = service_dict.get('auto_start', True) + del service_dict['auto_start'] + + project.services.append(Service(auto_start=auto_start, client=client, project=name, links=links, **service_dict)) return project @classmethod @@ -88,7 +94,7 @@ class Project(object): raise NoSuchService(name) - def get_services(self, service_names=None): + def get_services(self, service_names=None, auto_start=True): """ Returns a list of this project's services filtered by the provided list of names, or all services if @@ -100,7 +106,7 @@ class Project(object): do not exist. """ if service_names is None or len(service_names) == 0: - return self.services + return filter(lambda srv: srv.auto_start == auto_start, self.services) else: unsorted = [self.get_service(name) for name in service_names] return [s for s in self.services if s in unsorted] diff --git a/fig/service.py b/fig/service.py index 21a3ea40..a954dc4a 100644 --- a/fig/service.py +++ b/fig/service.py @@ -37,7 +37,7 @@ class ConfigError(ValueError): class Service(object): - def __init__(self, name, client=None, project='default', links=[], **options): + def __init__(self, name, auto_start=True, client=None, project='default', links=[], **options): if not re.match('^[a-zA-Z0-9]+$', name): raise ConfigError('Invalid name: %s' % name) if not re.match('^[a-zA-Z0-9]+$', project): @@ -45,7 +45,7 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) - supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose'] + supported_options = DOCKER_CONFIG_KEYS + ['auto_start', 'build', 'expose'] for k in options: if k not in supported_options: @@ -55,6 +55,7 @@ class Service(object): raise ConfigError(msg) self.name = name + self.auto_start = auto_start self.client = client self.project = project self.links = links or [] From edf6b560167f2ad0b63f17ff402de65d3760f5f9 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sat, 7 Jun 2014 08:22:54 +0000 Subject: [PATCH 0458/2154] Spike: Add --up option to `fig run` Signed-off-by: Chris Corbyn --- fig/cli/main.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index f5898645..1e2ea90f 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -202,9 +202,10 @@ 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. + Note that by default 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 either run `fig up -d db` + first, or use `fig run --up SERVICE COMMAND [ARGS...]`. Usage: run [options] SERVICE COMMAND [ARGS...] @@ -214,7 +215,13 @@ class TopLevelCommand(Command): -T Disable pseudo-tty allocation. By default `fig run` allocates a TTY. --rm Remove container after run. Ignored in detached mode. + --up Also start services that the command's service links to """ + + if options['--up']: + # FIXME: I'm not sure if this is good python form + self.up({'-d': True, 'SERVICE': None}) + service = self.project.get_service(options['SERVICE']) tty = True From 0c12db06ec8bafd7a640e09bc0e374038c062b8c Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sat, 7 Jun 2014 13:03:53 +0000 Subject: [PATCH 0459/2154] Move 'auto_start' option default to Service and add unit tests Signed-off-by: Chris Corbyn --- fig/project.py | 11 +++-------- fig/service.py | 6 ++++-- tests/unit/project_test.py | 2 +- tests/unit/service_test.py | 4 ++++ 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/fig/project.py b/fig/project.py index 1f1571f0..605e4c07 100644 --- a/fig/project.py +++ b/fig/project.py @@ -65,12 +65,7 @@ class Project(object): del service_dict['links'] - auto_start = True - if 'auto_start' in service_dict: - auto_start = service_dict.get('auto_start', True) - del service_dict['auto_start'] - - project.services.append(Service(auto_start=auto_start, client=client, project=name, links=links, **service_dict)) + project.services.append(Service(client=client, project=name, links=links, **service_dict)) return project @classmethod @@ -94,7 +89,7 @@ class Project(object): raise NoSuchService(name) - def get_services(self, service_names=None, auto_start=True): + def get_services(self, service_names=None): """ Returns a list of this project's services filtered by the provided list of names, or all services if @@ -106,7 +101,7 @@ class Project(object): do not exist. """ if service_names is None or len(service_names) == 0: - return filter(lambda srv: srv.auto_start == auto_start, self.services) + return filter(lambda s: s.options['auto_start'], self.services) else: unsorted = [self.get_service(name) for name in service_names] return [s for s in self.services if s in unsorted] diff --git a/fig/service.py b/fig/service.py index a954dc4a..7df3c46b 100644 --- a/fig/service.py +++ b/fig/service.py @@ -37,7 +37,7 @@ class ConfigError(ValueError): class Service(object): - def __init__(self, name, auto_start=True, client=None, project='default', links=[], **options): + def __init__(self, name, client=None, project='default', links=[], **options): if not re.match('^[a-zA-Z0-9]+$', name): raise ConfigError('Invalid name: %s' % name) if not re.match('^[a-zA-Z0-9]+$', project): @@ -45,6 +45,9 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) + if 'auto_start' not in options: + options['auto_start'] = True + supported_options = DOCKER_CONFIG_KEYS + ['auto_start', 'build', 'expose'] for k in options: @@ -55,7 +58,6 @@ class Service(object): raise ConfigError(msg) self.name = name - self.auto_start = auto_start self.client = client self.project = project self.links = links or [] diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 4a2ad142..f5bacc65 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -13,7 +13,7 @@ class ProjectTest(unittest.TestCase): { 'name': 'db', 'image': 'ubuntu' - } + }, ], None) self.assertEqual(len(project.services), 2) self.assertEqual(project.get_service('web').name, 'web') diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 490cb60d..330e0411 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -20,6 +20,10 @@ class ServiceTest(unittest.TestCase): Service('a') Service('foo') + def test_auto_start_defaults_true(self): + service = Service(name='foo', project='bar') + self.assertEqual(service.options['auto_start'], True) + def test_project_validation(self): self.assertRaises(ConfigError, lambda: Service(name='foo', project='_')) Service(name='foo', project='bar') From dfc74e2a774f2ed09bc277614da8a92ed8ae452a Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sat, 7 Jun 2014 13:19:28 +0000 Subject: [PATCH 0460/2154] Write integration test for 'auto_start' behaviour Signed-off-by: Chris Corbyn --- tests/integration/project_test.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index fa7e3858..28dab507 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -59,6 +59,21 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() + def test_project_up_without_auto_start(self): + console = self.create_service('console', auto_start=False) + 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()), 1) + self.assertEqual(len(db.containers()), 1) + 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) From 22c531dea7cf0e9e7de686907033bd1d07c3caef Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sat, 7 Jun 2014 13:29:28 +0000 Subject: [PATCH 0461/2154] Add unit tests for Project.get_services() Signed-off-by: Chris Corbyn --- tests/unit/project_test.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index f5bacc65..7c0ee4cd 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -67,3 +67,29 @@ class ProjectTest(unittest.TestCase): ) project = Project('test', [web], None) self.assertEqual(project.get_service('web'), web) + + def test_get_services_returns_all_auto_started_without_args(self): + web = Service( + project='figtest', + name='web', + ) + console = Service( + project='figtest', + name='console', + auto_start=False + ) + project = Project('test', [web, console], None) + self.assertEqual(project.get_services(), [web]) + + def test_get_services_returns_listed_services_with_args(self): + web = Service( + project='figtest', + name='web', + ) + console = Service( + project='figtest', + name='console', + auto_start=False + ) + project = Project('test', [web, console], None) + self.assertEqual(project.get_services(['console']), [console]) From 13a296049b1be6950a3b0c58581c39a037981b31 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sat, 7 Jun 2014 13:44:07 +0000 Subject: [PATCH 0462/2154] Update cli integration test for 'auto_start' behaviour Signed-off-by: Chris Corbyn --- tests/fixtures/simple-figfile/fig.yml | 1 + tests/integration/cli_test.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/tests/fixtures/simple-figfile/fig.yml b/tests/fixtures/simple-figfile/fig.yml index 22532375..26b5f439 100644 --- a/tests/fixtures/simple-figfile/fig.yml +++ b/tests/fixtures/simple-figfile/fig.yml @@ -2,5 +2,6 @@ simple: image: ubuntu command: /bin/sleep 300 another: + auto_start: false image: ubuntu command: /bin/sleep 300 diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 125b018e..c94ac20c 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -43,6 +43,13 @@ 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()), 0) + def test_rm(self): service = self.command.project.get_service('simple') service.create_container() From b081077f2be2d4f2c81e1d513a25d46f5b9892e7 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 05:51:40 +0000 Subject: [PATCH 0463/2154] Remove FIXME, as there's nothing to fix :) Signed-off-by: Chris Corbyn --- fig/cli/main.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 1e2ea90f..aa3b1428 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -219,7 +219,6 @@ class TopLevelCommand(Command): """ if options['--up']: - # FIXME: I'm not sure if this is good python form self.up({'-d': True, 'SERVICE': None}) service = self.project.get_service(options['SERVICE']) From b672861ffdf4afeaad409f53f7db4256b4c03632 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 06:32:42 +0000 Subject: [PATCH 0464/2154] Spike: Start linked containers on `fig run` by default Signed-off-by: Chris Corbyn --- fig/cli/main.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index aa3b1428..12776005 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -202,27 +202,29 @@ class TopLevelCommand(Command): $ fig run web python manage.py shell - Note that by default 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 either run `fig up -d db` - first, or use `fig run --up SERVICE COMMAND [ARGS...]`. + 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-links 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. - --up Also start services that the command's service links to + -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-links Don't start linked services. """ - if options['--up']: - self.up({'-d': True, 'SERVICE': None}) - service = self.project.get_service(options['SERVICE']) + if not options['--no-links']: + self.up({ + '-d': True, + 'SERVICE': self._get_linked_service_names(service) + }) + tty = True if options['-d'] or options['-T'] or not sys.stdin.isatty(): tty = False @@ -338,5 +340,8 @@ class TopLevelCommand(Command): raw=raw, ) + def _get_linked_service_names(self, service): + return [s.name for (s, _) in service.links] + def list_containers(containers): return ", ".join(c.name for c in containers) From 6bfe5e049d4440054b460b92d254b5504b4890aa Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 07:03:18 +0000 Subject: [PATCH 0465/2154] Spike: Implement --no-links for `fig up` Signed-off-by: Chris Corbyn --- fig/cli/main.py | 22 +++++++++++++--------- fig/project.py | 10 +++++++++- fig/service.py | 3 +++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 12776005..594a20ae 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -220,10 +220,10 @@ class TopLevelCommand(Command): service = self.project.get_service(options['SERVICE']) if not options['--no-links']: - self.up({ - '-d': True, - 'SERVICE': self._get_linked_service_names(service) - }) + self.project.up( + service_names=service.get_linked_names(), + start_links=True + ) tty = True if options['-d'] or options['-T'] or not sys.stdin.isatty(): @@ -306,12 +306,16 @@ class TopLevelCommand(Command): 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-links Don't start linked services. """ detached = options['-d'] - to_attach = self.project.up(service_names=options['SERVICE']) + start_links = not options['--no-links'] + service_names = options['SERVICE'] + + to_attach = self.project.up(service_names=service_names, start_links=start_links) if not detached: print("Attaching to", list_containers(to_attach)) @@ -321,12 +325,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}) diff --git a/fig/project.py b/fig/project.py index 605e4c07..5152c5d7 100644 --- a/fig/project.py +++ b/fig/project.py @@ -125,10 +125,18 @@ class Project(object): else: log.info('%s uses an image, skipping' % service.name) - def up(self, service_names=None): + def up(self, service_names=None, start_links=True): new_containers = [] for service in self.get_services(service_names): + linked_services = service.get_linked_names() + + if start_links and len(linked_services) > 0: + new_containers.extend(self.up( + service_names=linked_services, + start_links=True + )) + for (_, new) in service.recreate_containers(): new_containers.append(new) diff --git a/fig/service.py b/fig/service.py index 7df3c46b..748b13e0 100644 --- a/fig/service.py +++ b/fig/service.py @@ -242,6 +242,9 @@ class Service(object): ) return container + 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: From 9dd53ecdaa6476d77c8877bfd907d22e0e5d8da1 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 08:51:09 +0000 Subject: [PATCH 0466/2154] Fix bug with duplicate service entries in `fig up` Signed-off-by: Chris Corbyn --- fig/project.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/fig/project.py b/fig/project.py index 5152c5d7..15e25d54 100644 --- a/fig/project.py +++ b/fig/project.py @@ -89,22 +89,32 @@ 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 + by the provided list of names, or all auto_start services if service_names is None or []. + If include_links is specified, returns a list prepended with the needed + links for service_names, in order of dependency. + Preserves the original order of self.services. Raises NoSuchService if any of the named services do not exist. """ if service_names is None or len(service_names) == 0: - return filter(lambda s: s.options['auto_start'], self.services) + return [s for s in self.services if s.options['auto_start']] 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._prepend_with_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): @@ -128,15 +138,7 @@ class Project(object): def up(self, service_names=None, start_links=True): new_containers = [] - for service in self.get_services(service_names): - linked_services = service.get_linked_names() - - if start_links and len(linked_services) > 0: - new_containers.extend(self.up( - service_names=linked_services, - start_links=True - )) - + for service in self.get_services(service_names, include_links=start_links): for (_, new) in service.recreate_containers(): new_containers.append(new) @@ -153,6 +155,14 @@ class Project(object): l.append(container) return l + def _prepend_with_links(self, acc, service): + linked_services = self.get_services( + service_names=service.get_linked_names(), + include_links=True + ) + linked_services.append(service) + return acc + linked_services + class NoSuchService(Exception): def __init__(self, name): From 14cbe40543cdc7526b2e5c8958d6a032cf2f2f0f Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 08:55:14 +0000 Subject: [PATCH 0467/2154] Update doc string to reflect new behaviour. Signed-off-by: Chris Corbyn --- fig/project.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fig/project.py b/fig/project.py index 15e25d54..1703be68 100644 --- a/fig/project.py +++ b/fig/project.py @@ -95,13 +95,13 @@ class Project(object): by the provided list of names, or all auto_start services if service_names is None or []. - If include_links is specified, returns a list prepended with the needed - links for service_names, in order of dependency. + 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 [s for s in self.services if s.options['auto_start']] From 949df9772613f4f8ea7a57bf0725c02d9833388c Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 09:09:57 +0000 Subject: [PATCH 0468/2154] Fix issue with infinite recursion when service_names is empty Signed-off-by: Chris Corbyn --- fig/project.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/fig/project.py b/fig/project.py index 1703be68..ac1477ed 100644 --- a/fig/project.py +++ b/fig/project.py @@ -104,7 +104,10 @@ class Project(object): Raises NoSuchService if any of the named services do not exist. """ if service_names is None or len(service_names) == 0: - return [s for s in self.services if s.options['auto_start']] + return self.get_services( + service_names=[s.name for s in self.services if s.options['auto_start']], + include_links=include_links + ) else: unsorted = [self.get_service(name) for name in service_names] services = [s for s in self.services if s in unsorted] @@ -156,10 +159,19 @@ class Project(object): return l def _prepend_with_links(self, acc, service): - linked_services = self.get_services( - service_names=service.get_linked_names(), - include_links=True - ) + if service in acc: + return acc + + 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 From 3d8ce448b861bdb67dc3379c236ca659b835c29e Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 09:48:59 +0000 Subject: [PATCH 0469/2154] Spike: Re-use existing containers for `fig run` Signed-off-by: Chris Corbyn --- fig/cli/main.py | 15 ++++++++++++--- fig/project.py | 7 ++----- fig/service.py | 15 +++++++++++---- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 594a20ae..a107a3fb 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -222,7 +222,8 @@ class TopLevelCommand(Command): if not options['--no-links']: self.project.up( service_names=service.get_linked_names(), - start_links=True + start_links=True, + keep_old=True ) tty = True @@ -301,7 +302,9 @@ 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 --keep-old` will re-use existing + containers. Usage: up [options] [SERVICE...] @@ -309,13 +312,19 @@ class TopLevelCommand(Command): -d Detached mode: Run containers in the background, print new container names. --no-links Don't start linked services. + --keep-old If containers already exist, don't recreate them. """ detached = options['-d'] start_links = not options['--no-links'] + keep_old = options['--keep-old'] service_names = options['SERVICE'] - to_attach = self.project.up(service_names=service_names, start_links=start_links) + to_attach = self.project.up( + service_names=service_names, + start_links=start_links, + keep_old=keep_old + ) if not detached: print("Attaching to", list_containers(to_attach)) diff --git a/fig/project.py b/fig/project.py index ac1477ed..51461cd3 100644 --- a/fig/project.py +++ b/fig/project.py @@ -138,11 +138,11 @@ class Project(object): else: log.info('%s uses an image, skipping' % service.name) - def up(self, service_names=None, start_links=True): + def up(self, service_names=None, start_links=True, keep_old=False): new_containers = [] for service in self.get_services(service_names, include_links=start_links): - for (_, new) in service.recreate_containers(): + for (_, new) in service.recreate_containers(keep_old): new_containers.append(new) return new_containers @@ -159,9 +159,6 @@ class Project(object): return l def _prepend_with_links(self, acc, service): - if service in acc: - return acc - linked_names = service.get_linked_names() if len(linked_names) > 0: diff --git a/fig/service.py b/fig/service.py index 748b13e0..cd9cbf83 100644 --- a/fig/service.py +++ b/fig/service.py @@ -76,9 +76,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(): @@ -156,7 +154,7 @@ class Service(object): return Container.create(self.client, **container_options) raise - def recreate_containers(self, **override_options): + def recreate_containers(self, keep_old=False, **override_options): """ If a container for this service doesn't exist, create and start one. If there are any, stop them, create+start new ones, and remove the old containers. @@ -168,6 +166,8 @@ class Service(object): container = self.create_container(**override_options) self.start_container(container) return [(None, container)] + elif keep_old: + return [(None, self.start_container_if_stopped(c)) for c in containers] else: tuples = [] @@ -201,6 +201,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) From ac541e208f1a03e733295a5634fb258b44e94510 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 10:04:39 +0000 Subject: [PATCH 0470/2154] Remove obsolete method _get_linked_service_names() Signed-off-by: Chris Corbyn --- fig/cli/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index a107a3fb..6c00cb49 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -353,8 +353,5 @@ class TopLevelCommand(Command): raw=raw, ) - def _get_linked_service_names(self, service): - return [s.name for (s, _) in service.links] - def list_containers(containers): return ", ".join(c.name for c in containers) From c0231bdb700e6e3823b9fcd54d49f0d149c97c7e Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 10:08:40 +0000 Subject: [PATCH 0471/2154] Rename _prepend_with_links() -> _inject_links() Signed-off-by: Chris Corbyn --- fig/project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fig/project.py b/fig/project.py index 51461cd3..4e74c3af 100644 --- a/fig/project.py +++ b/fig/project.py @@ -113,7 +113,7 @@ class Project(object): services = [s for s in self.services if s in unsorted] if include_links: - services = reduce(self._prepend_with_links, services, []) + services = reduce(self._inject_links, services, []) uniques = [] [uniques.append(s) for s in services if s not in uniques] @@ -158,7 +158,7 @@ class Project(object): l.append(container) return l - def _prepend_with_links(self, acc, service): + def _inject_links(self, acc, service): linked_names = service.get_linked_names() if len(linked_names) > 0: From 5d92f12f8e33506c986efdf366371a372f3594bb Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 10:58:13 +0000 Subject: [PATCH 0472/2154] Add unit tests for include_links in get_services() Signed-off-by: Chris Corbyn --- tests/unit/project_test.py | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 7c0ee4cd..0703ae78 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -93,3 +93,44 @@ class ProjectTest(unittest.TestCase): ) 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] + ) From a6c8319b5df7c353de12c87e0896087749159240 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 11:11:52 +0000 Subject: [PATCH 0473/2154] Add integration tests for Service.recreate_containers() with keep_old Signed-off-by: Chris Corbyn --- tests/integration/service_test.py | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 78ddbd85..8f4d3f79 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -132,6 +132,52 @@ class ServiceTest(DockerClientTestCase): self.assertNotEqual(old_container.id, new_container.id) self.assertRaises(APIError, lambda: self.client.inspect_container(intermediate_container.id)) + def test_recreate_containers_with_keep_old_running(self): + service = self.create_service( + 'db', + environment={'FOO': '1'}, + volumes=['/var/db'], + entrypoint=['ps'], + command=['ax'] + ) + old_container = service.create_container() + service.start_container(old_container) + + num_containers_before = len(self.client.containers(all=True)) + + tuples = service.recreate_containers(keep_old=True) + self.assertEqual(len(tuples), 1) + + intermediate_container = tuples[0][0] + new_container = tuples[0][1] + + self.assertIsNone(intermediate_container) + self.assertEqual(len(self.client.containers(all=True)), num_containers_before) + self.assertEqual(old_container.id, new_container.id) + + def test_recreate_containers_with_keep_old_stopped(self): + service = self.create_service( + 'db', + environment={'FOO': '1'}, + volumes=['/var/db'], + entrypoint=['ps'], + command=['ax'] + ) + old_container = service.create_container() + old_container.stop() + + num_containers_before = len(self.client.containers(all=True)) + + tuples = service.recreate_containers(keep_old=True) + self.assertEqual(len(tuples), 1) + + intermediate_container = tuples[0][0] + new_container = tuples[0][1] + + self.assertIsNone(intermediate_container) + self.assertEqual(len(self.client.containers(all=True)), num_containers_before) + self.assertEqual(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'}) From d8b0fa294ecc975093e8d8539ee52151a2136e76 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 11:32:16 +0000 Subject: [PATCH 0474/2154] Add integration tests for Project.up() w/ start_links and keep_old Signed-off-by: Chris Corbyn --- tests/integration/project_test.py | 73 +++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 28dab507..f2b9075e 100644 --- a/tests/integration/project_test.py +++ b/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,28 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() + def test_project_up_with_keep_old(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(keep_old=True) + 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_without_auto_start(self): console = self.create_service('console', auto_start=False) db = self.create_service('db') @@ -74,6 +111,42 @@ class ProjectTest(DockerClientTestCase): 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_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'], 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) From 18728a64b9940bb2e29d04a2fc64356be5393efb Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 12:33:08 +0000 Subject: [PATCH 0475/2154] Write tests for --no-links changes to `fig up` Signed-off-by: Chris Corbyn --- tests/integration/cli_test.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index c94ac20c..c9494fc9 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from .testcases import DockerClientTestCase -from mock import patch +#from mock import patch from fig.cli.main import TopLevelCommand from fig.packages.six import StringIO @@ -50,6 +50,26 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(service.containers()), 1) self.assertEqual(len(another.containers()), 0) + def test_up_with_links(self): + self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.dispatch([str('up'), str('-d'), str('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_links(self): + self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.dispatch([str('up'), str('-d'), str('--no-links'), str('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_rm(self): service = self.command.project.get_service('simple') service.create_container() From 94a31642481bf5f6d21e73c783561c05318e619e Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 12:37:42 +0000 Subject: [PATCH 0476/2154] Re-add missing import for patch Signed-off-by: Chris Corbyn --- tests/integration/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index c9494fc9..0380b2b8 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from .testcases import DockerClientTestCase -#from mock import patch +from mock import patch from fig.cli.main import TopLevelCommand from fig.packages.six import StringIO From 655d347ea2b298fd9928718d9c0dfc66fe189205 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 13:03:58 +0000 Subject: [PATCH 0477/2154] Write integration tests on new `fig run` linking behaviour Signed-off-by: Chris Corbyn --- tests/integration/cli_test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 0380b2b8..dbdd2a8a 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -4,14 +4,18 @@ 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() @@ -70,6 +74,26 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(db.containers()), 0) self.assertEqual(len(console.containers()), 0) + @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([str('run'), str('web'), str('/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_links(self, mock_stdout): + mock_stdout.fileno = lambda: 1 + + self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.dispatch([str('run'), str('--no-links'), str('web'), str('/bin/true')], None) + db = self.command.project.get_service('db') + self.assertEqual(len(db.containers()), 0) + def test_rm(self): service = self.command.project.get_service('simple') service.create_container() From 6c4299039a1e6c13dea0b9c3eaa08a1bab345ceb Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 13:16:14 +0000 Subject: [PATCH 0478/2154] Write integration tests for `--keep-old` in the CLI Signed-off-by: Chris Corbyn --- tests/integration/cli_test.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index dbdd2a8a..c615b167 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -74,6 +74,35 @@ class CLITestCase(DockerClientTestCase): 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([str('up'), str('-d'), str('--keep-old')], 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 @@ -94,6 +123,24 @@ class CLITestCase(DockerClientTestCase): 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([str('up'), str('-d'), str('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([str('run'), str('web'), str('/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() From a04143e2a7aec2e61fe9e89ef2938c04ff018bbb Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 13:16:59 +0000 Subject: [PATCH 0479/2154] Remove unused: from __future__ import unicode_literals. This is not being used and it confuses the Command class. Rather than try to fix the Command class, I've taken the pragmatic approach and removed the trigger that confuses it. Signed-off-by: Chris Corbyn --- tests/integration/cli_test.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index c615b167..1000e8da 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from __future__ import absolute_import from .testcases import DockerClientTestCase from mock import patch @@ -56,7 +55,7 @@ class CLITestCase(DockerClientTestCase): def test_up_with_links(self): self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch([str('up'), str('-d'), str('web')], None) + 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') @@ -66,7 +65,7 @@ class CLITestCase(DockerClientTestCase): def test_up_with_no_links(self): self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch([str('up'), str('-d'), str('--no-links'), str('web')], None) + self.command.dispatch(['up', '-d', '--no-links', 'web'], None) web = self.command.project.get_service('web') db = self.command.project.get_service('db') console = self.command.project.get_service('console') @@ -95,7 +94,7 @@ class CLITestCase(DockerClientTestCase): old_ids = [c.id for c in service.containers()] - self.command.dispatch([str('up'), str('-d'), str('--keep-old')], None) + self.command.dispatch(['up', '-d', '--keep-old'], None) self.assertEqual(len(service.containers()), 1) new_ids = [c.id for c in service.containers()] @@ -108,7 +107,7 @@ class CLITestCase(DockerClientTestCase): mock_stdout.fileno = lambda: 1 self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch([str('run'), str('web'), str('/bin/true')], None) + 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) @@ -119,7 +118,7 @@ class CLITestCase(DockerClientTestCase): mock_stdout.fileno = lambda: 1 self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch([str('run'), str('--no-links'), str('web'), str('/bin/true')], None) + self.command.dispatch(['run', '--no-links', 'web', '/bin/true'], None) db = self.command.project.get_service('db') self.assertEqual(len(db.containers()), 0) @@ -128,13 +127,13 @@ class CLITestCase(DockerClientTestCase): mock_stdout.fileno = lambda: 1 self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch([str('up'), str('-d'), str('db')], None) + 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([str('run'), str('web'), str('/bin/true')], None) + self.command.dispatch(['run', 'web', '/bin/true'], None) self.assertEqual(len(db.containers()), 1) new_ids = [c.id for c in db.containers()] From ab1fbc96c3cb1f4add960d55b5d3d7fcfa88a436 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 22:47:09 +0000 Subject: [PATCH 0480/2154] Move keep_old check up into Project Signed-off-by: Chris Corbyn --- fig/project.py | 12 +++++--- fig/service.py | 14 ++++++++-- tests/integration/project_test.py | 30 +++++++++++++++++++- tests/integration/service_test.py | 46 ------------------------------- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/fig/project.py b/fig/project.py index 4e74c3af..5c798d38 100644 --- a/fig/project.py +++ b/fig/project.py @@ -139,13 +139,17 @@ class Project(object): log.info('%s uses an image, skipping' % service.name) def up(self, service_names=None, start_links=True, keep_old=False): - new_containers = [] + running_containers = [] for service in self.get_services(service_names, include_links=start_links): - for (_, new) in service.recreate_containers(keep_old): - new_containers.append(new) + if keep_old: + for container in service.start_or_create_containers(): + running_containers.append(container) + else: + for (_, container) in service.recreate_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): diff --git a/fig/service.py b/fig/service.py index cd9cbf83..7de0ae7d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -154,7 +154,7 @@ class Service(object): return Container.create(self.client, **container_options) raise - def recreate_containers(self, keep_old=False, **override_options): + def recreate_containers(self, **override_options): """ If a container for this service doesn't exist, create and start one. If there are any, stop them, create+start new ones, and remove the old containers. @@ -166,8 +166,6 @@ class Service(object): container = self.create_container(**override_options) self.start_container(container) return [(None, container)] - elif keep_old: - return [(None, self.start_container_if_stopped(c)) for c in containers] else: tuples = [] @@ -249,6 +247,16 @@ 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] diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index f2b9075e..0c5c1aa7 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -74,7 +74,7 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() - def test_project_up_with_keep_old(self): + def test_project_up_with_keep_old_running(self): web = self.create_service('web') db = self.create_service('db', volumes=['/var/db']) project = Project('figtest', [web, db], self.client) @@ -96,6 +96,34 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() + def test_project_up_with_keep_old_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(keep_old=True) + + 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_auto_start(self): console = self.create_service('console', auto_start=False) db = self.create_service('db') diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 8f4d3f79..78ddbd85 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -132,52 +132,6 @@ class ServiceTest(DockerClientTestCase): self.assertNotEqual(old_container.id, new_container.id) self.assertRaises(APIError, lambda: self.client.inspect_container(intermediate_container.id)) - def test_recreate_containers_with_keep_old_running(self): - service = self.create_service( - 'db', - environment={'FOO': '1'}, - volumes=['/var/db'], - entrypoint=['ps'], - command=['ax'] - ) - old_container = service.create_container() - service.start_container(old_container) - - num_containers_before = len(self.client.containers(all=True)) - - tuples = service.recreate_containers(keep_old=True) - self.assertEqual(len(tuples), 1) - - intermediate_container = tuples[0][0] - new_container = tuples[0][1] - - self.assertIsNone(intermediate_container) - self.assertEqual(len(self.client.containers(all=True)), num_containers_before) - self.assertEqual(old_container.id, new_container.id) - - def test_recreate_containers_with_keep_old_stopped(self): - service = self.create_service( - 'db', - environment={'FOO': '1'}, - volumes=['/var/db'], - entrypoint=['ps'], - command=['ax'] - ) - old_container = service.create_container() - old_container.stop() - - num_containers_before = len(self.client.containers(all=True)) - - tuples = service.recreate_containers(keep_old=True) - self.assertEqual(len(tuples), 1) - - intermediate_container = tuples[0][0] - new_container = tuples[0][1] - - self.assertIsNone(intermediate_container) - self.assertEqual(len(self.client.containers(all=True)), num_containers_before) - self.assertEqual(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'}) From 85b96197999d26da42d74a3768ec02833a7e2946 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 23:00:15 +0000 Subject: [PATCH 0481/2154] Document 'auto_start' in fig.yml Signed-off-by: Chris Corbyn --- docs/yml.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/yml.md b/docs/yml.md index 24484d4f..8a69dffc 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -54,6 +54,9 @@ expose: volumes: - cache/:/tmp/cache +-- Do not automatically run this service on `fig up` (default: true) +auto_start: false + -- Add environment variables. environment: RACK_ENV: development From 74e067c6e62818438fee0c88fcbce65db5929d1d Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 23:05:54 +0000 Subject: [PATCH 0482/2154] Document --keep-old flag in CLI reference Signed-off-by: Chris Corbyn --- docs/cli.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 9eb87f1f..3d5871ad 100644 --- a/docs/cli.md +++ b/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-links` flag: + + $ fig run --no-links 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 --keep-old`. This will still start any stopped containers, if needed. [volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/ From 1d1e23611b4e8caf87d6d2601bc8ee1e81d7e5db Mon Sep 17 00:00:00 2001 From: d11wtq Date: Sun, 8 Jun 2014 23:20:51 +0000 Subject: [PATCH 0483/2154] Rename --keep-old to --no-recreate Signed-off-by: Chris Corbyn --- docs/cli.md | 2 +- fig/cli/main.py | 16 ++++++++-------- fig/project.py | 8 ++++---- tests/integration/cli_test.py | 2 +- tests/integration/project_test.py | 8 ++++---- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 3d5871ad..5e99e834 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -82,6 +82,6 @@ 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. -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 --keep-old`. This will still start any stopped containers, if needed. +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/ diff --git a/fig/cli/main.py b/fig/cli/main.py index 6c00cb49..cfb6a778 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -223,7 +223,7 @@ class TopLevelCommand(Command): self.project.up( service_names=service.get_linked_names(), start_links=True, - keep_old=True + recreate=False ) tty = True @@ -303,27 +303,27 @@ 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. If you do not want existing - containers to be recreated, `fig up --keep-old` will re-use 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. - --no-links Don't start linked services. - --keep-old If containers already exist, don't recreate them. + -d Detached mode: Run containers in the background, + print new container names. + --no-links Don't start linked services. + --no-recreate If containers already exist, don't recreate them. """ detached = options['-d'] start_links = not options['--no-links'] - keep_old = options['--keep-old'] + recreate = not options['--no-recreate'] service_names = options['SERVICE'] to_attach = self.project.up( service_names=service_names, start_links=start_links, - keep_old=keep_old + recreate=recreate ) if not detached: diff --git a/fig/project.py b/fig/project.py index 5c798d38..5e2b1fcc 100644 --- a/fig/project.py +++ b/fig/project.py @@ -138,15 +138,15 @@ class Project(object): else: log.info('%s uses an image, skipping' % service.name) - def up(self, service_names=None, start_links=True, keep_old=False): + def up(self, service_names=None, start_links=True, recreate=True): running_containers = [] for service in self.get_services(service_names, include_links=start_links): - if keep_old: - for container in service.start_or_create_containers(): + if recreate: + for (_, container) in service.recreate_containers(): running_containers.append(container) else: - for (_, container) in service.recreate_containers(): + for container in service.start_or_create_containers(): running_containers.append(container) return running_containers diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 1000e8da..ba309ef1 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -94,7 +94,7 @@ class CLITestCase(DockerClientTestCase): old_ids = [c.id for c in service.containers()] - self.command.dispatch(['up', '-d', '--keep-old'], None) + self.command.dispatch(['up', '-d', '--no-recreate'], None) self.assertEqual(len(service.containers()), 1) new_ids = [c.id for c in service.containers()] diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 0c5c1aa7..dcca570b 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -74,7 +74,7 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() - def test_project_up_with_keep_old_running(self): + 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) @@ -86,7 +86,7 @@ class ProjectTest(DockerClientTestCase): old_db_id = project.containers()[0].id db_volume_path = project.containers()[0].inspect()['Volumes']['/var/db'] - project.up(keep_old=True) + project.up(recreate=False) self.assertEqual(len(project.containers()), 2) db_container = [c for c in project.containers() if 'db' in c.name][0] @@ -96,7 +96,7 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() - def test_project_up_with_keep_old_stopped(self): + 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) @@ -112,7 +112,7 @@ class ProjectTest(DockerClientTestCase): old_db_id = old_containers[0].id db_volume_path = old_containers[0].inspect()['Volumes']['/var/db'] - project.up(keep_old=True) + project.up(recreate=False) new_containers = project.containers(stopped=True) self.assertEqual(len(new_containers), 2) From da80eca28c6c3cac4b66711e35986dd34c307419 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 10 Jun 2014 14:55:43 -0700 Subject: [PATCH 0484/2154] Update to docs to Docker 1.0.0 Signed-off-by: Ben Firshman --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index b8d79b9b..b7068710 100644 --- a/docs/install.md +++ b/docs/install.md @@ -6,9 +6,9 @@ title: Installing Fig Installing Fig ============== -First, install Docker version 0.11.1. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): +First, install Docker version 1.0.0. If you're on OS X, you can use [docker-osx](https://github.com/noplay/docker-osx): - $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.1/docker-osx > /usr/local/bin/docker-osx + $ curl https://raw.githubusercontent.com/noplay/docker-osx/1.0.0/docker-osx > /usr/local/bin/docker-osx $ chmod +x /usr/local/bin/docker-osx $ docker-osx shell From e71e82f8ac167ae840af1395bb474239c5d431a8 Mon Sep 17 00:00:00 2001 From: Chris Corbyn Date: Wed, 11 Jun 2014 10:09:54 +0000 Subject: [PATCH 0485/2154] Add missing fixture file Signed-off-by: Chris Corbyn --- tests/fixtures/links-figfile/fig.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/fixtures/links-figfile/fig.yml diff --git a/tests/fixtures/links-figfile/fig.yml b/tests/fixtures/links-figfile/fig.yml new file mode 100644 index 00000000..95543633 --- /dev/null +++ b/tests/fixtures/links-figfile/fig.yml @@ -0,0 +1,11 @@ +db: + image: ubuntu + command: /bin/sleep 300 +web: + image: ubuntu + command: /bin/sleep 300 + links: + - db:db +console: + image: ubuntu + command: /bin/sleep 300 From 3a342fb25d3bc82a56ad66c2f4f8f58529a9394c Mon Sep 17 00:00:00 2001 From: d11wtq Date: Wed, 11 Jun 2014 10:25:50 +0000 Subject: [PATCH 0486/2154] Use busybox in fixtures, instead of ubuntu Signed-off-by: d11wtq --- .../fixtures/longer-filename-figfile/fig.yaml | 2 +- tests/fixtures/multiple-figfiles/fig.yml | 4 +-- tests/fixtures/multiple-figfiles/fig2.yml | 2 +- tests/fixtures/simple-dockerfile/Dockerfile | 2 +- tests/fixtures/simple-figfile/fig.yml | 4 +-- tests/integration/testcases.py | 4 +-- tests/unit/container_test.py | 10 +++---- tests/unit/project_test.py | 26 +++++++++---------- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/fixtures/longer-filename-figfile/fig.yaml b/tests/fixtures/longer-filename-figfile/fig.yaml index 81efaa2a..31528940 100644 --- a/tests/fixtures/longer-filename-figfile/fig.yaml +++ b/tests/fixtures/longer-filename-figfile/fig.yaml @@ -1,3 +1,3 @@ definedinyamlnotyml: - image: ubuntu + image: busybox:latest command: /bin/sleep 300 \ No newline at end of file diff --git a/tests/fixtures/multiple-figfiles/fig.yml b/tests/fixtures/multiple-figfiles/fig.yml index 22532375..3538ab09 100644 --- a/tests/fixtures/multiple-figfiles/fig.yml +++ b/tests/fixtures/multiple-figfiles/fig.yml @@ -1,6 +1,6 @@ simple: - image: ubuntu + image: busybox:latest command: /bin/sleep 300 another: - image: ubuntu + image: busybox:latest command: /bin/sleep 300 diff --git a/tests/fixtures/multiple-figfiles/fig2.yml b/tests/fixtures/multiple-figfiles/fig2.yml index 330ed771..523becac 100644 --- a/tests/fixtures/multiple-figfiles/fig2.yml +++ b/tests/fixtures/multiple-figfiles/fig2.yml @@ -1,3 +1,3 @@ yetanother: - image: ubuntu + image: busybox:latest command: /bin/sleep 300 diff --git a/tests/fixtures/simple-dockerfile/Dockerfile b/tests/fixtures/simple-dockerfile/Dockerfile index b7fd870f..d1ceac6b 100644 --- a/tests/fixtures/simple-dockerfile/Dockerfile +++ b/tests/fixtures/simple-dockerfile/Dockerfile @@ -1,2 +1,2 @@ -FROM ubuntu +FROM busybox:latest CMD echo "success" diff --git a/tests/fixtures/simple-figfile/fig.yml b/tests/fixtures/simple-figfile/fig.yml index 22532375..3538ab09 100644 --- a/tests/fixtures/simple-figfile/fig.yml +++ b/tests/fixtures/simple-figfile/fig.yml @@ -1,6 +1,6 @@ simple: - image: ubuntu + image: busybox:latest command: /bin/sleep 300 another: - image: ubuntu + image: busybox:latest command: /bin/sleep 300 diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index f913b7c3..ba6bb300 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -10,7 +10,7 @@ class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls.client = Client(docker_url()) - cls.client.pull('ubuntu', tag='latest') + cls.client.pull('busybox', tag='latest') def setUp(self): for c in self.client.containers(all=True): @@ -28,7 +28,7 @@ class DockerClientTestCase(unittest.TestCase): project='figtest', name=name, client=self.client, - image="ubuntu", + image="busybox:latest", **kwargs ) diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 44589241..cb6053e4 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -6,7 +6,7 @@ class ContainerTest(unittest.TestCase): def test_from_ps(self): container = Container.from_ps(None, { "Id":"abc", - "Image":"ubuntu:12.04", + "Image":"busybox:latest", "Command":"sleep 300", "Created":1387384730, "Status":"Up 8 seconds", @@ -17,7 +17,7 @@ class ContainerTest(unittest.TestCase): }, has_been_inspected=True) self.assertEqual(container.dictionary, { "ID": "abc", - "Image":"ubuntu:12.04", + "Image":"busybox:latest", "Name": "/figtest_db_1", }) @@ -39,7 +39,7 @@ class ContainerTest(unittest.TestCase): def test_number(self): container = Container.from_ps(None, { "Id":"abc", - "Image":"ubuntu:12.04", + "Image":"busybox:latest", "Command":"sleep 300", "Created":1387384730, "Status":"Up 8 seconds", @@ -53,7 +53,7 @@ class ContainerTest(unittest.TestCase): def test_name(self): container = Container.from_ps(None, { "Id":"abc", - "Image":"ubuntu:12.04", + "Image":"busybox:latest", "Command":"sleep 300", "Names":["/figtest_db_1"] }, has_been_inspected=True) @@ -62,7 +62,7 @@ class ContainerTest(unittest.TestCase): def test_name_without_project(self): container = Container.from_ps(None, { "Id":"abc", - "Image":"ubuntu:12.04", + "Image":"busybox:latest", "Command":"sleep 300", "Names":["/figtest_db_1"] }, has_been_inspected=True) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 4a2ad142..bba2e1f5 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -8,29 +8,29 @@ class ProjectTest(unittest.TestCase): project = Project.from_dicts('figtest', [ { 'name': 'web', - 'image': 'ubuntu' + 'image': 'busybox:latest' }, { 'name': 'db', - 'image': 'ubuntu' - } + 'image': 'busybox:latest' + }, ], None) self.assertEqual(len(project.services), 2) self.assertEqual(project.get_service('web').name, 'web') - self.assertEqual(project.get_service('web').options['image'], 'ubuntu') + self.assertEqual(project.get_service('web').options['image'], 'busybox:latest') self.assertEqual(project.get_service('db').name, 'db') - self.assertEqual(project.get_service('db').options['image'], 'ubuntu') + self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') def test_from_dict_sorts_in_dependency_order(self): project = Project.from_dicts('figtest', [ { 'name': 'web', - 'image': 'ubuntu', + 'image': 'busybox:latest', 'links': ['db'], }, { 'name': 'db', - 'image': 'ubuntu' + 'image': 'busybox:latest' } ], None) @@ -40,22 +40,22 @@ class ProjectTest(unittest.TestCase): def test_from_config(self): project = Project.from_config('figtest', { 'web': { - 'image': 'ubuntu', + 'image': 'busybox:latest', }, 'db': { - 'image': 'ubuntu', + 'image': 'busybox:latest', }, }, None) self.assertEqual(len(project.services), 2) self.assertEqual(project.get_service('web').name, 'web') - self.assertEqual(project.get_service('web').options['image'], 'ubuntu') + self.assertEqual(project.get_service('web').options['image'], 'busybox:latest') self.assertEqual(project.get_service('db').name, 'db') - self.assertEqual(project.get_service('db').options['image'], 'ubuntu') + self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') def test_from_config_throws_error_when_not_dict(self): with self.assertRaises(ConfigurationError): project = Project.from_config('figtest', { - 'web': 'ubuntu', + 'web': 'busybox:latest', }, None) def test_get_service(self): @@ -63,7 +63,7 @@ class ProjectTest(unittest.TestCase): project='figtest', name='web', client=None, - image="ubuntu", + image="busybox:latest", ) project = Project('test', [web], None) self.assertEqual(project.get_service('web'), web) From 6e485df0845618af78ee19869c5df924e1a3ce93 Mon Sep 17 00:00:00 2001 From: d11wtq Date: Wed, 11 Jun 2014 10:41:24 +0000 Subject: [PATCH 0487/2154] Rename --no-links to --only Signed-off-by: d11wtq --- docs/cli.md | 4 ++-- fig/cli/main.py | 24 ++++++++++++------------ tests/integration/cli_test.py | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 5e99e834..ac22feee 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -53,9 +53,9 @@ 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-links` flag: +If you do not want linked containers to be started when running the one-off command, specify the `--only` flag: - $ fig run --no-links web python manage.py shell + $ fig run --only web python manage.py shell ## scale diff --git a/fig/cli/main.py b/fig/cli/main.py index cfb6a778..3847e982 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -204,22 +204,22 @@ class TopLevelCommand(Command): 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-links SERVICE COMMAND [ARGS...]`. + `fig run --only 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. - --no-links Don't start linked services. + -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. + --only Don't start linked services. """ service = self.project.get_service(options['SERVICE']) - if not options['--no-links']: + if not options['--only']: self.project.up( service_names=service.get_linked_names(), start_links=True, @@ -309,14 +309,14 @@ class TopLevelCommand(Command): Usage: up [options] [SERVICE...] Options: - -d Detached mode: Run containers in the background, - print new container names. - --no-links Don't start linked services. + -d Detached mode: Run containers in the background, + print new container names. + --only Don't start linked services. --no-recreate If containers already exist, don't recreate them. """ detached = options['-d'] - start_links = not options['--no-links'] + start_links = not options['--only'] recreate = not options['--no-recreate'] service_names = options['SERVICE'] diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index ba309ef1..00e1b79c 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -65,7 +65,7 @@ class CLITestCase(DockerClientTestCase): def test_up_with_no_links(self): self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch(['up', '-d', '--no-links', 'web'], None) + self.command.dispatch(['up', '-d', '--only', 'web'], None) web = self.command.project.get_service('web') db = self.command.project.get_service('db') console = self.command.project.get_service('console') @@ -118,7 +118,7 @@ class CLITestCase(DockerClientTestCase): mock_stdout.fileno = lambda: 1 self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch(['run', '--no-links', 'web', '/bin/true'], None) + self.command.dispatch(['run', '--only', 'web', '/bin/true'], None) db = self.command.project.get_service('db') self.assertEqual(len(db.containers()), 0) From 2293a6012ca9b65cba5f8361289e1014ecaddd61 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 11 Jun 2014 12:15:07 -0700 Subject: [PATCH 0488/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cli.html b/cli.html index 09075afb..3c93ceb7 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 1ca13b9a..066ed61a 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 69c0eb75..3558f5ed 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 5673babf..a95a4ef6 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index d5aa3796..6cf241b3 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -19,8 +19,8 @@

    Installing Fig

    -

    First, install Docker version 0.11.1. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/0.11.1/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker version 1.0.0. If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/1.0.0/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index 6763e322..8f4662be 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index fb241721..4890f1f1 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index a5cdcb0f..b95afdc8 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From feb8ad7b4cbfdba86aa56742835a877ffb53f420 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 16 Jun 2014 23:32:50 +0100 Subject: [PATCH 0489/2154] Update Dockerfile reference/tutorial links --- docs/django.md | 2 +- docs/index.md | 2 +- docs/rails.md | 2 +- docs/wordpress.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/django.md b/docs/django.md index 82b557ee..a7ef33cf 100644 --- a/docs/django.md +++ b/docs/django.md @@ -19,7 +19,7 @@ Let's set up the three files that'll get us started. First, our app is going to RUN pip install -r requirements.txt ADD . /code/ -That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/). +That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). Second, we define our Python dependencies in a file called `requirements.txt`: diff --git a/docs/index.md b/docs/index.md index 46273308..aa6e1145 100644 --- a/docs/index.md +++ b/docs/index.md @@ -85,7 +85,7 @@ Next, we want to create a Docker image containing all of our app's dependencies. WORKDIR /code RUN pip install -r requirements.txt -This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/). +This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). We then define a set of services using `fig.yml`: diff --git a/docs/rails.md b/docs/rails.md index 2dbf9003..9325ad50 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -18,7 +18,7 @@ Let's set up the three files that'll get us started. First, our app is going to RUN bundle install ADD . /myapp -That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/). +That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`. diff --git a/docs/wordpress.md b/docs/wordpress.md index 5eff3487..70165925 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -17,7 +17,7 @@ FROM orchardup/php5 ADD . /code ``` -This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Dockerfile tutorial](https://www.docker.io/learn/dockerfile/) and the [Dockerfile reference](http://docs.docker.io/en/latest/reference/builder/). +This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). Next up, `fig.yml` starts our web service and a separate MySQL instance: From 3f3b2bfbc35140d1e95f298530207c798f68a805 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 16 Jun 2014 23:33:00 +0100 Subject: [PATCH 0490/2154] update --- cli.html | 2 +- django.html | 4 ++-- env.html | 2 +- index.html | 4 ++-- install.html | 2 +- rails.html | 4 ++-- wordpress.html | 4 ++-- yml.html | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli.html b/cli.html index 3c93ceb7..fa410356 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 066ed61a..06dedb95 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    @@ -31,7 +31,7 @@ ADD requirements.txt /code/ RUN pip install -r requirements.txt ADD . /code/
    -

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll install our application inside an image with Python installed alongside all of our Python dependencies. For more information on how to write Dockerfiles, see the Docker user guide and the Dockerfile reference.

    Second, we define our Python dependencies in a file called requirements.txt:

    Django
    diff --git a/env.html b/env.html
    index 3558f5ed..93b8f9ab 100644
    --- a/env.html
    +++ b/env.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/index.html b/index.html index a95a4ef6..58ab04ca 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    @@ -89,7 +89,7 @@ ADD . /code WORKDIR /code RUN pip install -r requirements.txt
    -

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the Docker user guide and the Dockerfile reference.

    We then define a set of services using fig.yml:

    web:
    diff --git a/install.html b/install.html
    index 6cf241b3..49dcdec7 100644
    --- a/install.html
    +++ b/install.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    diff --git a/rails.html b/rails.html index 8f4662be..429f9c35 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    @@ -30,7 +30,7 @@ ADD Gemfile /myapp/Gemfile RUN bundle install ADD . /myapp
    -

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    That'll put our application code inside an image with Ruby, Bundler and all our dependencies. For more information on how to write Dockerfiles, see the Docker user guide and the Dockerfile reference.

    Next, we have a bootstrap Gemfile which just loads Rails. It'll be overwritten in a moment by rails new.

    source 'https://rubygems.org'
    diff --git a/wordpress.html b/wordpress.html
    index 4890f1f1..f6d91dce 100644
    --- a/wordpress.html
    +++ b/wordpress.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    @@ -26,7 +26,7 @@
    FROM orchardup/php5
     ADD . /code
     
    -

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Dockerfile tutorial and the Dockerfile reference.

    +

    This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the Docker user guide and the Dockerfile reference.

    Next up, fig.yml starts our web service and a separate MySQL instance:

    web:
    diff --git a/yml.html b/yml.html
    index b95afdc8..f8c49027 100644
    --- a/yml.html
    +++ b/yml.html
    @@ -6,7 +6,7 @@
         
         
         
    -    
    +    
       
       
         
    From e517061010835af60c4dc29008f4b734d31cbaeb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 29 May 2014 11:18:51 +0100 Subject: [PATCH 0491/2154] Add /venv to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1623c04f..1046518b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /build /dist /docs/_site +/venv fig.spec From cfcabce593af61c328b4817ec40a7854be4148fb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 29 May 2014 11:19:30 +0100 Subject: [PATCH 0492/2154] Extract stream_output to module --- fig/progress_stream.py | 81 ++++++++++++++++++++++++++++++++++++++++++ fig/service.py | 80 +---------------------------------------- 2 files changed, 82 insertions(+), 79 deletions(-) create mode 100644 fig/progress_stream.py diff --git a/fig/progress_stream.py b/fig/progress_stream.py new file mode 100644 index 00000000..b0160f04 --- /dev/null +++ b/fig/progress_stream.py @@ -0,0 +1,81 @@ +import json +import os + + +class StreamOutputError(Exception): + pass + + +def stream_output(output, stream): + is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno()) + all_events = [] + lines = {} + diff = 0 + + for chunk in output: + event = json.loads(chunk) + all_events.append(event) + + if 'progress' in event or 'progressDetail' in event: + image_id = event['id'] + + if image_id in lines: + diff = len(lines) - lines[image_id] + else: + lines[image_id] = len(lines) + stream.write("\n") + diff = 0 + + if is_terminal: + # move cursor up `diff` rows + stream.write("%c[%dA" % (27, diff)) + + print_output_event(event, stream, is_terminal) + + if 'id' in event and is_terminal: + # move cursor back down + stream.write("%c[%dB" % (27, diff)) + + stream.flush() + + return all_events + + +def print_output_event(event, stream, is_terminal): + if 'errorDetail' in event: + raise StreamOutputError(event['errorDetail']['message']) + + terminator = '' + + if is_terminal and 'stream' not in event: + # erase current line + stream.write("%c[2K\r" % 27) + terminator = "\r" + pass + elif 'progressDetail' in event: + return + + if 'time' in event: + stream.write("[%s] " % event['time']) + + if 'id' in event: + stream.write("%s: " % event['id']) + + if 'from' in event: + stream.write("(from %s) " % event['from']) + + status = event.get('status', '') + + if 'progress' in event: + stream.write("%s %s%s" % (status, event['progress'], terminator)) + elif 'progressDetail' in event: + detail = event['progressDetail'] + if 'current' in detail: + percentage = float(detail['current']) / float(detail['total']) * 100 + stream.write('%s (%.1f%%)%s' % (status, percentage, terminator)) + else: + stream.write('%s%s' % (status, terminator)) + elif 'stream' in event: + stream.write("%s%s" % (event['stream'], terminator)) + else: + stream.write("%s%s\n" % (status, terminator)) diff --git a/fig/service.py b/fig/service.py index 21a3ea40..2fc7125d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -5,8 +5,8 @@ import logging import re import os import sys -import json from .container import Container +from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) @@ -343,84 +343,6 @@ class Service(object): return True -class StreamOutputError(Exception): - pass - - -def stream_output(output, stream): - is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno()) - all_events = [] - lines = {} - diff = 0 - - for chunk in output: - event = json.loads(chunk) - all_events.append(event) - - if 'progress' in event or 'progressDetail' in event: - image_id = event['id'] - - if image_id in lines: - diff = len(lines) - lines[image_id] - else: - lines[image_id] = len(lines) - stream.write("\n") - diff = 0 - - if is_terminal: - # move cursor up `diff` rows - stream.write("%c[%dA" % (27, diff)) - - print_output_event(event, stream, is_terminal) - - if 'id' in event and is_terminal: - # move cursor back down - stream.write("%c[%dB" % (27, diff)) - - stream.flush() - - return all_events - -def print_output_event(event, stream, is_terminal): - if 'errorDetail' in event: - raise StreamOutputError(event['errorDetail']['message']) - - terminator = '' - - if is_terminal and 'stream' not in event: - # erase current line - stream.write("%c[2K\r" % 27) - terminator = "\r" - pass - elif 'progressDetail' in event: - return - - if 'time' in event: - stream.write("[%s] " % event['time']) - - if 'id' in event: - stream.write("%s: " % event['id']) - - if 'from' in event: - stream.write("(from %s) " % event['from']) - - status = event.get('status', '') - - if 'progress' in event: - stream.write("%s %s%s" % (status, event['progress'], terminator)) - elif 'progressDetail' in event: - detail = event['progressDetail'] - if 'current' in detail: - percentage = float(detail['current']) / float(detail['total']) * 100 - stream.write('%s (%.1f%%)%s' % (status, percentage, terminator)) - else: - stream.write('%s%s' % (status, terminator)) - elif 'stream' in event: - stream.write("%s%s" % (event['stream'], terminator)) - else: - stream.write("%s%s\n" % (status, terminator)) - - NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') From c246897af130e97eaab120c467ae008e35230f76 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 29 May 2014 12:11:26 +0100 Subject: [PATCH 0493/2154] Pass script/test arguments through to nosetests --- script/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test b/script/test index 1ceefaad..5f0f21f4 100755 --- a/script/test +++ b/script/test @@ -1,2 +1,2 @@ #!/bin/sh -nosetests +nosetests $@ From 9eb3697b40c40176f2dffcffd5120765b4428684 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 29 May 2014 11:51:15 +0100 Subject: [PATCH 0494/2154] Encode all progress stream output as UTF-8 Closes #231. --- fig/progress_stream.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fig/progress_stream.py b/fig/progress_stream.py index b0160f04..2121629b 100644 --- a/fig/progress_stream.py +++ b/fig/progress_stream.py @@ -1,5 +1,6 @@ import json import os +import codecs class StreamOutputError(Exception): @@ -8,6 +9,7 @@ class StreamOutputError(Exception): def stream_output(output, stream): is_terminal = hasattr(stream, 'fileno') and os.isatty(stream.fileno()) + stream = codecs.getwriter('utf-8')(stream) all_events = [] lines = {} diff = 0 From 262248d8a6bfc78ba0a54232991868baf084a19b Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 29 May 2014 12:11:08 +0100 Subject: [PATCH 0495/2154] Firm up tests for split_buffer --- tests/unit/split_buffer_test.py | 47 ++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/tests/unit/split_buffer_test.py b/tests/unit/split_buffer_test.py index a78e99a6..41dc50e4 100644 --- a/tests/unit/split_buffer_test.py +++ b/tests/unit/split_buffer_test.py @@ -6,32 +6,47 @@ from .. import unittest class SplitBufferTest(unittest.TestCase): def test_single_line_chunks(self): def reader(): - yield "abc\n" - yield "def\n" - yield "ghi\n" + yield b'abc\n' + yield b'def\n' + yield b'ghi\n' - self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi\n"]) + self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi\n']) def test_no_end_separator(self): def reader(): - yield "abc\n" - yield "def\n" - yield "ghi" + yield b'abc\n' + yield b'def\n' + yield b'ghi' - self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi"]) + self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi']) def test_multiple_line_chunk(self): def reader(): - yield "abc\ndef\nghi" + yield b'abc\ndef\nghi' - self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "def\n", "ghi"]) + self.assert_produces(reader, [b'abc\n', b'def\n', b'ghi']) def test_chunked_line(self): def reader(): - yield "a" - yield "b" - yield "c" - yield "\n" - yield "d" + yield b'a' + yield b'b' + yield b'c' + yield b'\n' + yield b'd' - self.assertEqual(list(split_buffer(reader(), '\n')), ["abc\n", "d"]) + self.assert_produces(reader, [b'abc\n', b'd']) + + def test_preserves_unicode_sequences_within_lines(self): + string = u"a\u2022c\n".encode('utf-8') + + def reader(): + yield string + + self.assert_produces(reader, [string]) + + def assert_produces(self, reader, expectations): + split = split_buffer(reader(), b'\n') + + for (actual, expected) in zip(split, expectations): + self.assertEqual(type(actual), type(expected)) + self.assertEqual(actual, expected) From d0b5bcf26ae83be151cc5fc841a80a15cc301f50 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Jun 2014 14:50:57 +0100 Subject: [PATCH 0496/2154] Pass byte strings straight through LogPrinter --- fig/cli/log_printer.py | 9 +++--- script/test | 2 +- tests/unit/log_printer_test.py | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 tests/unit/log_printer_test.py diff --git a/fig/cli/log_printer.py b/fig/cli/log_printer.py index e302aecb..0dc419e1 100644 --- a/fig/cli/log_printer.py +++ b/fig/cli/log_printer.py @@ -10,16 +10,17 @@ from .utils import split_buffer class LogPrinter(object): - def __init__(self, containers, attach_params=None): + def __init__(self, containers, attach_params=None, output=sys.stdout): self.containers = containers self.attach_params = attach_params or {} self.prefix_width = self._calculate_prefix_width(containers) self.generators = self._make_log_generators() + self.output = output def run(self): mux = Multiplexer(self.generators) for line in mux.loop(): - sys.stdout.write(line.encode(sys.__stdout__.encoding or 'utf-8')) + self.output.write(line) def _calculate_prefix_width(self, containers): """ @@ -45,12 +46,12 @@ class LogPrinter(object): return generators def _make_log_generator(self, container, color_fn): - prefix = color_fn(self._generate_prefix(container)) + prefix = color_fn(self._generate_prefix(container)).encode('utf-8') # Attach to container before log printer starts running line_generator = split_buffer(self._attach(container), '\n') for line in line_generator: - yield prefix + line.decode('utf-8') + yield prefix + line exit_code = container.wait() yield color_fn("%s exited with code %s\n" % (container.name, exit_code)) diff --git a/script/test b/script/test index 5f0f21f4..c2cc315d 100755 --- a/script/test +++ b/script/test @@ -1,2 +1,2 @@ #!/bin/sh -nosetests $@ +PYTHONIOENCODING=ascii nosetests $@ diff --git a/tests/unit/log_printer_test.py b/tests/unit/log_printer_test.py new file mode 100644 index 00000000..da2a8327 --- /dev/null +++ b/tests/unit/log_printer_test.py @@ -0,0 +1,57 @@ +from __future__ import unicode_literals +from __future__ import absolute_import +import os + +from fig.cli.log_printer import LogPrinter +from .. import unittest + + +class LogPrinterTest(unittest.TestCase): + def test_single_container(self): + def reader(*args, **kwargs): + yield "hello\nworld" + + container = MockContainer(reader) + output = run_log_printer([container]) + + self.assertIn('hello', output) + self.assertIn('world', output) + + def test_unicode(self): + glyph = u'\u2022'.encode('utf-8') + + def reader(*args, **kwargs): + yield glyph + b'\n' + + container = MockContainer(reader) + output = run_log_printer([container]) + + self.assertIn(glyph, output) + + +def run_log_printer(containers): + r, w = os.pipe() + reader, writer = os.fdopen(r, 'r'), os.fdopen(w, 'w') + printer = LogPrinter(containers, output=writer) + printer.run() + writer.close() + return reader.read() + + +class MockContainer(object): + def __init__(self, reader): + self._reader = reader + + @property + def name(self): + return 'myapp_web_1' + + @property + def name_without_project(self): + return 'web_1' + + def attach(self, *args, **kwargs): + return self._reader() + + def wait(self, *args, **kwargs): + return 0 From 2bd6e3d0a553166992f74622159192f8e9ef48b9 Mon Sep 17 00:00:00 2001 From: Tobias Bradtke Date: Tue, 27 May 2014 18:22:18 +0200 Subject: [PATCH 0497/2154] Do not encode chunk, just write as is. --- fig/cli/socketclient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py index 518b7af4..6cc1f2c5 100644 --- a/fig/cli/socketclient.py +++ b/fig/cli/socketclient.py @@ -81,7 +81,7 @@ class SocketClient: chunk = socket.recv(4096) if chunk: - stream.write(chunk.encode(stream.encoding or 'utf-8')) + stream.write(chunk) stream.flush() else: break From eed274c632f8382d251254712dd441906d398ce2 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Jun 2014 16:32:23 +0100 Subject: [PATCH 0498/2154] Ship 0.4.2 --- CHANGES.md | 5 +++++ docs/install.md | 4 ++-- fig/__init__.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 91d84793..520f62cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change log ========== +0.4.2 (2014-06-18) +------------------ + + - Fix various encoding errors when using `fig run`, `fig up` and `fig build`. + 0.4.1 (2014-05-08) ------------------ diff --git a/docs/install.md b/docs/install.md index b7068710..eca82ce2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun Next, install Fig. On OS X: - $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/darwin > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/darwin > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig On 64-bit Linux: - $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/linux > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/linux > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): diff --git a/fig/__init__.py b/fig/__init__.py index 39011dec..18871f43 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.4.1' +__version__ = '0.4.2' From c0fdf7bd39e389f6648fcc304b4371336c8c5773 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Jun 2014 17:26:38 +0100 Subject: [PATCH 0499/2154] Use ubuntu:13.10 image to build docs stackbrew/ubuntu and ubuntu are the same thing now. Signed-off-by: Ben Firshman --- docs/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Dockerfile b/docs/Dockerfile index 3102b418..2639848a 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -1,4 +1,4 @@ -FROM stackbrew/ubuntu:13.10 +FROM ubuntu:13.10 RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python RUN locale-gen en_US.UTF-8 ADD Gemfile /code/ From 2541807dfd06995927d14e502f9c7ab946d503de Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 18 Jun 2014 17:27:59 +0100 Subject: [PATCH 0500/2154] update --- Dockerfile | 2 +- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3102b418..2639848a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM stackbrew/ubuntu:13.10 +FROM ubuntu:13.10 RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python RUN locale-gen en_US.UTF-8 ADD Gemfile /code/ diff --git a/cli.html b/cli.html index fa410356..a0ab1e33 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    diff --git a/django.html b/django.html index 06dedb95..6d355b14 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - +
    diff --git a/env.html b/env.html index 93b8f9ab..5e7ae608 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - +
    diff --git a/index.html b/index.html index 58ab04ca..3ea9289a 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - +
    diff --git a/install.html b/install.html index 49dcdec7..e1977378 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -27,11 +27,11 @@ $ docker-osx shell

    Docker has guides for Ubuntu and other platforms in their documentation.

    Next, install Fig. On OS X:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/darwin > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/darwin > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    On 64-bit Linux:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.1/linux > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/linux > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    diff --git a/rails.html b/rails.html index 429f9c35..0936d787 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index f6d91dce..adde7ff6 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index f8c49027..1cf89536 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    From 6b8044e92c875d04af27c6502e4307c13b39c6e0 Mon Sep 17 00:00:00 2001 From: j0hnsmith Date: Thu, 19 Jun 2014 11:57:55 +0100 Subject: [PATCH 0501/2154] add net param support Signed-off-by: Peter Flood --- docs/yml.md | 2 ++ fig/packages/docker/client.py | 5 ++++- fig/service.py | 8 +++++++- tests/integration/service_test.py | 15 +++++++++++++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index 24484d4f..5a5a2af6 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -59,3 +59,5 @@ environment: RACK_ENV: development ``` +-- Networking mode. Use the same values as the docker client --net parameter +net: "host" diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 7f00b4c4..79505c41 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -679,7 +679,7 @@ class Client(requests.Session): True) def start(self, container, binds=None, volumes_from=None, port_bindings=None, - lxc_conf=None, publish_all_ports=False, links=None, privileged=False): + lxc_conf=None, publish_all_ports=False, links=None, privileged=False, network_mode=None): if isinstance(container, dict): container = container.get('Id') @@ -726,6 +726,9 @@ class Client(requests.Session): start_config['Privileged'] = privileged + if network_mode: + start_config['NetworkMode'] = network_mode + url = self._url("/containers/{0}/start".format(container)) res = self._post_json(url, data=start_config) self._raise_for_status(res) diff --git a/fig/service.py b/fig/service.py index 2fc7125d..d6f44cfd 100644 --- a/fig/service.py +++ b/fig/service.py @@ -11,7 +11,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint', 'privileged'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint', 'privileged', 'net'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -229,6 +229,7 @@ class Service(object): } privileged = options.get('privileged', False) + net = options.get('net', 'bridge') container.start( links=self._get_links(link_to_self=override_options.get('one_off', False)), @@ -236,6 +237,7 @@ class Service(object): binds=volume_bindings, volumes_from=volumes_from, privileged=privileged, + network_mode=net, ) return container @@ -297,6 +299,10 @@ class Service(object): if 'privileged' in container_options: del container_options['privileged'] + # net is only required for starting containers, not for creating them + if 'net' in container_options: + del container_options['net'] + return container_options def build(self): diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 78ddbd85..aaefbd40 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -253,3 +253,18 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(containers), 2) for container in containers: self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp']) + + def test_network_mode_none(self): + service = self.create_service('web', net='none') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['NetworkMode'], 'none') + + def test_network_mode_bridged(self): + service = self.create_service('web', net='bridge') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['NetworkMode'], 'bridge') + + def test_network_mode_host(self): + service = self.create_service('web', net='host') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['NetworkMode'], 'host') From cafe68a92d366942d6bd9579405ec1bc1881def3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 2 May 2014 15:07:20 +0100 Subject: [PATCH 0502/2154] Better error message when service names are invalid --- fig/service.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fig/service.py b/fig/service.py index 2fc7125d..fd141c7d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -21,6 +21,8 @@ DOCKER_CONFIG_HINTS = { 'volume' : 'volumes', } +VALID_NAME_CHARS = '[a-zA-Z0-9]' + class BuildError(Exception): def __init__(self, service, reason): @@ -38,10 +40,10 @@ class ConfigError(ValueError): class Service(object): def __init__(self, name, client=None, project='default', links=[], **options): - if not re.match('^[a-zA-Z0-9]+$', name): - raise ConfigError('Invalid name: %s' % name) - if not re.match('^[a-zA-Z0-9]+$', project): - raise ConfigError('Invalid project: %s' % project) + if not re.match('^%s+$' % VALID_NAME_CHARS, name): + raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS)) + if not re.match('^%s+$' % VALID_NAME_CHARS, project): + raise ConfigError('Invalid project name "%s" - only %s are allowed' % (project, VALID_NAME_CHARS)) if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) From 0fc9cc65d135e509097677beeeef985567e2f47f Mon Sep 17 00:00:00 2001 From: Chris Corbyn Date: Sat, 21 Jun 2014 10:30:36 +0000 Subject: [PATCH 0503/2154] Rename '--only' => '--no-deps' Signed-off-by: Chris Corbyn --- docs/cli.md | 4 ++-- fig/cli/main.py | 24 ++++++++++++------------ tests/integration/cli_test.py | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index ac22feee..a697ab7a 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -53,9 +53,9 @@ 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 `--only` flag: +If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag: - $ fig run --only web python manage.py shell + $ fig run --no-deps web python manage.py shell ## scale diff --git a/fig/cli/main.py b/fig/cli/main.py index 3847e982..9e4e4669 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -204,22 +204,22 @@ class TopLevelCommand(Command): By default, linked services will be started, unless they are already running. If you do not want to start linked services, use - `fig run --only SERVICE COMMAND [ARGS...]`. + `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. - --only Don't start linked services. + -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['--only']: + if not options['--no-deps']: self.project.up( service_names=service.get_linked_names(), start_links=True, @@ -309,14 +309,14 @@ class TopLevelCommand(Command): Usage: up [options] [SERVICE...] Options: - -d Detached mode: Run containers in the background, - print new container names. - --only Don't start linked services. + -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'] - start_links = not options['--only'] + start_links = not options['--no-deps'] recreate = not options['--no-recreate'] service_names = options['SERVICE'] diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 00e1b79c..87c9680b 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -51,7 +51,7 @@ class CLITestCase(DockerClientTestCase): 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()), 0) + self.assertEqual(len(another.containers()), 1) def test_up_with_links(self): self.command.base_dir = 'tests/fixtures/links-figfile' @@ -63,9 +63,9 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(db.containers()), 1) self.assertEqual(len(console.containers()), 0) - def test_up_with_no_links(self): + def test_up_with_no_deps(self): self.command.base_dir = 'tests/fixtures/links-figfile' - self.command.dispatch(['up', '-d', '--only', 'web'], None) + 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') @@ -114,11 +114,11 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(console.containers()), 0) @patch('sys.stdout', new_callable=StringIO) - def test_run_with_no_links(self, mock_stdout): + 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', '--only', 'web', '/bin/true'], None) + self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None) db = self.command.project.get_service('db') self.assertEqual(len(db.containers()), 0) From 247691ca4470e685e0f42fb6e455f0b88b04470b Mon Sep 17 00:00:00 2001 From: Chris Corbyn Date: Sat, 21 Jun 2014 10:39:36 +0000 Subject: [PATCH 0504/2154] Remove auto_start option from fig.yml. This option is redundant now that services can be started along with links. Signed-off-by: Chris Corbyn --- docs/yml.md | 3 --- fig/project.py | 6 +++--- fig/service.py | 5 +---- tests/fixtures/simple-figfile/fig.yml | 1 - tests/integration/project_test.py | 10 +++++----- tests/unit/project_test.py | 6 ++---- tests/unit/service_test.py | 4 ---- 7 files changed, 11 insertions(+), 24 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index 8a69dffc..24484d4f 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -54,9 +54,6 @@ expose: volumes: - cache/:/tmp/cache --- Do not automatically run this service on `fig up` (default: true) -auto_start: false - -- Add environment variables. environment: RACK_ENV: development diff --git a/fig/project.py b/fig/project.py index 5e2b1fcc..a3b78f5d 100644 --- a/fig/project.py +++ b/fig/project.py @@ -92,8 +92,8 @@ class Project(object): 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 auto_start 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. @@ -105,7 +105,7 @@ class Project(object): """ if service_names is None or len(service_names) == 0: return self.get_services( - service_names=[s.name for s in self.services if s.options['auto_start']], + service_names=[s.name for s in self.services], include_links=include_links ) else: diff --git a/fig/service.py b/fig/service.py index 7de0ae7d..c49f0fd8 100644 --- a/fig/service.py +++ b/fig/service.py @@ -45,10 +45,7 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) - if 'auto_start' not in options: - options['auto_start'] = True - - supported_options = DOCKER_CONFIG_KEYS + ['auto_start', 'build', 'expose'] + supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose'] for k in options: if k not in supported_options: diff --git a/tests/fixtures/simple-figfile/fig.yml b/tests/fixtures/simple-figfile/fig.yml index e301dc62..3538ab09 100644 --- a/tests/fixtures/simple-figfile/fig.yml +++ b/tests/fixtures/simple-figfile/fig.yml @@ -2,6 +2,5 @@ simple: image: busybox:latest command: /bin/sleep 300 another: - auto_start: false image: busybox:latest command: /bin/sleep 300 diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index dcca570b..f5247682 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -124,17 +124,17 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() - def test_project_up_without_auto_start(self): - console = self.create_service('console', auto_start=False) + 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()), 1) + self.assertEqual(len(project.containers()), 2) self.assertEqual(len(db.containers()), 1) - self.assertEqual(len(console.containers()), 0) + self.assertEqual(len(console.containers()), 1) project.kill() project.remove_stopped() @@ -157,7 +157,7 @@ class ProjectTest(DockerClientTestCase): project.kill() project.remove_stopped() - def test_project_up_with_no_links(self): + 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')]) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 5acd2b4c..ce80333f 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -68,7 +68,7 @@ class ProjectTest(unittest.TestCase): project = Project('test', [web], None) self.assertEqual(project.get_service('web'), web) - def test_get_services_returns_all_auto_started_without_args(self): + def test_get_services_returns_all_services_without_args(self): web = Service( project='figtest', name='web', @@ -76,10 +76,9 @@ class ProjectTest(unittest.TestCase): console = Service( project='figtest', name='console', - auto_start=False ) project = Project('test', [web, console], None) - self.assertEqual(project.get_services(), [web]) + self.assertEqual(project.get_services(), [web, console]) def test_get_services_returns_listed_services_with_args(self): web = Service( @@ -89,7 +88,6 @@ class ProjectTest(unittest.TestCase): console = Service( project='figtest', name='console', - auto_start=False ) project = Project('test', [web, console], None) self.assertEqual(project.get_services(['console']), [console]) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 330e0411..490cb60d 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -20,10 +20,6 @@ class ServiceTest(unittest.TestCase): Service('a') Service('foo') - def test_auto_start_defaults_true(self): - service = Service(name='foo', project='bar') - self.assertEqual(service.options['auto_start'], True) - def test_project_validation(self): self.assertRaises(ConfigError, lambda: Service(name='foo', project='_')) Service(name='foo', project='bar') From d0f65906ed0cb458507c94d77133a64a46c90d57 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 25 Jun 2014 11:47:29 +0100 Subject: [PATCH 0505/2154] Update to docker-py 0.3.2 From https://github.com/dotcloud/docker-py/commit/59ced5700c1e4983c4a4e69cb185437f348c4afa Now only supports Docker 1.0.0 / remote API v1.12. Signed-off-by: Ben Firshman --- fig/container.py | 4 +- fig/packages/docker/__init__.py | 4 +- fig/packages/docker/client.py | 154 ++++++++++++++++++++------ fig/packages/docker/errors.py | 4 + fig/packages/docker/utils/__init__.py | 3 +- fig/packages/docker/utils/utils.py | 23 +++- fig/packages/docker/version.py | 1 + fig/service.py | 2 - tests/unit/container_test.py | 4 +- 9 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 fig/packages/docker/version.py diff --git a/fig/container.py b/fig/container.py index 4ac42a44..e89e34b9 100644 --- a/fig/container.py +++ b/fig/container.py @@ -17,7 +17,7 @@ class Container(object): Construct a container object from the output of GET /containers/json. """ new_dictionary = { - 'ID': dictionary['Id'], + 'Id': dictionary['Id'], 'Image': dictionary['Image'], } for name in dictionary.get('Names', []): @@ -36,7 +36,7 @@ class Container(object): @property def id(self): - return self.dictionary['ID'] + return self.dictionary['Id'] @property def image(self): diff --git a/fig/packages/docker/__init__.py b/fig/packages/docker/__init__.py index e10a5761..343766d7 100644 --- a/fig/packages/docker/__init__.py +++ b/fig/packages/docker/__init__.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .version import version + +__version__ = version __title__ = 'docker-py' -__version__ = '0.3.0' from .client import Client # flake8: noqa diff --git a/fig/packages/docker/client.py b/fig/packages/docker/client.py index 79505c41..abcd4a17 100644 --- a/fig/packages/docker/client.py +++ b/fig/packages/docker/client.py @@ -16,6 +16,7 @@ import json import re import shlex import struct +import warnings import requests import requests.exceptions @@ -29,7 +30,7 @@ from . import errors if not six.PY3: import websocket -DEFAULT_DOCKER_API_VERSION = '1.9' +DEFAULT_DOCKER_API_VERSION = '1.12' DEFAULT_TIMEOUT_SECONDS = 60 STREAM_HEADER_SIZE_BYTES = 8 @@ -95,7 +96,8 @@ class Client(requests.Session): mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, network_disabled=False, entrypoint=None, - cpu_shares=None, working_dir=None, domainname=None): + cpu_shares=None, working_dir=None, domainname=None, + memswap_limit=0): if isinstance(command, six.string_types): command = shlex.split(str(command)) if isinstance(environment, dict): @@ -121,8 +123,12 @@ class Client(requests.Session): volumes_dict[vol] = {} volumes = volumes_dict - if volumes_from and not isinstance(volumes_from, six.string_types): - volumes_from = ','.join(volumes_from) + if volumes_from: + if not isinstance(volumes_from, six.string_types): + volumes_from = ','.join(volumes_from) + else: + # Force None, an empty list or dict causes client.start to fail + volumes_from = None attach_stdin = False attach_stdout = False @@ -137,6 +143,14 @@ class Client(requests.Session): attach_stdin = True stdin_once = True + if utils.compare_version('1.10', self._version) >= 0: + message = ('{0!r} parameter has no effect on create_container().' + ' It has been moved to start()') + if dns is not None: + raise errors.DockerException(message.format('dns')) + if volumes_from is not None: + raise errors.DockerException(message.format('volumes_from')) + return { 'Hostname': hostname, 'Domainname': domainname, @@ -158,7 +172,8 @@ class Client(requests.Session): 'NetworkDisabled': network_disabled, 'Entrypoint': entrypoint, 'CpuShares': cpu_shares, - 'WorkingDir': working_dir + 'WorkingDir': working_dir, + 'MemorySwap': memswap_limit } def _post_json(self, url, data, **kwargs): @@ -235,7 +250,7 @@ class Client(requests.Session): start = walker + STREAM_HEADER_SIZE_BYTES end = start + length walker = end - yield str(buf[start:end]) + yield buf[start:end] def _multiplexed_socket_stream_helper(self, response): """A generator of multiplexed data blocks coming from a response @@ -296,8 +311,10 @@ class Client(requests.Session): return stream_result() if stream else \ self._result(response, binary=True) + sep = bytes() if six.PY3 else str() + return stream and self._multiplexed_socket_stream_helper(response) or \ - ''.join([x for x in self._multiplexed_buffer_helper(response)]) + sep.join([x for x in self._multiplexed_buffer_helper(response)]) def attach_socket(self, container, params=None, ws=False): if params is None: @@ -318,14 +335,20 @@ class Client(requests.Session): u, None, params=self._attach_params(params), stream=True)) def build(self, path=None, tag=None, quiet=False, fileobj=None, - nocache=False, rm=False, stream=False, timeout=None): + nocache=False, rm=False, stream=False, timeout=None, + custom_context=False, encoding=None): remote = context = headers = None if path is None and fileobj is None: raise TypeError("Either path or fileobj needs to be provided.") - if fileobj is not None: + if custom_context: + if not fileobj: + raise TypeError("You must specify fileobj with custom_context") + context = fileobj + elif fileobj is not None: context = utils.mkbuildcontext(fileobj) - elif path.startswith(('http://', 'https://', 'git://', 'github.com/')): + elif path.startswith(('http://', 'https://', + 'git://', 'github.com/')): remote = path else: context = utils.tar(path) @@ -341,8 +364,11 @@ class Client(requests.Session): 'nocache': nocache, 'rm': rm } + if context is not None: headers = {'Content-Type': 'application/tar'} + if encoding: + headers['Content-Encoding'] = encoding if utils.compare_version('1.9', self._version) >= 0: # If we don't have any auth data so far, try reloading the config @@ -393,10 +419,11 @@ class Client(requests.Session): json=True) def containers(self, quiet=False, all=False, trunc=True, latest=False, - since=None, before=None, limit=-1): + since=None, before=None, limit=-1, size=False): params = { 'limit': 1 if latest else limit, 'all': 1 if all else 0, + 'size': 1 if size else 0, 'trunc_cmd': 1 if trunc else 0, 'since': since, 'before': before @@ -424,12 +451,13 @@ class Client(requests.Session): mem_limit=0, ports=None, environment=None, dns=None, volumes=None, volumes_from=None, network_disabled=False, name=None, entrypoint=None, - cpu_shares=None, working_dir=None, domainname=None): + cpu_shares=None, working_dir=None, domainname=None, + memswap_limit=0): config = self._container_config( image, command, hostname, user, detach, stdin_open, tty, mem_limit, ports, environment, dns, volumes, volumes_from, network_disabled, - entrypoint, cpu_shares, working_dir, domainname + entrypoint, cpu_shares, working_dir, domainname, memswap_limit ) return self.create_container_from_config(config, name) @@ -458,6 +486,12 @@ class Client(requests.Session): self._raise_for_status(res) return res.raw + def get_image(self, image): + res = self._get(self._url("/images/{0}/get".format(image)), + stream=True) + self._raise_for_status(res) + return res.raw + def history(self, image): res = self._get(self._url("/images/{0}/history".format(image))) self._raise_for_status(res) @@ -513,6 +547,10 @@ class Client(requests.Session): True) def insert(self, image, url, path): + if utils.compare_version('1.12', self._version) >= 0: + raise errors.DeprecatedMethod( + 'insert is not available for API version >=1.12' + ) api_url = self._url("/images/" + image + "/insert") params = { 'url': url, @@ -544,6 +582,10 @@ class Client(requests.Session): self._raise_for_status(res) + def load_image(self, data): + res = self._post(self._url("/images/load"), data=data) + self._raise_for_status(res) + def login(self, username, password=None, email=None, registry=None, reauth=False): # If we don't have any auth data so far, try reloading the config file @@ -572,7 +614,27 @@ class Client(requests.Session): self._auth_configs[registry] = req_data return self._result(response, json=True) - def logs(self, container, stdout=True, stderr=True, stream=False): + def logs(self, container, stdout=True, stderr=True, stream=False, + timestamps=False): + if isinstance(container, dict): + container = container.get('Id') + if utils.compare_version('1.11', self._version) >= 0: + params = {'stderr': stderr and 1 or 0, + 'stdout': stdout and 1 or 0, + 'timestamps': timestamps and 1 or 0, + 'follow': stream and 1 or 0} + url = self._url("/containers/{0}/logs".format(container)) + res = self._get(url, params=params, stream=stream) + if stream: + return self._multiplexed_socket_stream_helper(res) + elif six.PY3: + return bytes().join( + [x for x in self._multiplexed_buffer_helper(res)] + ) + else: + return str().join( + [x for x in self._multiplexed_buffer_helper(res)] + ) return self.attach( container, stdout=stdout, @@ -581,6 +643,9 @@ class Client(requests.Session): logs=True ) + def ping(self): + return self._result(self._get(self._url('/_ping'))) + def port(self, container, private_port): if isinstance(container, dict): container = container.get('Id') @@ -597,6 +662,8 @@ class Client(requests.Session): return h_ports def pull(self, repository, tag=None, stream=False): + if not tag: + repository, tag = utils.parse_repository_tag(repository) registry, repo_name = auth.resolve_repository_name(repository) if repo_name.count(":") == 1: repository, tag = repository.rsplit(":", 1) @@ -653,16 +720,17 @@ class Client(requests.Session): return stream and self._stream_helper(response) \ or self._result(response) - def remove_container(self, container, v=False, link=False): + def remove_container(self, container, v=False, link=False, force=False): if isinstance(container, dict): container = container.get('Id') - params = {'v': v, 'link': link} + params = {'v': v, 'link': link, 'force': force} res = self._delete(self._url("/containers/" + container), params=params) self._raise_for_status(res) - def remove_image(self, image): - res = self._delete(self._url("/images/" + image)) + def remove_image(self, image, force=False, noprune=False): + params = {'force': force, 'noprune': noprune} + res = self._delete(self._url("/images/" + image), params=params) self._raise_for_status(res) def restart(self, container, timeout=10): @@ -678,8 +746,9 @@ class Client(requests.Session): params={'term': term}), True) - def start(self, container, binds=None, volumes_from=None, port_bindings=None, - lxc_conf=None, publish_all_ports=False, links=None, privileged=False, network_mode=None): + def start(self, container, binds=None, port_bindings=None, lxc_conf=None, + publish_all_ports=False, links=None, privileged=False, + dns=None, dns_search=None, volumes_from=None, network_mode=None): if isinstance(container, dict): container = container.get('Id') @@ -693,19 +762,7 @@ class Client(requests.Session): 'LxcConf': lxc_conf } if binds: - bind_pairs = [ - '%s:%s:%s' % ( - h, d['bind'], - 'ro' if 'ro' in d and d['ro'] else 'rw' - ) for h, d in binds.items() - ] - - start_config['Binds'] = bind_pairs - - if volumes_from and not isinstance(volumes_from, six.string_types): - volumes_from = ','.join(volumes_from) - - start_config['VolumesFrom'] = volumes_from + start_config['Binds'] = utils.convert_volume_binds(binds) if port_bindings: start_config['PortBindings'] = utils.convert_port_bindings( @@ -726,6 +783,28 @@ class Client(requests.Session): start_config['Privileged'] = privileged + if utils.compare_version('1.10', self._version) >= 0: + if dns is not None: + start_config['Dns'] = dns + if volumes_from is not None: + if isinstance(volumes_from, six.string_types): + volumes_from = volumes_from.split(',') + start_config['VolumesFrom'] = volumes_from + else: + warning_message = ('{0!r} parameter is discarded. It is only' + ' available for API version greater or equal' + ' than 1.10') + + if dns is not None: + warnings.warn(warning_message.format('dns'), + DeprecationWarning) + if volumes_from is not None: + warnings.warn(warning_message.format('volumes_from'), + DeprecationWarning) + + if dns_search: + start_config['DnsSearch'] = dns_search + if network_mode: start_config['NetworkMode'] = network_mode @@ -733,6 +812,15 @@ class Client(requests.Session): res = self._post_json(url, data=start_config) self._raise_for_status(res) + def resize(self, container, height, width): + if isinstance(container, dict): + container = container.get('Id') + + params = {'h': height, 'w': width} + url = self._url("/containers/{0}/resize".format(container)) + res = self._post(url, params=params) + self._raise_for_status(res) + def stop(self, container, timeout=10): if isinstance(container, dict): container = container.get('Id') diff --git a/fig/packages/docker/errors.py b/fig/packages/docker/errors.py index 9aad700d..85a6d452 100644 --- a/fig/packages/docker/errors.py +++ b/fig/packages/docker/errors.py @@ -59,3 +59,7 @@ class InvalidRepository(DockerException): class InvalidConfigFile(DockerException): pass + + +class DeprecatedMethod(DockerException): + pass diff --git a/fig/packages/docker/utils/__init__.py b/fig/packages/docker/utils/__init__.py index 8a85975d..86ddd35b 100644 --- a/fig/packages/docker/utils/__init__.py +++ b/fig/packages/docker/utils/__init__.py @@ -1,3 +1,4 @@ from .utils import ( - compare_version, convert_port_bindings, mkbuildcontext, ping, tar, parse_repository_tag + compare_version, convert_port_bindings, convert_volume_binds, + mkbuildcontext, ping, tar, parse_repository_tag ) # flake8: noqa diff --git a/fig/packages/docker/utils/utils.py b/fig/packages/docker/utils/utils.py index 0e4c1c1f..da81bdc5 100644 --- a/fig/packages/docker/utils/utils.py +++ b/fig/packages/docker/utils/utils.py @@ -92,6 +92,13 @@ def _convert_port_binding(binding): result['HostIp'] = binding[0] else: result['HostPort'] = binding[0] + elif isinstance(binding, dict): + if 'HostPort' in binding: + result['HostPort'] = binding['HostPort'] + if 'HostIp' in binding: + result['HostIp'] = binding['HostIp'] + else: + raise ValueError(binding) else: result['HostPort'] = binding @@ -116,13 +123,25 @@ def convert_port_bindings(port_bindings): return result +def convert_volume_binds(binds): + result = [] + for k, v in binds.items(): + if isinstance(v, dict): + result.append('%s:%s:%s' % ( + k, v['bind'], 'ro' if v.get('ro', False) else 'rw' + )) + else: + result.append('%s:%s:rw' % (k, v)) + return result + + def parse_repository_tag(repo): column_index = repo.rfind(':') if column_index < 0: - return repo, "" + return repo, None tag = repo[column_index+1:] slash_index = tag.find('/') if slash_index < 0: return repo[:column_index], tag - return repo, "" + return repo, None diff --git a/fig/packages/docker/version.py b/fig/packages/docker/version.py new file mode 100644 index 00000000..5189669c --- /dev/null +++ b/fig/packages/docker/version.py @@ -0,0 +1 @@ +version = "0.3.2" diff --git a/fig/service.py b/fig/service.py index 7d1f0acb..58d69cb4 100644 --- a/fig/service.py +++ b/fig/service.py @@ -183,7 +183,6 @@ class Service(object): intermediate_container = Container.create( self.client, image=container.image, - volumes_from=container.id, entrypoint=['echo'], command=[], ) @@ -192,7 +191,6 @@ class Service(object): container.remove() options = dict(override_options) - options['volumes_from'] = intermediate_container.id new_container = self.create_container(**options) self.start_container(new_container, volumes_from=intermediate_container.id) diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index cb6053e4..6e5b9884 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -16,14 +16,14 @@ class ContainerTest(unittest.TestCase): "Names":["/figtest_db_1"] }, has_been_inspected=True) self.assertEqual(container.dictionary, { - "ID": "abc", + "Id": "abc", "Image":"busybox:latest", "Name": "/figtest_db_1", }) def test_environment(self): container = Container(None, { - 'ID': 'abc', + 'Id': 'abc', 'Config': { 'Env': [ 'FOO=BAR', From 3770aac1afac24793a68cd8aa6ce2699aaed8fc5 Mon Sep 17 00:00:00 2001 From: Chris Corbyn Date: Wed, 25 Jun 2014 13:54:57 +0000 Subject: [PATCH 0506/2154] Use dockerpty instead for pseudo-tty behaviour. Signed-off-by: Chris Corbyn --- fig/cli/main.py | 19 +----- fig/cli/socketclient.py | 126 ---------------------------------------- requirements.txt | 1 + 3 files changed, 4 insertions(+), 142 deletions(-) delete mode 100644 fig/cli/socketclient.py diff --git a/fig/cli/main.py b/fig/cli/main.py index 9e4e4669..fdac8a2f 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -6,6 +6,7 @@ import re import signal from inspect import getdoc +import dockerpty from .. import __version__ from ..project import NoSuchService, ConfigurationError @@ -18,7 +19,6 @@ from .utils import yesno from ..packages.docker.errors import APIError from .errors import UserError from .docopt_command import NoSuchCommand -from .socketclient import SocketClient log = logging.getLogger(__name__) @@ -240,9 +240,8 @@ class TopLevelCommand(Command): service.start_container(container, ports=None, one_off=True) print(container.name) else: - with self._attach_to_container(container.id, raw=tty) as c: - service.start_container(container, ports=None, one_off=True) - c.run() + service.start_container(container, ports=None, one_off=True) + dockerpty.start(self.client, container.id) exit_code = container.wait() if options['--rm']: log.info("Removing %s..." % container.name) @@ -341,17 +340,5 @@ class TopLevelCommand(Command): print("Gracefully stopping... (press Ctrl+C again to force)") 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}) - socket_out = self.client.attach_socket(container_id, params={'stdout': 1, 'logs': 1, 'stream': 1}) - socket_err = self.client.attach_socket(container_id, params={'stderr': 1, 'logs': 1, 'stream': 1}) - - return SocketClient( - socket_in=socket_in, - socket_out=socket_out, - socket_err=socket_err, - raw=raw, - ) - def list_containers(containers): return ", ".join(c.name for c in containers) diff --git a/fig/cli/socketclient.py b/fig/cli/socketclient.py deleted file mode 100644 index 6cc1f2c5..00000000 --- a/fig/cli/socketclient.py +++ /dev/null @@ -1,126 +0,0 @@ -from __future__ import print_function -# Adapted from https://github.com/benthor/remotty/blob/master/socketclient.py - -import sys -import tty -import fcntl -import os -import termios -import threading -import errno - -import logging -log = logging.getLogger(__name__) - - -class SocketClient: - def __init__(self, - socket_in=None, - socket_out=None, - socket_err=None, - raw=True, - ): - self.socket_in = socket_in - self.socket_out = socket_out - self.socket_err = socket_err - self.raw = raw - - self.stdin_fileno = sys.stdin.fileno() - - def __enter__(self): - self.create() - return self - - def __exit__(self, type, value, trace): - self.destroy() - - def create(self): - if os.isatty(sys.stdin.fileno()): - self.settings = termios.tcgetattr(sys.stdin.fileno()) - else: - self.settings = None - - if self.socket_in is not None: - self.set_blocking(sys.stdin, False) - self.set_blocking(sys.stdout, True) - self.set_blocking(sys.stderr, True) - - if self.raw: - tty.setraw(sys.stdin.fileno()) - - def set_blocking(self, file, blocking): - fd = file.fileno() - flags = fcntl.fcntl(fd, fcntl.F_GETFL) - flags = (flags & ~os.O_NONBLOCK) if blocking else (flags | os.O_NONBLOCK) - fcntl.fcntl(fd, fcntl.F_SETFL, flags) - - def run(self): - if self.socket_in is not None: - self.start_background_thread(target=self.send, args=(self.socket_in, sys.stdin)) - - recv_threads = [] - - if self.socket_out is not None: - recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_out, sys.stdout))) - - if self.socket_err is not None: - recv_threads.append(self.start_background_thread(target=self.recv, args=(self.socket_err, sys.stderr))) - - for t in recv_threads: - t.join() - - def start_background_thread(self, **kwargs): - thread = threading.Thread(**kwargs) - thread.daemon = True - thread.start() - return thread - - def recv(self, socket, stream): - try: - while True: - chunk = socket.recv(4096) - - if chunk: - stream.write(chunk) - stream.flush() - else: - break - except Exception as e: - log.debug(e) - - def send(self, socket, stream): - while True: - chunk = stream.read(1) - - if chunk == '': - socket.close() - break - else: - try: - socket.send(chunk) - except Exception as e: - if hasattr(e, 'errno') and e.errno == errno.EPIPE: - break - else: - raise e - - def destroy(self): - if self.settings is not None: - termios.tcsetattr(self.stdin_fileno, termios.TCSADRAIN, self.settings) - - sys.stdout.flush() - -if __name__ == '__main__': - import websocket - - if len(sys.argv) != 2: - sys.stderr.write("Usage: python socketclient.py WEBSOCKET_URL\n") - sys.exit(1) - - url = sys.argv[1] - socket = websocket.create_connection(url) - - print("connected\r") - - with SocketClient(socket, interactive=True) as client: - client.run() diff --git a/requirements.txt b/requirements.txt index bec41811..4eea6cd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ PyYAML==3.10 requests==2.2.1 texttable==0.8.1 websocket-client==0.11.0 +dockerpty==0.0.8 From 8c6b516aa0f04ae376df2bca8d228b79f2eb282c Mon Sep 17 00:00:00 2001 From: Chris Corbyn Date: Thu, 26 Jun 2014 11:41:43 +0000 Subject: [PATCH 0507/2154] Update dockerpty to fix discovered race condition. Signed-off-by: Chris Corbyn --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4eea6cd3..ae8a8b7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ PyYAML==3.10 requests==2.2.1 texttable==0.8.1 websocket-client==0.11.0 -dockerpty==0.0.8 +dockerpty==0.1.0 From 3c48884dbb237bfc92eef7e54b256cd730c833b6 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 27 Jun 2014 14:41:36 +0100 Subject: [PATCH 0508/2154] Update dockerpty; stub it out in tests Its current behaviour occasionally causes tests to hang; until this is resolved, we'll stub it out. We weren't testing the output of 'run' anyhow (though we should be). --- requirements.txt | 2 +- tests/integration/cli_test.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index ae8a8b7d..06c6be5b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ PyYAML==3.10 requests==2.2.1 texttable==0.8.1 websocket-client==0.11.0 -dockerpty==0.1.0 +dockerpty==0.1.1 diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 87c9680b..2660ca00 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -102,7 +102,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(old_ids, new_ids) - @patch('sys.stdout', new_callable=StringIO) + @patch('dockerpty.start') def test_run_with_links(self, mock_stdout): mock_stdout.fileno = lambda: 1 @@ -113,7 +113,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(db.containers()), 1) self.assertEqual(len(console.containers()), 0) - @patch('sys.stdout', new_callable=StringIO) + @patch('dockerpty.start') def test_run_with_no_deps(self, mock_stdout): mock_stdout.fileno = lambda: 1 @@ -122,7 +122,7 @@ class CLITestCase(DockerClientTestCase): db = self.command.project.get_service('db') self.assertEqual(len(db.containers()), 0) - @patch('sys.stdout', new_callable=StringIO) + @patch('dockerpty.start') def test_run_does_not_recreate_linked_containers(self, mock_stdout): mock_stdout.fileno = lambda: 1 From 4f7cbc3812655f45d939ddf0ebd1417341aca388 Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Wed, 25 Jun 2014 18:49:54 +0800 Subject: [PATCH 0509/2154] Support for host address in port bindings (Closes #267) --- fig/service.py | 25 +++++++++++++++++++------ tests/integration/service_test.py | 21 +++++++++++++++++++++ tests/unit/service_test.py | 17 ++++++++++++++++- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/fig/service.py b/fig/service.py index a6e4971b..613a79c0 100644 --- a/fig/service.py +++ b/fig/service.py @@ -214,12 +214,7 @@ class Service(object): if options.get('ports', None) is not None: for port in options['ports']: - port = str(port) - if ':' in port: - external_port, internal_port = port.split(':', 1) - else: - external_port, internal_port = (None, port) - + internal_port, external_port = split_port(port) port_bindings[internal_port] = external_port volume_bindings = {} @@ -407,3 +402,21 @@ def split_volume(v): return v.split(':', 1) else: return (None, v) + + +def split_port(port): + port = str(port) + external_ip = None + if ':' in port: + external_port, internal_port = port.rsplit(':', 1) + if ':' in external_port: + external_ip, external_port = external_port.split(':', 1) + else: + external_port, internal_port = (None, port) + if external_ip: + if external_port: + external_port = (external_ip, external_port) + else: + external_port = (external_ip,) + return internal_port, external_port + diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index aaefbd40..1b0f80fc 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -231,6 +231,27 @@ class ServiceTest(DockerClientTestCase): self.assertIn('8000/tcp', container['NetworkSettings']['Ports']) self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001') + def test_port_with_explicit_interface(self): + service = self.create_service('web', ports=[ + '127.0.0.1:8001:8000', + '0.0.0.0:9001:9000', + ]) + container = service.start_container().inspect() + self.assertEqual(container['NetworkSettings']['Ports'], { + '8000/tcp': [ + { + 'HostIp': '127.0.0.1', + 'HostPort': '8001', + }, + ], + '9000/tcp': [ + { + 'HostIp': '0.0.0.0', + 'HostPort': '9001', + }, + ], + }) + def test_scale(self): service = self.create_service('web') service.scale(1) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 490cb60d..8acdc056 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from .. import unittest from fig import Service -from fig.service import ConfigError +from fig.service import ConfigError, split_port class ServiceTest(unittest.TestCase): def test_name_validations(self): @@ -27,3 +27,18 @@ class ServiceTest(unittest.TestCase): def test_config_validation(self): self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000'])) Service(name='foo', ports=['8000']) + + def test_split_port(self): + internal_port, external_port = split_port("127.0.0.1:1000:2000") + self.assertEqual(internal_port, "2000") + self.assertEqual(external_port, ("127.0.0.1", "1000")) + + internal_port, external_port = split_port("127.0.0.1::2000") + self.assertEqual(internal_port, "2000") + self.assertEqual(external_port, ("127.0.0.1",)) + + internal_port, external_port = split_port("1000:2000") + self.assertEqual(internal_port, "2000") + self.assertEqual(external_port, "1000") + + From e5916b2faead06ad9f505ec2de34239ed59b905a Mon Sep 17 00:00:00 2001 From: Satoshi Amemiya Date: Thu, 29 May 2014 16:40:11 +0900 Subject: [PATCH 0510/2154] Support volumes_from option Signed-off-by: Satoshi Amemiya --- fig/project.py | 55 ++++++++++++++++++++++--------- fig/service.py | 25 +++++++++++--- tests/integration/service_test.py | 12 +++++++ tests/unit/project_test.py | 13 ++++++-- 4 files changed, 81 insertions(+), 24 deletions(-) diff --git a/fig/project.py b/fig/project.py index a3b78f5d..a0b9150b 100644 --- a/fig/project.py +++ b/fig/project.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals from __future__ import absolute_import import logging from .service import Service +from .container import Container +from .packages.docker.errors import APIError log = logging.getLogger(__name__) @@ -18,11 +20,13 @@ def sort_service_dicts(services): if n['name'] in temporary_marked: if n['name'] in get_service_names(n.get('links', [])): raise DependencyError('A service can not link to itself: %s' % n['name']) + if n['name'] in n.get('volumes_from', []): + raise DependencyError('A service can not mount itself as volume: %s' % n['name']) else: raise DependencyError('Circular import between %s' % ' and '.join(temporary_marked)) if n in unmarked: temporary_marked.add(n['name']) - dependents = [m for m in services if n['name'] in get_service_names(m.get('links', []))] + dependents = [m for m in services if (n['name'] in get_service_names(m.get('links', []))) or (n['name'] in m.get('volumes_from', []))] for m in dependents: visit(m) temporary_marked.remove(n['name']) @@ -50,22 +54,10 @@ class Project(object): """ project = cls(name, [], client) for service_dict in sort_service_dicts(service_dicts): - # Reference links by object - links = [] - if 'links' in service_dict: - for link in service_dict.get('links', []): - if ':' in link: - service_name, link_name = link.split(':', 1) - else: - service_name, link_name = link, None - try: - links.append((project.get_service(service_name), link_name)) - except NoSuchService: - raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name)) + links = project.get_links(service_dict) + volumes_from = project.get_volumes_from(service_dict) - del service_dict['links'] - - project.services.append(Service(client=client, project=name, links=links, **service_dict)) + project.services.append(Service(client=client, project=name, links=links, volumes_from=volumes_from, **service_dict)) return project @classmethod @@ -119,6 +111,37 @@ class Project(object): [uniques.append(s) for s in services if s not in uniques] return uniques + def get_links(self, service_dict): + links = [] + if 'links' in service_dict: + for link in service_dict.get('links', []): + if ':' in link: + service_name, link_name = link.split(':', 1) + else: + service_name, link_name = link, None + try: + links.append((self.get_service(service_name), link_name)) + except NoSuchService: + raise ConfigurationError('Service "%s" has a link to service "%s" which does not exist.' % (service_dict['name'], service_name)) + del service_dict['links'] + return links + + def get_volumes_from(self, service_dict): + volumes_from = [] + if 'volumes_from' in service_dict: + for volume_name in service_dict.get('volumes_from', []): + try: + service = self.get_service(volume_name) + volumes_from.append(service) + except NoSuchService: + try: + container = Container.from_id(client, volume_name) + volumes_from.append(Container.from_id(client, volume_name)) + except APIError: + raise ConfigurationError('Service "%s" mounts volumes from "%s", which is not the name of a service or container.' % (service_dict['name'], volume_name)) + del service_dict['volumes_from'] + return volumes_from + def start(self, service_names=None, **options): for service in self.get_services(service_names): service.start(**options) diff --git a/fig/service.py b/fig/service.py index a6e4971b..9666522d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -11,7 +11,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'volumes_from', 'entrypoint', 'privileged', 'net'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -39,7 +39,7 @@ class ConfigError(ValueError): class Service(object): - def __init__(self, name, client=None, project='default', links=[], **options): + def __init__(self, name, client=None, project='default', links=[], volumes_from=[], **options): if not re.match('^%s+$' % VALID_NAME_CHARS, name): raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS)) if not re.match('^%s+$' % VALID_NAME_CHARS, project): @@ -60,6 +60,7 @@ class Service(object): self.client = client self.project = project self.links = links or [] + self.volumes_from = volumes_from or [] self.options = options def containers(self, stopped=False, one_off=False): @@ -190,7 +191,7 @@ class Service(object): options = dict(override_options) new_container = self.create_container(**options) - self.start_container(new_container, volumes_from=intermediate_container.id) + self.start_container(new_container, intermediate_container=intermediate_container) intermediate_container.remove() @@ -203,7 +204,7 @@ class Service(object): log.info("Starting %s..." % container.name) return self.start_container(container, **options) - def start_container(self, container=None, volumes_from=None, **override_options): + def start_container(self, container=None, intermediate_container=None,**override_options): if container is None: container = self.create_container(**override_options) @@ -240,7 +241,7 @@ class Service(object): links=self._get_links(link_to_self=override_options.get('one_off', False)), port_bindings=port_bindings, binds=volume_bindings, - volumes_from=volumes_from, + volumes_from=self._get_volumes_from(intermediate_container), privileged=privileged, network_mode=net, ) @@ -287,6 +288,20 @@ class Service(object): links.append((container.name, container.name_without_project)) return links + def _get_volumes_from(self, intermediate_container=None): + volumes_from = [] + for v in self.volumes_from: + if isinstance(v, Service): + for container in v.containers(stopped=True): + volumes_from.append(container.id) + elif isinstance(v, Container): + volumes_from.append(v.id) + + if intermediate_container: + volumes_from.append(intermediate_container.id) + + return volumes_from + def _get_container_create_options(self, override_options, one_off=False): container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options) container_options.update(override_options) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index aaefbd40..e6c02ac7 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from fig import Service from fig.service import CannotBeScaledError +from fig.container import Container from fig.packages.docker.errors import APIError from .testcases import DockerClientTestCase @@ -96,6 +97,16 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) self.assertIn('/host-tmp', container.inspect()['Volumes']) + def test_create_container_with_volumes_from(self): + volume_service = self.create_service('data') + volume_container_1 = volume_service.create_container() + volume_container_2 = Container.create(self.client, image='busybox:latest', command=["/bin/sleep", "300"]) + host_service = self.create_service('host', volumes_from=[volume_service, volume_container_2]) + host_container = host_service.create_container() + host_service.start_container(host_container) + self.assertIn(volume_container_1.id, host_container.inspect()['HostConfig']['VolumesFrom']) + self.assertIn(volume_container_2.id, host_container.inspect()['HostConfig']['VolumesFrom']) + def test_recreate_containers(self): service = self.create_service( 'db', @@ -127,6 +138,7 @@ class ServiceTest(DockerClientTestCase): self.assertIn('FOO=2', new_container.dictionary['Config']['Env']) self.assertEqual(new_container.name, 'figtest_db_1') self.assertEqual(new_container.inspect()['Volumes']['/var/db'], volume_path) + self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom']) self.assertEqual(len(self.client.containers(all=True)), num_containers_before) self.assertNotEqual(old_container.id, new_container.id) diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index ce80333f..5c8d35b1 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -30,12 +30,19 @@ class ProjectTest(unittest.TestCase): }, { 'name': 'db', - 'image': 'busybox:latest' + 'image': 'busybox:latest', + 'volumes_from': ['volume'] + }, + { + 'name': 'volume', + 'image': 'busybox:latest', + 'volumes': ['/tmp'], } ], None) - self.assertEqual(project.services[0].name, 'db') - self.assertEqual(project.services[1].name, 'web') + self.assertEqual(project.services[0].name, 'volume') + self.assertEqual(project.services[1].name, 'db') + self.assertEqual(project.services[2].name, 'web') def test_from_config(self): project = Project.from_config('figtest', { From 944e15fa657314e6f0c8503fee6d785334e1d0b5 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 2 Jul 2014 11:28:02 +0100 Subject: [PATCH 0511/2154] Stop `fig run` starting everything when a service has no links This was thanks to the semantics of project.up(), which starts everything if you pass it an empty list of service names. (That logic should probably be moved out to main.py.) Signed-off-by: Aanand Prasad --- fig/cli/main.py | 13 ++++++++----- tests/integration/cli_test.py | 8 ++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index fdac8a2f..854d5463 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -220,11 +220,14 @@ class TopLevelCommand(Command): 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 - ) + deps = service.get_linked_names() + + if len(deps) > 0: + self.project.up( + service_names=deps, + start_links=True, + recreate=False, + ) tty = True if options['-d'] or options['-T'] or not sys.stdin.isatty(): diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 2660ca00..ecfee0f9 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -103,9 +103,13 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') - def test_run_with_links(self, mock_stdout): - mock_stdout.fileno = lambda: 1 + def test_run_service_without_links(self, mock_stdout): + self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.dispatch(['run', 'console', '/bin/true'], None) + self.assertEqual(len(self.command.project.containers()), 0) + @patch('dockerpty.start') + def test_run_service_with_links(self, mock_stdout): self.command.base_dir = 'tests/fixtures/links-figfile' self.command.dispatch(['run', 'web', '/bin/true'], None) db = self.command.project.get_service('db') From 361294d20b4bde2eb5c7f0b6997ba48060fdaf44 Mon Sep 17 00:00:00 2001 From: Chris Corbyn Date: Sat, 5 Jul 2014 08:23:21 +0000 Subject: [PATCH 0512/2154] Update dockerpty for non-TTY multiplexing. Signed-off-by: Chris Corbyn --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 06c6be5b..2855a5c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,4 @@ PyYAML==3.10 requests==2.2.1 texttable==0.8.1 websocket-client==0.11.0 -dockerpty==0.1.1 +dockerpty==0.2.1 From 2a9aef13321ff91995a0212603ddb88bc71045e7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 9 Jul 2014 17:34:17 -0700 Subject: [PATCH 0513/2154] Use concise fig up output in docs Signed-off-by: Ben Firshman --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index aa6e1145..a75e357b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -113,8 +113,8 @@ Now if we run `fig up`, it'll pull a Redis image, build an image for our own cod Building web... Starting figtest_redis_1... Starting figtest_web_1... - figtest_redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 - figtest_web_1 | * Running on http://0.0.0.0:5000/ + redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 + web_1 | * Running on http://0.0.0.0:5000/ Open up [http://localhost:5000](http://localhost:5000) in your browser (or [http://localdocker:5000](http://localdocker:5000) if you're using [docker-osx](https://github.com/noplay/docker-osx)) and you should see it running! From 251aa7efb60d42f0b608b40e988e883c7eaafd46 Mon Sep 17 00:00:00 2001 From: Richard Morrison Date: Thu, 10 Jul 2014 14:57:54 +0100 Subject: [PATCH 0514/2154] Use yaml.safe_load instead of yaml.load http://pyyaml.org/wiki/PyYAMLDocumentation#LoadingYAML Signed-off-by: Richard Morrison --- fig/cli/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index cb256983..3b7a3b96 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -59,7 +59,7 @@ class Command(DocoptCommand): yaml_path = self.yaml_path if yaml_path is None: yaml_path = self.check_yaml_filename() - config = yaml.load(open(yaml_path)) + config = yaml.safe_load(open(yaml_path)) except IOError as e: if e.errno == errno.ENOENT: raise errors.FigFileNotFound(os.path.basename(e.filename)) From d83bdd5164125da4a1dae0161d8c6dae0b63529f Mon Sep 17 00:00:00 2001 From: Alexey Lebedeff Date: Sun, 6 Jul 2014 20:09:30 +0400 Subject: [PATCH 0515/2154] Change working dir through fig.yml Closes #144 Signed-off-by: Alexey Lebedeff --- fig/service.py | 3 ++- tests/integration/service_test.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index f21302f6..c0cc297c 100644 --- a/fig/service.py +++ b/fig/service.py @@ -11,7 +11,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -19,6 +19,7 @@ DOCKER_CONFIG_HINTS = { 'priviliged': 'privileged', 'privilige' : 'privileged', 'volume' : 'volumes', + 'workdir' : 'working_dir', } VALID_NAME_CHARS = '[a-zA-Z0-9]' diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index a8fb102a..315e14ce 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -301,3 +301,8 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('web', net='host') container = service.start_container().inspect() self.assertEqual(container['HostConfig']['NetworkMode'], 'host') + + def test_working_dir_param(self): + service = self.create_service('container', working_dir='/working/dir/sample') + container = service.create_container().inspect() + self.assertEqual(container['Config']['WorkingDir'], '/working/dir/sample') From 789e1ba82b537db589bf2a4f473cc4f64701b0ab Mon Sep 17 00:00:00 2001 From: Sam Hanes Date: Thu, 10 Jul 2014 15:56:26 -0700 Subject: [PATCH 0516/2154] Add domainname to allowed container config. Signed-off-by: Sam Hanes --- fig/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index c0cc297c..0feb56d8 100644 --- a/fig/service.py +++ b/fig/service.py @@ -11,7 +11,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', From 699bbe9ca2c40b16cb89170a806aae793396e058 Mon Sep 17 00:00:00 2001 From: Sam Hanes Date: Thu, 10 Jul 2014 15:56:38 -0700 Subject: [PATCH 0517/2154] Split the domainname out of qualified hostnames. Docker doesn't like it when a fully qualified hostname is passed in the `hostname` parameter. When an FQDN is provided with `-h` the official CLI puts the first component in `hostname` and the rest in `domainname`. This change replicates that behavior when the user specifies an FQDN in `hostname` in their `fig.yml`. Signed-off-by: Sam Hanes --- fig/service.py | 11 +++++++++++ tests/unit/service_test.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/fig/service.py b/fig/service.py index 0feb56d8..0c198269 100644 --- a/fig/service.py +++ b/fig/service.py @@ -304,6 +304,17 @@ class Service(object): container_options['name'] = self.next_container_name(one_off) + # If a qualified hostname was given, split it into an + # unqualified hostname and a domainname unless domainname + # was also given explicitly. This matches the behavior of + # the official Docker CLI in that scenario. + if ('hostname' in container_options + and 'domainname' not in container_options + and '.' in container_options['hostname']): + parts = container_options['hostname'].partition('.') + container_options['hostname'] = parts[0] + container_options['domainname'] = parts[2] + if 'ports' in container_options or 'expose' in self.options: ports = [] all_ports = container_options.get('ports', []) + self.options.get('expose', []) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 8acdc056..fe2c61c3 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -41,4 +41,40 @@ class ServiceTest(unittest.TestCase): self.assertEqual(internal_port, "2000") self.assertEqual(external_port, "1000") + def test_split_domainname_none(self): + service = Service('foo', + hostname = 'name', + ) + service.next_container_name = lambda x: 'foo' + opts = service._get_container_create_options({}) + self.assertEqual(opts['hostname'], 'name', 'hostname') + self.assertFalse('domainname' in opts, 'domainname') + def test_split_domainname_fqdn(self): + service = Service('foo', + hostname = 'name.domain.tld', + ) + service.next_container_name = lambda x: 'foo' + opts = service._get_container_create_options({}) + self.assertEqual(opts['hostname'], 'name', 'hostname') + self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') + + def test_split_domainname_both(self): + service = Service('foo', + hostname = 'name', + domainname = 'domain.tld', + ) + service.next_container_name = lambda x: 'foo' + opts = service._get_container_create_options({}) + self.assertEqual(opts['hostname'], 'name', 'hostname') + self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') + + def test_split_domainname_weird(self): + service = Service('foo', + hostname = 'name.sub', + domainname = 'domain.tld', + ) + service.next_container_name = lambda x: 'foo' + opts = service._get_container_create_options({}) + self.assertEqual(opts['hostname'], 'name.sub', 'hostname') + self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') From 2ce3685e3251e2fec3ab95fad4a572242e50bfb5 Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Fri, 11 Jul 2014 09:11:25 -0700 Subject: [PATCH 0518/2154] Fix typo in script dir name Signed-off-by: Ryan Brainard --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b660f6a0..5cc0dda0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ that should get you started. 1. Clone your forked repository locally `git clone git@github.com:kvz/fig.git`. 1. Enter the local directory `cd fig`. 1. Set up a development environment `python setup.py develop`. That will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repo. So from any of your fig projects, `fig` now refers to your development project. Time to start hacking : ) -1. Works for you? Run the test suite via `./scripts/test` to verify it won't break other usecases. +1. Works for you? Run the test suite via `./script/test` to verify it won't break other usecases. 1. All good? Commit and push to GitHub, and submit a pull request. ## Running the test suite From 44a91e6ba8bf64ef7206f48239638bb9f042145f Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Fri, 11 Jul 2014 10:18:05 -0700 Subject: [PATCH 0519/2154] Resolve environment without values to values on host For parity with the Docker CLI, allow environment variables without values to be automatically resolved to their values on the host. Signed-off-by: Ryan Brainard Conflicts: tests/integration/service_test.py --- fig/service.py | 18 ++++++++++++++++++ tests/integration/service_test.py | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/fig/service.py b/fig/service.py index 0c198269..e3cbb296 100644 --- a/fig/service.py +++ b/fig/service.py @@ -330,6 +330,11 @@ class Service(object): if 'volumes' in container_options: container_options['volumes'] = dict((split_volume(v)[1], {}) for v in container_options['volumes']) + if 'environment' in container_options: + if isinstance(container_options['environment'], list): + container_options['environment'] = dict(split_env(e) for e in container_options['environment']) + container_options['environment'] = dict(resolve_env(k,v) for k,v in container_options['environment'].iteritems()) + if self.can_be_built(): if len(self.client.images(name=self._build_tag_name())) == 0: self.build() @@ -447,3 +452,16 @@ def split_port(port): external_port = (external_ip,) return internal_port, external_port +def split_env(env): + if '=' in env: + return env.split('=', 1) + else: + return env, None + +def resolve_env(key,val): + if val is not None: + return key, val + elif key in os.environ: + return key, os.environ[key] + else: + return key, '' diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 315e14ce..4700f410 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -5,6 +5,7 @@ from fig.service import CannotBeScaledError from fig.container import Container from fig.packages.docker.errors import APIError from .testcases import DockerClientTestCase +import os class ServiceTest(DockerClientTestCase): def test_containers(self): @@ -306,3 +307,23 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('container', working_dir='/working/dir/sample') container = service.create_container().inspect() self.assertEqual(container['Config']['WorkingDir'], '/working/dir/sample') + + def test_split_env(self): + service = self.create_service('web', environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS=']) + env = service.start_container().environment + for k,v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.iteritems(): + self.assertEqual(env[k], v) + + def test_resolve_env(self): + service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}) + os.environ['FILE_DEF'] = 'E1' + os.environ['FILE_DEF_EMPTY'] = 'E2' + os.environ['ENV_DEF'] = 'E3' + try: + env = service.start_container().environment + for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.iteritems(): + self.assertEqual(env[k], v) + finally: + del os.environ['FILE_DEF'] + del os.environ['FILE_DEF_EMPTY'] + del os.environ['ENV_DEF'] From d1052ff666e5bf413b88e10ec5e640daf3e7876b Mon Sep 17 00:00:00 2001 From: Ryan Brainard Date: Fri, 11 Jul 2014 10:07:50 -0700 Subject: [PATCH 0520/2154] Add documention for key-only environment Signed-off-by: Ryan Brainard --- docs/yml.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/yml.md b/docs/yml.md index 5a5a2af6..6204122f 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -55,8 +55,11 @@ volumes: - cache/:/tmp/cache -- Add environment variables. +-- Environment variables with only a key are resolved to values on the host +-- machine, which can be helpful for secret or host-specific values. environment: RACK_ENV: development + SESSION_SECRET: ``` -- Networking mode. Use the same values as the docker client --net parameter From d528f9f6423602d3ec655abe775968bc2e5b6ceb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 11 Jul 2014 11:58:59 -0700 Subject: [PATCH 0521/2154] Regardless of dependencies, `fig up` only attaches to what you specify Without this, if you go: $ fig up -d db $ fig up web you'll get output for both db and web, and Ctrl-C will kill them both. Signed-off-by: Aanand Prasad --- fig/cli/main.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 854d5463..44aab982 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -322,12 +322,14 @@ class TopLevelCommand(Command): recreate = not options['--no-recreate'] service_names = options['SERVICE'] - to_attach = self.project.up( + self.project.up( service_names=service_names, start_links=start_links, recreate=recreate ) + to_attach = [c for s in self.project.get_services(service_names) for c in s.containers()] + if not detached: print("Attaching to", list_containers(to_attach)) log_printer = LogPrinter(to_attach, attach_params={"logs": True}) From 4afcdbdb3c580281577dc13c0a0b5a52c1ef6f41 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 11 Jul 2014 11:11:54 -0700 Subject: [PATCH 0522/2154] Add docs for volumes_from Signed-off-by: Ben Firshman --- docs/yml.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/yml.md b/docs/yml.md index 6204122f..a299669c 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -54,6 +54,11 @@ expose: volumes: - cache/:/tmp/cache +-- Mount all of the volumes from another service or container +volumes_from: + - service_name + - container_name + -- Add environment variables. -- Environment variables with only a key are resolved to values on the host -- machine, which can be helpful for secret or host-specific values. From 54894659058bf796bd58ea6f89b8361b656f0887 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 11 Jul 2014 11:17:25 -0700 Subject: [PATCH 0523/2154] Ship 0.5.0 Signed-off-by: Ben Firshman --- CHANGES.md | 30 ++++++++++++++++++++++++++++++ fig/__init__.py | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 520f62cc..1ed060d6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,36 @@ Change log ========== +0.5.0 (2014-07-11) +------------------ + + - Fig now starts links when you run `fig run` or `fig up`. + + For example, if you have a `web` service which depends on a `db` service, `fig run web ...` will start the `db` service. + + - Environment variables can now be resolved from the environment that Fig is running in. Just specify it as a blank variable in your `fig.yml` and, if set, it'll be resolved: + ``` + environment: + RACK_ENV: development + SESSION_SECRET: + ``` + + - `volumes_from` is now supported in `fig.yml`. All of the volumes from the specified services and containers will be mounted: + + ``` + volumes_from: + - service_name + - container_name + ``` + + - The `net` and `workdir` options are now supported in `fig.yml`. + - The `hostname` option now works in the same way as the Docker CLI, splitting out into a `domainname` option. + - TTY behaviour is far more robust, and resizes are supported correctly. + - Load YAML files safely. + +Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca and @mozz100 for their help with this release! + + 0.4.2 (2014-06-18) ------------------ diff --git a/fig/__init__.py b/fig/__init__.py index 18871f43..db027d36 100644 --- a/fig/__init__.py +++ b/fig/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service -__version__ = '0.4.2' +__version__ = '0.5.0' From 9f0cfbdfd2a24d4b300fa03f9ffb2532bcb964b7 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 11 Jul 2014 14:15:46 -0700 Subject: [PATCH 0524/2154] Document release process Signed-off-by: Ben Firshman --- CONTRIBUTING.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5cc0dda0..9e47481c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,3 +73,21 @@ The easiest way to do this is to use the `--signoff` flag when committing. E.g.: $ git commit --signoff + +## Release process + +1. Open pull request that: + + - Updates version in `fig/__init__.py` + - Updates version in `docs/install.md` + - Adds release notes to `CHANGES.md` + +2. Create unpublished GitHub release with release notes + +3. Build Linux version on any Docker host with `script/build-linux` and attach to release + +4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release + +5. Publish GitHub release, creating tag + + From 36f4e30dbaa86495a097ecdc29dfcf334bbb1783 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Fri, 11 Jul 2014 14:15:55 -0700 Subject: [PATCH 0525/2154] Add 0.5.0 to docs Signed-off-by: Ben Firshman --- docs/install.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/install.md b/docs/install.md index eca82ce2..5a86cf93 100644 --- a/docs/install.md +++ b/docs/install.md @@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun Next, install Fig. On OS X: - $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/darwin > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.5.0/darwin > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig On 64-bit Linux: - $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/linux > /usr/local/bin/fig + $ curl -L https://github.com/orchardup/fig/releases/download/0.5.0/linux > /usr/local/bin/fig $ chmod +x /usr/local/bin/fig Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): From f43783e1bfbca5e928da10de19f938a69a762ecb Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 11 Jul 2014 14:34:08 -0700 Subject: [PATCH 0526/2154] update --- cli.html | 11 ++++++++--- django.html | 2 +- env.html | 2 +- index.html | 6 +++--- install.html | 6 +++--- rails.html | 2 +- wordpress.html | 2 +- yml.html | 15 +++++++++++++-- 8 files changed, 31 insertions(+), 15 deletions(-) diff --git a/cli.html b/cli.html index a0ab1e33..a9218503 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - +
    @@ -56,13 +56,16 @@

    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.

    Links are also created between one-off commands and the other containers for that service so you can do stuff like this:

    $ 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.

    @@ -83,9 +86,11 @@ For example:

    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.

    Open up http://localhost:5000 in your browser (or http://localdocker:5000 if you're using docker-osx) and you should see it running!

    diff --git a/install.html b/install.html index e1977378..9357f281 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - +
    @@ -27,11 +27,11 @@ $ docker-osx shell

    Docker has guides for Ubuntu and other platforms in their documentation.

    Next, install Fig. On OS X:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/darwin > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.5.0/darwin > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    On 64-bit Linux:

    -
    $ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/linux > /usr/local/bin/fig
    +
    $ curl -L https://github.com/orchardup/fig/releases/download/0.5.0/linux > /usr/local/bin/fig
     $ chmod +x /usr/local/bin/fig
     

    Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

    diff --git a/rails.html b/rails.html index 0936d787..fbd653d8 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - +
    diff --git a/wordpress.html b/wordpress.html index adde7ff6..10812f41 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - +
    diff --git a/yml.html b/yml.html index 1cf89536..7d198b79 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - +
    @@ -65,10 +65,21 @@ volumes: - cache/:/tmp/cache +-- Mount all of the volumes from another service or container +volumes_from: + - service_name + - container_name + -- Add environment variables. +-- Environment variables with only a key are resolved to values on the host +-- machine, which can be helpful for secret or host-specific values. environment: RACK_ENV: development -
    + SESSION_SECRET: +
    +

    -- Networking mode. Use the same values as the docker client --net parameter +net: "host"

    +
    -

    See the fig.yml reference for more information on how it works.

    +

    See the fig.yml reference for more information on how it works.

    We can now start a Django project using fig run:

    $ fig run web django-admin.py startproject figexample .
    diff --git a/env.html b/env.html
    index 89091290..98d83fe5 100644
    --- a/env.html
    +++ b/env.html
    @@ -6,7 +6,8 @@
         
         
         
    -    
    +    
    +    
       
       
         
    diff --git a/index.html b/index.html index 6263a2f1..308c91f2 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,8 @@ - + +
    diff --git a/install.html b/install.html index eef03192..8c1ad3c0 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,8 @@ - + +
    @@ -19,8 +20,8 @@

    Installing Fig

    -

    First, install Docker version 1.0.0. If you're on OS X, you can use docker-osx:

    -
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/1.0.0/docker-osx > /usr/local/bin/docker-osx
    +

    First, install Docker version 1.0 or greater. If you're on OS X, you can use docker-osx:

    +
    $ curl https://raw.githubusercontent.com/noplay/docker-osx/1.1.1/docker-osx > /usr/local/bin/docker-osx
     $ chmod +x /usr/local/bin/docker-osx
     $ docker-osx shell
     
    diff --git a/rails.html b/rails.html index 771c4cc4..00554d91 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,8 @@ - + +
    diff --git a/wordpress.html b/wordpress.html index cd62b0ad..b33f481c 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,8 @@ - + +
    diff --git a/yml.html b/yml.html index 366bb84b..34b10fc3 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,8 @@ - + +
    From 7f06d468271c5e8aad6a280621455e407894219d Mon Sep 17 00:00:00 2001 From: Mark Steve Samson Date: Sun, 20 Jul 2014 16:27:05 +0800 Subject: [PATCH 0554/2154] Add test for building with --no-cache Signed-off-by: Mark Steve Samson --- tests/fixtures/simple-dockerfile/fig.yml | 2 ++ tests/integration/cli_test.py | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/fixtures/simple-dockerfile/fig.yml diff --git a/tests/fixtures/simple-dockerfile/fig.yml b/tests/fixtures/simple-dockerfile/fig.yml new file mode 100644 index 00000000..a3f56d46 --- /dev/null +++ b/tests/fixtures/simple-dockerfile/fig.yml @@ -0,0 +1,2 @@ +simple: + build: tests/fixtures/simple-dockerfile diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 92aed85f..9ab21b44 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -46,6 +46,18 @@ class CLITestCase(DockerClientTestCase): self.assertNotIn('multiplefigfiles_another_1', output) self.assertIn('multiplefigfiles_yetanother_1', output) + @patch('sys.stdout', new_callable=StringIO) + def test_build_no_cache(self, mock_stdout): + self.command.base_dir = 'tests/fixtures/simple-dockerfile' + self.command.dispatch(['build', 'simple'], None) + mock_stdout.truncate(0) + cache_indicator = 'Using cache' + self.command.dispatch(['build', 'simple'], None) + self.assertIn(cache_indicator, output) + mock_stdout.truncate(0) + self.command.dispatch(['build', '--no-cache', 'simple'], None) + self.assertNotIn(cache_indicator, output) + def test_up(self): self.command.dispatch(['up', '-d'], None) service = self.command.project.get_service('simple') @@ -193,3 +205,4 @@ class CLITestCase(DockerClientTestCase): self.command.scale({'SERVICE=NUM': ['simple=0', 'another=0']}) self.assertEqual(len(project.get_service('simple').containers()), 0) self.assertEqual(len(project.get_service('another').containers()), 0) + From b559880a80b7cccbf8b8aef09e10f42c6e1dcb96 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 22 Jul 2014 15:36:32 -0700 Subject: [PATCH 0555/2154] Document missing .yml options Signed-off-by: Aanand Prasad --- docs/yml.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/yml.md b/docs/yml.md index 0bf26749..63b0efbf 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -129,3 +129,19 @@ dns: - 8.8.8.8 - 9.9.9.9 ``` + +### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged + +Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. + +``` +working_dir: /code +entrypoint: /code/entrypoint.sh +user: postgresql + +hostname: foo +domainname: foo.com + +mem_limit: 1000000000 +privileged: true +``` From b2b7d4ca7c3d19408968601e78894096de178c61 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 23 Jul 2014 17:54:02 -0700 Subject: [PATCH 0556/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 15 ++++++++++++++- 8 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cli.html b/cli.html index aa003f06..fa8b6c65 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - + diff --git a/django.html b/django.html index fed979bb..753cce48 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - + diff --git a/env.html b/env.html index 98d83fe5..a0ae9f95 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - + diff --git a/index.html b/index.html index 308c91f2..f8ce64e6 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - + diff --git a/install.html b/install.html index 8c1ad3c0..2ceaf651 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - + diff --git a/rails.html b/rails.html index 00554d91..f1b7e81a 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - + diff --git a/wordpress.html b/wordpress.html index b33f481c..a5346b29 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - + diff --git a/yml.html b/yml.html index 34b10fc3..5bb9c576 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - + @@ -109,6 +109,19 @@ net: "host" dns: - 8.8.8.8 - 9.9.9.9 +
    +

    working_dir, entrypoint, user, hostname, domainname, mem_limit, privileged

    + +

    Each of these is a single value, analogous to its docker run counterpart.

    +
    working_dir: /code
    +entrypoint: /code/entrypoint.sh
    +user: postgresql
    +
    +hostname: foo
    +domainname: foo.com
    +
    +mem_limit: 1000000000
    +privileged: true
     
    diff --git a/wordpress.html b/wordpress.html index ecdefb37..818d65a0 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - + @@ -21,7 +21,7 @@

    Getting started with Fig and Wordpress

    Fig makes it nice and easy to run Wordpress in an isolated environment. Install Fig, then download Wordpress into the current directory:

    -
    $ curl http://wordpress.org/wordpress-3.8.1.tar.gz | tar -xvzf -
    +
    $ curl https://wordpress.org/latest.tar.gz | tar -xvzf -
     

    This will create a directory called wordpress, which you can rename to the name of your project if you wish. Inside that directory, we create Dockerfile, a file that defines what environment your app is going to run in:

    FROM orchardup/php5
    @@ -124,7 +124,7 @@ if(file_exists($root.$path))
     
             
    - +
    diff --git a/yml.html b/yml.html index b4b7dc43..8000ed6f 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - + @@ -167,7 +167,7 @@ privileged: true
    - +
    From f98323b79e8371df63dd448064d1255fa86dbf1c Mon Sep 17 00:00:00 2001 From: Andrew Burkett Date: Thu, 6 Nov 2014 16:19:58 -0800 Subject: [PATCH 0673/2154] Support multiple port mappings for same internal port Signed-off-by: Andrew Burkett --- fig/service.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/fig/service.py b/fig/service.py index e6dcf012..0648aeb8 100644 --- a/fig/service.py +++ b/fig/service.py @@ -251,7 +251,13 @@ class Service(object): def start_container(self, container=None, intermediate_container=None, **override_options): container = container or self.create_container(**override_options) options = dict(self.options, **override_options) - ports = dict(split_port(port) for port in options.get('ports') or []) + ports = {} + for port in options.get('ports') or []: + internal_port, external = split_port(port) + if internal_port in ports: + ports[internal_port].append(external) + else: + ports[internal_port] = [external] volume_bindings = dict( build_volume_binding(parse_volume_spec(volume)) From 4f6d02867b4512bd44fe6780037edabc1c7310c2 Mon Sep 17 00:00:00 2001 From: Andrew Burkett Date: Thu, 6 Nov 2014 17:54:45 -0800 Subject: [PATCH 0674/2154] Move to build_port_bindings(). Added Tests Signed-off-by: Andrew Burkett --- fig/service.py | 21 +++++++++++++-------- tests/unit/service_test.py | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/fig/service.py b/fig/service.py index 0648aeb8..bbbef7bc 100644 --- a/fig/service.py +++ b/fig/service.py @@ -251,13 +251,7 @@ class Service(object): def start_container(self, container=None, intermediate_container=None, **override_options): container = container or self.create_container(**override_options) options = dict(self.options, **override_options) - ports = {} - for port in options.get('ports') or []: - internal_port, external = split_port(port) - if internal_port in ports: - ports[internal_port].append(external) - else: - ports[internal_port] = [external] + port_bindings = build_port_bindings(options.get('ports') or []) volume_bindings = dict( build_volume_binding(parse_volume_spec(volume)) @@ -270,7 +264,7 @@ class Service(object): container.start( links=self._get_links(link_to_self=options.get('one_off', False)), - port_bindings=ports, + port_bindings=port_bindings, binds=volume_bindings, volumes_from=self._get_volumes_from(intermediate_container), privileged=privileged, @@ -498,6 +492,17 @@ def build_volume_binding(volume_spec): return os.path.abspath(os.path.expandvars(external)), internal +def build_port_bindings(ports): + port_bindings = {} + for port in ports: + internal_port, external = split_port(port) + if internal_port in port_bindings: + port_bindings[internal_port].append(external) + else: + port_bindings[internal_port] = [external] + return port_bindings + + def split_port(port): parts = str(port).split(':') if not 1 <= len(parts) <= 3: diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index f1d1c79d..119b4144 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -13,6 +13,7 @@ from fig.container import Container from fig.service import ( ConfigError, split_port, + build_port_bindings, parse_volume_spec, build_volume_binding, APIError, @@ -114,6 +115,19 @@ class ServiceTest(unittest.TestCase): with self.assertRaises(ConfigError): split_port("0.0.0.0:1000:2000:tcp") + def test_build_port_bindings_with_one_port(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000"]) + self.assertEqual(port_bindings["1000"],[("127.0.0.1","1000")]) + + def test_build_port_bindings_with_matching_internal_ports(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000","127.0.0.1:2000:1000"]) + self.assertEqual(port_bindings["1000"],[("127.0.0.1","1000"),("127.0.0.1","2000")]) + + def test_build_port_bindings_with_nonmatching_internal_ports(self): + port_bindings = build_port_bindings(["127.0.0.1:1000:1000","127.0.0.1:2000:2000"]) + self.assertEqual(port_bindings["1000"],[("127.0.0.1","1000")]) + self.assertEqual(port_bindings["2000"],[("127.0.0.1","2000")]) + def test_split_domainname_none(self): service = Service('foo', hostname='name', client=self.mock_client) self.mock_client.containers.return_value = [] From 04da6b035e74e12446724cc6129d3aaf54a6d1d2 Mon Sep 17 00:00:00 2001 From: Paul B Date: Tue, 28 Oct 2014 16:31:09 +0100 Subject: [PATCH 0675/2154] Add restart option to Fig. Related to #478 Signed-off-by: Paul Bonaud --- docs/yml.md | 4 +++- fig/service.py | 23 +++++++++++++++++++++-- tests/integration/service_test.py | 11 +++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index dedfa5c1..059d165c 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -142,7 +142,7 @@ dns: - 9.9.9.9 ``` -### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged +### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. @@ -156,4 +156,6 @@ domainname: foo.com mem_limit: 1000000000 privileged: true + +restart: always ``` diff --git a/fig/service.py b/fig/service.py index bbbef7bc..1685111c 100644 --- a/fig/service.py +++ b/fig/service.py @@ -15,7 +15,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir', 'restart'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -262,6 +262,8 @@ class Service(object): net = options.get('net', 'bridge') dns = options.get('dns', None) + restart = parse_restart_spec(options.get('restart', None)) + container.start( links=self._get_links(link_to_self=options.get('one_off', False)), port_bindings=port_bindings, @@ -270,6 +272,7 @@ class Service(object): privileged=privileged, network_mode=net, dns=dns, + restart_policy=restart ) return container @@ -376,7 +379,7 @@ class Service(object): container_options['image'] = self._build_tag_name() # Delete options which are only used when starting - for key in ['privileged', 'net', 'dns']: + for key in ['privileged', 'net', 'dns', 'restart']: if key in container_options: del container_options[key] @@ -466,6 +469,22 @@ def get_container_name(container): return name[1:] +def parse_restart_spec(restart_config): + if not restart_config: + return None + parts = restart_config.split(':') + if len(parts) > 2: + raise ConfigError("Restart %s has incorrect format, should be " + "mode[:max_retry]" % restart_config) + if len(parts) == 2: + name, max_retry_count = parts + else: + name, = parts + max_retry_count = 0 + + return {'Name': name, 'MaximumRetryCount': int(max_retry_count)} + + def parse_volume_spec(volume_config): parts = volume_config.split(':') if len(parts) > 3: diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index c288edf6..117cf99d 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -365,6 +365,17 @@ class ServiceTest(DockerClientTestCase): container = service.start_container().inspect() self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8', '9.9.9.9']) + def test_restart_always_value(self): + service = self.create_service('web', restart='always') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['RestartPolicy']['Name'], 'always') + + def test_restart_on_failure_value(self): + service = self.create_service('web', restart='on-failure:5') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['RestartPolicy']['Name'], 'on-failure') + self.assertEqual(container['HostConfig']['RestartPolicy']['MaximumRetryCount'], 5) + def test_working_dir_param(self): service = self.create_service('container', working_dir='/working/dir/sample') container = service.create_container().inspect() From bb85e238e0d450f8e2f3e81daaa518b260b1c8a1 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 20 Nov 2014 17:23:43 +0000 Subject: [PATCH 0676/2154] Add fig as entrypoint to Dockerfile A step towards "docker run fig". Signed-off-by: Ben Firshman --- Dockerfile | 2 ++ script/build-linux | 4 ++-- script/test | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index cc6b9990..c430950b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,3 +13,5 @@ ADD . /code/ RUN python setup.py install RUN chown -R user /code/ + +ENTRYPOINT ["/usr/local/bin/fig"] diff --git a/script/build-linux b/script/build-linux index 3dc2c643..b7b6edf5 100755 --- a/script/build-linux +++ b/script/build-linux @@ -3,6 +3,6 @@ set -ex mkdir -p `pwd`/dist chmod 777 `pwd`/dist docker build -t fig . -docker run -u user -v `pwd`/dist:/code/dist fig pyinstaller -F bin/fig +docker run -u user -v `pwd`/dist:/code/dist --entrypoint pyinstaller fig -F bin/fig mv dist/fig dist/fig-Linux-x86_64 -docker run -u user -v `pwd`/dist:/code/dist fig dist/fig-Linux-x86_64 --version +docker run -u user -v `pwd`/dist:/code/dist --entrypoint dist/fig-Linux-x86_64 fig --version diff --git a/script/test b/script/test index 79cc7e6b..54c2077f 100755 --- a/script/test +++ b/script/test @@ -1,5 +1,5 @@ #!/bin/sh set -ex docker build -t fig . -docker run -v /var/run/docker.sock:/var/run/docker.sock fig flake8 fig -docker run -v /var/run/docker.sock:/var/run/docker.sock fig nosetests $@ +docker run -v /var/run/docker.sock:/var/run/docker.sock --entrypoint flake8 fig fig +docker run -v /var/run/docker.sock:/var/run/docker.sock --entrypoint nosetests fig $@ From 0150b38b8f3e178cb79d3a56a7bde9148bc753f6 Mon Sep 17 00:00:00 2001 From: Alex Brandt Date: Sat, 25 Oct 2014 21:25:20 -0500 Subject: [PATCH 0677/2154] Add tests to sdist. In order to validate installation it's very convenient to have the tests as part of the source distribution. This greatly assists native packaging that might occur (i.e. Gentoo ebuilds). Signed-off-by: Alex Brandt --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 1328f20e..ca9ecbd5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,7 @@ include requirements.txt include requirements-dev.txt include tox.ini include *.md -recursive-exclude tests * +recursive-include tests * global-exclude *.pyc global-exclude *.pyo global-exclude *.un~ From e34a62956e21672c6b3676cf9cc56edc182edce1 Mon Sep 17 00:00:00 2001 From: Dan Tenenbaum Date: Wed, 3 Dec 2014 09:44:35 -0800 Subject: [PATCH 0678/2154] interpolate service_name in error message when service has no configuration options. Signed-off-by: Dan Tenenbaum --- fig/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fig/project.py b/fig/project.py index 569df38d..93c102c7 100644 --- a/fig/project.py +++ b/fig/project.py @@ -67,7 +67,7 @@ class Project(object): dicts = [] for service_name, service in list(config.items()): if not isinstance(service, dict): - raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.') + raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.' % service_name) service['name'] = service_name dicts.append(service) return cls.from_dicts(name, dicts, client) From 4e8337c16892d6429822ee06548397b99ddbc52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC?= Date: Mon, 1 Dec 2014 16:50:38 +0100 Subject: [PATCH 0679/2154] Respect --allow-insecure-ssl option for dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Étienne Bersac --- fig/cli/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fig/cli/main.py b/fig/cli/main.py index e98fea86..2c6a0402 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -293,6 +293,7 @@ class TopLevelCommand(Command): service_names=deps, start_links=True, recreate=False, + insecure_registry=insecure_registry, ) tty = True From 5c581805386d5088dabc3656df1afdb69084fb85 Mon Sep 17 00:00:00 2001 From: Tyler Fenby Date: Thu, 6 Nov 2014 14:38:58 -0500 Subject: [PATCH 0680/2154] Add capability add/drop introduced in Docker 1.2 Signed-off-by: Tyler Fenby --- docs/yml.md | 14 ++++++++++++++ fig/service.py | 10 +++++++--- tests/integration/service_test.py | 10 ++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index 059d165c..3096ba83 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -142,6 +142,20 @@ dns: - 9.9.9.9 ``` +### cap_add, cap_drop + +Add or drop container capabilities. +See `man 7 capabilities` for a full list. + +``` +cap_add: + - ALL + +cap_drop: + - NET_ADMIN + - SYS_ADMIN +``` + ### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. diff --git a/fig/service.py b/fig/service.py index 1685111c..645b6adf 100644 --- a/fig/service.py +++ b/fig/service.py @@ -15,7 +15,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir', 'restart'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir', 'restart', 'cap_add', 'cap_drop'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -261,6 +261,8 @@ class Service(object): privileged = options.get('privileged', False) net = options.get('net', 'bridge') dns = options.get('dns', None) + cap_add = options.get('cap_add', None) + cap_drop = options.get('cap_drop', None) restart = parse_restart_spec(options.get('restart', None)) @@ -272,7 +274,9 @@ class Service(object): privileged=privileged, network_mode=net, dns=dns, - restart_policy=restart + restart_policy=restart, + cap_add=cap_add, + cap_drop=cap_drop, ) return container @@ -379,7 +383,7 @@ class Service(object): container_options['image'] = self._build_tag_name() # Delete options which are only used when starting - for key in ['privileged', 'net', 'dns', 'restart']: + for key in ['privileged', 'net', 'dns', 'restart', 'cap_add', 'cap_drop']: if key in container_options: del container_options[key] diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 117cf99d..9d3e0b12 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -376,6 +376,16 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(container['HostConfig']['RestartPolicy']['Name'], 'on-failure') self.assertEqual(container['HostConfig']['RestartPolicy']['MaximumRetryCount'], 5) + def test_cap_add_list(self): + service = self.create_service('web', cap_add=['SYS_ADMIN', 'NET_ADMIN']) + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['CapAdd'], ['SYS_ADMIN', 'NET_ADMIN']) + + def test_cap_drop_list(self): + service = self.create_service('web', cap_drop=['SYS_ADMIN', 'NET_ADMIN']) + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['CapDrop'], ['SYS_ADMIN', 'NET_ADMIN']) + def test_working_dir_param(self): service = self.create_service('container', working_dir='/working/dir/sample') container = service.create_container().inspect() From ab77cef7ab41119f3e899af169c0a9053c0ce380 Mon Sep 17 00:00:00 2001 From: Alexander Leishman Date: Mon, 8 Dec 2014 13:04:02 -0800 Subject: [PATCH 0681/2154] Add favicon.ico and links in header Signed-off-by: Alexander Leishman --- docs/_layouts/default.html | 2 ++ docs/img/favicon.ico | Bin 0 -> 1150 bytes 2 files changed, 2 insertions(+) create mode 100644 docs/img/favicon.ico diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index cc191918..1f0de508 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -8,6 +8,8 @@ + +
    diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..71c02f1136c5ab430b5c0a61db023684877cf546 GIT binary patch literal 1150 zcmZuwc}x>l7=Nf+7MCT=Kk(nhjG+t|_gxCK1xh)JN|_u<87K}StRRAJAcPnaflWjN zDdG|a2F6j2f`wktld}}jLOBLP0&JRT;sU$)?TLxWjCuKe-}}DzzTbD`Jwj~gvb7~t zenQS4BE*gma-4#?sOPnxDnjVlkq=*E-+qU1aEm38`IW@$WVT&W(T%LiZkeHWXmY2t zZG5+|Su-VS>d8#KcJrh3N-;@uJoZ0^^rA|VQBZO)zVJpwK~wKY*F)={ExHY)UY-L# zV+t{6Cy;yFv|G}pz9SNM9LZ~v?8kGzE+)*>0vkc$^<&{VRi(oEfoIkF6*Tle0sku_ zTp0>5V@BZ_qC`;iC@wY3?v)Q1%ekW3Ly2W~3Fkt^`=y<#bB%iQV!mX2Pf%+@eDMq- zQ^w#PD2F>^7;cOqFnCIMg~$*l(BYco@%EKYO)w$v9VqLNi-uP={%D)pLS}^pf^TLJ z6sHG+=JX3!!-FA*i~j)pB4qg7djL+}GGtfH;kt5GoO$a$r`@!$)u!3R+3#i%9IHhj zPXp#D0|F@rE5V2Wo(j(XQn&^^p!xx=L5}2$)2Pu~7c1rCT~)m+NVqVCAezrBbQs<& zB|-#7Fi&gYAJI?yAA~1+7@nb0obZsq-nj=pYz2z$&+pvUFF#Grn*ryv4(xa>d?VDf zW)*lsBN)*VxCZyZnfCAGBZ0GTFC0Dk;2S1~PnZf9f3)mX%U5<2vZld0r3Ej=0484x zzbIP&Ny>Ld19tKx*oj&?`##vabmLP;3H-v;h)J77)yOX|ZfTYt2@A~#rT0T9{JCmy zlg1E{G6_Mp8Sz;r>X*SSuos`Xbi$L_hrmb$lJcg}Xj+@7P|g;V_bmPuB^*Nlt=*ra zL|ob!qJ$F&PZ|d&!2sV#HQa)vIPNZ?96hu@1qvE0|JLX%CBmA)kAKiDv|MemBAWIb zLwS-#Cirkvbe3{(ztr!)8E=*hj$ZxvB48L9WwWT!ESZZW2K#TNCX)4&=IG6_#kxA( z+RMCZ3$D~zA-rq`D^7#pm=SOj4PXiM2VTeNTfk3(o4 zX|}GDGO6)!gZ8oa&oj%plKJ)KR?CLGeR0#Mdh(lDvAVr3S=#*je8b>#LCHNfFSq7R z{M#FTtCcvU)*m?6YB*f1wAkhk%pMhWj-SYBH;T`E-}f@9yiMf9ze@f&NXT;=Le!L? S literal 0 HcmV?d00001 From 98b6d7be78121a8a353eb670bb651d4b0321ca75 Mon Sep 17 00:00:00 2001 From: Ben Langfeld Date: Fri, 12 Sep 2014 00:57:23 -0300 Subject: [PATCH 0682/2154] Add support for 'env_file' key Signed-off-by: Ben Langfeld --- docs/yml.md | 15 +++++++ fig/service.py | 41 +++++++++++++++--- tests/fixtures/env/one.env | 4 ++ tests/fixtures/env/resolve.env | 4 ++ tests/fixtures/env/two.env | 2 + tests/integration/service_test.py | 6 +++ tests/unit/service_test.py | 69 +++++++++++++++++++++++++++++++ 7 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/env/one.env create mode 100644 tests/fixtures/env/resolve.env create mode 100644 tests/fixtures/env/two.env diff --git a/docs/yml.md b/docs/yml.md index 3096ba83..bd6914f8 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -120,6 +120,21 @@ environment: - SESSION_SECRET ``` +### env_file + +Add environment variables from a file. Can be a single value or a list. + +Environment variables specified in `environment` override these values. + +``` +env_file: + - .env +``` + +``` +RACK_ENV: development +``` + ### net Networking mode. Use the same values as the docker client `--net` parameter. diff --git a/fig/service.py b/fig/service.py index 645b6adf..6622db83 100644 --- a/fig/service.py +++ b/fig/service.py @@ -15,7 +15,7 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir', 'restart', 'cap_add', 'cap_drop'] +DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'env_file', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir', 'restart', 'cap_add', 'cap_drop'] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -372,10 +372,7 @@ class Service(object): (parse_volume_spec(v).internal, {}) for v in container_options['volumes']) - if 'environment' in container_options: - if isinstance(container_options['environment'], list): - container_options['environment'] = dict(split_env(e) for e in container_options['environment']) - container_options['environment'] = dict(resolve_env(k, v) for k, v in container_options['environment'].iteritems()) + container_options['environment'] = merge_environment(container_options) if self.can_be_built(): if len(self.client.images(name=self._build_tag_name())) == 0: @@ -383,7 +380,7 @@ class Service(object): container_options['image'] = self._build_tag_name() # Delete options which are only used when starting - for key in ['privileged', 'net', 'dns', 'restart', 'cap_add', 'cap_drop']: + for key in ['privileged', 'net', 'dns', 'restart', 'cap_add', 'cap_drop', 'env_file']: if key in container_options: del container_options[key] @@ -543,6 +540,25 @@ def split_port(port): return internal_port, (external_ip, external_port or None) +def merge_environment(options): + env = {} + + if 'env_file' in options: + if isinstance(options['env_file'], list): + for f in options['env_file']: + env.update(env_vars_from_file(f)) + else: + env.update(env_vars_from_file(options['env_file'])) + + if 'environment' in options: + if isinstance(options['environment'], list): + env.update(dict(split_env(e) for e in options['environment'])) + else: + env.update(options['environment']) + + return dict(resolve_env(k, v) for k, v in env.iteritems()) + + def split_env(env): if '=' in env: return env.split('=', 1) @@ -557,3 +573,16 @@ def resolve_env(key, val): return key, os.environ[key] else: return key, '' + + +def env_vars_from_file(filename): + """ + Read in a line delimited file of environment variables. + """ + env = {} + for line in open(filename, 'r'): + line = line.strip() + if line and not line.startswith('#'): + k, v = split_env(line) + env[k] = v + return env diff --git a/tests/fixtures/env/one.env b/tests/fixtures/env/one.env new file mode 100644 index 00000000..75a4f62f --- /dev/null +++ b/tests/fixtures/env/one.env @@ -0,0 +1,4 @@ +ONE=2 +TWO=1 +THREE=3 +FOO=bar diff --git a/tests/fixtures/env/resolve.env b/tests/fixtures/env/resolve.env new file mode 100644 index 00000000..720520d2 --- /dev/null +++ b/tests/fixtures/env/resolve.env @@ -0,0 +1,4 @@ +FILE_DEF=F1 +FILE_DEF_EMPTY= +ENV_DEF +NO_DEF diff --git a/tests/fixtures/env/two.env b/tests/fixtures/env/two.env new file mode 100644 index 00000000..3b21871a --- /dev/null +++ b/tests/fixtures/env/two.env @@ -0,0 +1,2 @@ +FOO=baz +DOO=dah diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 9d3e0b12..3eae62ae 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -397,6 +397,12 @@ class ServiceTest(DockerClientTestCase): for k,v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.iteritems(): self.assertEqual(env[k], v) + def test_env_from_file_combined_with_env(self): + service = self.create_service('web', environment=['ONE=1', 'TWO=2', 'THREE=3'], env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env']) + env = service.start_container().environment + for k,v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.iteritems(): + self.assertEqual(env[k], v) + def test_resolve_env(self): service = self.create_service('web', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}) os.environ['FILE_DEF'] = 'E1' diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 119b4144..e562ebc3 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -247,3 +247,72 @@ class ServiceVolumesTest(unittest.TestCase): self.assertEqual( binding, ('/home/user', dict(bind='/home/user', ro=False))) + +class ServiceEnvironmentTest(unittest.TestCase): + + def setUp(self): + self.mock_client = mock.create_autospec(docker.Client) + self.mock_client.containers.return_value = [] + + def test_parse_environment(self): + service = Service('foo', + environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='], + client=self.mock_client, + ) + options = service._get_container_create_options({}) + self.assertEqual( + options['environment'], + {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''} + ) + + @mock.patch.dict(os.environ) + def test_resolve_environment(self): + os.environ['FILE_DEF'] = 'E1' + os.environ['FILE_DEF_EMPTY'] = 'E2' + os.environ['ENV_DEF'] = 'E3' + service = Service('foo', + environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}, + client=self.mock_client, + ) + options = service._get_container_create_options({}) + self.assertEqual( + options['environment'], + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''} + ) + + def test_env_from_file(self): + service = Service('foo', + env_file='tests/fixtures/env/one.env', + client=self.mock_client, + ) + options = service._get_container_create_options({}) + self.assertEqual( + options['environment'], + {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'bar'} + ) + + def test_env_from_multiple_files(self): + service = Service('foo', + env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env'], + client=self.mock_client, + ) + options = service._get_container_create_options({}) + self.assertEqual( + options['environment'], + {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'} + ) + + @mock.patch.dict(os.environ) + def test_resolve_environment_from_file(self): + os.environ['FILE_DEF'] = 'E1' + os.environ['FILE_DEF_EMPTY'] = 'E2' + os.environ['ENV_DEF'] = 'E3' + service = Service('foo', + env_file=['tests/fixtures/env/resolve.env'], + client=self.mock_client, + ) + options = service._get_container_create_options({}) + self.assertEqual( + options['environment'], + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''} + ) From c12d1d73f032b191c10768c87c1b3c015664e338 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 9 Dec 2014 10:49:37 -0800 Subject: [PATCH 0683/2154] Remove containers in scripts So we don't clutter up Docker with loads of stopped containers. Signed-off-by: Ben Firshman --- script/build-docs | 2 +- script/build-linux | 4 ++-- script/test | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/script/build-docs b/script/build-docs index cafba23f..abcaec4a 100755 --- a/script/build-docs +++ b/script/build-docs @@ -1,5 +1,5 @@ #!/bin/bash set -ex pushd docs -fig run jekyll jekyll build +fig run --rm jekyll jekyll build popd diff --git a/script/build-linux b/script/build-linux index b7b6edf5..f7b99210 100755 --- a/script/build-linux +++ b/script/build-linux @@ -3,6 +3,6 @@ set -ex mkdir -p `pwd`/dist chmod 777 `pwd`/dist docker build -t fig . -docker run -u user -v `pwd`/dist:/code/dist --entrypoint pyinstaller fig -F bin/fig +docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller fig -F bin/fig mv dist/fig dist/fig-Linux-x86_64 -docker run -u user -v `pwd`/dist:/code/dist --entrypoint dist/fig-Linux-x86_64 fig --version +docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/fig-Linux-x86_64 fig --version diff --git a/script/test b/script/test index 54c2077f..e73ba893 100755 --- a/script/test +++ b/script/test @@ -1,5 +1,5 @@ #!/bin/sh set -ex docker build -t fig . -docker run -v /var/run/docker.sock:/var/run/docker.sock --entrypoint flake8 fig fig -docker run -v /var/run/docker.sock:/var/run/docker.sock --entrypoint nosetests fig $@ +docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint flake8 fig fig +docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint nosetests fig $@ From 788741025efa6d2d6d834037014c383f9f873f92 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 9 Dec 2014 10:56:36 -0800 Subject: [PATCH 0684/2154] Upgrade to docker-py 0.6.0 Force using remote API version 1.14 so Fig is still compatible with Docker 1.2. Signed-off-by: Ben Firshman --- fig/cli/docker_client.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fig/cli/docker_client.py b/fig/cli/docker_client.py index 99615e01..88f6147c 100644 --- a/fig/cli/docker_client.py +++ b/fig/cli/docker_client.py @@ -31,4 +31,4 @@ def docker_client(): ca_cert=ca_cert, ) - return Client(base_url=base_url, tls=tls_config) + return Client(base_url=base_url, tls=tls_config, version='1.14') diff --git a/requirements.txt b/requirements.txt index 59aa90f0..2ccdf59a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ PyYAML==3.10 -docker-py==0.5.3 +docker-py==0.6.0 dockerpty==0.3.2 docopt==0.6.1 requests==2.2.1 diff --git a/setup.py b/setup.py index fe839f5a..4cf8e589 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ install_requires = [ 'requests >= 2.2.1, < 3', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.11.0, < 0.12', - 'docker-py >= 0.5.3, < 0.6', + 'docker-py >= 0.6.0, < 0.7', 'dockerpty >= 0.3.2, < 0.4', 'six >= 1.3.0, < 2', ] From 3c0f297ba60b19e3f6415a8c487258b6dd28f507 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 11 Dec 2014 10:08:39 -0800 Subject: [PATCH 0685/2154] Some minor cleanup from yelp/fig Signed-off-by: Daniel Nephin --- fig/cli/docker_client.py | 3 ++- fig/service.py | 29 ++++++++++++++++++++++++++-- tests/unit/cli/docker_client_test.py | 6 ++++++ tests/unit/service_test.py | 12 +++++------- tox.ini | 4 ++-- 5 files changed, 42 insertions(+), 12 deletions(-) diff --git a/fig/cli/docker_client.py b/fig/cli/docker_client.py index 88f6147c..b2794844 100644 --- a/fig/cli/docker_client.py +++ b/fig/cli/docker_client.py @@ -31,4 +31,5 @@ def docker_client(): ca_cert=ca_cert, ) - return Client(base_url=base_url, tls=tls_config, version='1.14') + timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)) + return Client(base_url=base_url, tls=tls_config, version='1.14', timeout=timeout) diff --git a/fig/service.py b/fig/service.py index 6622db83..5789e2a5 100644 --- a/fig/service.py +++ b/fig/service.py @@ -15,7 +15,30 @@ from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) -DOCKER_CONFIG_KEYS = ['image', 'command', 'hostname', 'domainname', 'user', 'detach', 'stdin_open', 'tty', 'mem_limit', 'ports', 'environment', 'env_file', 'dns', 'volumes', 'entrypoint', 'privileged', 'volumes_from', 'net', 'working_dir', 'restart', 'cap_add', 'cap_drop'] +DOCKER_CONFIG_KEYS = [ + 'cap_add', + 'cap_drop', + 'command', + 'detach', + 'dns', + 'domainname', + 'entrypoint', + 'env_file', + 'environment', + 'hostname', + 'image', + 'mem_limit', + 'net', + 'ports', + 'privileged', + 'restart', + 'stdin_open', + 'tty', + 'user', + 'volumes', + 'volumes_from', + 'working_dir', +] DOCKER_CONFIG_HINTS = { 'link' : 'links', 'port' : 'ports', @@ -337,7 +360,9 @@ class Service(object): return volumes_from def _get_container_create_options(self, override_options, one_off=False): - container_options = dict((k, self.options[k]) for k in DOCKER_CONFIG_KEYS if k in self.options) + container_options = dict( + (k, self.options[k]) + for k in DOCKER_CONFIG_KEYS if k in self.options) container_options.update(override_options) container_options['name'] = self._next_container_name( diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index c8821073..67575ee0 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -14,3 +14,9 @@ class DockerClientTestCase(unittest.TestCase): with mock.patch.dict(os.environ): del os.environ['HOME'] docker_client.docker_client() + + def test_docker_client_with_custom_timeout(self): + with mock.patch.dict(os.environ): + os.environ['DOCKER_CLIENT_TIMEOUT'] = timeout = "300" + client = docker_client.docker_client() + self.assertEqual(client._timeout, int(timeout)) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index e562ebc3..5df6679d 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -165,23 +165,21 @@ class ServiceTest(unittest.TestCase): self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') def test_get_container_not_found(self): - mock_client = mock.create_autospec(docker.Client) - mock_client.containers.return_value = [] - service = Service('foo', client=mock_client) + self.mock_client.containers.return_value = [] + service = Service('foo', client=self.mock_client) self.assertRaises(ValueError, service.get_container) @mock.patch('fig.service.Container', autospec=True) def test_get_container(self, mock_container_class): - mock_client = mock.create_autospec(docker.Client) container_dict = dict(Name='default_foo_2') - mock_client.containers.return_value = [container_dict] - service = Service('foo', client=mock_client) + self.mock_client.containers.return_value = [container_dict] + service = Service('foo', client=self.mock_client) container = service.get_container(number=2) self.assertEqual(container, mock_container_class.from_ps.return_value) mock_container_class.from_ps.assert_called_once_with( - mock_client, container_dict) + self.mock_client, container_dict) @mock.patch('fig.service.log', autospec=True) def test_pull_image(self, mock_log): diff --git a/tox.ini b/tox.ini index f6a81d0b..a20d984b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py26,py27,py32,py33,pypy +envlist = py26,py27 [testenv] usedevelop=True @@ -7,7 +7,7 @@ deps = -rrequirements.txt -rrequirements-dev.txt commands = - nosetests {posargs} + nosetests -v {posargs} flake8 fig [flake8] From 8ebec9a67f470e94c5dd8f446f5f4197d6db486f Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Mon, 8 Dec 2014 16:03:42 -0800 Subject: [PATCH 0686/2154] Pull latest tag by default This was changed in Docker recently: https://github.com/docker/docker/pull/7759 This means we aren't pulling loads of tags when we only use the latest. Signed-off-by: Ben Firshman --- fig/service.py | 22 ++++++++++++++++++++-- tests/unit/service_test.py | 23 +++++++++++++++++++---- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/fig/service.py b/fig/service.py index 645b6adf..c86c8a97 100644 --- a/fig/service.py +++ b/fig/service.py @@ -381,6 +381,8 @@ class Service(object): if len(self.client.images(name=self._build_tag_name())) == 0: self.build() container_options['image'] = self._build_tag_name() + else: + container_options['image'] = self._get_image_name(container_options['image']) # Delete options which are only used when starting for key in ['privileged', 'net', 'dns', 'restart', 'cap_add', 'cap_drop']: @@ -389,6 +391,12 @@ class Service(object): return container_options + def _get_image_name(self, image): + repo, tag = parse_repository_tag(image) + if tag == "": + tag = "latest" + return '%s:%s' % (repo, tag) + def build(self, no_cache=False): log.info('Building %s...' % self.name) @@ -435,9 +443,10 @@ class Service(object): def pull(self, insecure_registry=False): if 'image' in self.options: - log.info('Pulling %s (%s)...' % (self.name, self.options.get('image'))) + image_name = self._get_image_name(self.options['image']) + log.info('Pulling %s (%s)...' % (self.name, image_name)) self.client.pull( - self.options.get('image'), + image_name, insecure_registry=insecure_registry ) @@ -509,6 +518,15 @@ def parse_volume_spec(volume_config): return VolumeSpec(external, internal, mode) +def parse_repository_tag(s): + if ":" not in s: + return s, "" + repo, tag = s.rsplit(":", 1) + if "/" in tag: + return s, "" + return repo, tag + + def build_volume_binding(volume_spec): internal = {'bind': volume_spec.internal, 'ro': volume_spec.mode == 'ro'} external = os.path.expanduser(volume_spec.external) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 119b4144..b4c8a6dc 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -17,6 +17,7 @@ from fig.service import ( parse_volume_spec, build_volume_binding, APIError, + parse_repository_tag, ) @@ -131,7 +132,7 @@ class ServiceTest(unittest.TestCase): def test_split_domainname_none(self): service = Service('foo', hostname='name', client=self.mock_client) self.mock_client.containers.return_value = [] - opts = service._get_container_create_options({}) + opts = service._get_container_create_options({'image': 'foo'}) self.assertEqual(opts['hostname'], 'name', 'hostname') self.assertFalse('domainname' in opts, 'domainname') @@ -140,7 +141,7 @@ class ServiceTest(unittest.TestCase): hostname='name.domain.tld', client=self.mock_client) self.mock_client.containers.return_value = [] - opts = service._get_container_create_options({}) + opts = service._get_container_create_options({'image': 'foo'}) self.assertEqual(opts['hostname'], 'name', 'hostname') self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') @@ -150,7 +151,7 @@ class ServiceTest(unittest.TestCase): domainname='domain.tld', client=self.mock_client) self.mock_client.containers.return_value = [] - opts = service._get_container_create_options({}) + opts = service._get_container_create_options({'image': 'foo'}) self.assertEqual(opts['hostname'], 'name', 'hostname') self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') @@ -160,7 +161,7 @@ class ServiceTest(unittest.TestCase): domainname='domain.tld', client=self.mock_client) self.mock_client.containers.return_value = [] - opts = service._get_container_create_options({}) + opts = service._get_container_create_options({'image': 'foo'}) self.assertEqual(opts['hostname'], 'name.sub', 'hostname') self.assertEqual(opts['domainname'], 'domain.tld', 'domainname') @@ -205,6 +206,20 @@ class ServiceTest(unittest.TestCase): self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True, stream=True) mock_log.info.assert_called_once_with('Pulling image someimage:sometag...') + def test_parse_repository_tag(self): + self.assertEqual(parse_repository_tag("root"), ("root", "")) + self.assertEqual(parse_repository_tag("root:tag"), ("root", "tag")) + self.assertEqual(parse_repository_tag("user/repo"), ("user/repo", "")) + self.assertEqual(parse_repository_tag("user/repo:tag"), ("user/repo", "tag")) + self.assertEqual(parse_repository_tag("url:5000/repo"), ("url:5000/repo", "")) + self.assertEqual(parse_repository_tag("url:5000/repo:tag"), ("url:5000/repo", "tag")) + + def test_latest_is_used_when_tag_is_not_specified(self): + service = Service('foo', client=self.mock_client, image='someimage') + Container.create = mock.Mock() + service.create_container() + self.assertEqual(Container.create.call_args[1]['image'], 'someimage:latest') + class ServiceVolumesTest(unittest.TestCase): From 8f8e322de26e546d17f61998cbbc337c0d1b025b Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 11 Dec 2014 14:25:26 -0800 Subject: [PATCH 0687/2154] Include image name for env_file tests. Signed-off-by: Daniel Nephin --- tests/unit/service_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 8c9ed69f..336f783f 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -271,6 +271,7 @@ class ServiceEnvironmentTest(unittest.TestCase): service = Service('foo', environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS='], client=self.mock_client, + image='image_name', ) options = service._get_container_create_options({}) self.assertEqual( @@ -286,6 +287,7 @@ class ServiceEnvironmentTest(unittest.TestCase): service = Service('foo', environment={'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': None, 'NO_DEF': None}, client=self.mock_client, + image='image_name', ) options = service._get_container_create_options({}) self.assertEqual( @@ -297,6 +299,7 @@ class ServiceEnvironmentTest(unittest.TestCase): service = Service('foo', env_file='tests/fixtures/env/one.env', client=self.mock_client, + image='image_name', ) options = service._get_container_create_options({}) self.assertEqual( @@ -308,6 +311,7 @@ class ServiceEnvironmentTest(unittest.TestCase): service = Service('foo', env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env'], client=self.mock_client, + image='image_name', ) options = service._get_container_create_options({}) self.assertEqual( @@ -323,6 +327,7 @@ class ServiceEnvironmentTest(unittest.TestCase): service = Service('foo', env_file=['tests/fixtures/env/resolve.env'], client=self.mock_client, + image='image_name', ) options = service._get_container_create_options({}) self.assertEqual( From cc834aa5642f2648a18a035478e03a5ce42e8e3e Mon Sep 17 00:00:00 2001 From: Stephen Quebe Date: Mon, 15 Sep 2014 18:18:03 -0400 Subject: [PATCH 0688/2154] Added map service ports option for run command. When using the fig run command, ports defined in fig.yml can be mapped to the host computer using the -service-ports option. Signed-off-by: Stephen Quebe --- docs/cli.md | 5 ++++- fig/cli/main.py | 10 ++++++++-- tests/integration/cli_test.py | 37 +++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 822f1b78..74c82bed 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -77,7 +77,7 @@ For example: 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. +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 by default no ports will be created in case they collide. Links are also created between one-off commands and the other containers for that service so you can do stuff like this: @@ -87,6 +87,9 @@ If you do not want linked containers to be started when running the one-off comm $ fig run --no-deps web python manage.py shell +If you want the service's ports to be created and mapped to the host, specify the `--service-ports` flag: + $ fig run --service-ports web python manage.py shell + ### scale Set number of containers to run for a service. diff --git a/fig/cli/main.py b/fig/cli/main.py index 2c6a0402..2ba411f7 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -278,6 +278,8 @@ class TopLevelCommand(Command): -e KEY=VAL Set an environment variable (can be used multiple times) --no-deps Don't start linked services. --rm Remove container after run. Ignored in detached mode. + --service-ports Run command with the service's ports enabled and mapped + to the host. -T Disable pseudo-tty allocation. By default `fig run` allocates a TTY. """ @@ -325,11 +327,15 @@ class TopLevelCommand(Command): insecure_registry=insecure_registry, **container_options ) + + service_ports = None + if options['--service-ports']: + service_ports = service.options['ports'] if options['-d']: - service.start_container(container, ports=None, one_off=True) + service.start_container(container, ports=service_ports, one_off=True) print(container.name) else: - service.start_container(container, ports=None, one_off=True) + service.start_container(container, ports=service_ports, one_off=True) dockerpty.start(project.client, container.id, interactive=not options['-T']) exit_code = container.wait() if options['--rm']: diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index f03d72d2..cdeca921 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -237,6 +237,43 @@ class CLITestCase(DockerClientTestCase): # make sure a value with a = don't crash out self.assertEqual('moto=bobo', container.environment['allo']) + @patch('dockerpty.start') + def test_run_service_without_map_ports(self, __): + # create one off container + self.command.base_dir = 'tests/fixtures/ports-figfile' + self.command.dispatch(['run', '-d', 'simple'], None) + container = self.project.get_service('simple').containers(one_off=True)[0] + + # get port information + port_random = container.get_local_port(3000) + port_assigned = container.get_local_port(3001) + + # close all one off containers we just created + container.stop() + + # check the ports + self.assertEqual(port_random, None) + self.assertEqual(port_assigned, None) + + @patch('dockerpty.start') + def test_run_service_with_map_ports(self, __): + # create one off container + self.command.base_dir = 'tests/fixtures/ports-figfile' + self.command.dispatch(['run', '-d', '--service-ports', 'simple'], None) + container = self.project.get_service('simple').containers(one_off=True)[0] + + # get port information + port_random = container.get_local_port(3000) + port_assigned = container.get_local_port(3001) + + # close all one off containers we just created + container.stop() + + # check the ports + self.assertNotEqual(port_random, None) + self.assertIn("0.0.0.0", port_random) + self.assertEqual(port_assigned, "0.0.0.0:9999") + def test_rm(self): service = self.project.get_service('simple') service.create_container() From 05544ce2417336f11e61ffc734fb6014dc76a58d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 12 Dec 2014 17:53:02 -0800 Subject: [PATCH 0689/2154] Stronger integration tests for volume binds Signed-off-by: Aanand Prasad --- tests/integration/service_test.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 3eae62ae..864b30a9 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import os +from os import path from fig import Service from fig.service import CannotBeScaledError @@ -95,10 +96,20 @@ class ServiceTest(DockerClientTestCase): self.assertIn('/var/db', container.inspect()['Volumes']) def test_create_container_with_specified_volume(self): - service = self.create_service('db', volumes=['/tmp:/host-tmp']) + host_path = '/tmp/host-path' + container_path = '/container-path' + + service = self.create_service('db', volumes=['%s:%s' % (host_path, container_path)]) container = service.create_container() service.start_container(container) - self.assertIn('/host-tmp', container.inspect()['Volumes']) + + volumes = container.inspect()['Volumes'] + self.assertIn(container_path, volumes) + + # Match the last component ("host-path"), because boot2docker symlinks /tmp + actual_host_path = volumes[container_path] + self.assertTrue(path.basename(actual_host_path) == path.basename(host_path), + msg=("Last component differs: %s, %s" % (actual_host_path, host_path))) def test_create_container_with_volumes_from(self): volume_service = self.create_service('data') From 5182bd0968473f9dd6af48900faf827c44d9ae89 Mon Sep 17 00:00:00 2001 From: Mohammad Salehe Date: Sun, 17 Aug 2014 17:48:24 +0430 Subject: [PATCH 0690/2154] Add dns_search support in yml config Signed-off-by: Mohammad Salehe --- docs/yml.md | 11 +++++++++++ fig/service.py | 5 ++++- tests/integration/service_test.py | 10 ++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/yml.md b/docs/yml.md index bd6914f8..9ee2d27a 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -171,6 +171,17 @@ cap_drop: - SYS_ADMIN ``` +### dns_search + +Custom DNS search domains. Can be a single value or a list. + +``` +dns_search: example.com +dns: + - dc1.example.com + - dc2.example.com +``` + ### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. diff --git a/fig/service.py b/fig/service.py index 2abcc6d4..7c34be92 100644 --- a/fig/service.py +++ b/fig/service.py @@ -21,6 +21,7 @@ DOCKER_CONFIG_KEYS = [ 'command', 'detach', 'dns', + 'dns_search', 'domainname', 'entrypoint', 'env_file', @@ -284,6 +285,7 @@ class Service(object): privileged = options.get('privileged', False) net = options.get('net', 'bridge') dns = options.get('dns', None) + dns_search = options.get('dns_search', None) cap_add = options.get('cap_add', None) cap_drop = options.get('cap_drop', None) @@ -297,6 +299,7 @@ class Service(object): privileged=privileged, network_mode=net, dns=dns, + dns_search=dns_search, restart_policy=restart, cap_add=cap_add, cap_drop=cap_drop, @@ -407,7 +410,7 @@ class Service(object): container_options['image'] = self._get_image_name(container_options['image']) # Delete options which are only used when starting - for key in ['privileged', 'net', 'dns', 'restart', 'cap_add', 'cap_drop', 'env_file']: + for key in ['privileged', 'net', 'dns', 'dns_search', 'restart', 'cap_add', 'cap_drop', 'env_file']: if key in container_options: del container_options[key] diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 864b30a9..d755b5af 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -397,6 +397,16 @@ class ServiceTest(DockerClientTestCase): container = service.start_container().inspect() self.assertEqual(container['HostConfig']['CapDrop'], ['SYS_ADMIN', 'NET_ADMIN']) + def test_dns_search_single_value(self): + service = self.create_service('web', dns_search='example.com') + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['DnsSearch'], ['example.com']) + + def test_dns_search_list(self): + service = self.create_service('web', dns_search=['dc1.example.com', 'dc2.example.com']) + container = service.start_container().inspect() + self.assertEqual(container['HostConfig']['DnsSearch'], ['dc1.example.com', 'dc2.example.com']) + def test_working_dir_param(self): service = self.create_service('container', working_dir='/working/dir/sample') container = service.create_container().inspect() From 3c105c6db2902e786145d51ac21271265aa96282 Mon Sep 17 00:00:00 2001 From: Mohammad Salehe Date: Wed, 1 Oct 2014 20:10:50 +0330 Subject: [PATCH 0691/2154] Fix typo in dns_search documentation Signed-off-by: Mohammad Salehe --- docs/yml.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/yml.md b/docs/yml.md index 9ee2d27a..a911e450 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -177,7 +177,7 @@ Custom DNS search domains. Can be a single value or a list. ``` dns_search: example.com -dns: +dns_search: - dc1.example.com - dc2.example.com ``` From e89826fe43969dcc450d98b397e6222b1414e103 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 17 Dec 2014 18:31:22 -0800 Subject: [PATCH 0692/2154] Don't attach stdin and stdout when in detach mode This is primarily to make it work with Swarm, which checks that AttachStd{in,out,err} is false when creating containers. Signed-off-by: Ben Firshman --- fig/cli/main.py | 3 +++ fig/project.py | 6 +++--- fig/service.py | 10 +++++++--- tests/integration/cli_test.py | 13 +++++++++++++ tests/integration/service_test.py | 8 ++++++++ 5 files changed, 34 insertions(+), 6 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index 2c6a0402..5f5861c3 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -294,6 +294,7 @@ class TopLevelCommand(Command): start_links=True, recreate=False, insecure_registry=insecure_registry, + detach=options['-d'] ) tty = True @@ -309,6 +310,7 @@ class TopLevelCommand(Command): 'command': command, 'tty': tty, 'stdin_open': not options['-d'], + 'detach': options['-d'], } if options['-e']: @@ -432,6 +434,7 @@ class TopLevelCommand(Command): start_links=start_links, recreate=recreate, insecure_registry=insecure_registry, + detach=options['-d'] ) to_attach = [c for s in project.get_services(service_names) for c in s.containers()] diff --git a/fig/project.py b/fig/project.py index 93c102c7..8d71a4c2 100644 --- a/fig/project.py +++ b/fig/project.py @@ -167,14 +167,14 @@ class Project(object): else: log.info('%s uses an image, skipping' % service.name) - def up(self, service_names=None, start_links=True, recreate=True, insecure_registry=False): + def up(self, service_names=None, start_links=True, recreate=True, insecure_registry=False, detach=False): running_containers = [] for service in self.get_services(service_names, include_links=start_links): if recreate: - for (_, container) in service.recreate_containers(insecure_registry=insecure_registry): + for (_, container) in service.recreate_containers(insecure_registry=insecure_registry, detach=detach): running_containers.append(container) else: - for container in service.start_or_create_containers(insecure_registry=insecure_registry): + for container in service.start_or_create_containers(insecure_registry=insecure_registry, detach=detach): running_containers.append(container) return running_containers diff --git a/fig/service.py b/fig/service.py index 2abcc6d4..5693083d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -157,7 +157,7 @@ class Service(object): # Create enough containers containers = self.containers(stopped=True) while len(containers) < desired_num: - containers.append(self.create_container()) + containers.append(self.create_container(detach=True)) running_containers = [] stopped_containers = [] @@ -251,6 +251,7 @@ class Service(object): image=container.image, entrypoint=['/bin/echo'], command=[], + detach=True, ) intermediate_container.start(volumes_from=container.id) intermediate_container.wait() @@ -303,12 +304,15 @@ class Service(object): ) return container - def start_or_create_containers(self, insecure_registry=False): + def start_or_create_containers(self, insecure_registry=False, detach=False): containers = self.containers(stopped=True) if not containers: log.info("Creating %s..." % self._next_container_name(containers)) - new_container = self.create_container(insecure_registry=insecure_registry) + new_container = self.create_container( + insecure_registry=insecure_registry, + detach=detach + ) return [self.start_container(new_container)] else: return [self.start_container_if_stopped(c) for c in containers] diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index f03d72d2..76e033c4 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -92,6 +92,12 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(service.containers()), 1) self.assertEqual(len(another.containers()), 1) + # Ensure containers don't have stdin and stdout connected in -d mode + config = service.containers()[0].inspect()['Config'] + self.assertFalse(config['AttachStderr']) + self.assertFalse(config['AttachStdout']) + self.assertFalse(config['AttachStdin']) + def test_up_with_links(self): self.command.base_dir = 'tests/fixtures/links-figfile' self.command.dispatch(['up', '-d', 'web'], None) @@ -146,6 +152,13 @@ class CLITestCase(DockerClientTestCase): self.command.dispatch(['run', 'console', '/bin/true'], None) self.assertEqual(len(self.project.containers()), 0) + # Ensure stdin/out was open + container = self.project.containers(stopped=True, one_off=True)[0] + config = container.inspect()['Config'] + self.assertTrue(config['AttachStderr']) + self.assertTrue(config['AttachStdout']) + self.assertTrue(config['AttachStdin']) + @patch('dockerpty.start') def test_run_service_with_links(self, __): self.command.base_dir = 'tests/fixtures/links-figfile' diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 864b30a9..e29ee7e9 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -332,6 +332,14 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('web') service.scale(1) self.assertEqual(len(service.containers()), 1) + + # Ensure containers don't have stdout or stdin connected + container = service.containers()[0] + config = container.inspect()['Config'] + self.assertFalse(config['AttachStderr']) + self.assertFalse(config['AttachStdout']) + self.assertFalse(config['AttachStdin']) + service.scale(3) self.assertEqual(len(service.containers()), 3) service.scale(1) From 42577072443384d2dc35cc4f827bc57239a42757 Mon Sep 17 00:00:00 2001 From: Jason Bernardino Alonso Date: Wed, 8 Oct 2014 22:31:04 -0400 Subject: [PATCH 0693/2154] Accept an external_links list in the service configuration dictionary to create links to containers outside of the project Signed-off-by: Jason Bernardino Alonso Signed-off-by: Mauricio de Abreu Antunes --- docs/yml.md | 12 ++++++++++++ fig/service.py | 12 ++++++++++-- tests/integration/service_test.py | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/yml.md b/docs/yml.md index a911e450..71d2cb7a 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -58,6 +58,18 @@ An entry with the alias' name will be created in `/etc/hosts` inside containers Environment variables will also be created - see the [environment variable reference](env.html) for details. +### external_links + +Link to containers started outside this `fig.yml` or even outside of fig, especially for containers that provide shared or common services. +`external_links` follow semantics similar to `links` when specifying both the container name and the link alias (`CONTAINER:ALIAS`). + +``` +external_links: + - redis_1 + - project_db_1:mysql + - project_db_1:postgresql +``` + ### ports Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen). diff --git a/fig/service.py b/fig/service.py index bd3000c6..f88b466d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -74,7 +74,7 @@ ServiceName = namedtuple('ServiceName', 'project service number') class Service(object): - def __init__(self, name, client=None, project='default', links=None, volumes_from=None, **options): + def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, **options): if not re.match('^%s+$' % VALID_NAME_CHARS, name): raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS)) if not re.match('^%s+$' % VALID_NAME_CHARS, project): @@ -82,7 +82,8 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) - supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose'] + supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose', + 'external_links'] for k in options: if k not in supported_options: @@ -95,6 +96,7 @@ class Service(object): self.client = client self.project = project self.links = links or [] + self.external_links = external_links or [] self.volumes_from = volumes_from or [] self.options = options @@ -345,6 +347,12 @@ class Service(object): links.append((container.name, self.name)) links.append((container.name, container.name)) links.append((container.name, container.name_without_project)) + for external_link in self.external_links: + if ':' not in external_link: + link_name = external_link + else: + external_link, link_name = external_link.split(':') + links.append((external_link, link_name)) return links def _get_volumes_from(self, intermediate_container=None): diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index a1740272..bbe348ee 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -219,6 +219,26 @@ class ServiceTest(DockerClientTestCase): ]), ) + def test_start_container_with_external_links(self): + db = self.create_service('db') + web = self.create_service('web', external_links=['figtest_db_1', + 'figtest_db_2', + 'figtest_db_3:db_3']) + + db.start_container() + db.start_container() + db.start_container() + web.start_container() + + self.assertEqual( + set(web.containers()[0].links()), + set([ + 'figtest_db_1', + 'figtest_db_2', + 'db_3', + ]), + ) + def test_start_normal_container_does_not_create_links_to_its_own_service(self): db = self.create_service('db') From c885aaa5f8aa2ee9af8492ea5736930509161f18 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 31 Dec 2014 18:05:34 +0000 Subject: [PATCH 0694/2154] ROADMAP.md Signed-off-by: Aanand Prasad --- ROADMAP.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 ROADMAP.md diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..c99188f0 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,20 @@ +# Roadmap + +## Fig 1.1 + +- All this cool stuff: https://github.com/docker/fig/issues?q=milestone%3A1.1.0+ + +## Compose 1.2 + +- Project-wide rename and rebrand to Docker Compose, with new names for the command-line tool and configuration file +- Version specifier in configuration file +- A “fig watch” command which automatically kicks off builds while editing code +- It should be possible to somehow define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by e.g. apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp) +- A way to share config between apps ([#318](https://github.com/docker/fig/issues/318)) + +## Future + +- Fig uses Docker container names to separate and identify containers in different apps and belonging to different services within apps; this should really be done in a less hacky and more performant way, by attaching metadata to containers and doing the filtering on the server side. **This requires changes to the Docker daemon.** +- The config file should be parameterisable so that config can be partially modified for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#426](https://github.com/docker/fig/issues/426)) +- Fig’s brute-force “delete and recreate everything” approach is great for dev and testing, and with manual command-line scoping can be made to work in production (e.g. “fig up -d web” will update *just* the web service), but a smarter solution is needed, its logic probably based around convergence from “current” to “desired” app state. +- Compose should recommend a simple technique for zero-downtime deploys. This will likely involve new Docker networking methods that allow for a load balancer container to be dynamically hooked up to new app containers and disconnected from old ones. From 5b777ee5f18c0c8e3766550c62b69a89fa7a5642 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 26 Oct 2014 13:22:16 -0400 Subject: [PATCH 0695/2154] Cleanup service unit tests and restructure some service create logic. Signed-off-by: Daniel Nephin --- fig/service.py | 49 +++++++++++++----- tests/integration/service_test.py | 85 ++++++++++++++++--------------- tests/unit/service_test.py | 26 +++++++--- 3 files changed, 98 insertions(+), 62 deletions(-) diff --git a/fig/service.py b/fig/service.py index bd3000c6..461bc1fe 100644 --- a/fig/service.py +++ b/fig/service.py @@ -50,6 +50,17 @@ DOCKER_CONFIG_HINTS = { 'workdir' : 'working_dir', } +DOCKER_START_KEYS = [ + 'cap_add', + 'cap_drop', + 'dns', + 'dns_search', + 'env_file', + 'net', + 'privileged', + 'restart', +] + VALID_NAME_CHARS = '[a-zA-Z0-9]' @@ -145,7 +156,8 @@ class Service(object): def scale(self, desired_num): """ - Adjusts the number of containers to the specified number and ensures they are running. + Adjusts the number of containers to the specified number and ensures + they are running. - creates containers until there are at least `desired_num` - stops containers until there are at most `desired_num` running @@ -192,12 +204,24 @@ class Service(object): log.info("Removing %s..." % c.name) c.remove(**options) - def create_container(self, one_off=False, insecure_registry=False, **override_options): + def create_container(self, + one_off=False, + insecure_registry=False, + do_build=True, + **override_options): """ Create a container for this service. If the image doesn't exist, attempt to pull it. """ - container_options = self._get_container_create_options(override_options, one_off=one_off) + container_options = self._get_container_create_options( + override_options, + one_off=one_off) + + if (do_build and + self.can_be_built() and + not self.client.images(name=self.full_name)): + self.build() + try: return Container.create(self.client, **container_options) except APIError as e: @@ -273,8 +297,7 @@ class Service(object): log.info("Starting %s..." % container.name) return self.start_container(container, **options) - def start_container(self, container=None, intermediate_container=None, **override_options): - container = container or self.create_container(**override_options) + def start_container(self, container, intermediate_container=None, **override_options): options = dict(self.options, **override_options) port_bindings = build_port_bindings(options.get('ports') or []) @@ -407,16 +430,13 @@ class Service(object): container_options['environment'] = merge_environment(container_options) if self.can_be_built(): - if len(self.client.images(name=self._build_tag_name())) == 0: - self.build() - container_options['image'] = self._build_tag_name() + container_options['image'] = self.full_name else: container_options['image'] = self._get_image_name(container_options['image']) # Delete options which are only used when starting - for key in ['privileged', 'net', 'dns', 'dns_search', 'restart', 'cap_add', 'cap_drop', 'env_file']: - if key in container_options: - del container_options[key] + for key in DOCKER_START_KEYS: + container_options.pop(key, None) return container_options @@ -431,7 +451,7 @@ class Service(object): build_output = self.client.build( self.options['build'], - tag=self._build_tag_name(), + tag=self.full_name, stream=True, rm=True, nocache=no_cache, @@ -451,14 +471,15 @@ class Service(object): image_id = match.group(1) if image_id is None: - raise BuildError(self) + raise BuildError(self, event if all_events else 'Unknown') return image_id def can_be_built(self): return 'build' in self.options - def _build_tag_name(self): + @property + def full_name(self): """ The tag to give to images built for this service. """ diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index a1740272..0e08ac1c 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -10,19 +10,24 @@ from docker.errors import APIError from .testcases import DockerClientTestCase +def create_and_start_container(service, **override_options): + container = service.create_container(**override_options) + return service.start_container(container, **override_options) + + class ServiceTest(DockerClientTestCase): def test_containers(self): foo = self.create_service('foo') bar = self.create_service('bar') - foo.start_container() + create_and_start_container(foo) self.assertEqual(len(foo.containers()), 1) self.assertEqual(foo.containers()[0].name, 'figtest_foo_1') self.assertEqual(len(bar.containers()), 0) - bar.start_container() - bar.start_container() + create_and_start_container(bar) + create_and_start_container(bar) self.assertEqual(len(foo.containers()), 1) self.assertEqual(len(bar.containers()), 2) @@ -39,7 +44,7 @@ class ServiceTest(DockerClientTestCase): def test_project_is_added_to_container_name(self): service = self.create_service('web') - service.start_container() + create_and_start_container(service) self.assertEqual(service.containers()[0].name, 'figtest_web_1') def test_start_stop(self): @@ -65,7 +70,7 @@ class ServiceTest(DockerClientTestCase): def test_kill_remove(self): service = self.create_service('scalingtest') - service.start_container() + create_and_start_container(service) self.assertEqual(len(service.containers()), 1) service.remove_stopped() @@ -177,21 +182,21 @@ class ServiceTest(DockerClientTestCase): def test_start_container_passes_through_options(self): db = self.create_service('db') - db.start_container(environment={'FOO': 'BAR'}) + create_and_start_container(db, environment={'FOO': 'BAR'}) self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') def test_start_container_inherits_options_from_constructor(self): db = self.create_service('db', environment={'FOO': 'BAR'}) - db.start_container() + create_and_start_container(db) self.assertEqual(db.containers()[0].environment['FOO'], 'BAR') def test_start_container_creates_links(self): db = self.create_service('db') web = self.create_service('web', links=[(db, None)]) - db.start_container() - db.start_container() - web.start_container() + create_and_start_container(db) + create_and_start_container(db) + create_and_start_container(web) self.assertEqual( set(web.containers()[0].links()), @@ -206,9 +211,9 @@ class ServiceTest(DockerClientTestCase): db = self.create_service('db') web = self.create_service('web', links=[(db, 'custom_link_name')]) - db.start_container() - db.start_container() - web.start_container() + create_and_start_container(db) + create_and_start_container(db) + create_and_start_container(web) self.assertEqual( set(web.containers()[0].links()), @@ -222,19 +227,19 @@ class ServiceTest(DockerClientTestCase): def test_start_normal_container_does_not_create_links_to_its_own_service(self): db = self.create_service('db') - db.start_container() - db.start_container() + create_and_start_container(db) + create_and_start_container(db) - c = db.start_container() + c = create_and_start_container(db) self.assertEqual(set(c.links()), set([])) def test_start_one_off_container_creates_links_to_its_own_service(self): db = self.create_service('db') - db.start_container() - db.start_container() + create_and_start_container(db) + create_and_start_container(db) - c = db.start_container(one_off=True) + c = create_and_start_container(db, one_off=True) self.assertEqual( set(c.links()), @@ -252,7 +257,7 @@ class ServiceTest(DockerClientTestCase): build='tests/fixtures/simple-dockerfile', project='figtest', ) - container = service.start_container() + container = create_and_start_container(service) container.wait() self.assertIn('success', container.logs()) self.assertEqual(len(self.client.images(name='figtest_test')), 1) @@ -265,45 +270,45 @@ class ServiceTest(DockerClientTestCase): build='this/does/not/exist/and/will/throw/error', project='figtest', ) - container = service.start_container() + container = create_and_start_container(service) container.wait() self.assertIn('success', container.logs()) def test_start_container_creates_ports(self): service = self.create_service('web', ports=[8000]) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/tcp']) self.assertNotEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_stays_unpriviliged(self): service = self.create_service('web') - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], False) def test_start_container_becomes_priviliged(self): service = self.create_service('web', privileged = True) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], True) def test_expose_does_not_publish_ports(self): service = self.create_service('web', expose=[8000]) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertEqual(container['NetworkSettings']['Ports'], {'8000/tcp': None}) def test_start_container_creates_port_with_explicit_protocol(self): service = self.create_service('web', ports=['8000/udp']) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertEqual(list(container['NetworkSettings']['Ports'].keys()), ['8000/udp']) def test_start_container_creates_fixed_external_ports(self): service = self.create_service('web', ports=['8000:8000']) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertIn('8000/tcp', container['NetworkSettings']['Ports']) self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8000') def test_start_container_creates_fixed_external_ports_when_it_is_different_to_internal_port(self): service = self.create_service('web', ports=['8001:8000']) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertIn('8000/tcp', container['NetworkSettings']['Ports']) self.assertEqual(container['NetworkSettings']['Ports']['8000/tcp'][0]['HostPort'], '8001') @@ -312,7 +317,7 @@ class ServiceTest(DockerClientTestCase): '127.0.0.1:8001:8000', '0.0.0.0:9001:9000/udp', ]) - container = service.start_container().inspect() + container = create_and_start_container(service).inspect() self.assertEqual(container['NetworkSettings']['Ports'], { '8000/tcp': [ { @@ -361,28 +366,28 @@ class ServiceTest(DockerClientTestCase): def test_network_mode_none(self): service = self.create_service('web', net='none') - container = service.start_container() + container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.NetworkMode'), 'none') def test_network_mode_bridged(self): service = self.create_service('web', net='bridge') - container = service.start_container() + container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge') def test_network_mode_host(self): service = self.create_service('web', net='host') - container = service.start_container() + container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.NetworkMode'), 'host') def test_dns_single_value(self): service = self.create_service('web', dns='8.8.8.8') - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8']) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8']) def test_dns_list(self): service = self.create_service('web', dns=['8.8.8.8', '9.9.9.9']) - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['Dns'], ['8.8.8.8', '9.9.9.9']) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8', '9.9.9.9']) def test_restart_always_value(self): service = self.create_service('web', restart='always') @@ -417,12 +422,12 @@ class ServiceTest(DockerClientTestCase): def test_working_dir_param(self): service = self.create_service('container', working_dir='/working/dir/sample') - container = service.create_container().inspect() - self.assertEqual(container['Config']['WorkingDir'], '/working/dir/sample') + container = service.create_container() + self.assertEqual(container.get('Config.WorkingDir'), '/working/dir/sample') def test_split_env(self): service = self.create_service('web', environment=['NORMAL=F1', 'CONTAINS_EQUALS=F=2', 'TRAILING_EQUALS=']) - env = service.start_container().environment + env = create_and_start_container(service).environment for k,v in {'NORMAL': 'F1', 'CONTAINS_EQUALS': 'F=2', 'TRAILING_EQUALS': ''}.iteritems(): self.assertEqual(env[k], v) @@ -438,7 +443,7 @@ class ServiceTest(DockerClientTestCase): os.environ['FILE_DEF_EMPTY'] = 'E2' os.environ['ENV_DEF'] = 'E3' try: - env = service.start_container().environment + env = create_and_start_container(service).environment for k,v in {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}.iteritems(): self.assertEqual(env[k], v) finally: diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 336f783f..88ebd1d6 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -189,20 +189,30 @@ class ServiceTest(unittest.TestCase): self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True) mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...') + @mock.patch('fig.service.Container', autospec=True) @mock.patch('fig.service.log', autospec=True) - def test_create_container_from_insecure_registry(self, mock_log): + def test_create_container_from_insecure_registry( + self, + mock_log, + mock_container): service = Service('foo', client=self.mock_client, image='someimage:sometag') mock_response = mock.Mock(Response) mock_response.status_code = 404 mock_response.reason = "Not Found" - Container.create = mock.Mock() - Container.create.side_effect = APIError('Mock error', mock_response, "No such image") - try: + mock_container.create.side_effect = APIError( + 'Mock error', mock_response, "No such image") + + # We expect the APIError because our service requires a + # non-existent image. + with self.assertRaises(APIError): service.create_container(insecure_registry=True) - except APIError: # We expect the APIError because our service requires a non-existent image. - pass - self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True, stream=True) - mock_log.info.assert_called_once_with('Pulling image someimage:sometag...') + + self.mock_client.pull.assert_called_once_with( + 'someimage:sometag', + insecure_registry=True, + stream=True) + mock_log.info.assert_called_once_with( + 'Pulling image someimage:sometag...') def test_parse_repository_tag(self): self.assertEqual(parse_repository_tag("root"), ("root", "")) From 3056ae4be329333e0f63568fc7d20a1d379b4172 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 26 Oct 2014 13:23:15 -0400 Subject: [PATCH 0696/2154] Add a no-build option to fig up, to save time when services were already freshly built. Signed-off-by: Daniel Nephin --- fig/cli/main.py | 4 +++- fig/project.py | 18 +++++++++++++++--- fig/service.py | 20 ++++++++++++++------ tests/integration/service_test.py | 28 ++++++++++++++-------------- tests/unit/service_test.py | 17 +++++++++++++++++ 5 files changed, 63 insertions(+), 24 deletions(-) diff --git a/fig/cli/main.py b/fig/cli/main.py index c367266b..8cdaee62 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -425,6 +425,7 @@ class TopLevelCommand(Command): --no-color Produce monochrome output. --no-deps Don't start linked services. --no-recreate If containers already exist, don't recreate them. + --no-build Don't build an image, even if it's missing """ insecure_registry = options['--allow-insecure-ssl'] detached = options['-d'] @@ -440,7 +441,8 @@ class TopLevelCommand(Command): start_links=start_links, recreate=recreate, insecure_registry=insecure_registry, - detach=options['-d'] + detach=options['-d'], + do_build=not options['--no-build'], ) to_attach = [c for s in project.get_services(service_names) for c in s.containers()] diff --git a/fig/project.py b/fig/project.py index 8d71a4c2..e013da4e 100644 --- a/fig/project.py +++ b/fig/project.py @@ -167,14 +167,26 @@ class Project(object): else: log.info('%s uses an image, skipping' % service.name) - def up(self, service_names=None, start_links=True, recreate=True, insecure_registry=False, detach=False): + def up(self, + service_names=None, + start_links=True, + recreate=True, + insecure_registry=False, + detach=False, + do_build=True): running_containers = [] for service in self.get_services(service_names, include_links=start_links): if recreate: - for (_, container) in service.recreate_containers(insecure_registry=insecure_registry, detach=detach): + for (_, container) in service.recreate_containers( + insecure_registry=insecure_registry, + detach=detach, + do_build=do_build): running_containers.append(container) else: - for container in service.start_or_create_containers(insecure_registry=insecure_registry, detach=detach): + for container in service.start_or_create_containers( + insecure_registry=insecure_registry, + detach=detach, + do_build=do_build): running_containers.append(container) return running_containers diff --git a/fig/service.py b/fig/service.py index 461bc1fe..d06d271f 100644 --- a/fig/service.py +++ b/fig/service.py @@ -54,7 +54,7 @@ DOCKER_START_KEYS = [ 'cap_add', 'cap_drop', 'dns', - 'dns_search', + 'dns_search', 'env_file', 'net', 'privileged', @@ -236,7 +236,7 @@ class Service(object): return Container.create(self.client, **container_options) raise - def recreate_containers(self, insecure_registry=False, **override_options): + def recreate_containers(self, insecure_registry=False, do_build=True, **override_options): """ If a container for this service doesn't exist, create and start one. If there are any, stop them, create+start new ones, and remove the old containers. @@ -244,7 +244,10 @@ class Service(object): containers = self.containers(stopped=True) if not containers: log.info("Creating %s..." % self._next_container_name(containers)) - container = self.create_container(insecure_registry=insecure_registry, **override_options) + container = self.create_container( + insecure_registry=insecure_registry, + do_build=do_build, + **override_options) self.start_container(container) return [(None, container)] else: @@ -283,7 +286,7 @@ class Service(object): container.remove() options = dict(override_options) - new_container = self.create_container(**options) + new_container = self.create_container(do_build=False, **options) self.start_container(new_container, intermediate_container=intermediate_container) intermediate_container.remove() @@ -330,14 +333,19 @@ class Service(object): ) return container - def start_or_create_containers(self, insecure_registry=False, detach=False): + def start_or_create_containers( + self, + insecure_registry=False, + detach=False, + do_build=True): containers = self.containers(stopped=True) if not containers: log.info("Creating %s..." % self._next_container_name(containers)) new_container = self.create_container( insecure_registry=insecure_registry, - detach=detach + detach=detach, + do_build=do_build, ) return [self.start_container(new_container)] else: diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 0e08ac1c..fca4da67 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -391,34 +391,34 @@ class ServiceTest(DockerClientTestCase): def test_restart_always_value(self): service = self.create_service('web', restart='always') - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['RestartPolicy']['Name'], 'always') + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'always') def test_restart_on_failure_value(self): service = self.create_service('web', restart='on-failure:5') - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['RestartPolicy']['Name'], 'on-failure') - self.assertEqual(container['HostConfig']['RestartPolicy']['MaximumRetryCount'], 5) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'on-failure') + self.assertEqual(container.get('HostConfig.RestartPolicy.MaximumRetryCount'), 5) def test_cap_add_list(self): service = self.create_service('web', cap_add=['SYS_ADMIN', 'NET_ADMIN']) - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['CapAdd'], ['SYS_ADMIN', 'NET_ADMIN']) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.CapAdd'), ['SYS_ADMIN', 'NET_ADMIN']) def test_cap_drop_list(self): service = self.create_service('web', cap_drop=['SYS_ADMIN', 'NET_ADMIN']) - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['CapDrop'], ['SYS_ADMIN', 'NET_ADMIN']) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.CapDrop'), ['SYS_ADMIN', 'NET_ADMIN']) def test_dns_search_single_value(self): service = self.create_service('web', dns_search='example.com') - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['DnsSearch'], ['example.com']) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.DnsSearch'), ['example.com']) def test_dns_search_list(self): service = self.create_service('web', dns_search=['dc1.example.com', 'dc2.example.com']) - container = service.start_container().inspect() - self.assertEqual(container['HostConfig']['DnsSearch'], ['dc1.example.com', 'dc2.example.com']) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.DnsSearch'), ['dc1.example.com', 'dc2.example.com']) def test_working_dir_param(self): service = self.create_service('container', working_dir='/working/dir/sample') @@ -433,7 +433,7 @@ class ServiceTest(DockerClientTestCase): def test_env_from_file_combined_with_env(self): service = self.create_service('web', environment=['ONE=1', 'TWO=2', 'THREE=3'], env_file=['tests/fixtures/env/one.env', 'tests/fixtures/env/two.env']) - env = service.start_container().environment + env = create_and_start_container(service).environment for k,v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.iteritems(): self.assertEqual(env[k], v) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 88ebd1d6..68dcf06a 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -228,6 +228,23 @@ class ServiceTest(unittest.TestCase): service.create_container() self.assertEqual(Container.create.call_args[1]['image'], 'someimage:latest') + def test_create_container_with_build(self): + self.mock_client.images.return_value = [] + service = Service('foo', client=self.mock_client, build='.') + service.build = mock.create_autospec(service.build) + service.create_container(do_build=True) + + self.mock_client.images.assert_called_once_with(name=service.full_name) + service.build.assert_called_once_with() + + def test_create_container_no_build(self): + self.mock_client.images.return_value = [] + service = Service('foo', client=self.mock_client, build='.') + service.create_container(do_build=False) + + self.assertFalse(self.mock_client.images.called) + self.assertFalse(self.mock_client.build.called) + class ServiceVolumesTest(unittest.TestCase): From 91c90a722abb604598c73c34f56d83a2ba2eeadd Mon Sep 17 00:00:00 2001 From: Christophe Labouisse Date: Sat, 3 Jan 2015 19:18:22 +0100 Subject: [PATCH 0697/2154] Added missing options The stdin_open and tty options are supported by fig but were missing from the documentation. Signed-off-by: Christophe Labouisse --- docs/yml.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/yml.md b/docs/yml.md index a911e450..831e6a2f 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -182,7 +182,7 @@ dns_search: - dc2.example.com ``` -### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart +### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. @@ -198,4 +198,7 @@ mem_limit: 1000000000 privileged: true restart: always + +stdin_open: true +tty: true ``` From b252300e94843763c5d229d7a2b7bc8412ffd38a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 6 Jan 2015 11:14:39 +0000 Subject: [PATCH 0698/2154] Reorganised roadmap, adding high-level goals Signed-off-by: Ben Firshman --- ROADMAP.md | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c99188f0..0ee22895 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,20 +1,28 @@ # Roadmap -## Fig 1.1 +Fig will be incorporated as part of the Docker ecosystem and renamed Docker Compose. The command-line tool and configuration file will get new names, and its documentation will be moved to [docs.docker.com](https://docs.docker.com). -- All this cool stuff: https://github.com/docker/fig/issues?q=milestone%3A1.1.0+ +## More than just development environments -## Compose 1.2 +Over time we will extend Fig's remit to cover test, staging and production environments. This is not a simple task, and will take many incremental improvements such as: -- Project-wide rename and rebrand to Docker Compose, with new names for the command-line tool and configuration file -- Version specifier in configuration file -- A “fig watch” command which automatically kicks off builds while editing code -- It should be possible to somehow define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by e.g. apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp) -- A way to share config between apps ([#318](https://github.com/docker/fig/issues/318)) +- Fig’s brute-force “delete and recreate everything” approach is great for dev and testing, but it not sufficient for production environments. You should be able to define a "desired" state that Fig will intelligently converge to. +- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#426](https://github.com/docker/fig/issues/426)) +- Fig recommend a technique for zero-downtime deploys. -## Future +## Integration with Swarm -- Fig uses Docker container names to separate and identify containers in different apps and belonging to different services within apps; this should really be done in a less hacky and more performant way, by attaching metadata to containers and doing the filtering on the server side. **This requires changes to the Docker daemon.** -- The config file should be parameterisable so that config can be partially modified for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#426](https://github.com/docker/fig/issues/426)) -- Fig’s brute-force “delete and recreate everything” approach is great for dev and testing, and with manual command-line scoping can be made to work in production (e.g. “fig up -d web” will update *just* the web service), but a smarter solution is needed, its logic probably based around convergence from “current” to “desired” app state. -- Compose should recommend a simple technique for zero-downtime deploys. This will likely involve new Docker networking methods that allow for a load balancer container to be dynamically hooked up to new app containers and disconnected from old ones. +Fig should integrate really well with Swarm so you can take an application you've developed on your laptop and run it on a Swarm cluster. + +## Applications spanning multiple teams + +Fig works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Fig doesn't work as well. + +There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318). + +## An even better tool for development environments + +Fig is a great tool for development environments, but it could be even better. For example: + +- [Fig could watch your code and automatically kick off builds when something changes.](https://github.com/docker/fig/issues/184) +- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp) From 3b638f0c433f9f9245b118c22609e34d77c025a8 Mon Sep 17 00:00:00 2001 From: Richard Adams Date: Wed, 7 Jan 2015 16:45:48 +0000 Subject: [PATCH 0699/2154] tweaks to the rails tutorial to bring it inline with rail 4.2 release Due to a change in Rack, rails server now listens on localhost instead of 0.0.0.0 by default. Signed-off-by: Richard Adams --- docs/rails.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/rails.md b/docs/rails.md index 25678b29..4e3e7975 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -23,7 +23,7 @@ That'll put our application code inside an image with Ruby, Bundler and all our Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`. source 'https://rubygems.org' - gem 'rails', '4.0.2' + gem 'rails', '4.2.0' Finally, `fig.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration we need to link them together and expose the web app's port. @@ -33,7 +33,7 @@ Finally, `fig.yml` is where the magic happens. It describes what services our ap - "5432" web: build: . - command: bundle exec rackup -p 3000 + command: bundle exec rails s -p 3000 -b '0.0.0.0' volumes: - .:/myapp ports: @@ -86,7 +86,7 @@ We can now boot the app. If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain: myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1 - myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.0.0 (2013-11-22) [x86_64-linux-gnu] + myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.2.0 (2014-12-25) [x86_64-linux-gnu] myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000 Finally, we just need to create the database. In another terminal, run: From 9a90a273769c46258d54a5b8c20ec7305bd31823 Mon Sep 17 00:00:00 2001 From: Richard Adams Date: Wed, 7 Jan 2015 19:45:00 +0000 Subject: [PATCH 0700/2154] be explicit with a ruby version number in the Dockerfile Signed-off-by: Richard Adams --- docs/rails.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/rails.md b/docs/rails.md index 4e3e7975..9b1ecb04 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -10,7 +10,7 @@ We're going to use Fig to set up and run a Rails/PostgreSQL app. Before starting Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with: - FROM ruby + FROM ruby:2.2.0 RUN apt-get update -qq && apt-get install -y build-essential libpq-dev RUN mkdir /myapp WORKDIR /myapp From 69db596b5dda3d6865a5387a065e8c4e6a53fe58 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Mon, 10 Nov 2014 20:55:43 +0100 Subject: [PATCH 0701/2154] Bash completion for fig command Signed-off-by: Harald Albers --- contrib/completion/bash/fig | 316 ++++++++++++++++++++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 contrib/completion/bash/fig diff --git a/contrib/completion/bash/fig b/contrib/completion/bash/fig new file mode 100644 index 00000000..b44efa7b --- /dev/null +++ b/contrib/completion/bash/fig @@ -0,0 +1,316 @@ +#!bash +# +# bash completion for fig commands +# +# This work is based on the completion for the docker command. +# +# This script provides completion of: +# - commands and their options +# - service names +# - filepaths +# +# To enable the completions either: +# - place this file in /etc/bash_completion.d +# or +# - copy this file and add the line below to your .bashrc after +# bash completion features are loaded +# . docker.bash +# +# Note: +# Some completions require the current user to have sufficient permissions +# to execute the docker command. + + +# Extracts all service names from the figfile. +___fig_all_services_in_figfile() { + awk -F: '/^[a-zA-Z0-9]/{print $1}' "${fig_file:-fig.yml}" +} + +# All services, even those without an existing container +__fig_services_all() { + COMPREPLY=( $(compgen -W "$(___fig_all_services_in_figfile)" -- "$cur") ) +} + +# All services that have an entry with the given key in their figfile section +___fig_services_with_key() { + # flatten sections to one line, then filter lines containing the key and return section name. + awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' fig.yml | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' +} + +# All services that are defined by a Dockerfile reference +__fig_services_from_build() { + COMPREPLY=( $(compgen -W "$(___fig_services_with_key build)" -- "$cur") ) +} + +# All services that are defined by an image +__fig_services_from_image() { + COMPREPLY=( $(compgen -W "$(___fig_services_with_key image)" -- "$cur") ) +} + +# The services for which containers have been created, optionally filtered +# by a boolean expression passed in as argument. +__fig_services_with() { + local containers names + containers="$(fig 2>/dev/null ${fig_file:+-f $fig_file} ${fig_project:+-p $fig_project} ps -q)" + names=( $(docker 2>/dev/null inspect --format "{{if ${1:-true}}} {{ .Name }} {{end}}" $containers) ) + names=( ${names[@]%_*} ) # strip trailing numbers + names=( ${names[@]#*_} ) # strip project name + COMPREPLY=( $(compgen -W "${names[*]}" -- "$cur") ) +} + +# The services for which at least one running container exists +__fig_services_running() { + __fig_services_with '.State.Running' +} + +# The services for which at least one stopped container exists +__fig_services_stopped() { + __fig_services_with 'not .State.Running' +} + + +_fig_build() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--no-cache" -- "$cur" ) ) + ;; + *) + __fig_services_from_build + ;; + esac +} + + +_fig_fig() { + case "$prev" in + --file|-f) + _filedir + return + ;; + --project-name|-p) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help -h --verbose --version --file -f --project-name -p" -- "$cur" ) ) + ;; + *) + COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) + ;; + esac +} + + +_fig_help() { + COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) +} + + +_fig_kill() { + case "$prev" in + -s) + COMPREPLY=( $( compgen -W "SIGHUP SIGINT SIGKILL SIGUSR1 SIGUSR2" -- "$(echo $cur | tr '[:lower:]' '[:upper:]')" ) ) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "-s" -- "$cur" ) ) + ;; + *) + __fig_services_running + ;; + esac +} + + +_fig_logs() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--no-color" -- "$cur" ) ) + ;; + *) + __fig_services_all + ;; + esac +} + + +_fig_port() { + case "$prev" in + --protocol) + COMPREPLY=( $( compgen -W "tcp udp" -- "$cur" ) ) + return; + ;; + --index) + return; + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--protocol --index" -- "$cur" ) ) + ;; + *) + __fig_services_all + ;; + esac +} + + +_fig_ps() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "-q" -- "$cur" ) ) + ;; + *) + __fig_services_all + ;; + esac +} + + +_fig_pull() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--allow-insecure-ssl" -- "$cur" ) ) + ;; + *) + __fig_services_from_image + ;; + esac +} + + +_fig_restart() { + __fig_services_running +} + + +_fig_rm() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--force -v" -- "$cur" ) ) + ;; + *) + __fig_services_stopped + ;; + esac +} + + +_fig_run() { + case "$prev" in + -e) + COMPREPLY=( $( compgen -e -- "$cur" ) ) + compopt -o nospace + return + ;; + --entrypoint) + return + ;; + esac + + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm -T" -- "$cur" ) ) + ;; + *) + __fig_services_all + ;; + esac +} + + +_fig_scale() { + case "$prev" in + =) + COMPREPLY=("$cur") + ;; + *) + COMPREPLY=( $(compgen -S "=" -W "$(___fig_all_services_in_figfile)" -- "$cur") ) + compopt -o nospace + ;; + esac +} + + +_fig_start() { + __fig_services_stopped +} + + +_fig_stop() { + __fig_services_running +} + + +_fig_up() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --no-build --no-color --no-deps --no-recreate" -- "$cur" ) ) + ;; + *) + __fig_services_all + ;; + esac +} + + +_fig() { + local commands=( + build + help + kill + logs + port + ps + pull + restart + rm + run + scale + start + stop + up + ) + + COMPREPLY=() + local cur prev words cword + _get_comp_words_by_ref -n : cur prev words cword + + # search subcommand and invoke its handler. + # special treatment of some top-level options + local command='fig' + local counter=1 + local fig_file fig_project + while [ $counter -lt $cword ]; do + case "${words[$counter]}" in + -f|--file) + (( counter++ )) + fig_file="${words[$counter]}" + ;; + -p|--project-name) + (( counter++ )) + fig_project="${words[$counter]}" + ;; + -*) + ;; + *) + command="${words[$counter]}" + break + ;; + esac + (( counter++ )) + done + + local completions_func=_fig_${command} + declare -F $completions_func >/dev/null && $completions_func + + return 0 +} + +complete -F _fig fig From 2406a3936aa88ed6732eb0c66089bbe8c9df25de Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Thu, 25 Dec 2014 11:16:45 +0100 Subject: [PATCH 0702/2154] Documentation for bash completion Signed-off-by: Harald Albers --- docs/completion.md | 33 +++++++++++++++++++++++++++++++++ docs/install.md | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 docs/completion.md diff --git a/docs/completion.md b/docs/completion.md new file mode 100644 index 00000000..ec8d7766 --- /dev/null +++ b/docs/completion.md @@ -0,0 +1,33 @@ +--- +layout: default +title: Command Completion +--- + +Command Completion +================== + +Fig comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion) +for the bash shell. + +Installing Command Completion +----------------------------- + +Make sure bash completion is installed. If you use a current Linux in a non-minimal installation, bash completion should be available. +On a Mac, install with `brew install bash-completion` + +Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. + + curl -L https://raw.githubusercontent.com/docker/fig/master/contrib/completion/bash/fig > /etc/bash_completion.d/fig + +Completion will be available upon next login. + +Available completions +--------------------- +Depending on what you typed on the command line so far, it will complete + + - available fig commands + - options that are available for a particular command + - service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `fig scale`, completed service names will automatically have "=" appended. + - arguments for selected options, e.g. `fig kill -s` will complete some signals like SIGHUP and SIGUSR1. + +Enjoy working with fig faster and with less typos! diff --git a/docs/install.md b/docs/install.md index 14fd64c8..c91c3db0 100644 --- a/docs/install.md +++ b/docs/install.md @@ -20,6 +20,8 @@ Next, install Fig: curl -L https://github.com/docker/fig/releases/download/1.0.1/fig-`uname -s`-`uname -m` > /usr/local/bin/fig; chmod +x /usr/local/bin/fig +Optionally, install [command completion](completion.html) for the bash shell. + Releases are available for OS X and 64-bit Linux. Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): $ sudo pip install -U fig From 3ee8437eaa6a424e4346e3910f42a785a7666b1c Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 8 Jan 2015 10:55:40 -0500 Subject: [PATCH 0703/2154] Fix the failing test. Signed-off-by: Daniel Nephin --- tests/integration/service_test.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index a5c84e5a..234dec91 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -230,10 +230,9 @@ class ServiceTest(DockerClientTestCase): 'figtest_db_2', 'figtest_db_3:db_3']) - db.start_container() - db.start_container() - db.start_container() - web.start_container() + for _ in range(3): + create_and_start_container(db) + create_and_start_container(web) self.assertEqual( set(web.containers()[0].links()), From aa0c43df965e058b10c89ed0c17c661ba15e6e93 Mon Sep 17 00:00:00 2001 From: Christophe Labouisse Date: Sun, 11 Jan 2015 19:58:08 +0100 Subject: [PATCH 0704/2154] Add cpu_shares option in fig.yml This options maps exactly the docker run option with the same name. Signed-off-by: Christophe Labouisse --- docs/yml.md | 4 +++- fig/service.py | 2 ++ tests/integration/service_test.py | 6 ++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/yml.md b/docs/yml.md index 5d2c7237..81668b56 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -194,11 +194,13 @@ dns_search: - dc2.example.com ``` -### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty +### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. ``` +cpu_shares: 73 + working_dir: /code entrypoint: /code/entrypoint.sh user: postgresql diff --git a/fig/service.py b/fig/service.py index 647b427d..18ba3f61 100644 --- a/fig/service.py +++ b/fig/service.py @@ -18,6 +18,7 @@ log = logging.getLogger(__name__) DOCKER_CONFIG_KEYS = [ 'cap_add', 'cap_drop', + 'cpu_shares', 'command', 'detach', 'dns', @@ -41,6 +42,7 @@ DOCKER_CONFIG_KEYS = [ 'working_dir', ] DOCKER_CONFIG_HINTS = { + 'cpu_share' : 'cpu_shares', 'link' : 'links', 'port' : 'ports', 'privilege' : 'privileged', diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 234dec91..d01d118f 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -100,6 +100,12 @@ class ServiceTest(DockerClientTestCase): service.start_container(container) self.assertIn('/var/db', container.inspect()['Volumes']) + def test_create_container_with_cpu_shares(self): + service = self.create_service('db', cpu_shares=73) + container = service.create_container() + service.start_container(container) + self.assertEqual(container.inspect()['Config']['CpuShares'], 73) + def test_create_container_with_specified_volume(self): host_path = '/tmp/host-path' container_path = '/container-path' From 26f45efea2e76f95f1c9bc162ed2f6710f258a42 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sat, 6 Dec 2014 14:20:29 -0500 Subject: [PATCH 0705/2154] Remove unused intermediate_container from return value. Signed-off-by: Daniel Nephin --- fig/project.py | 16 ++++++---------- fig/service.py | 24 +++++++++++++----------- tests/integration/service_test.py | 23 ++++++++--------------- 3 files changed, 27 insertions(+), 36 deletions(-) diff --git a/fig/project.py b/fig/project.py index e013da4e..cb6404a7 100644 --- a/fig/project.py +++ b/fig/project.py @@ -176,18 +176,14 @@ class Project(object): do_build=True): running_containers = [] for service in self.get_services(service_names, include_links=start_links): - if recreate: - for (_, container) in service.recreate_containers( - insecure_registry=insecure_registry, + create_func = (service.recreate_containers if recreate + else service.start_or_create_containers) + + for container in create_func( + insecure_registry=insecure_registry, detach=detach, do_build=do_build): - running_containers.append(container) - else: - for container in service.start_or_create_containers( - insecure_registry=insecure_registry, - detach=detach, - do_build=do_build): - running_containers.append(container) + running_containers.append(container) return running_containers diff --git a/fig/service.py b/fig/service.py index 18ba3f61..64735f4d 100644 --- a/fig/service.py +++ b/fig/service.py @@ -242,8 +242,9 @@ class Service(object): def recreate_containers(self, insecure_registry=False, do_build=True, **override_options): """ - If a container for this service doesn't exist, create and start one. If there are - any, stop them, create+start new ones, and remove the old containers. + If a container for this service doesn't exist, create and start one. If + there are any, stop them, create+start new ones, and remove the old + containers. """ containers = self.containers(stopped=True) if not containers: @@ -253,21 +254,22 @@ class Service(object): do_build=do_build, **override_options) self.start_container(container) - return [(None, container)] + return [container] else: - tuples = [] - - for c in containers: - log.info("Recreating %s..." % c.name) - tuples.append(self.recreate_container(c, insecure_registry=insecure_registry, **override_options)) - - return tuples + return [ + self.recreate_container( + container, + insecure_registry=insecure_registry, + **override_options) + for container in containers + ] def recreate_container(self, container, **override_options): """Recreate a container. An intermediate container is created so that the new container has the same name, while still supporting `volumes-from` the original container. """ + log.info("Recreating %s..." % container.name) try: container.stop() except APIError as e: @@ -295,7 +297,7 @@ class Service(object): intermediate_container.remove() - return (intermediate_container, new_container) + return new_container def start_container_if_stopped(self, container, **options): if container.is_running: diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index d01d118f..fcf46038 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -148,30 +148,23 @@ class ServiceTest(DockerClientTestCase): self.assertIn('FOO=1', old_container.dictionary['Config']['Env']) self.assertEqual(old_container.name, 'figtest_db_1') service.start_container(old_container) - volume_path = old_container.inspect()['Volumes']['/etc'] + volume_path = old_container.get('Volumes')['/etc'] num_containers_before = len(self.client.containers(all=True)) service.options['environment']['FOO'] = '2' - tuples = service.recreate_containers() - self.assertEqual(len(tuples), 1) + containers = service.recreate_containers() + self.assertEqual(len(containers), 1) - intermediate_container = tuples[0][0] - new_container = tuples[0][1] - self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['/bin/echo']) - - self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep']) - self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300']) - self.assertIn('FOO=2', new_container.dictionary['Config']['Env']) + new_container = containers[0] + self.assertEqual(new_container.get('Config.Entrypoint'), ['sleep']) + self.assertEqual(new_container.get('Config.Cmd'), ['300']) + self.assertIn('FOO=2', new_container.get('Config.Env')) self.assertEqual(new_container.name, 'figtest_db_1') - self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path) - self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom']) + self.assertEqual(new_container.get('Volumes')['/etc'], volume_path) self.assertEqual(len(self.client.containers(all=True)), num_containers_before) self.assertNotEqual(old_container.id, new_container.id) - self.assertRaises(APIError, - self.client.inspect_container, - intermediate_container.id) def test_recreate_containers_when_containers_are_stopped(self): service = self.create_service( From 7eb476e61de7abf5a70ba8e824c7e2e765e2ac09 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sat, 6 Dec 2014 17:51:49 -0500 Subject: [PATCH 0706/2154] Resolves #447, fix volume logic for recreate container Signed-off-by: Daniel Nephin --- fig/project.py | 4 +- fig/service.py | 68 ++++++++++++++++++++++++------- tests/integration/service_test.py | 12 +++++- tests/unit/service_test.py | 63 ++++++++++++++++++++++------ 4 files changed, 117 insertions(+), 30 deletions(-) diff --git a/fig/project.py b/fig/project.py index cb6404a7..f5ac88e4 100644 --- a/fig/project.py +++ b/fig/project.py @@ -181,8 +181,8 @@ class Project(object): for container in create_func( insecure_registry=insecure_registry, - detach=detach, - do_build=do_build): + detach=detach, + do_build=do_build): running_containers.append(container) return running_containers diff --git a/fig/service.py b/fig/service.py index 64735f4d..3b1273d1 100644 --- a/fig/service.py +++ b/fig/service.py @@ -280,6 +280,7 @@ class Service(object): else: raise + intermediate_options = dict(self.options, **override_options) intermediate_container = Container.create( self.client, image=container.image, @@ -287,16 +288,21 @@ class Service(object): command=[], detach=True, ) - intermediate_container.start(volumes_from=container.id) + intermediate_container.start( + binds=get_container_data_volumes( + container, intermediate_options.get('volumes'))) intermediate_container.wait() container.remove() + # TODO: volumes are being passed to both start and create, this is + # probably unnecessary options = dict(override_options) new_container = self.create_container(do_build=False, **options) - self.start_container(new_container, intermediate_container=intermediate_container) + self.start_container( + new_container, + intermediate_container=intermediate_container) intermediate_container.remove() - return new_container def start_container_if_stopped(self, container, **options): @@ -309,12 +315,6 @@ class Service(object): def start_container(self, container, intermediate_container=None, **override_options): options = dict(self.options, **override_options) port_bindings = build_port_bindings(options.get('ports') or []) - - volume_bindings = dict( - build_volume_binding(parse_volume_spec(volume)) - for volume in options.get('volumes') or [] - if ':' in volume) - privileged = options.get('privileged', False) net = options.get('net', 'bridge') dns = options.get('dns', None) @@ -323,12 +323,14 @@ class Service(object): cap_drop = options.get('cap_drop', None) restart = parse_restart_spec(options.get('restart', None)) + binds = get_volume_bindings( + options.get('volumes'), intermediate_container) container.start( links=self._get_links(link_to_self=options.get('one_off', False)), port_bindings=port_bindings, - binds=volume_bindings, - volumes_from=self._get_volumes_from(intermediate_container), + binds=binds, + volumes_from=self._get_volumes_from(), privileged=privileged, network_mode=net, dns=dns, @@ -390,7 +392,7 @@ class Service(object): links.append((external_link, link_name)) return links - def _get_volumes_from(self, intermediate_container=None): + def _get_volumes_from(self): volumes_from = [] for volume_source in self.volumes_from: if isinstance(volume_source, Service): @@ -404,9 +406,6 @@ class Service(object): elif isinstance(volume_source, Container): volumes_from.append(volume_source.id) - if intermediate_container: - volumes_from.append(intermediate_container.id) - return volumes_from def _get_container_create_options(self, override_options, one_off=False): @@ -521,6 +520,45 @@ class Service(object): ) +def get_container_data_volumes(container, volumes_option): + """Find the container data volumes that are in `volumes_option`, and return + a mapping of volume bindings for those volumes. + """ + volumes = [] + for volume in volumes_option or []: + volume = parse_volume_spec(volume) + # No need to preserve host volumes + if volume.external: + continue + + volume_path = (container.get('Volumes') or {}).get(volume.internal) + # New volume, doesn't exist in the old container + if not volume_path: + continue + + # Copy existing volume from old container + volume = volume._replace(external=volume_path) + volumes.append(build_volume_binding(volume)) + + return dict(volumes) + + +def get_volume_bindings(volumes_option, intermediate_container): + """Return a list of volume bindings for a container. Container data volume + bindings are replaced by those in the intermediate container. + """ + volume_bindings = dict( + build_volume_binding(parse_volume_spec(volume)) + for volume in volumes_option or [] + if ':' in volume) + + if intermediate_container: + volume_bindings.update( + get_container_data_volumes(intermediate_container, volumes_option)) + + return volume_bindings + + NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index fcf46038..dadd8d4a 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -98,7 +98,7 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('db', volumes=['/var/db']) container = service.create_container() service.start_container(container) - self.assertIn('/var/db', container.inspect()['Volumes']) + self.assertIn('/var/db', container.get('Volumes')) def test_create_container_with_cpu_shares(self): service = self.create_service('db', cpu_shares=73) @@ -179,6 +179,16 @@ class ServiceTest(DockerClientTestCase): service.recreate_containers() self.assertEqual(len(service.containers(stopped=True)), 1) + def test_recreate_containers_with_volume_changes(self): + service = self.create_service('withvolumes', volumes=['/etc']) + old_container = create_and_start_container(service) + self.assertEqual(old_container.get('Volumes').keys(), ['/etc']) + + service = self.create_service('withvolumes') + container, = service.recreate_containers() + service.start_container(container) + self.assertEqual(container.get('Volumes'), {}) + def test_start_container_passes_through_options(self): db = self.create_service('db') create_and_start_container(db, environment={'FOO': 'BAR'}) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 68dcf06a..3e6b7c4e 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -11,13 +11,15 @@ from requests import Response from fig import Service from fig.container import Container from fig.service import ( - ConfigError, - split_port, - build_port_bindings, - parse_volume_spec, - build_volume_binding, APIError, + ConfigError, + build_port_bindings, + build_volume_binding, + get_container_data_volumes, + get_volume_bindings, parse_repository_tag, + parse_volume_spec, + split_port, ) @@ -57,13 +59,6 @@ class ServiceTest(unittest.TestCase): self.assertEqual(service._get_volumes_from(), [container_id]) - def test_get_volumes_from_intermediate_container(self): - container_id = 'aabbccddee' - service = Service('test') - container = mock.Mock(id=container_id, spec=Container) - - self.assertEqual(service._get_volumes_from(container), [container_id]) - def test_get_volumes_from_service_container_exists(self): container_ids = ['aabbccddee', '12345'] from_service = mock.create_autospec(Service) @@ -288,6 +283,50 @@ class ServiceVolumesTest(unittest.TestCase): binding, ('/home/user', dict(bind='/home/user', ro=False))) + def test_get_container_data_volumes(self): + options = [ + '/host/volume:/host/volume:ro', + '/new/volume', + '/existing/volume', + ] + + container = Container(None, { + 'Volumes': { + '/host/volume': '/host/volume', + '/existing/volume': '/var/lib/docker/aaaaaaaa', + '/removed/volume': '/var/lib/docker/bbbbbbbb', + }, + }, has_been_inspected=True) + + expected = { + '/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False}, + } + + binds = get_container_data_volumes(container, options) + self.assertEqual(binds, expected) + + def test_get_volume_bindings(self): + options = [ + '/host/volume:/host/volume:ro', + '/host/rw/volume:/host/rw/volume', + '/new/volume', + '/existing/volume', + ] + + intermediate_container = Container(None, { + 'Volumes': {'/existing/volume': '/var/lib/docker/aaaaaaaa'}, + }, has_been_inspected=True) + + expected = { + '/host/volume': {'bind': '/host/volume', 'ro': True}, + '/host/rw/volume': {'bind': '/host/rw/volume', 'ro': False}, + '/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False}, + } + + binds = get_volume_bindings(options, intermediate_container) + self.assertEqual(binds, expected) + + class ServiceEnvironmentTest(unittest.TestCase): def setUp(self): From 2dd1cc80ca1e6d46001e31be9dcbcb62a47a7ca1 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 20 Jan 2015 12:01:50 -0800 Subject: [PATCH 0707/2154] Revert "Merge pull request #711 from dnephin/fix_volumes_on_recreate" This reverts commit 55095ef488ffdf0afde66ad5d1136fdc9f1fbb5f, reversing changes made to 72095f54b26650affed47c3b888d77572d6ecbf0. Signed-off-by: Daniel Nephin --- fig/project.py | 20 ++++--- fig/service.py | 92 +++++++++---------------------- tests/integration/service_test.py | 35 ++++++------ tests/unit/service_test.py | 63 ++++----------------- 4 files changed, 66 insertions(+), 144 deletions(-) diff --git a/fig/project.py b/fig/project.py index f5ac88e4..e013da4e 100644 --- a/fig/project.py +++ b/fig/project.py @@ -176,14 +176,18 @@ class Project(object): do_build=True): running_containers = [] for service in self.get_services(service_names, include_links=start_links): - create_func = (service.recreate_containers if recreate - else service.start_or_create_containers) - - for container in create_func( - insecure_registry=insecure_registry, - detach=detach, - do_build=do_build): - running_containers.append(container) + if recreate: + for (_, container) in service.recreate_containers( + insecure_registry=insecure_registry, + detach=detach, + do_build=do_build): + running_containers.append(container) + else: + for container in service.start_or_create_containers( + insecure_registry=insecure_registry, + detach=detach, + do_build=do_build): + running_containers.append(container) return running_containers diff --git a/fig/service.py b/fig/service.py index 3b1273d1..18ba3f61 100644 --- a/fig/service.py +++ b/fig/service.py @@ -242,9 +242,8 @@ class Service(object): def recreate_containers(self, insecure_registry=False, do_build=True, **override_options): """ - If a container for this service doesn't exist, create and start one. If - there are any, stop them, create+start new ones, and remove the old - containers. + If a container for this service doesn't exist, create and start one. If there are + any, stop them, create+start new ones, and remove the old containers. """ containers = self.containers(stopped=True) if not containers: @@ -254,22 +253,21 @@ class Service(object): do_build=do_build, **override_options) self.start_container(container) - return [container] + return [(None, container)] else: - return [ - self.recreate_container( - container, - insecure_registry=insecure_registry, - **override_options) - for container in containers - ] + tuples = [] + + for c in containers: + log.info("Recreating %s..." % c.name) + tuples.append(self.recreate_container(c, insecure_registry=insecure_registry, **override_options)) + + return tuples def recreate_container(self, container, **override_options): """Recreate a container. An intermediate container is created so that the new container has the same name, while still supporting `volumes-from` the original container. """ - log.info("Recreating %s..." % container.name) try: container.stop() except APIError as e: @@ -280,7 +278,6 @@ class Service(object): else: raise - intermediate_options = dict(self.options, **override_options) intermediate_container = Container.create( self.client, image=container.image, @@ -288,22 +285,17 @@ class Service(object): command=[], detach=True, ) - intermediate_container.start( - binds=get_container_data_volumes( - container, intermediate_options.get('volumes'))) + intermediate_container.start(volumes_from=container.id) intermediate_container.wait() container.remove() - # TODO: volumes are being passed to both start and create, this is - # probably unnecessary options = dict(override_options) new_container = self.create_container(do_build=False, **options) - self.start_container( - new_container, - intermediate_container=intermediate_container) + self.start_container(new_container, intermediate_container=intermediate_container) intermediate_container.remove() - return new_container + + return (intermediate_container, new_container) def start_container_if_stopped(self, container, **options): if container.is_running: @@ -315,6 +307,12 @@ class Service(object): def start_container(self, container, intermediate_container=None, **override_options): options = dict(self.options, **override_options) port_bindings = build_port_bindings(options.get('ports') or []) + + volume_bindings = dict( + build_volume_binding(parse_volume_spec(volume)) + for volume in options.get('volumes') or [] + if ':' in volume) + privileged = options.get('privileged', False) net = options.get('net', 'bridge') dns = options.get('dns', None) @@ -323,14 +321,12 @@ class Service(object): cap_drop = options.get('cap_drop', None) restart = parse_restart_spec(options.get('restart', None)) - binds = get_volume_bindings( - options.get('volumes'), intermediate_container) container.start( links=self._get_links(link_to_self=options.get('one_off', False)), port_bindings=port_bindings, - binds=binds, - volumes_from=self._get_volumes_from(), + binds=volume_bindings, + volumes_from=self._get_volumes_from(intermediate_container), privileged=privileged, network_mode=net, dns=dns, @@ -392,7 +388,7 @@ class Service(object): links.append((external_link, link_name)) return links - def _get_volumes_from(self): + def _get_volumes_from(self, intermediate_container=None): volumes_from = [] for volume_source in self.volumes_from: if isinstance(volume_source, Service): @@ -406,6 +402,9 @@ class Service(object): elif isinstance(volume_source, Container): volumes_from.append(volume_source.id) + if intermediate_container: + volumes_from.append(intermediate_container.id) + return volumes_from def _get_container_create_options(self, override_options, one_off=False): @@ -520,45 +519,6 @@ class Service(object): ) -def get_container_data_volumes(container, volumes_option): - """Find the container data volumes that are in `volumes_option`, and return - a mapping of volume bindings for those volumes. - """ - volumes = [] - for volume in volumes_option or []: - volume = parse_volume_spec(volume) - # No need to preserve host volumes - if volume.external: - continue - - volume_path = (container.get('Volumes') or {}).get(volume.internal) - # New volume, doesn't exist in the old container - if not volume_path: - continue - - # Copy existing volume from old container - volume = volume._replace(external=volume_path) - volumes.append(build_volume_binding(volume)) - - return dict(volumes) - - -def get_volume_bindings(volumes_option, intermediate_container): - """Return a list of volume bindings for a container. Container data volume - bindings are replaced by those in the intermediate container. - """ - volume_bindings = dict( - build_volume_binding(parse_volume_spec(volume)) - for volume in volumes_option or [] - if ':' in volume) - - if intermediate_container: - volume_bindings.update( - get_container_data_volumes(intermediate_container, volumes_option)) - - return volume_bindings - - NAME_RE = re.compile(r'^([^_]+)_([^_]+)_(run_)?(\d+)$') diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index dadd8d4a..d01d118f 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -98,7 +98,7 @@ class ServiceTest(DockerClientTestCase): service = self.create_service('db', volumes=['/var/db']) container = service.create_container() service.start_container(container) - self.assertIn('/var/db', container.get('Volumes')) + self.assertIn('/var/db', container.inspect()['Volumes']) def test_create_container_with_cpu_shares(self): service = self.create_service('db', cpu_shares=73) @@ -148,23 +148,30 @@ class ServiceTest(DockerClientTestCase): self.assertIn('FOO=1', old_container.dictionary['Config']['Env']) self.assertEqual(old_container.name, 'figtest_db_1') service.start_container(old_container) - volume_path = old_container.get('Volumes')['/etc'] + volume_path = old_container.inspect()['Volumes']['/etc'] num_containers_before = len(self.client.containers(all=True)) service.options['environment']['FOO'] = '2' - containers = service.recreate_containers() - self.assertEqual(len(containers), 1) + tuples = service.recreate_containers() + self.assertEqual(len(tuples), 1) - new_container = containers[0] - self.assertEqual(new_container.get('Config.Entrypoint'), ['sleep']) - self.assertEqual(new_container.get('Config.Cmd'), ['300']) - self.assertIn('FOO=2', new_container.get('Config.Env')) + intermediate_container = tuples[0][0] + new_container = tuples[0][1] + self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['/bin/echo']) + + self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep']) + self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300']) + self.assertIn('FOO=2', new_container.dictionary['Config']['Env']) self.assertEqual(new_container.name, 'figtest_db_1') - self.assertEqual(new_container.get('Volumes')['/etc'], volume_path) + self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path) + self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom']) self.assertEqual(len(self.client.containers(all=True)), num_containers_before) self.assertNotEqual(old_container.id, new_container.id) + self.assertRaises(APIError, + self.client.inspect_container, + intermediate_container.id) def test_recreate_containers_when_containers_are_stopped(self): service = self.create_service( @@ -179,16 +186,6 @@ class ServiceTest(DockerClientTestCase): service.recreate_containers() self.assertEqual(len(service.containers(stopped=True)), 1) - def test_recreate_containers_with_volume_changes(self): - service = self.create_service('withvolumes', volumes=['/etc']) - old_container = create_and_start_container(service) - self.assertEqual(old_container.get('Volumes').keys(), ['/etc']) - - service = self.create_service('withvolumes') - container, = service.recreate_containers() - service.start_container(container) - self.assertEqual(container.get('Volumes'), {}) - def test_start_container_passes_through_options(self): db = self.create_service('db') create_and_start_container(db, environment={'FOO': 'BAR'}) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 3e6b7c4e..68dcf06a 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -11,15 +11,13 @@ from requests import Response from fig import Service from fig.container import Container from fig.service import ( - APIError, ConfigError, - build_port_bindings, - build_volume_binding, - get_container_data_volumes, - get_volume_bindings, - parse_repository_tag, - parse_volume_spec, split_port, + build_port_bindings, + parse_volume_spec, + build_volume_binding, + APIError, + parse_repository_tag, ) @@ -59,6 +57,13 @@ class ServiceTest(unittest.TestCase): self.assertEqual(service._get_volumes_from(), [container_id]) + def test_get_volumes_from_intermediate_container(self): + container_id = 'aabbccddee' + service = Service('test') + container = mock.Mock(id=container_id, spec=Container) + + self.assertEqual(service._get_volumes_from(container), [container_id]) + def test_get_volumes_from_service_container_exists(self): container_ids = ['aabbccddee', '12345'] from_service = mock.create_autospec(Service) @@ -283,50 +288,6 @@ class ServiceVolumesTest(unittest.TestCase): binding, ('/home/user', dict(bind='/home/user', ro=False))) - def test_get_container_data_volumes(self): - options = [ - '/host/volume:/host/volume:ro', - '/new/volume', - '/existing/volume', - ] - - container = Container(None, { - 'Volumes': { - '/host/volume': '/host/volume', - '/existing/volume': '/var/lib/docker/aaaaaaaa', - '/removed/volume': '/var/lib/docker/bbbbbbbb', - }, - }, has_been_inspected=True) - - expected = { - '/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False}, - } - - binds = get_container_data_volumes(container, options) - self.assertEqual(binds, expected) - - def test_get_volume_bindings(self): - options = [ - '/host/volume:/host/volume:ro', - '/host/rw/volume:/host/rw/volume', - '/new/volume', - '/existing/volume', - ] - - intermediate_container = Container(None, { - 'Volumes': {'/existing/volume': '/var/lib/docker/aaaaaaaa'}, - }, has_been_inspected=True) - - expected = { - '/host/volume': {'bind': '/host/volume', 'ro': True}, - '/host/rw/volume': {'bind': '/host/rw/volume', 'ro': False}, - '/var/lib/docker/aaaaaaaa': {'bind': '/existing/volume', 'ro': False}, - } - - binds = get_volume_bindings(options, intermediate_container) - self.assertEqual(binds, expected) - - class ServiceEnvironmentTest(unittest.TestCase): def setUp(self): From 17a8a7be4bb93e18b507ea5eef8dedbade719c40 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 20 Jan 2015 13:10:01 +0000 Subject: [PATCH 0708/2154] (Failing) test for preservation of volumes declared in images Signed-off-by: Aanand Prasad --- .../dockerfile-with-volume/Dockerfile | 3 +++ tests/integration/service_test.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 tests/fixtures/dockerfile-with-volume/Dockerfile diff --git a/tests/fixtures/dockerfile-with-volume/Dockerfile b/tests/fixtures/dockerfile-with-volume/Dockerfile new file mode 100644 index 00000000..2d6437cf --- /dev/null +++ b/tests/fixtures/dockerfile-with-volume/Dockerfile @@ -0,0 +1,3 @@ +FROM busybox +VOLUME /data +CMD sleep 3000 diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index d01d118f..b7b11d5f 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -186,6 +186,25 @@ class ServiceTest(DockerClientTestCase): service.recreate_containers() self.assertEqual(len(service.containers(stopped=True)), 1) + + def test_recreate_containers_with_image_declared_volume(self): + service = Service( + project='figtest', + name='db', + client=self.client, + build='tests/fixtures/dockerfile-with-volume', + ) + + old_container = create_and_start_container(service) + self.assertEqual(old_container.get('Volumes').keys(), ['/data']) + volume_path = old_container.get('Volumes')['/data'] + + service.recreate_containers() + new_container = service.containers()[0] + service.start_container(new_container) + self.assertEqual(new_container.get('Volumes').keys(), ['/data']) + self.assertEqual(new_container.get('Volumes')['/data'], volume_path) + def test_start_container_passes_through_options(self): db = self.create_service('db') create_and_start_container(db, environment={'FOO': 'BAR'}) From 608f29c7cb574e1f6a36f39a48f78676738738e0 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 16 Jan 2015 13:12:29 +0000 Subject: [PATCH 0709/2154] Unit tests for Service.containers() and get_container_name() Signed-off-by: Aanand Prasad --- tests/unit/service_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 68dcf06a..1f033380 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -17,6 +17,7 @@ from fig.service import ( parse_volume_spec, build_volume_binding, APIError, + get_container_name, parse_repository_tag, ) @@ -49,6 +50,25 @@ class ServiceTest(unittest.TestCase): self.assertRaises(ConfigError, lambda: Service(name='foo', port=['8000'])) Service(name='foo', ports=['8000']) + def test_get_container_name(self): + self.assertIsNone(get_container_name({})) + self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1') + self.assertEqual(get_container_name({'Names': ['/myproject_db_1', '/myproject_web_1/db']}), 'myproject_db_1') + + def test_containers(self): + service = Service('db', client=self.mock_client, project='myproject') + + self.mock_client.containers.return_value = [] + self.assertEqual(service.containers(), []) + + self.mock_client.containers.return_value = [ + {'Image': 'busybox', 'Id': 'OUT_1', 'Names': ['/myproject', '/foo/bar']}, + {'Image': 'busybox', 'Id': 'OUT_2', 'Names': ['/myproject_db']}, + {'Image': 'busybox', 'Id': 'OUT_3', 'Names': ['/db_1']}, + {'Image': 'busybox', 'Id': 'IN_1', 'Names': ['/myproject_db_1', '/myproject_web_1/db']}, + ] + self.assertEqual([c.id for c in service.containers()], ['IN_1']) + def test_get_volumes_from_container(self): container_id = 'aabbccddee' service = Service( From edb6b24b8ff1c13cefe3ee347d75557918f38aa8 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 16 Jan 2015 16:43:59 +0000 Subject: [PATCH 0710/2154] Handle Swarm-style prefixed names in Service.containers() Signed-off-by: Aanand Prasad --- fig/service.py | 5 ++--- tests/unit/service_test.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/fig/service.py b/fig/service.py index 18ba3f61..369d4e84 100644 --- a/fig/service.py +++ b/fig/service.py @@ -545,9 +545,8 @@ def get_container_name(container): if 'Name' in container: return container['Name'] # ps - for name in container['Names']: - if len(name.split('/')) == 2: - return name[1:] + shortest_name = min(container['Names'], key=lambda n: len(n.split('/'))) + return shortest_name.split('/')[-1] def parse_restart_spec(restart_config): diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 1f033380..6db603d0 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -54,6 +54,7 @@ class ServiceTest(unittest.TestCase): self.assertIsNone(get_container_name({})) self.assertEqual(get_container_name({'Name': 'myproject_db_1'}), 'myproject_db_1') self.assertEqual(get_container_name({'Names': ['/myproject_db_1', '/myproject_web_1/db']}), 'myproject_db_1') + self.assertEqual(get_container_name({'Names': ['/swarm-host-1/myproject_db_1', '/swarm-host-1/myproject_web_1/db']}), 'myproject_db_1') def test_containers(self): service = Service('db', client=self.mock_client, project='myproject') @@ -69,6 +70,17 @@ class ServiceTest(unittest.TestCase): ] self.assertEqual([c.id for c in service.containers()], ['IN_1']) + def test_containers_prefixed(self): + service = Service('db', client=self.mock_client, project='myproject') + + self.mock_client.containers.return_value = [ + {'Image': 'busybox', 'Id': 'OUT_1', 'Names': ['/swarm-host-1/myproject', '/swarm-host-1/foo/bar']}, + {'Image': 'busybox', 'Id': 'OUT_2', 'Names': ['/swarm-host-1/myproject_db']}, + {'Image': 'busybox', 'Id': 'OUT_3', 'Names': ['/swarm-host-1/db_1']}, + {'Image': 'busybox', 'Id': 'IN_1', 'Names': ['/swarm-host-1/myproject_db_1', '/swarm-host-1/myproject_web_1/db']}, + ] + self.assertEqual([c.id for c in service.containers()], ['IN_1']) + def test_get_volumes_from_container(self): container_id = 'aabbccddee' service = Service( From cbd3ca07c4644c62b3914cecfa60436a46d77836 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 16 Jan 2015 17:24:29 +0000 Subject: [PATCH 0711/2154] Handle Swarm-style prefixed names in Container.from_ps() Signed-off-by: Aanand Prasad --- fig/container.py | 15 ++++++++++++--- fig/service.py | 13 +------------ tests/unit/container_test.py | 14 +++++++++++++- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/fig/container.py b/fig/container.py index 0ab75512..69214598 100644 --- a/fig/container.py +++ b/fig/container.py @@ -22,10 +22,8 @@ class Container(object): new_dictionary = { 'Id': dictionary['Id'], 'Image': dictionary['Image'], + 'Name': '/' + get_container_name(dictionary), } - for name in dictionary.get('Names', []): - if len(name.split('/')) == 2: - new_dictionary['Name'] = name return cls(client, new_dictionary, **kwargs) @classmethod @@ -170,3 +168,14 @@ class Container(object): if type(self) != type(other): return False return self.id == other.id + + +def get_container_name(container): + if not container.get('Name') and not container.get('Names'): + return None + # inspect + if 'Name' in container: + return container['Name'] + # ps + shortest_name = min(container['Names'], key=lambda n: len(n.split('/'))) + return shortest_name.split('/')[-1] diff --git a/fig/service.py b/fig/service.py index 369d4e84..6743e171 100644 --- a/fig/service.py +++ b/fig/service.py @@ -9,7 +9,7 @@ import sys from docker.errors import APIError -from .container import Container +from .container import Container, get_container_name from .progress_stream import stream_output, StreamOutputError log = logging.getLogger(__name__) @@ -538,17 +538,6 @@ def parse_name(name): return ServiceName(project, service_name, int(suffix)) -def get_container_name(container): - if not container.get('Name') and not container.get('Names'): - return None - # inspect - if 'Name' in container: - return container['Name'] - # ps - shortest_name = min(container['Names'], key=lambda n: len(n.split('/'))) - return shortest_name.split('/')[-1] - - def parse_restart_spec(restart_config): if not restart_config: return None diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 18f7944e..26468118 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -20,7 +20,7 @@ class ContainerTest(unittest.TestCase): "Ports": None, "SizeRw": 0, "SizeRootFs": 0, - "Names": ["/figtest_db_1"], + "Names": ["/figtest_db_1", "/figtest_web_1/db"], "NetworkSettings": { "Ports": {}, }, @@ -36,6 +36,18 @@ class ContainerTest(unittest.TestCase): "Name": "/figtest_db_1", }) + def test_from_ps_prefixed(self): + self.container_dict['Names'] = ['/swarm-host-1' + n for n in self.container_dict['Names']] + + container = Container.from_ps(None, + self.container_dict, + has_been_inspected=True) + self.assertEqual(container.dictionary, { + "Id": "abc", + "Image":"busybox:latest", + "Name": "/figtest_db_1", + }) + def test_environment(self): container = Container(None, { 'Id': 'abc', From 2af7693e64010e11ba53f7fd923bb97f29b10063 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 12 Jan 2015 14:59:05 +0000 Subject: [PATCH 0712/2154] WIP: rename Fig to Compose Signed-off-by: Aanand Prasad --- .gitignore | 2 +- Dockerfile | 2 +- bin/compose | 3 + bin/fig | 3 - {fig => compose}/__init__.py | 0 {fig => compose}/cli/__init__.py | 0 {fig => compose}/cli/colors.py | 0 {fig => compose}/cli/command.py | 30 +++++---- {fig => compose}/cli/docker_client.py | 0 {fig => compose}/cli/docopt_command.py | 0 {fig => compose}/cli/errors.py | 4 +- {fig => compose}/cli/formatter.py | 0 {fig => compose}/cli/log_printer.py | 0 {fig => compose}/cli/main.py | 34 +++++------ {fig => compose}/cli/multiplexer.py | 0 {fig => compose}/cli/utils.py | 0 {fig => compose}/cli/verbose_proxy.py | 0 {fig => compose}/container.py | 0 {fig => compose}/progress_stream.py | 0 {fig => compose}/project.py | 2 +- {fig => compose}/service.py | 2 +- script/build-docs | 2 +- script/build-linux | 8 +-- script/build-osx | 6 +- script/clean | 2 +- script/deploy-docs | 29 --------- script/test | 6 +- setup.py | 10 +-- .../fixtures/commands-composefile/compose.yml | 5 ++ tests/fixtures/commands-figfile/fig.yml | 5 -- .../{fig.yml => compose.yml} | 0 .../compose.yml} | 0 .../fig.yml => links-composefile/compose.yml} | 0 .../compose.yaml} | 0 .../compose.yml} | 0 .../compose2.yml} | 0 .../{no-figfile => no-composefile}/.gitignore | 0 .../fig.yml => ports-composefile/compose.yml} | 0 .../compose.yml} | 0 .../{fig.yml => compose.yml} | 0 tests/integration/cli_test.py | 61 ++++++++++--------- tests/integration/project_test.py | 30 ++++----- tests/integration/service_test.py | 52 ++++++++-------- tests/integration/testcases.py | 12 ++-- tests/unit/cli/docker_client_test.py | 2 +- tests/unit/cli/verbose_proxy_test.py | 2 +- tests/unit/cli_test.py | 30 +++++---- tests/unit/container_test.py | 10 +-- tests/unit/log_printer_test.py | 2 +- tests/unit/progress_stream_test.py | 2 +- tests/unit/project_test.py | 34 +++++------ tests/unit/service_test.py | 14 ++--- tests/unit/sort_service_test.py | 2 +- tests/unit/split_buffer_test.py | 2 +- 54 files changed, 199 insertions(+), 211 deletions(-) create mode 100755 bin/compose delete mode 100755 bin/fig rename {fig => compose}/__init__.py (100%) rename {fig => compose}/cli/__init__.py (100%) rename {fig => compose}/cli/colors.py (100%) rename {fig => compose}/cli/command.py (76%) rename {fig => compose}/cli/docker_client.py (100%) rename {fig => compose}/cli/docopt_command.py (100%) rename {fig => compose}/cli/errors.py (94%) rename {fig => compose}/cli/formatter.py (100%) rename {fig => compose}/cli/log_printer.py (100%) rename {fig => compose}/cli/main.py (93%) rename {fig => compose}/cli/multiplexer.py (100%) rename {fig => compose}/cli/utils.py (100%) rename {fig => compose}/cli/verbose_proxy.py (100%) rename {fig => compose}/container.py (100%) rename {fig => compose}/progress_stream.py (100%) rename {fig => compose}/project.py (98%) rename {fig => compose}/service.py (99%) delete mode 100755 script/deploy-docs create mode 100644 tests/fixtures/commands-composefile/compose.yml delete mode 100644 tests/fixtures/commands-figfile/fig.yml rename tests/fixtures/dockerfile_with_entrypoint/{fig.yml => compose.yml} (100%) rename tests/fixtures/{environment-figfile/fig.yml => environment-composefile/compose.yml} (100%) rename tests/fixtures/{links-figfile/fig.yml => links-composefile/compose.yml} (100%) rename tests/fixtures/{longer-filename-figfile/fig.yaml => longer-filename-composefile/compose.yaml} (100%) rename tests/fixtures/{multiple-figfiles/fig.yml => multiple-composefiles/compose.yml} (100%) rename tests/fixtures/{multiple-figfiles/fig2.yml => multiple-composefiles/compose2.yml} (100%) rename tests/fixtures/{no-figfile => no-composefile}/.gitignore (100%) rename tests/fixtures/{ports-figfile/fig.yml => ports-composefile/compose.yml} (100%) rename tests/fixtures/{simple-figfile/fig.yml => simple-composefile/compose.yml} (100%) rename tests/fixtures/simple-dockerfile/{fig.yml => compose.yml} (100%) diff --git a/.gitignore b/.gitignore index d987f272..83533dd7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ /dist /docs/_site /venv -fig.spec +compose.spec diff --git a/Dockerfile b/Dockerfile index c430950b..85313b30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,4 @@ RUN python setup.py install RUN chown -R user /code/ -ENTRYPOINT ["/usr/local/bin/fig"] +ENTRYPOINT ["/usr/local/bin/compose"] diff --git a/bin/compose b/bin/compose new file mode 100755 index 00000000..5976e1d4 --- /dev/null +++ b/bin/compose @@ -0,0 +1,3 @@ +#!/usr/bin/env python +from compose.cli.main import main +main() diff --git a/bin/fig b/bin/fig deleted file mode 100755 index 550a5e24..00000000 --- a/bin/fig +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -from fig.cli.main import main -main() diff --git a/fig/__init__.py b/compose/__init__.py similarity index 100% rename from fig/__init__.py rename to compose/__init__.py diff --git a/fig/cli/__init__.py b/compose/cli/__init__.py similarity index 100% rename from fig/cli/__init__.py rename to compose/cli/__init__.py diff --git a/fig/cli/colors.py b/compose/cli/colors.py similarity index 100% rename from fig/cli/colors.py rename to compose/cli/colors.py diff --git a/fig/cli/command.py b/compose/cli/command.py similarity index 76% rename from fig/cli/command.py rename to compose/cli/command.py index e1e7e5e4..45b124a1 100644 --- a/fig/cli/command.py +++ b/compose/cli/command.py @@ -43,11 +43,15 @@ class Command(DocoptCommand): def perform_command(self, options, handler, command_options): if options['COMMAND'] == 'help': - # Skip looking up the figfile. + # Skip looking up the compose file. handler(None, command_options) return - explicit_config_path = options.get('--file') or os.environ.get('FIG_FILE') + if 'FIG_FILE' in os.environ: + log.warn('The FIG_FILE environment variable is deprecated.') + log.warn('Please use COMPOSE_FILE instead.') + + explicit_config_path = options.get('--file') or os.environ.get('COMPOSE_FILE') or os.environ.get('FIG_FILE') project = self.get_project( self.get_config_path(explicit_config_path), project_name=options.get('--project-name'), @@ -59,7 +63,7 @@ class Command(DocoptCommand): client = docker_client() if verbose: version_info = six.iteritems(client.version()) - log.info("Fig version %s", __version__) + log.info("Compose version %s", __version__) log.info("Docker base_url: %s", client.base_url) log.info("Docker version: %s", ", ".join("%s=%s" % item for item in version_info)) @@ -72,7 +76,7 @@ class Command(DocoptCommand): return yaml.safe_load(fh) except IOError as e: if e.errno == errno.ENOENT: - raise errors.FigFileNotFound(os.path.basename(e.filename)) + raise errors.ComposeFileNotFound(os.path.basename(e.filename)) raise errors.UserError(six.text_type(e)) def get_project(self, config_path, project_name=None, verbose=False): @@ -88,7 +92,11 @@ class Command(DocoptCommand): def normalize_name(name): return re.sub(r'[^a-z0-9]', '', name.lower()) - project_name = project_name or os.environ.get('FIG_PROJECT_NAME') + if 'FIG_PROJECT_NAME' in os.environ: + log.warn('The FIG_PROJECT_NAME environment variable is deprecated.') + log.warn('Please use COMPOSE_PROJECT_NAME instead.') + + project_name = project_name or os.environ.get('COMPOSE_PROJECT_NAME') or os.environ.get('FIG_PROJECT_NAME') if project_name is not None: return normalize_name(project_name) @@ -102,13 +110,13 @@ class Command(DocoptCommand): if file_path: return os.path.join(self.base_dir, file_path) - if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')): - log.warning("Fig just read the file 'fig.yaml' on startup, rather " - "than 'fig.yml'") - log.warning("Please be aware that fig.yml the expected extension " + if os.path.exists(os.path.join(self.base_dir, 'compose.yaml')): + log.warning("Fig just read the file 'compose.yaml' on startup, rather " + "than 'compose.yml'") + log.warning("Please be aware that .yml is the expected extension " "in most cases, and using .yaml can cause compatibility " "issues in future") - return os.path.join(self.base_dir, 'fig.yaml') + return os.path.join(self.base_dir, 'compose.yaml') - return os.path.join(self.base_dir, 'fig.yml') + return os.path.join(self.base_dir, 'compose.yml') diff --git a/fig/cli/docker_client.py b/compose/cli/docker_client.py similarity index 100% rename from fig/cli/docker_client.py rename to compose/cli/docker_client.py diff --git a/fig/cli/docopt_command.py b/compose/cli/docopt_command.py similarity index 100% rename from fig/cli/docopt_command.py rename to compose/cli/docopt_command.py diff --git a/fig/cli/errors.py b/compose/cli/errors.py similarity index 94% rename from fig/cli/errors.py rename to compose/cli/errors.py index 53d1af36..d0d94724 100644 --- a/fig/cli/errors.py +++ b/compose/cli/errors.py @@ -55,8 +55,8 @@ class ConnectionErrorGeneric(UserError): """ % url) -class FigFileNotFound(UserError): +class ComposeFileNotFound(UserError): def __init__(self, filename): - super(FigFileNotFound, self).__init__(""" + super(ComposeFileNotFound, self).__init__(""" Can't find %s. Are you in the right directory? """ % filename) diff --git a/fig/cli/formatter.py b/compose/cli/formatter.py similarity index 100% rename from fig/cli/formatter.py rename to compose/cli/formatter.py diff --git a/fig/cli/log_printer.py b/compose/cli/log_printer.py similarity index 100% rename from fig/cli/log_printer.py rename to compose/cli/log_printer.py diff --git a/fig/cli/main.py b/compose/cli/main.py similarity index 93% rename from fig/cli/main.py rename to compose/cli/main.py index 8cdaee62..39191624 100644 --- a/fig/cli/main.py +++ b/compose/cli/main.py @@ -71,13 +71,13 @@ class TopLevelCommand(Command): """Fast, isolated development environments using Docker. Usage: - fig [options] [COMMAND] [ARGS...] - fig -h|--help + compose [options] [COMMAND] [ARGS...] + compose -h|--help Options: --verbose Show more output --version Print version and exit - -f, --file FILE Specify an alternate fig file (default: fig.yml) + -f, --file FILE Specify an alternate compose file (default: compose.yml) -p, --project-name NAME Specify an alternate project name (default: directory name) Commands: @@ -99,7 +99,7 @@ class TopLevelCommand(Command): """ def docopt_options(self): options = super(TopLevelCommand, self).docopt_options() - options['version'] = "fig %s" % __version__ + options['version'] = "compose %s" % __version__ return options def build(self, project, options): @@ -107,8 +107,8 @@ class TopLevelCommand(Command): Build or rebuild services. 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. + e.g. `composetest_db`. If you change a service's `Dockerfile` or the + contents of its build directory, you can run `compose build` to rebuild it. Usage: build [options] [SERVICE...] @@ -261,11 +261,11 @@ class TopLevelCommand(Command): For example: - $ fig run web python manage.py shell + $ compose run web python manage.py shell 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...]`. + `compose run --no-deps SERVICE COMMAND [ARGS...]`. Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] @@ -280,7 +280,7 @@ class TopLevelCommand(Command): --rm Remove container after run. Ignored in detached mode. --service-ports Run command with the service's ports enabled and mapped to the host. - -T Disable pseudo-tty allocation. By default `fig run` + -T Disable pseudo-tty allocation. By default `compose run` allocates a TTY. """ service = project.get_service(options['SERVICE']) @@ -352,7 +352,7 @@ class TopLevelCommand(Command): Numbers are specified in the form `service=num` as arguments. For example: - $ fig scale web=2 worker=3 + $ compose scale web=2 worker=3 Usage: scale [SERVICE=NUM...] """ @@ -372,7 +372,7 @@ class TopLevelCommand(Command): 'Service "%s" cannot be scaled because it specifies a port ' 'on the host. If multiple containers for this service were ' 'created, the port would clash.\n\nRemove the ":" from the ' - 'port definition in fig.yml so Docker can choose a random ' + 'port definition in compose.yml so Docker can choose a random ' 'port for each container.' % service_name) def start(self, project, options): @@ -387,7 +387,7 @@ class TopLevelCommand(Command): """ Stop running containers without removing them. - They can be started again with `fig start`. + They can be started again with `compose start`. Usage: stop [SERVICE...] """ @@ -405,14 +405,14 @@ class TopLevelCommand(Command): """ Build, (re)create, start and attach to containers for a service. - 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`, + By default, `compose up` will aggregate the output of each container, and + when it exits, all containers will be stopped. If you run `compose 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 + If there are existing containers for a service, `compose up` will stop and recreate them (preserving mounted volumes with volumes-from), - 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 + so that changes in `compose.yml` are picked up. If you do not want existing + containers to be recreated, `compose up --no-recreate` will re-use existing containers. Usage: up [options] [SERVICE...] diff --git a/fig/cli/multiplexer.py b/compose/cli/multiplexer.py similarity index 100% rename from fig/cli/multiplexer.py rename to compose/cli/multiplexer.py diff --git a/fig/cli/utils.py b/compose/cli/utils.py similarity index 100% rename from fig/cli/utils.py rename to compose/cli/utils.py diff --git a/fig/cli/verbose_proxy.py b/compose/cli/verbose_proxy.py similarity index 100% rename from fig/cli/verbose_proxy.py rename to compose/cli/verbose_proxy.py diff --git a/fig/container.py b/compose/container.py similarity index 100% rename from fig/container.py rename to compose/container.py diff --git a/fig/progress_stream.py b/compose/progress_stream.py similarity index 100% rename from fig/progress_stream.py rename to compose/progress_stream.py diff --git a/fig/project.py b/compose/project.py similarity index 98% rename from fig/project.py rename to compose/project.py index e013da4e..b707d637 100644 --- a/fig/project.py +++ b/compose/project.py @@ -67,7 +67,7 @@ class Project(object): dicts = [] for service_name, service in list(config.items()): if not isinstance(service, dict): - raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.' % service_name) + raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your compose.yml must map to a dictionary of configuration options.' % service_name) service['name'] = service_name dicts.append(service) return cls.from_dicts(name, dicts, client) diff --git a/fig/service.py b/compose/service.py similarity index 99% rename from fig/service.py rename to compose/service.py index 6743e171..70958405 100644 --- a/fig/service.py +++ b/compose/service.py @@ -127,7 +127,7 @@ class Service(object): return project == self.project and name == self.name def get_container(self, number=1): - """Return a :class:`fig.container.Container` for this service. The + """Return a :class:`compose.container.Container` for this service. The container must be active, and match `number`. """ for container in self.client.containers(): diff --git a/script/build-docs b/script/build-docs index abcaec4a..edcc5238 100755 --- a/script/build-docs +++ b/script/build-docs @@ -1,5 +1,5 @@ #!/bin/bash set -ex pushd docs -fig run --rm jekyll jekyll build +compose run --rm jekyll jekyll build popd diff --git a/script/build-linux b/script/build-linux index f7b99210..aa69fa7b 100755 --- a/script/build-linux +++ b/script/build-linux @@ -2,7 +2,7 @@ set -ex mkdir -p `pwd`/dist chmod 777 `pwd`/dist -docker build -t fig . -docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller fig -F bin/fig -mv dist/fig dist/fig-Linux-x86_64 -docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/fig-Linux-x86_64 fig --version +docker build -t compose . +docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller compose -F bin/compose +mv dist/compose dist/compose-Linux-x86_64 +docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/compose-Linux-x86_64 compose --version diff --git a/script/build-osx b/script/build-osx index 359e9a03..9b239caf 100755 --- a/script/build-osx +++ b/script/build-osx @@ -5,6 +5,6 @@ virtualenv venv venv/bin/pip install -r requirements.txt venv/bin/pip install -r requirements-dev.txt venv/bin/pip install . -venv/bin/pyinstaller -F bin/fig -mv dist/fig dist/fig-Darwin-x86_64 -dist/fig-Darwin-x86_64 --version +venv/bin/pyinstaller -F bin/compose +mv dist/compose dist/compose-Darwin-x86_64 +dist/compose-Darwin-x86_64 --version diff --git a/script/clean b/script/clean index d9fa444c..0c845012 100755 --- a/script/clean +++ b/script/clean @@ -1,3 +1,3 @@ #!/bin/sh find . -type f -name '*.pyc' -delete -rm -rf docs/_site build dist fig.egg-info +rm -rf docs/_site build dist compose.egg-info diff --git a/script/deploy-docs b/script/deploy-docs deleted file mode 100755 index 424472f8..00000000 --- a/script/deploy-docs +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -set -ex - -script/build-docs - -pushd docs/_site - -export GIT_DIR=.git-gh-pages -export GIT_WORK_TREE=. - -if [ ! -d "$GIT_DIR" ]; then - git init -fi - -if !(git remote | grep origin); then - git remote add origin git@github.com:docker/fig.git -fi - -git fetch origin -git reset --soft origin/gh-pages - -echo ".git-gh-pages" > .gitignore - -git add -A . - -git commit -m "update" || echo "didn't commit" -git push origin master:gh-pages - -popd diff --git a/script/test b/script/test index e73ba893..b7e7245d 100755 --- a/script/test +++ b/script/test @@ -1,5 +1,5 @@ #!/bin/sh set -ex -docker build -t fig . -docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint flake8 fig fig -docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint nosetests fig $@ +docker build -t compose . +docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint flake8 compose compose +docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint nosetests compose $@ diff --git a/setup.py b/setup.py index 4cf8e589..7c21c53d 100644 --- a/setup.py +++ b/setup.py @@ -48,10 +48,10 @@ if sys.version_info < (2, 7): setup( - name='fig', - version=find_version("fig", "__init__.py"), - description='Fast, isolated development environments using Docker', - url='http://www.fig.sh/', + name='compose', + version=find_version("compose", "__init__.py"), + description='Multi-container orchestration for Docker', + url='https://www.docker.com/', author='Docker, Inc.', license='Apache License 2.0', packages=find_packages(exclude=[ 'tests.*', 'tests' ]), @@ -61,6 +61,6 @@ setup( tests_require=tests_require, entry_points=""" [console_scripts] - fig=fig.cli.main:main + compose=compose.cli.main:main """, ) diff --git a/tests/fixtures/commands-composefile/compose.yml b/tests/fixtures/commands-composefile/compose.yml new file mode 100644 index 00000000..87602bd6 --- /dev/null +++ b/tests/fixtures/commands-composefile/compose.yml @@ -0,0 +1,5 @@ +implicit: + image: composetest_test +explicit: + image: composetest_test + command: [ "/bin/true" ] diff --git a/tests/fixtures/commands-figfile/fig.yml b/tests/fixtures/commands-figfile/fig.yml deleted file mode 100644 index 707c18a7..00000000 --- a/tests/fixtures/commands-figfile/fig.yml +++ /dev/null @@ -1,5 +0,0 @@ -implicit: - image: figtest_test -explicit: - image: figtest_test - command: [ "/bin/true" ] diff --git a/tests/fixtures/dockerfile_with_entrypoint/fig.yml b/tests/fixtures/dockerfile_with_entrypoint/compose.yml similarity index 100% rename from tests/fixtures/dockerfile_with_entrypoint/fig.yml rename to tests/fixtures/dockerfile_with_entrypoint/compose.yml diff --git a/tests/fixtures/environment-figfile/fig.yml b/tests/fixtures/environment-composefile/compose.yml similarity index 100% rename from tests/fixtures/environment-figfile/fig.yml rename to tests/fixtures/environment-composefile/compose.yml diff --git a/tests/fixtures/links-figfile/fig.yml b/tests/fixtures/links-composefile/compose.yml similarity index 100% rename from tests/fixtures/links-figfile/fig.yml rename to tests/fixtures/links-composefile/compose.yml diff --git a/tests/fixtures/longer-filename-figfile/fig.yaml b/tests/fixtures/longer-filename-composefile/compose.yaml similarity index 100% rename from tests/fixtures/longer-filename-figfile/fig.yaml rename to tests/fixtures/longer-filename-composefile/compose.yaml diff --git a/tests/fixtures/multiple-figfiles/fig.yml b/tests/fixtures/multiple-composefiles/compose.yml similarity index 100% rename from tests/fixtures/multiple-figfiles/fig.yml rename to tests/fixtures/multiple-composefiles/compose.yml diff --git a/tests/fixtures/multiple-figfiles/fig2.yml b/tests/fixtures/multiple-composefiles/compose2.yml similarity index 100% rename from tests/fixtures/multiple-figfiles/fig2.yml rename to tests/fixtures/multiple-composefiles/compose2.yml diff --git a/tests/fixtures/no-figfile/.gitignore b/tests/fixtures/no-composefile/.gitignore similarity index 100% rename from tests/fixtures/no-figfile/.gitignore rename to tests/fixtures/no-composefile/.gitignore diff --git a/tests/fixtures/ports-figfile/fig.yml b/tests/fixtures/ports-composefile/compose.yml similarity index 100% rename from tests/fixtures/ports-figfile/fig.yml rename to tests/fixtures/ports-composefile/compose.yml diff --git a/tests/fixtures/simple-figfile/fig.yml b/tests/fixtures/simple-composefile/compose.yml similarity index 100% rename from tests/fixtures/simple-figfile/fig.yml rename to tests/fixtures/simple-composefile/compose.yml diff --git a/tests/fixtures/simple-dockerfile/fig.yml b/tests/fixtures/simple-dockerfile/compose.yml similarity index 100% rename from tests/fixtures/simple-dockerfile/fig.yml rename to tests/fixtures/simple-dockerfile/compose.yml diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index 2f7ecb59..cf939837 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -5,7 +5,7 @@ from six import StringIO from mock import patch from .testcases import DockerClientTestCase -from fig.cli.main import TopLevelCommand +from compose.cli.main import TopLevelCommand class CLITestCase(DockerClientTestCase): @@ -14,7 +14,7 @@ class CLITestCase(DockerClientTestCase): self.old_sys_exit = sys.exit sys.exit = lambda code=0: None self.command = TopLevelCommand() - self.command.base_dir = 'tests/fixtures/simple-figfile' + self.command.base_dir = 'tests/fixtures/simple-composefile' def tearDown(self): sys.exit = self.old_sys_exit @@ -27,43 +27,44 @@ class CLITestCase(DockerClientTestCase): def test_help(self): old_base_dir = self.command.base_dir - self.command.base_dir = 'tests/fixtures/no-figfile' + self.command.base_dir = 'tests/fixtures/no-composefile' with self.assertRaises(SystemExit) as exc_context: self.command.dispatch(['help', 'up'], None) self.assertIn('Usage: up [options] [SERVICE...]', str(exc_context.exception)) # self.project.kill() fails during teardown - # unless there is a figfile. + # unless there is a composefile. self.command.base_dir = old_base_dir + # TODO: address the "Inappropriate ioctl for device" warnings in test output @patch('sys.stdout', new_callable=StringIO) def test_ps(self, mock_stdout): self.project.get_service('simple').create_container() self.command.dispatch(['ps'], None) - self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue()) + self.assertIn('simplecomposefile_simple_1', mock_stdout.getvalue()) @patch('sys.stdout', new_callable=StringIO) - def test_ps_default_figfile(self, mock_stdout): - self.command.base_dir = 'tests/fixtures/multiple-figfiles' + def test_ps_default_composefile(self, mock_stdout): + self.command.base_dir = 'tests/fixtures/multiple-composefiles' self.command.dispatch(['up', '-d'], None) self.command.dispatch(['ps'], None) output = mock_stdout.getvalue() - self.assertIn('multiplefigfiles_simple_1', output) - self.assertIn('multiplefigfiles_another_1', output) - self.assertNotIn('multiplefigfiles_yetanother_1', output) + self.assertIn('multiplecomposefiles_simple_1', output) + self.assertIn('multiplecomposefiles_another_1', output) + self.assertNotIn('multiplecomposefiles_yetanother_1', output) @patch('sys.stdout', new_callable=StringIO) - def test_ps_alternate_figfile(self, mock_stdout): - self.command.base_dir = 'tests/fixtures/multiple-figfiles' - self.command.dispatch(['-f', 'fig2.yml', 'up', '-d'], None) - self.command.dispatch(['-f', 'fig2.yml', 'ps'], None) + def test_ps_alternate_composefile(self, mock_stdout): + self.command.base_dir = 'tests/fixtures/multiple-composefiles' + self.command.dispatch(['-f', 'compose2.yml', 'up', '-d'], None) + self.command.dispatch(['-f', 'compose2.yml', 'ps'], None) output = mock_stdout.getvalue() - self.assertNotIn('multiplefigfiles_simple_1', output) - self.assertNotIn('multiplefigfiles_another_1', output) - self.assertIn('multiplefigfiles_yetanother_1', output) + self.assertNotIn('multiplecomposefiles_simple_1', output) + self.assertNotIn('multiplecomposefiles_another_1', output) + self.assertIn('multiplecomposefiles_yetanother_1', output) - @patch('fig.service.log') + @patch('compose.service.log') def test_pull(self, mock_logging): self.command.dispatch(['pull'], None) mock_logging.info.assert_any_call('Pulling simple (busybox:latest)...') @@ -99,7 +100,7 @@ class CLITestCase(DockerClientTestCase): self.assertFalse(config['AttachStdin']) def test_up_with_links(self): - self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.base_dir = 'tests/fixtures/links-composefile' self.command.dispatch(['up', '-d', 'web'], None) web = self.project.get_service('web') db = self.project.get_service('db') @@ -109,7 +110,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(console.containers()), 0) def test_up_with_no_deps(self): - self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.base_dir = 'tests/fixtures/links-composefile' self.command.dispatch(['up', '-d', '--no-deps', 'web'], None) web = self.project.get_service('web') db = self.project.get_service('db') @@ -148,7 +149,7 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_service_without_links(self, mock_stdout): - self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.base_dir = 'tests/fixtures/links-composefile' self.command.dispatch(['run', 'console', '/bin/true'], None) self.assertEqual(len(self.project.containers()), 0) @@ -161,7 +162,7 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_service_with_links(self, __): - self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.base_dir = 'tests/fixtures/links-composefile' self.command.dispatch(['run', 'web', '/bin/true'], None) db = self.project.get_service('db') console = self.project.get_service('console') @@ -170,14 +171,14 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_with_no_deps(self, __): - self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.base_dir = 'tests/fixtures/links-composefile' self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None) db = self.project.get_service('db') self.assertEqual(len(db.containers()), 0) @patch('dockerpty.start') def test_run_does_not_recreate_linked_containers(self, __): - self.command.base_dir = 'tests/fixtures/links-figfile' + self.command.base_dir = 'tests/fixtures/links-composefile' self.command.dispatch(['up', '-d', 'db'], None) db = self.project.get_service('db') self.assertEqual(len(db.containers()), 1) @@ -193,8 +194,8 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_without_command(self, __): - self.command.base_dir = 'tests/fixtures/commands-figfile' - self.check_build('tests/fixtures/simple-dockerfile', tag='figtest_test') + self.command.base_dir = 'tests/fixtures/commands-composefile' + self.check_build('tests/fixtures/simple-dockerfile', tag='composetest_test') for c in self.project.containers(stopped=True, one_off=True): c.remove() @@ -233,7 +234,7 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_service_with_environement_overridden(self, _): name = 'service' - self.command.base_dir = 'tests/fixtures/environment-figfile' + self.command.base_dir = 'tests/fixtures/environment-composefile' self.command.dispatch( ['run', '-e', 'foo=notbar', '-e', 'allo=moto=bobo', '-e', 'alpha=beta', name], @@ -253,7 +254,7 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_service_without_map_ports(self, __): # create one off container - self.command.base_dir = 'tests/fixtures/ports-figfile' + self.command.base_dir = 'tests/fixtures/ports-composefile' self.command.dispatch(['run', '-d', 'simple'], None) container = self.project.get_service('simple').containers(one_off=True)[0] @@ -271,7 +272,7 @@ class CLITestCase(DockerClientTestCase): @patch('dockerpty.start') def test_run_service_with_map_ports(self, __): # create one off container - self.command.base_dir = 'tests/fixtures/ports-figfile' + self.command.base_dir = 'tests/fixtures/ports-composefile' self.command.dispatch(['run', '-d', '--service-ports', 'simple'], None) container = self.project.get_service('simple').containers(one_off=True)[0] @@ -368,7 +369,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(project.get_service('another').containers()), 0) def test_port(self): - self.command.base_dir = 'tests/fixtures/ports-figfile' + self.command.base_dir = 'tests/fixtures/ports-composefile' self.command.dispatch(['up', '-d'], None) container = self.project.get_service('simple').get_container() diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index ce087245..2577fd61 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,13 +1,13 @@ from __future__ import unicode_literals -from fig.project import Project, ConfigurationError -from fig.container import Container +from compose.project import Project, ConfigurationError +from compose.container import Container from .testcases import DockerClientTestCase class ProjectTest(DockerClientTestCase): def test_volumes_from_service(self): project = Project.from_config( - name='figtest', + name='composetest', config={ 'data': { 'image': 'busybox:latest', @@ -29,14 +29,14 @@ class ProjectTest(DockerClientTestCase): self.client, image='busybox:latest', volumes=['/var/data'], - name='figtest_data_container', + name='composetest_data_container', ) project = Project.from_config( - name='figtest', + name='composetest', config={ 'db': { 'image': 'busybox:latest', - 'volumes_from': ['figtest_data_container'], + 'volumes_from': ['composetest_data_container'], }, }, client=self.client, @@ -47,7 +47,7 @@ class ProjectTest(DockerClientTestCase): def test_start_stop_kill_remove(self): web = self.create_service('web') db = self.create_service('db') - project = Project('figtest', [web, db], self.client) + project = Project('composetest', [web, db], self.client) project.start() @@ -80,7 +80,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up(self): web = self.create_service('web') db = self.create_service('db', volumes=['/var/db']) - project = Project('figtest', [web, db], self.client) + project = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -95,7 +95,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_recreates_containers(self): web = self.create_service('web') db = self.create_service('db', volumes=['/etc']) - project = Project('figtest', [web, db], self.client) + project = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -117,7 +117,7 @@ class ProjectTest(DockerClientTestCase): 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 = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -140,7 +140,7 @@ class ProjectTest(DockerClientTestCase): 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 = Project('composetest', [web, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -169,7 +169,7 @@ class ProjectTest(DockerClientTestCase): 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 = Project('composetest', [console, db], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -186,7 +186,7 @@ class ProjectTest(DockerClientTestCase): 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 = Project('composetest', [web, db, console], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -204,7 +204,7 @@ class ProjectTest(DockerClientTestCase): 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 = Project('composetest', [web, db, console], self.client) project.start() self.assertEqual(len(project.containers()), 0) @@ -219,7 +219,7 @@ class ProjectTest(DockerClientTestCase): def test_unscale_after_restart(self): web = self.create_service('web') - project = Project('figtest', [web], self.client) + project = Project('composetest', [web], self.client) project.start() diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index b7b11d5f..e11672bf 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -3,9 +3,9 @@ from __future__ import absolute_import import os from os import path -from fig import Service -from fig.service import CannotBeScaledError -from fig.container import Container +from compose import Service +from compose.service import CannotBeScaledError +from compose.container import Container from docker.errors import APIError from .testcases import DockerClientTestCase @@ -23,7 +23,7 @@ class ServiceTest(DockerClientTestCase): create_and_start_container(foo) self.assertEqual(len(foo.containers()), 1) - self.assertEqual(foo.containers()[0].name, 'figtest_foo_1') + self.assertEqual(foo.containers()[0].name, 'composetest_foo_1') self.assertEqual(len(bar.containers()), 0) create_and_start_container(bar) @@ -33,8 +33,8 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(bar.containers()), 2) names = [c.name for c in bar.containers()] - self.assertIn('figtest_bar_1', names) - self.assertIn('figtest_bar_2', names) + self.assertIn('composetest_bar_1', names) + self.assertIn('composetest_bar_2', names) def test_containers_one_off(self): db = self.create_service('db') @@ -45,7 +45,7 @@ class ServiceTest(DockerClientTestCase): def test_project_is_added_to_container_name(self): service = self.create_service('web') create_and_start_container(service) - self.assertEqual(service.containers()[0].name, 'figtest_web_1') + self.assertEqual(service.containers()[0].name, 'composetest_web_1') def test_start_stop(self): service = self.create_service('scalingtest') @@ -86,13 +86,13 @@ class ServiceTest(DockerClientTestCase): def test_create_container_with_one_off(self): db = self.create_service('db') container = db.create_container(one_off=True) - self.assertEqual(container.name, 'figtest_db_run_1') + self.assertEqual(container.name, 'composetest_db_run_1') def test_create_container_with_one_off_when_existing_container_is_running(self): db = self.create_service('db') db.start() container = db.create_container(one_off=True) - self.assertEqual(container.name, 'figtest_db_run_1') + self.assertEqual(container.name, 'composetest_db_run_1') def test_create_container_with_unspecified_volume(self): service = self.create_service('db', volumes=['/var/db']) @@ -146,7 +146,7 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['sleep']) self.assertEqual(old_container.dictionary['Config']['Cmd'], ['300']) self.assertIn('FOO=1', old_container.dictionary['Config']['Env']) - self.assertEqual(old_container.name, 'figtest_db_1') + self.assertEqual(old_container.name, 'composetest_db_1') service.start_container(old_container) volume_path = old_container.inspect()['Volumes']['/etc'] @@ -163,7 +163,7 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep']) self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300']) self.assertIn('FOO=2', new_container.dictionary['Config']['Env']) - self.assertEqual(new_container.name, 'figtest_db_1') + self.assertEqual(new_container.name, 'composetest_db_1') self.assertEqual(new_container.inspect()['Volumes']['/etc'], volume_path) self.assertIn(intermediate_container.id, new_container.dictionary['HostConfig']['VolumesFrom']) @@ -226,8 +226,8 @@ class ServiceTest(DockerClientTestCase): self.assertEqual( set(web.containers()[0].links()), set([ - 'figtest_db_1', 'db_1', - 'figtest_db_2', 'db_2', + 'composetest_db_1', 'db_1', + 'composetest_db_2', 'db_2', 'db', ]), ) @@ -243,17 +243,17 @@ class ServiceTest(DockerClientTestCase): self.assertEqual( set(web.containers()[0].links()), set([ - 'figtest_db_1', 'db_1', - 'figtest_db_2', 'db_2', + 'composetest_db_1', 'db_1', + 'composetest_db_2', 'db_2', 'custom_link_name', ]), ) def test_start_container_with_external_links(self): db = self.create_service('db') - web = self.create_service('web', external_links=['figtest_db_1', - 'figtest_db_2', - 'figtest_db_3:db_3']) + web = self.create_service('web', external_links=['composetest_db_1', + 'composetest_db_2', + 'composetest_db_3:db_3']) for _ in range(3): create_and_start_container(db) @@ -262,8 +262,8 @@ class ServiceTest(DockerClientTestCase): self.assertEqual( set(web.containers()[0].links()), set([ - 'figtest_db_1', - 'figtest_db_2', + 'composetest_db_1', + 'composetest_db_2', 'db_3', ]), ) @@ -288,8 +288,8 @@ class ServiceTest(DockerClientTestCase): self.assertEqual( set(c.links()), set([ - 'figtest_db_1', 'db_1', - 'figtest_db_2', 'db_2', + 'composetest_db_1', 'db_1', + 'composetest_db_2', 'db_2', 'db', ]), ) @@ -299,20 +299,20 @@ class ServiceTest(DockerClientTestCase): name='test', client=self.client, build='tests/fixtures/simple-dockerfile', - project='figtest', + project='composetest', ) container = create_and_start_container(service) container.wait() self.assertIn('success', container.logs()) - self.assertEqual(len(self.client.images(name='figtest_test')), 1) + self.assertEqual(len(self.client.images(name='composetest_test')), 1) def test_start_container_uses_tagged_image_if_it_exists(self): - self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test') + self.client.build('tests/fixtures/simple-dockerfile', tag='composetest_test') service = Service( name='test', client=self.client, build='this/does/not/exist/and/will/throw/error', - project='figtest', + project='composetest', ) container = create_and_start_container(service) container.wait() diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 9e65b904..53882561 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from __future__ import absolute_import -from fig.service import Service -from fig.cli.docker_client import docker_client -from fig.progress_stream import stream_output +from compose.service import Service +from compose.cli.docker_client import docker_client +from compose.progress_stream import stream_output from .. import unittest @@ -13,18 +13,18 @@ class DockerClientTestCase(unittest.TestCase): def setUp(self): for c in self.client.containers(all=True): - if c['Names'] and 'figtest' in c['Names'][0]: + if c['Names'] and 'composetest' in c['Names'][0]: self.client.kill(c['Id']) self.client.remove_container(c['Id']) for i in self.client.images(): - if isinstance(i.get('Tag'), basestring) and 'figtest' in i['Tag']: + if isinstance(i.get('Tag'), basestring) and 'composetest' in i['Tag']: self.client.remove_image(i) def create_service(self, name, **kwargs): if 'command' not in kwargs: kwargs['command'] = ["/bin/sleep", "300"] return Service( - project='figtest', + project='composetest', name=name, client=self.client, image="busybox:latest", diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 67575ee0..abd40ef0 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -5,7 +5,7 @@ import os import mock from tests import unittest -from fig.cli import docker_client +from compose.cli import docker_client class DockerClientTestCase(unittest.TestCase): diff --git a/tests/unit/cli/verbose_proxy_test.py b/tests/unit/cli/verbose_proxy_test.py index 90067f0d..59417bb3 100644 --- a/tests/unit/cli/verbose_proxy_test.py +++ b/tests/unit/cli/verbose_proxy_test.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import from tests import unittest -from fig.cli import verbose_proxy +from compose.cli import verbose_proxy class VerboseProxyTestCase(unittest.TestCase): diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index bc3daa11..1154d3de 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -6,8 +6,8 @@ from .. import unittest import mock -from fig.cli import main -from fig.cli.main import TopLevelCommand +from compose.cli import main +from compose.cli.main import TopLevelCommand from six import StringIO @@ -16,18 +16,18 @@ class CLITestCase(unittest.TestCase): cwd = os.getcwd() try: - os.chdir('tests/fixtures/simple-figfile') + os.chdir('tests/fixtures/simple-composefile') command = TopLevelCommand() project_name = command.get_project_name(command.get_config_path()) - self.assertEquals('simplefigfile', project_name) + self.assertEquals('simplecomposefile', project_name) finally: os.chdir(cwd) def test_project_name_with_explicit_base_dir(self): command = TopLevelCommand() - command.base_dir = 'tests/fixtures/simple-figfile' + command.base_dir = 'tests/fixtures/simple-composefile' project_name = command.get_project_name(command.get_config_path()) - self.assertEquals('simplefigfile', project_name) + self.assertEquals('simplecomposefile', project_name) def test_project_name_with_explicit_uppercase_base_dir(self): command = TopLevelCommand() @@ -41,7 +41,7 @@ class CLITestCase(unittest.TestCase): project_name = command.get_project_name(None, project_name=name) self.assertEquals('explicitprojectname', project_name) - def test_project_name_from_environment(self): + def test_project_name_from_environment_old_var(self): command = TopLevelCommand() name = 'namefromenv' with mock.patch.dict(os.environ): @@ -49,18 +49,26 @@ class CLITestCase(unittest.TestCase): project_name = command.get_project_name(None) self.assertEquals(project_name, name) + def test_project_name_from_environment_new_var(self): + command = TopLevelCommand() + name = 'namefromenv' + with mock.patch.dict(os.environ): + os.environ['COMPOSE_PROJECT_NAME'] = name + project_name = command.get_project_name(None) + self.assertEquals(project_name, name) + def test_yaml_filename_check(self): command = TopLevelCommand() - command.base_dir = 'tests/fixtures/longer-filename-figfile' - with mock.patch('fig.cli.command.log', autospec=True) as mock_log: + command.base_dir = 'tests/fixtures/longer-filename-composefile' + with mock.patch('compose.cli.command.log', autospec=True) as mock_log: self.assertTrue(command.get_config_path()) self.assertEqual(mock_log.warning.call_count, 2) def test_get_project(self): command = TopLevelCommand() - command.base_dir = 'tests/fixtures/longer-filename-figfile' + command.base_dir = 'tests/fixtures/longer-filename-composefile' project = command.get_project(command.get_config_path()) - self.assertEqual(project.name, 'longerfilenamefigfile') + self.assertEqual(project.name, 'longerfilenamecomposefile') self.assertTrue(project.client) self.assertTrue(project.services) diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 26468118..a5f3f7d3 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -4,7 +4,7 @@ from .. import unittest import mock import docker -from fig.container import Container +from compose.container import Container class ContainerTest(unittest.TestCase): @@ -20,7 +20,7 @@ class ContainerTest(unittest.TestCase): "Ports": None, "SizeRw": 0, "SizeRootFs": 0, - "Names": ["/figtest_db_1", "/figtest_web_1/db"], + "Names": ["/composetest_db_1", "/composetest_web_1/db"], "NetworkSettings": { "Ports": {}, }, @@ -33,7 +33,7 @@ class ContainerTest(unittest.TestCase): self.assertEqual(container.dictionary, { "Id": "abc", "Image":"busybox:latest", - "Name": "/figtest_db_1", + "Name": "/composetest_db_1", }) def test_from_ps_prefixed(self): @@ -45,7 +45,7 @@ class ContainerTest(unittest.TestCase): self.assertEqual(container.dictionary, { "Id": "abc", "Image":"busybox:latest", - "Name": "/figtest_db_1", + "Name": "/composetest_db_1", }) def test_environment(self): @@ -73,7 +73,7 @@ class ContainerTest(unittest.TestCase): container = Container.from_ps(None, self.container_dict, has_been_inspected=True) - self.assertEqual(container.name, "figtest_db_1") + self.assertEqual(container.name, "composetest_db_1") def test_name_without_project(self): container = Container.from_ps(None, diff --git a/tests/unit/log_printer_test.py b/tests/unit/log_printer_test.py index 40dca775..e40a1f75 100644 --- a/tests/unit/log_printer_test.py +++ b/tests/unit/log_printer_test.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from __future__ import absolute_import import os -from fig.cli.log_printer import LogPrinter +from compose.cli.log_printer import LogPrinter from .. import unittest diff --git a/tests/unit/progress_stream_test.py b/tests/unit/progress_stream_test.py index 29759d1d..b53f2eb9 100644 --- a/tests/unit/progress_stream_test.py +++ b/tests/unit/progress_stream_test.py @@ -5,7 +5,7 @@ from tests import unittest import mock from six import StringIO -from fig import progress_stream +from compose import progress_stream class ProgressStreamTestCase(unittest.TestCase): diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 5c8d35b1..d7aca64c 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -1,11 +1,11 @@ from __future__ import unicode_literals from .. import unittest -from fig.service import Service -from fig.project import Project, ConfigurationError +from compose.service import Service +from compose.project import Project, ConfigurationError class ProjectTest(unittest.TestCase): def test_from_dict(self): - project = Project.from_dicts('figtest', [ + project = Project.from_dicts('composetest', [ { 'name': 'web', 'image': 'busybox:latest' @@ -22,7 +22,7 @@ class ProjectTest(unittest.TestCase): self.assertEqual(project.get_service('db').options['image'], 'busybox:latest') def test_from_dict_sorts_in_dependency_order(self): - project = Project.from_dicts('figtest', [ + project = Project.from_dicts('composetest', [ { 'name': 'web', 'image': 'busybox:latest', @@ -45,7 +45,7 @@ class ProjectTest(unittest.TestCase): self.assertEqual(project.services[2].name, 'web') def test_from_config(self): - project = Project.from_config('figtest', { + project = Project.from_config('composetest', { 'web': { 'image': 'busybox:latest', }, @@ -61,13 +61,13 @@ class ProjectTest(unittest.TestCase): def test_from_config_throws_error_when_not_dict(self): with self.assertRaises(ConfigurationError): - project = Project.from_config('figtest', { + project = Project.from_config('composetest', { 'web': 'busybox:latest', }, None) def test_get_service(self): web = Service( - project='figtest', + project='composetest', name='web', client=None, image="busybox:latest", @@ -77,11 +77,11 @@ class ProjectTest(unittest.TestCase): def test_get_services_returns_all_services_without_args(self): web = Service( - project='figtest', + project='composetest', name='web', ) console = Service( - project='figtest', + project='composetest', name='console', ) project = Project('test', [web, console], None) @@ -89,11 +89,11 @@ class ProjectTest(unittest.TestCase): def test_get_services_returns_listed_services_with_args(self): web = Service( - project='figtest', + project='composetest', name='web', ) console = Service( - project='figtest', + project='composetest', name='console', ) project = Project('test', [web, console], None) @@ -101,20 +101,20 @@ class ProjectTest(unittest.TestCase): def test_get_services_with_include_links(self): db = Service( - project='figtest', + project='composetest', name='db', ) web = Service( - project='figtest', + project='composetest', name='web', links=[(db, 'database')] ) cache = Service( - project='figtest', + project='composetest', name='cache' ) console = Service( - project='figtest', + project='composetest', name='console', links=[(web, 'web')] ) @@ -126,11 +126,11 @@ class ProjectTest(unittest.TestCase): def test_get_services_removes_duplicates_following_links(self): db = Service( - project='figtest', + project='composetest', name='db', ) web = Service( - project='figtest', + project='composetest', name='web', links=[(db, 'database')] ) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 6db603d0..0a7239b0 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -8,9 +8,9 @@ import mock import docker from requests import Response -from fig import Service -from fig.container import Container -from fig.service import ( +from compose import Service +from compose.container import Container +from compose.service import ( ConfigError, split_port, build_port_bindings, @@ -203,7 +203,7 @@ class ServiceTest(unittest.TestCase): self.assertRaises(ValueError, service.get_container) - @mock.patch('fig.service.Container', autospec=True) + @mock.patch('compose.service.Container', autospec=True) def test_get_container(self, mock_container_class): container_dict = dict(Name='default_foo_2') self.mock_client.containers.return_value = [container_dict] @@ -214,15 +214,15 @@ class ServiceTest(unittest.TestCase): mock_container_class.from_ps.assert_called_once_with( self.mock_client, container_dict) - @mock.patch('fig.service.log', autospec=True) + @mock.patch('compose.service.log', autospec=True) def test_pull_image(self, mock_log): service = Service('foo', client=self.mock_client, image='someimage:sometag') service.pull(insecure_registry=True) self.mock_client.pull.assert_called_once_with('someimage:sometag', insecure_registry=True) mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...') - @mock.patch('fig.service.Container', autospec=True) - @mock.patch('fig.service.log', autospec=True) + @mock.patch('compose.service.Container', autospec=True) + @mock.patch('compose.service.log', autospec=True) def test_create_container_from_insecure_registry( self, mock_log, diff --git a/tests/unit/sort_service_test.py b/tests/unit/sort_service_test.py index e2a7bdb3..420353c8 100644 --- a/tests/unit/sort_service_test.py +++ b/tests/unit/sort_service_test.py @@ -1,4 +1,4 @@ -from fig.project import sort_service_dicts, DependencyError +from compose.project import sort_service_dicts, DependencyError from .. import unittest diff --git a/tests/unit/split_buffer_test.py b/tests/unit/split_buffer_test.py index 41dc50e4..3322fb55 100644 --- a/tests/unit/split_buffer_test.py +++ b/tests/unit/split_buffer_test.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import -from fig.cli.utils import split_buffer +from compose.cli.utils import split_buffer from .. import unittest class SplitBufferTest(unittest.TestCase): From 620e29b63ffd6a8b609c24a176889532673137d0 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 20 Jan 2015 11:27:10 +0000 Subject: [PATCH 0713/2154] Rename binary to docker-compose and config file to docker-compose.yml Signed-off-by: Aanand Prasad --- .gitignore | 2 +- Dockerfile | 2 +- bin/{compose => docker-compose} | 0 compose/cli/command.py | 10 +++---- compose/cli/main.py | 30 +++++++++---------- script/build-docs | 2 +- script/build-linux | 8 ++--- script/build-osx | 6 ++-- script/test | 6 ++-- setup.py | 4 +-- .../{compose.yml => docker-compose.yml} | 0 .../{compose.yml => docker-compose.yml} | 0 .../{compose.yml => docker-compose.yml} | 0 .../{compose.yml => docker-compose.yml} | 0 .../{compose.yaml => docker-compose.yaml} | 0 .../{compose.yml => docker-compose.yml} | 0 .../{compose.yml => docker-compose.yml} | 0 .../{compose.yml => docker-compose.yml} | 0 .../{compose.yml => docker-compose.yml} | 0 19 files changed, 35 insertions(+), 35 deletions(-) rename bin/{compose => docker-compose} (100%) rename tests/fixtures/commands-composefile/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/dockerfile_with_entrypoint/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/environment-composefile/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/links-composefile/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/longer-filename-composefile/{compose.yaml => docker-compose.yaml} (100%) rename tests/fixtures/multiple-composefiles/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/ports-composefile/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/simple-composefile/{compose.yml => docker-compose.yml} (100%) rename tests/fixtures/simple-dockerfile/{compose.yml => docker-compose.yml} (100%) diff --git a/.gitignore b/.gitignore index 83533dd7..da7fe7fa 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ /dist /docs/_site /venv -compose.spec +docker-compose.spec diff --git a/Dockerfile b/Dockerfile index 85313b30..ee9fb4a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,4 +14,4 @@ RUN python setup.py install RUN chown -R user /code/ -ENTRYPOINT ["/usr/local/bin/compose"] +ENTRYPOINT ["/usr/local/bin/docker-compose"] diff --git a/bin/compose b/bin/docker-compose similarity index 100% rename from bin/compose rename to bin/docker-compose diff --git a/compose/cli/command.py b/compose/cli/command.py index 45b124a1..dc7683a3 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -110,13 +110,13 @@ class Command(DocoptCommand): if file_path: return os.path.join(self.base_dir, file_path) - if os.path.exists(os.path.join(self.base_dir, 'compose.yaml')): - log.warning("Fig just read the file 'compose.yaml' on startup, rather " - "than 'compose.yml'") + if os.path.exists(os.path.join(self.base_dir, 'docker-compose.yaml')): + log.warning("Fig just read the file 'docker-compose.yaml' on startup, rather " + "than 'docker-compose.yml'") log.warning("Please be aware that .yml is the expected extension " "in most cases, and using .yaml can cause compatibility " "issues in future") - return os.path.join(self.base_dir, 'compose.yaml') + return os.path.join(self.base_dir, 'docker-compose.yaml') - return os.path.join(self.base_dir, 'compose.yml') + return os.path.join(self.base_dir, 'docker-compose.yml') diff --git a/compose/cli/main.py b/compose/cli/main.py index 39191624..cfe29cd0 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -71,13 +71,13 @@ class TopLevelCommand(Command): """Fast, isolated development environments using Docker. Usage: - compose [options] [COMMAND] [ARGS...] - compose -h|--help + docker-compose [options] [COMMAND] [ARGS...] + docker-compose -h|--help Options: --verbose Show more output --version Print version and exit - -f, --file FILE Specify an alternate compose file (default: compose.yml) + -f, --file FILE Specify an alternate compose file (default: docker-compose.yml) -p, --project-name NAME Specify an alternate project name (default: directory name) Commands: @@ -99,7 +99,7 @@ class TopLevelCommand(Command): """ def docopt_options(self): options = super(TopLevelCommand, self).docopt_options() - options['version'] = "compose %s" % __version__ + options['version'] = "docker-compose %s" % __version__ return options def build(self, project, options): @@ -261,11 +261,11 @@ class TopLevelCommand(Command): For example: - $ compose run web python manage.py shell + $ docker-compose run web python manage.py shell By default, linked services will be started, unless they are already running. If you do not want to start linked services, use - `compose run --no-deps SERVICE COMMAND [ARGS...]`. + `docker-compose run --no-deps SERVICE COMMAND [ARGS...]`. Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] @@ -280,7 +280,7 @@ class TopLevelCommand(Command): --rm Remove container after run. Ignored in detached mode. --service-ports Run command with the service's ports enabled and mapped to the host. - -T Disable pseudo-tty allocation. By default `compose run` + -T Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY. """ service = project.get_service(options['SERVICE']) @@ -352,7 +352,7 @@ class TopLevelCommand(Command): Numbers are specified in the form `service=num` as arguments. For example: - $ compose scale web=2 worker=3 + $ docker-compose scale web=2 worker=3 Usage: scale [SERVICE=NUM...] """ @@ -372,7 +372,7 @@ class TopLevelCommand(Command): 'Service "%s" cannot be scaled because it specifies a port ' 'on the host. If multiple containers for this service were ' 'created, the port would clash.\n\nRemove the ":" from the ' - 'port definition in compose.yml so Docker can choose a random ' + 'port definition in docker-compose.yml so Docker can choose a random ' 'port for each container.' % service_name) def start(self, project, options): @@ -387,7 +387,7 @@ class TopLevelCommand(Command): """ Stop running containers without removing them. - They can be started again with `compose start`. + They can be started again with `docker-compose start`. Usage: stop [SERVICE...] """ @@ -405,14 +405,14 @@ class TopLevelCommand(Command): """ Build, (re)create, start and attach to containers for a service. - By default, `compose up` will aggregate the output of each container, and - when it exits, all containers will be stopped. If you run `compose up -d`, + By default, `docker-compose up` will aggregate the output of each container, and + when it exits, all containers will be stopped. If you run `docker-compose up -d`, it'll start the containers in the background and leave them running. - If there are existing containers for a service, `compose up` will stop + If there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with volumes-from), - so that changes in `compose.yml` are picked up. If you do not want existing - containers to be recreated, `compose up --no-recreate` will re-use existing + so that changes in `docker-compose.yml` are picked up. If you do not want existing + containers to be recreated, `docker-compose up --no-recreate` will re-use existing containers. Usage: up [options] [SERVICE...] diff --git a/script/build-docs b/script/build-docs index edcc5238..57dc82a2 100755 --- a/script/build-docs +++ b/script/build-docs @@ -1,5 +1,5 @@ #!/bin/bash set -ex pushd docs -compose run --rm jekyll jekyll build +docker-compose run --rm jekyll jekyll build popd diff --git a/script/build-linux b/script/build-linux index aa69fa7b..07c9d7ec 100755 --- a/script/build-linux +++ b/script/build-linux @@ -2,7 +2,7 @@ set -ex mkdir -p `pwd`/dist chmod 777 `pwd`/dist -docker build -t compose . -docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller compose -F bin/compose -mv dist/compose dist/compose-Linux-x86_64 -docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/compose-Linux-x86_64 compose --version +docker build -t docker-compose . +docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint pyinstaller docker-compose -F bin/docker-compose +mv dist/docker-compose dist/docker-compose-Linux-x86_64 +docker run -u user -v `pwd`/dist:/code/dist --rm --entrypoint dist/docker-compose-Linux-x86_64 docker-compose --version diff --git a/script/build-osx b/script/build-osx index 9b239caf..26309744 100755 --- a/script/build-osx +++ b/script/build-osx @@ -5,6 +5,6 @@ virtualenv venv venv/bin/pip install -r requirements.txt venv/bin/pip install -r requirements-dev.txt venv/bin/pip install . -venv/bin/pyinstaller -F bin/compose -mv dist/compose dist/compose-Darwin-x86_64 -dist/compose-Darwin-x86_64 --version +venv/bin/pyinstaller -F bin/docker-compose +mv dist/docker-compose dist/docker-compose-Darwin-x86_64 +dist/docker-compose-Darwin-x86_64 --version diff --git a/script/test b/script/test index b7e7245d..fef16b80 100755 --- a/script/test +++ b/script/test @@ -1,5 +1,5 @@ #!/bin/sh set -ex -docker build -t compose . -docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint flake8 compose compose -docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint nosetests compose $@ +docker build -t docker-compose . +docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint flake8 docker-compose compose +docker run -v /var/run/docker.sock:/var/run/docker.sock --rm --entrypoint nosetests docker-compose $@ diff --git a/setup.py b/setup.py index 7c21c53d..68c11bd9 100644 --- a/setup.py +++ b/setup.py @@ -48,7 +48,7 @@ if sys.version_info < (2, 7): setup( - name='compose', + name='docker-compose', version=find_version("compose", "__init__.py"), description='Multi-container orchestration for Docker', url='https://www.docker.com/', @@ -61,6 +61,6 @@ setup( tests_require=tests_require, entry_points=""" [console_scripts] - compose=compose.cli.main:main + docker-compose=compose.cli.main:main """, ) diff --git a/tests/fixtures/commands-composefile/compose.yml b/tests/fixtures/commands-composefile/docker-compose.yml similarity index 100% rename from tests/fixtures/commands-composefile/compose.yml rename to tests/fixtures/commands-composefile/docker-compose.yml diff --git a/tests/fixtures/dockerfile_with_entrypoint/compose.yml b/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml similarity index 100% rename from tests/fixtures/dockerfile_with_entrypoint/compose.yml rename to tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml diff --git a/tests/fixtures/environment-composefile/compose.yml b/tests/fixtures/environment-composefile/docker-compose.yml similarity index 100% rename from tests/fixtures/environment-composefile/compose.yml rename to tests/fixtures/environment-composefile/docker-compose.yml diff --git a/tests/fixtures/links-composefile/compose.yml b/tests/fixtures/links-composefile/docker-compose.yml similarity index 100% rename from tests/fixtures/links-composefile/compose.yml rename to tests/fixtures/links-composefile/docker-compose.yml diff --git a/tests/fixtures/longer-filename-composefile/compose.yaml b/tests/fixtures/longer-filename-composefile/docker-compose.yaml similarity index 100% rename from tests/fixtures/longer-filename-composefile/compose.yaml rename to tests/fixtures/longer-filename-composefile/docker-compose.yaml diff --git a/tests/fixtures/multiple-composefiles/compose.yml b/tests/fixtures/multiple-composefiles/docker-compose.yml similarity index 100% rename from tests/fixtures/multiple-composefiles/compose.yml rename to tests/fixtures/multiple-composefiles/docker-compose.yml diff --git a/tests/fixtures/ports-composefile/compose.yml b/tests/fixtures/ports-composefile/docker-compose.yml similarity index 100% rename from tests/fixtures/ports-composefile/compose.yml rename to tests/fixtures/ports-composefile/docker-compose.yml diff --git a/tests/fixtures/simple-composefile/compose.yml b/tests/fixtures/simple-composefile/docker-compose.yml similarity index 100% rename from tests/fixtures/simple-composefile/compose.yml rename to tests/fixtures/simple-composefile/docker-compose.yml diff --git a/tests/fixtures/simple-dockerfile/compose.yml b/tests/fixtures/simple-dockerfile/docker-compose.yml similarity index 100% rename from tests/fixtures/simple-dockerfile/compose.yml rename to tests/fixtures/simple-dockerfile/docker-compose.yml From c981ce929a0d410c6033096354f84ea06459abed Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 20 Jan 2015 15:23:16 +0000 Subject: [PATCH 0714/2154] Update README.md, ROADMAP.md and CONTRIBUTING.md Signed-off-by: Aanand Prasad --- CONTRIBUTING.md | 16 ++++++++-------- README.md | 8 +++----- ROADMAP.md | 16 +++++++--------- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb101955..a9f7e564 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to Fig +# Contributing to Compose ## TL;DR @@ -11,14 +11,14 @@ Pull requests will need: ## Development environment -If you're looking contribute to [Fig](http://www.fig.sh/) +If you're looking contribute to Compose but you're new to the project or maybe even to Python, here are the steps that should get you started. -1. Fork [https://github.com/docker/fig](https://github.com/docker/fig) to your username. -1. Clone your forked repository locally `git clone git@github.com:yourusername/fig.git`. -1. Enter the local directory `cd fig`. -1. Set up a development environment by running `python setup.py develop`. This will install the dependencies and set up a symlink from your `fig` executable to the checkout of the repository. When you now run `fig` from anywhere on your machine, it will run your development version of Fig. +1. Fork [https://github.com/docker/compose](https://github.com/docker/compose) to your username. +1. Clone your forked repository locally `git clone git@github.com:yourusername/compose.git`. +1. Enter the local directory `cd compose`. +1. Set up a development environment by running `python setup.py develop`. This will install the dependencies and set up a symlink from your `docker-compose` executable to the checkout of the repository. When you now run `docker-compose` from anywhere on your machine, it will run your development version of Compose. ## Running the test suite @@ -84,7 +84,7 @@ Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyIn 1. Open pull request that: - - Updates version in `fig/__init__.py` + - Updates version in `compose/__init__.py` - Updates version in `docs/install.md` - Adds release notes to `CHANGES.md` @@ -92,7 +92,7 @@ Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyIn 3. Build Linux version on any Docker host with `script/build-linux` and attach to release -4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release as `fig-Darwin-x86_64` and `fig-Linux-x86_64`. +4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release as `docker-compose-Darwin-x86_64` and `docker-compose-Linux-x86_64`. 5. Publish GitHub release, creating tag diff --git a/README.md b/README.md index fc978996..6efe779b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Fig -=== +Docker Compose +============== [![wercker status](https://app.wercker.com/status/d5dbac3907301c3d5ce735e2d5e95a5b/s/master "wercker status")](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b) @@ -29,9 +29,7 @@ db: (No more installing Postgres on your laptop!) -Then type `fig up`, and Fig will start and run your entire app: - -![example fig run](https://orchardup.com/static/images/fig-example-large.gif) +Then type `docker-compose up`, and Compose will start and run your entire app. There are commands to: diff --git a/ROADMAP.md b/ROADMAP.md index 0ee22895..68c85836 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,28 +1,26 @@ # Roadmap -Fig will be incorporated as part of the Docker ecosystem and renamed Docker Compose. The command-line tool and configuration file will get new names, and its documentation will be moved to [docs.docker.com](https://docs.docker.com). - ## More than just development environments -Over time we will extend Fig's remit to cover test, staging and production environments. This is not a simple task, and will take many incremental improvements such as: +Over time we will extend Compose's remit to cover test, staging and production environments. This is not a simple task, and will take many incremental improvements such as: -- Fig’s brute-force “delete and recreate everything” approach is great for dev and testing, but it not sufficient for production environments. You should be able to define a "desired" state that Fig will intelligently converge to. +- Compose’s brute-force “delete and recreate everything” approach is great for dev and testing, but it not sufficient for production environments. You should be able to define a "desired" state that Compose will intelligently converge to. - It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#426](https://github.com/docker/fig/issues/426)) -- Fig recommend a technique for zero-downtime deploys. +- Compose should recommend a technique for zero-downtime deploys. ## Integration with Swarm -Fig should integrate really well with Swarm so you can take an application you've developed on your laptop and run it on a Swarm cluster. +Compose should integrate really well with Swarm so you can take an application you've developed on your laptop and run it on a Swarm cluster. ## Applications spanning multiple teams -Fig works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Fig doesn't work as well. +Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well. There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318). ## An even better tool for development environments -Fig is a great tool for development environments, but it could be even better. For example: +Compose is a great tool for development environments, but it could be even better. For example: -- [Fig could watch your code and automatically kick off builds when something changes.](https://github.com/docker/fig/issues/184) +- [Compose could watch your code and automatically kick off builds when something changes.](https://github.com/docker/fig/issues/184) - It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp) From e3c4a662d9c2acd3b4190da90c0a671b29742e2e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 20 Jan 2015 17:29:28 +0000 Subject: [PATCH 0715/2154] Find-and-replace on docs Signed-off-by: Aanand Prasad --- docs/cli.md | 38 ++++++++++++++++----------------- docs/completion.md | 12 +++++------ docs/django.md | 28 ++++++++++++------------- docs/env.md | 8 +++---- docs/index.md | 52 +++++++++++++++++++++++----------------------- docs/install.md | 16 +++++++------- docs/rails.md | 28 ++++++++++++------------- docs/wordpress.md | 12 +++++------ docs/yml.md | 18 ++++++++-------- 9 files changed, 106 insertions(+), 106 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 74c82bed..f719a10c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,6 +1,6 @@ --- layout: default -title: Fig CLI reference +title: Compose CLI reference --- CLI reference @@ -8,7 +8,7 @@ CLI reference Most commands are run against one or more services. If the service is omitted, it will apply to all services. -Run `fig [COMMAND] --help` for full usage. +Run `docker-compose [COMMAND] --help` for full usage. ## Options @@ -22,7 +22,7 @@ Run `fig [COMMAND] --help` for full usage. ### -f, --file FILE - Specify an alternate fig file (default: fig.yml) + Specify an alternate docker-compose file (default: docker-compose.yml) ### -p, --project-name NAME @@ -34,7 +34,7 @@ Run `fig [COMMAND] --help` for full usage. Build or rebuild services. -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. +Services are built once and then tagged as `project_service`, e.g. `docker-composetest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `docker-compose build` to rebuild it. ### help @@ -44,7 +44,7 @@ Get help on a command. Force stop running containers by sending a `SIGKILL` signal. Optionally the signal can be passed, for example: - $ fig kill -s SIGINT + $ docker-compose kill -s SIGINT ### logs @@ -73,22 +73,22 @@ Run a one-off command on a service. For example: - $ fig run web python manage.py shell + $ docker-compose run web python manage.py shell 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 by default no ports will be created in case they collide. +One-off commands are started in new containers with the same condocker-compose 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 by default no ports will be created in case they collide. Links are also created between one-off commands and the other containers for that service so you can do stuff like this: - $ fig run db psql -h db -U docker + $ docker-compose run db psql -h db -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 + $ docker-compose run --no-deps web python manage.py shell If you want the service's ports to be created and mapped to the host, specify the `--service-ports` flag: - $ fig run --service-ports web python manage.py shell + $ docker-compose run --service-ports web python manage.py shell ### scale @@ -97,7 +97,7 @@ Set number of containers to run for a service. Numbers are specified in the form `service=num` as arguments. For example: - $ fig scale web=2 worker=3 + $ docker-compose scale web=2 worker=3 ### start @@ -105,7 +105,7 @@ Start existing containers for a service. ### stop -Stop running containers without removing them. They can be started again with `fig start`. +Stop running containers without removing them. They can be started again with `docker-compose start`. ### up @@ -113,26 +113,26 @@ 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. +By default, `docker-compose up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `docker-compose up -d`, it'll start the containers in the background and leave them running. -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. +By default if there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `docker-compose.yml` are picked up. If you do no want containers to be stopped and recreated, use `docker-compose up --no-recreate`. This will still start any stopped containers, if needed. [volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/ ## Environment Variables -Several environment variables can be used to configure Fig's behaviour. +Several environment variables can be used to condocker-composeure Compose's behaviour. -Variables starting with `DOCKER_` are the same as those used to configure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values. +Variables starting with `DOCKER_` are the same as those used to condocker-composeure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values. ### FIG\_PROJECT\_NAME -Set the project name, which is prepended to the name of every container started by Fig. Defaults to the `basename` of the current working directory. +Set the project name, which is prepended to the name of every container started by Compose. Defaults to the `basename` of the current working directory. ### FIG\_FILE -Set the path to the `fig.yml` to use. Defaults to `fig.yml` in the current working directory. +Set the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` in the current working directory. ### DOCKER\_HOST @@ -144,4 +144,4 @@ When set to anything other than an empty string, enables TLS communication with ### DOCKER\_CERT\_PATH -Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`. +Condocker-composeure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`. diff --git a/docs/completion.md b/docs/completion.md index ec8d7766..1a2d49ee 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -6,7 +6,7 @@ title: Command Completion Command Completion ================== -Fig comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion) +Compose comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion) for the bash shell. Installing Command Completion @@ -17,7 +17,7 @@ On a Mac, install with `brew install bash-completion` Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. - curl -L https://raw.githubusercontent.com/docker/fig/master/contrib/completion/bash/fig > /etc/bash_completion.d/fig + curl -L https://raw.githubusercontent.com/docker/docker-compose/master/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose Completion will be available upon next login. @@ -25,9 +25,9 @@ Available completions --------------------- Depending on what you typed on the command line so far, it will complete - - available fig commands + - available docker-compose commands - options that are available for a particular command - - service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `fig scale`, completed service names will automatically have "=" appended. - - arguments for selected options, e.g. `fig kill -s` will complete some signals like SIGHUP and SIGUSR1. + - service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `docker-compose scale`, completed service names will automatically have "=" appended. + - arguments for selected options, e.g. `docker-compose kill -s` will complete some signals like SIGHUP and SIGUSR1. -Enjoy working with fig faster and with less typos! +Enjoy working with docker-compose faster and with less typos! diff --git a/docs/django.md b/docs/django.md index 1fc0f77e..2a7e9cea 100644 --- a/docs/django.md +++ b/docs/django.md @@ -1,12 +1,12 @@ --- layout: default -title: Getting started with Fig and Django +title: Getting started with Compose and Django --- -Getting started with Fig and Django +Getting started with Compose and Django =================================== -Let's use Fig to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html). +Let's use Compose to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.html). Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with: @@ -25,7 +25,7 @@ Second, we define our Python dependencies in a file called `requirements.txt`: Django psycopg2 -Simple enough. Finally, this is all tied together with a file called `fig.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose. +Simple enough. Finally, this is all tied together with a file called `docker-compose.yml`. It describes the services that our app comprises of (a web server and database), what Docker images they use, how they link together, what volumes will be mounted inside the containers and what ports they expose. db: image: postgres @@ -39,20 +39,20 @@ Simple enough. Finally, this is all tied together with a file called `fig.yml`. links: - db -See the [`fig.yml` reference](yml.html) for more information on how it works. +See the [`docker-compose.yml` reference](yml.html) for more information on how it works. -We can now start a Django project using `fig run`: +We can now start a Django project using `docker-compose run`: - $ fig run web django-admin.py startproject figexample . + $ docker-compose run web django-admin.py startproject docker-composeexample . -First, Fig will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject figexample .` inside a container using that image. +First, Compose will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject docker-composeexample .` inside a container using that image. This will generate a Django app inside the current directory: $ ls - Dockerfile fig.yml figexample manage.py requirements.txt + Dockerfile docker-compose.yml docker-composeexample manage.py requirements.txt -First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `figexample/settings.py` to read: +First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `docker-composeexample/settings.py` to read: DATABASES = { 'default': { @@ -66,7 +66,7 @@ First thing we need to do is set up the database connection. Replace the `DATABA These settings are determined by the [postgres](https://registry.hub.docker.com/_/postgres/) Docker image we are using. -Then, run `fig up`: +Then, run `docker-compose up`: Recreating myapp_db_1... Recreating myapp_web_1... @@ -79,13 +79,13 @@ Then, run `fig up`: myapp_web_1 | myapp_web_1 | 0 errors found myapp_web_1 | January 27, 2014 - 12:12:40 - myapp_web_1 | Django version 1.6.1, using settings 'figexample.settings' + myapp_web_1 | Django version 1.6.1, using settings 'docker-composeexample.settings' myapp_web_1 | Starting development server at http://0.0.0.0:8000/ myapp_web_1 | Quit the server with CONTROL-C. And your Django app should be running at port 8000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). -You can also run management commands with Docker. To set up your database, for example, run `fig up` and in another terminal run: +You can also run management commands with Docker. To set up your database, for example, run `docker-compose up` and in another terminal run: - $ fig run web python manage.py syncdb + $ docker-compose run web python manage.py syncdb diff --git a/docs/env.md b/docs/env.md index b38cd176..8644c8ae 100644 --- a/docs/env.md +++ b/docs/env.md @@ -1,16 +1,16 @@ --- layout: default -title: Fig environment variables reference +title: Compose environment variables reference --- Environment variables reference =============================== -**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [fig.yml documentation](yml.html#links) for details. +**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](yml.html#links) for details. -Fig uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. +Compose uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. -To see what environment variables are available to a service, run `fig run SERVICE env`. +To see what environment variables are available to a service, run `docker-compose run SERVICE env`. name\_PORT
    Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432` diff --git a/docs/index.md b/docs/index.md index f2cdc83c..9cb46536 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- layout: default -title: Fig | Fast, isolated development environments using Docker +title: Compose | Fast, isolated development environments using Docker --- Fast, isolated development environments using Docker. @@ -12,7 +12,7 @@ Define your app's environment with a `Dockerfile` so it can be reproduced anywhe WORKDIR /code RUN pip install -r requirements.txt -Define the services that make up your app in `fig.yml` so they can be run together in an isolated environment: +Define the services that make up your app in `docker-compose.yml` so they can be run together in an isolated environment: ```yaml web: @@ -28,9 +28,9 @@ db: (No more installing Postgres on your laptop!) -Then type `fig up`, and Fig will start and run your entire app: +Then type `docker-compose up`, and Compose will start and run your entire app: -![example fig run](https://orchardup.com/static/images/fig-example-large.gif) +![example docker-compose run](https://orchardup.com/static/images/docker-compose-example-large.gif) There are commands to: @@ -43,14 +43,14 @@ There are commands to: Quick start ----------- -Let's get a basic Python web app running on Fig. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it. +Let's get a basic Python web app running on Compose. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it. -First, [install Docker and Fig](install.html). +First, [install Docker and Compose](install.html). You'll want to make a directory for the project: - $ mkdir figtest - $ cd figtest + $ mkdir docker-composetest + $ cd docker-composetest Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis: @@ -84,7 +84,7 @@ Next, we want to create a Docker image containing all of our app's dependencies. This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). -We then define a set of services using `fig.yml`: +We then define a set of services using `docker-compose.yml`: web: build: . @@ -103,38 +103,38 @@ This defines two services: - `web`, which is built from `Dockerfile` in the current directory. It also says to run the command `python app.py` inside the image, forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image. - `redis`, which uses the public image [redis](https://registry.hub.docker.com/_/redis/). -Now if we run `fig up`, it'll pull a Redis image, build an image for our own code, and start everything up: +Now if we run `docker-compose up`, it'll pull a Redis image, build an image for our own code, and start everything up: - $ fig up + $ docker-compose up Pulling image redis... Building web... - Starting figtest_redis_1... - Starting figtest_web_1... + Starting docker-composetest_redis_1... + Starting docker-composetest_web_1... redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 web_1 | * Running on http://0.0.0.0:5000/ The web app should now be listening on port 5000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). -If you want to run your services in the background, you can pass the `-d` flag to `fig up` and use `fig ps` to see what is currently running: +If you want to run your services in the background, you can pass the `-d` flag to `docker-compose up` and use `docker-compose ps` to see what is currently running: - $ fig up -d - Starting figtest_redis_1... - Starting figtest_web_1... - $ fig ps + $ docker-compose up -d + Starting docker-composetest_redis_1... + Starting docker-composetest_web_1... + $ docker-compose ps Name Command State Ports ------------------------------------------------------------------- - figtest_redis_1 /usr/local/bin/run Up - figtest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp + docker-composetest_redis_1 /usr/local/bin/run Up + docker-composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp -`fig run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service: +`docker-compose run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service: - $ fig run web env + $ docker-compose run web env -See `fig --help` other commands that are available. +See `docker-compose --help` other commands that are available. -If you started Fig with `fig up -d`, you'll probably want to stop your services once you've finished with them: +If you started Compose with `docker-compose up -d`, you'll probably want to stop your services once you've finished with them: - $ fig stop + $ docker-compose stop -That's more-or-less how Fig works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/fig). +That's more-or-less how Compose works. See the reference section below for full details on the commands, condocker-composeuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/docker-compose). diff --git a/docs/install.md b/docs/install.md index c91c3db0..b1fa8345 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,14 +1,14 @@ --- layout: default -title: Installing Fig +title: Installing Compose --- -Installing Fig +Installing Compose ============== First, install Docker version 1.3 or greater. -If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll configure Docker and Fig to talk to it: +If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll condocker-composeure Docker and Compose to talk to it: $(boot2docker shellinit) @@ -16,14 +16,14 @@ To persist the environment variables across shell sessions, you can add that lin There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) and [other platforms](https://docs.docker.com/installation/) in Docker’s documentation. -Next, install Fig: +Next, install Compose: - curl -L https://github.com/docker/fig/releases/download/1.0.1/fig-`uname -s`-`uname -m` > /usr/local/bin/fig; chmod +x /usr/local/bin/fig + curl -L https://github.com/docker/docker-compose/releases/download/1.0.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose Optionally, install [command completion](completion.html) for the bash shell. -Releases are available for OS X and 64-bit Linux. Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing): +Releases are available for OS X and 64-bit Linux. Compose is also available as a Python package if you're on another platform (or if you prefer that sort of thing): - $ sudo pip install -U fig + $ sudo pip install -U docker-compose -That should be all you need! Run `fig --version` to see if it worked. +That should be all you need! Run `docker-compose --version` to see if it worked. diff --git a/docs/rails.md b/docs/rails.md index 25678b29..d2f5343b 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -1,12 +1,12 @@ --- layout: default -title: Getting started with Fig and Rails +title: Getting started with Compose and Rails --- -Getting started with Fig and Rails +Getting started with Compose and Rails ================================== -We're going to use Fig to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Fig installed](install.html). +We're going to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.html). Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with: @@ -25,7 +25,7 @@ Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten source 'https://rubygems.org' gem 'rails', '4.0.2' -Finally, `fig.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration we need to link them together and expose the web app's port. +Finally, `docker-compose.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the condocker-composeuration we need to link them together and expose the web app's port. db: image: postgres @@ -41,17 +41,17 @@ Finally, `fig.yml` is where the magic happens. It describes what services our ap links: - db -With those files in place, we can now generate the Rails skeleton app using `fig run`: +With those files in place, we can now generate the Rails skeleton app using `docker-compose run`: - $ fig run web rails new . --force --database=postgresql --skip-bundle + $ docker-compose run web rails new . --force --database=postgresql --skip-bundle -First, Fig will build the image for the `web` service using the `Dockerfile`. Then it'll run `rails new` inside a new container, using that image. Once it's done, you should have a fresh app generated: +First, Compose will build the image for the `web` service using the `Dockerfile`. Then it'll run `rails new` inside a new container, using that image. Once it's done, you should have a fresh app generated: $ ls - Dockerfile app fig.yml tmp + Dockerfile app docker-compose.yml tmp Gemfile bin lib vendor - Gemfile.lock config log - README.rdoc config.ru public + Gemfile.lock condocker-compose log + README.rdoc condocker-compose.ru public Rakefile db test Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've got a Javascript runtime: @@ -60,7 +60,7 @@ Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've go Now that we've got a new `Gemfile`, we need to build the image again. (This, and changes to the Dockerfile itself, should be the only times you'll need to rebuild). - $ fig build + $ docker-compose build The app is now bootable, but we're not quite there yet. By default, Rails expects a database to be running on `localhost` - we need to point it at the `db` container instead. We also need to change the database and username to align with the defaults set by the `postgres` image. @@ -81,7 +81,7 @@ Open up your newly-generated `database.yml`. Replace its contents with the follo We can now boot the app. - $ fig up + $ docker-compose up If all's well, you should see some PostgreSQL output, and then—after a few seconds—the familiar refrain: @@ -91,8 +91,8 @@ If all's well, you should see some PostgreSQL output, and then—after a few sec Finally, we just need to create the database. In another terminal, run: - $ fig run web rake db:create + $ docker-compose run web rake db:create And we're rolling—your app should now be running on port 3000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). -![Screenshot of Rails' stock index.html](https://orchardup.com/static/images/fig-rails-screenshot.png) +![Screenshot of Rails' stock index.html](https://orchardup.com/static/images/docker-compose-rails-screenshot.png) diff --git a/docs/wordpress.md b/docs/wordpress.md index e2c43252..9c69b2ee 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -1,12 +1,12 @@ --- layout: default -title: Getting started with Fig and Wordpress +title: Getting started with Compose and Wordpress --- -Getting started with Fig and Wordpress +Getting started with Compose and Wordpress ====================================== -Fig makes it nice and easy to run Wordpress in an isolated environment. [Install Fig](install.html), then download Wordpress into the current directory: +Compose makes it nice and easy to run Wordpress in an isolated environment. [Install Compose](install.html), then download Wordpress into the current directory: $ curl https://wordpress.org/latest.tar.gz | tar -xvzf - @@ -19,7 +19,7 @@ ADD . /code This instructs Docker on how to build an image that contains PHP and Wordpress. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). -Next up, `fig.yml` starts our web service and a separate MySQL instance: +Next up, `docker-compose.yml` starts our web service and a separate MySQL instance: ``` web: @@ -37,7 +37,7 @@ db: MYSQL_DATABASE: wordpress ``` -Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to point the database configuration at the `db` container: +Two supporting files are needed to get this working - first up, `wp-condocker-compose.php` is the standard Wordpress condocker-compose file with a single change to point the database condocker-composeuration at the `db` container: ``` Date: Tue, 20 Jan 2015 17:44:48 +0000 Subject: [PATCH 0716/2154] Manual fixes to docs Signed-off-by: Aanand Prasad --- docs/cli.md | 12 ++++++------ docs/completion.md | 2 +- docs/django.md | 10 +++++----- docs/index.md | 26 +++++++++++--------------- docs/install.md | 2 +- docs/rails.md | 8 +++----- docs/wordpress.md | 2 +- docs/yml.md | 4 ++-- 8 files changed, 30 insertions(+), 36 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index f719a10c..8fd4b800 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -22,7 +22,7 @@ Run `docker-compose [COMMAND] --help` for full usage. ### -f, --file FILE - Specify an alternate docker-compose file (default: docker-compose.yml) + Specify an alternate compose file (default: docker-compose.yml) ### -p, --project-name NAME @@ -34,7 +34,7 @@ Run `docker-compose [COMMAND] --help` for full usage. Build or rebuild services. -Services are built once and then tagged as `project_service`, e.g. `docker-composetest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `docker-compose build` to rebuild it. +Services are built once and then tagged as `project_service`, e.g. `composetest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `docker-compose build` to rebuild it. ### help @@ -77,7 +77,7 @@ For example: By default, linked services will be started, unless they are already running. -One-off commands are started in new containers with the same condocker-compose 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 by default no ports will be created in case they collide. +One-off commands are started in new containers with the same configuration 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 by default no ports will be created in case they collide. Links are also created between one-off commands and the other containers for that service so you can do stuff like this: @@ -122,9 +122,9 @@ By default if there are existing containers for a service, `docker-compose up` w ## Environment Variables -Several environment variables can be used to condocker-composeure Compose's behaviour. +Several environment variables can be used to configure Compose's behaviour. -Variables starting with `DOCKER_` are the same as those used to condocker-composeure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values. +Variables starting with `DOCKER_` are the same as those used to configure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values. ### FIG\_PROJECT\_NAME @@ -144,4 +144,4 @@ When set to anything other than an empty string, enables TLS communication with ### DOCKER\_CERT\_PATH -Condocker-composeure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`. +Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`. diff --git a/docs/completion.md b/docs/completion.md index 1a2d49ee..2710a635 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -30,4 +30,4 @@ Depending on what you typed on the command line so far, it will complete - service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `docker-compose scale`, completed service names will automatically have "=" appended. - arguments for selected options, e.g. `docker-compose kill -s` will complete some signals like SIGHUP and SIGUSR1. -Enjoy working with docker-compose faster and with less typos! +Enjoy working with Compose faster and with less typos! diff --git a/docs/django.md b/docs/django.md index 2a7e9cea..93e778b3 100644 --- a/docs/django.md +++ b/docs/django.md @@ -43,16 +43,16 @@ See the [`docker-compose.yml` reference](yml.html) for more information on how i We can now start a Django project using `docker-compose run`: - $ docker-compose run web django-admin.py startproject docker-composeexample . + $ docker-compose run web django-admin.py startproject composeexample . -First, Compose will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject docker-composeexample .` inside a container using that image. +First, Compose will build an image for the `web` service using the `Dockerfile`. It will then run `django-admin.py startproject composeexample .` inside a container using that image. This will generate a Django app inside the current directory: $ ls - Dockerfile docker-compose.yml docker-composeexample manage.py requirements.txt + Dockerfile docker-compose.yml composeexample manage.py requirements.txt -First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `docker-composeexample/settings.py` to read: +First thing we need to do is set up the database connection. Replace the `DATABASES = ...` definition in `composeexample/settings.py` to read: DATABASES = { 'default': { @@ -79,7 +79,7 @@ Then, run `docker-compose up`: myapp_web_1 | myapp_web_1 | 0 errors found myapp_web_1 | January 27, 2014 - 12:12:40 - myapp_web_1 | Django version 1.6.1, using settings 'docker-composeexample.settings' + myapp_web_1 | Django version 1.6.1, using settings 'composeexample.settings' myapp_web_1 | Starting development server at http://0.0.0.0:8000/ myapp_web_1 | Quit the server with CONTROL-C. diff --git a/docs/index.md b/docs/index.md index 9cb46536..00f48be9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,10 +1,8 @@ --- layout: default -title: Compose | Fast, isolated development environments using Docker +title: Compose: Multi-container orchestration for Docker --- -Fast, isolated development environments using Docker. - Define your app's environment with a `Dockerfile` so it can be reproduced anywhere: FROM python:2.7 @@ -28,9 +26,7 @@ db: (No more installing Postgres on your laptop!) -Then type `docker-compose up`, and Compose will start and run your entire app: - -![example docker-compose run](https://orchardup.com/static/images/docker-compose-example-large.gif) +Then type `docker-compose up`, and Compose will start and run your entire app. There are commands to: @@ -49,8 +45,8 @@ First, [install Docker and Compose](install.html). You'll want to make a directory for the project: - $ mkdir docker-composetest - $ cd docker-composetest + $ mkdir composetest + $ cd composetest Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis: @@ -108,8 +104,8 @@ Now if we run `docker-compose up`, it'll pull a Redis image, build an image for $ docker-compose up Pulling image redis... Building web... - Starting docker-composetest_redis_1... - Starting docker-composetest_web_1... + Starting composetest_redis_1... + Starting composetest_web_1... redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 web_1 | * Running on http://0.0.0.0:5000/ @@ -118,13 +114,13 @@ The web app should now be listening on port 5000 on your docker daemon (if you'r If you want to run your services in the background, you can pass the `-d` flag to `docker-compose up` and use `docker-compose ps` to see what is currently running: $ docker-compose up -d - Starting docker-composetest_redis_1... - Starting docker-composetest_web_1... + Starting composetest_redis_1... + Starting composetest_web_1... $ docker-compose ps Name Command State Ports ------------------------------------------------------------------- - docker-composetest_redis_1 /usr/local/bin/run Up - docker-composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp + composetest_redis_1 /usr/local/bin/run Up + composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp `docker-compose run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service: @@ -137,4 +133,4 @@ If you started Compose with `docker-compose up -d`, you'll probably want to stop $ docker-compose stop -That's more-or-less how Compose works. See the reference section below for full details on the commands, condocker-composeuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/docker-compose). +That's more-or-less how Compose works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/docker-compose). diff --git a/docs/install.md b/docs/install.md index b1fa8345..e61cd0fc 100644 --- a/docs/install.md +++ b/docs/install.md @@ -8,7 +8,7 @@ Installing Compose First, install Docker version 1.3 or greater. -If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll condocker-composeure Docker and Compose to talk to it: +If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll configure Docker and Compose to talk to it: $(boot2docker shellinit) diff --git a/docs/rails.md b/docs/rails.md index d2f5343b..59ee20b4 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -25,7 +25,7 @@ Next, we have a bootstrap `Gemfile` which just loads Rails. It'll be overwritten source 'https://rubygems.org' gem 'rails', '4.0.2' -Finally, `docker-compose.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the condocker-composeuration we need to link them together and expose the web app's port. +Finally, `docker-compose.yml` is where the magic happens. It describes what services our app comprises (a database and a web app), how to get each one's Docker image (the database just runs on a pre-made PostgreSQL image, and the web app is built from the current directory), and the configuration we need to link them together and expose the web app's port. db: image: postgres @@ -50,8 +50,8 @@ First, Compose will build the image for the `web` service using the `Dockerfile` $ ls Dockerfile app docker-compose.yml tmp Gemfile bin lib vendor - Gemfile.lock condocker-compose log - README.rdoc condocker-compose.ru public + Gemfile.lock config log + README.rdoc config.ru public Rakefile db test Uncomment the line in your new `Gemfile` which loads `therubyracer`, so we've got a Javascript runtime: @@ -94,5 +94,3 @@ Finally, we just need to create the database. In another terminal, run: $ docker-compose run web rake db:create And we're rolling—your app should now be running on port 3000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). - -![Screenshot of Rails' stock index.html](https://orchardup.com/static/images/docker-compose-rails-screenshot.png) diff --git a/docs/wordpress.md b/docs/wordpress.md index 9c69b2ee..47808fe6 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -37,7 +37,7 @@ db: MYSQL_DATABASE: wordpress ``` -Two supporting files are needed to get this working - first up, `wp-condocker-compose.php` is the standard Wordpress condocker-compose file with a single change to point the database condocker-composeuration at the `db` container: +Two supporting files are needed to get this working - first up, `wp-config.php` is the standard Wordpress config file with a single change to point the database configuration at the `db` container: ``` Date: Tue, 20 Jan 2015 17:49:13 +0000 Subject: [PATCH 0717/2154] Remove all website-related stuff from docs Signed-off-by: Aanand Prasad --- docs/.gitignore-gh-pages | 1 - docs/CNAME | 1 - docs/Dockerfile | 10 -- docs/Gemfile | 3 - docs/Gemfile.lock | 62 ------------ docs/_config.yml | 3 - docs/_layouts/default.html | 73 --------------- docs/css/bootstrap.min.css | 7 -- docs/css/fig.css | 187 ------------------------------------- docs/fig.yml | 8 -- docs/img/favicon.ico | Bin 1150 -> 0 bytes docs/img/logo.png | Bin 133640 -> 0 bytes script/build-docs | 5 - script/open-docs | 2 - 14 files changed, 362 deletions(-) delete mode 100644 docs/.gitignore-gh-pages delete mode 100644 docs/CNAME delete mode 100644 docs/Dockerfile delete mode 100644 docs/Gemfile delete mode 100644 docs/Gemfile.lock delete mode 100644 docs/_config.yml delete mode 100644 docs/_layouts/default.html delete mode 100644 docs/css/bootstrap.min.css delete mode 100644 docs/css/fig.css delete mode 100644 docs/fig.yml delete mode 100644 docs/img/favicon.ico delete mode 100644 docs/img/logo.png delete mode 100755 script/build-docs delete mode 100755 script/open-docs diff --git a/docs/.gitignore-gh-pages b/docs/.gitignore-gh-pages deleted file mode 100644 index 0baf0152..00000000 --- a/docs/.gitignore-gh-pages +++ /dev/null @@ -1 +0,0 @@ -/_site diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 537d9d46..00000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -www.fig.sh diff --git a/docs/Dockerfile b/docs/Dockerfile deleted file mode 100644 index 2639848a..00000000 --- a/docs/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM ubuntu:13.10 -RUN apt-get -qq update && apt-get install -y ruby1.8 bundler python -RUN locale-gen en_US.UTF-8 -ADD Gemfile /code/ -ADD Gemfile.lock /code/ -WORKDIR /code -RUN bundle install -ADD . /code -EXPOSE 4000 -CMD bundle exec jekyll build diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 97355ea7..00000000 --- a/docs/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source 'https://rubygems.org' - -gem 'github-pages' diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index 447ae05c..00000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -1,62 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - RedCloth (4.2.9) - blankslate (2.1.2.4) - classifier (1.3.3) - fast-stemmer (>= 1.0.0) - colorator (0.1) - commander (4.1.5) - highline (~> 1.6.11) - fast-stemmer (1.0.2) - ffi (1.9.3) - github-pages (12) - RedCloth (= 4.2.9) - jekyll (= 1.4.2) - kramdown (= 1.2.0) - liquid (= 2.5.4) - maruku (= 0.7.0) - rdiscount (= 2.1.7) - redcarpet (= 2.3.0) - highline (1.6.20) - jekyll (1.4.2) - classifier (~> 1.3) - colorator (~> 0.1) - commander (~> 4.1.3) - liquid (~> 2.5.2) - listen (~> 1.3) - maruku (~> 0.7.0) - pygments.rb (~> 0.5.0) - redcarpet (~> 2.3.0) - safe_yaml (~> 0.9.7) - toml (~> 0.1.0) - kramdown (1.2.0) - liquid (2.5.4) - listen (1.3.1) - rb-fsevent (>= 0.9.3) - rb-inotify (>= 0.9) - rb-kqueue (>= 0.2) - maruku (0.7.0) - parslet (1.5.0) - blankslate (~> 2.0) - posix-spawn (0.3.8) - pygments.rb (0.5.4) - posix-spawn (~> 0.3.6) - yajl-ruby (~> 1.1.0) - rb-fsevent (0.9.4) - rb-inotify (0.9.3) - ffi (>= 0.5.0) - rb-kqueue (0.2.0) - ffi (>= 0.5.0) - rdiscount (2.1.7) - redcarpet (2.3.0) - safe_yaml (0.9.7) - toml (0.1.0) - parslet (~> 1.5.0) - yajl-ruby (1.1.0) - -PLATFORMS - ruby - -DEPENDENCIES - github-pages diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index b7abc223..00000000 --- a/docs/_config.yml +++ /dev/null @@ -1,3 +0,0 @@ -markdown: redcarpet -encoding: utf-8 - diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html deleted file mode 100644 index 1f0de508..00000000 --- a/docs/_layouts/default.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - {{ page.title }} - - - - - - - - - -
    - - -
    {{ content }}
    - - -
    - - - - diff --git a/docs/css/bootstrap.min.css b/docs/css/bootstrap.min.css deleted file mode 100644 index c547283b..00000000 --- a/docs/css/bootstrap.min.css +++ /dev/null @@ -1,7 +0,0 @@ -/*! - * Bootstrap v3.0.3 (http://getbootstrap.com) - * Copyright 2013 Twitter, Inc. - * Licensed under http://www.apache.org/licenses/LICENSE-2.0 - */ - -/*! normalize.css v2.1.3 | MIT License | git.io/normalize */article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden],template{display:none}html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a{background:transparent}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{margin:.67em 0;font-size:2em}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{height:0;-moz-box-sizing:content-box;box-sizing:content-box}mark{color:#000;background:#ff0}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid #c0c0c0}legend{padding:0;border:0}button,input,select,textarea{margin:0;font-family:inherit;font-size:100%}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{padding:0;box-sizing:border-box}input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}@page{margin:2cm .5cm}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}*,*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.428571429;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}img{vertical-align:middle}.img-responsive{display:block;height:auto;max-width:100%}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;height:auto;max-width:100%;padding:4px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#999}h1,h2,h3{margin-top:20px;margin-bottom:10px}h1 small,h2 small,h3 small,h1 .small,h2 .small,h3 .small{font-size:65%}h4,h5,h6{margin-top:10px;margin-bottom:10px}h4 small,h5 small,h6 small,h4 .small,h5 .small,h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:200;line-height:1.4}@media(min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}.text-muted{color:#999}.text-primary{color:#428bca}.text-primary:hover{color:#3071a9}.text-warning{color:#8a6d3b}.text-warning:hover{color:#66512c}.text-danger{color:#a94442}.text-danger:hover{color:#843534}.text-success{color:#3c763d}.text-success:hover{color:#2b542c}.text-info{color:#31708f}.text-info:hover{color:#245269}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}.list-inline>li:first-child{padding-left:0}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.428571429}dt{font-weight:bold}dd{margin-left:0}@media(min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}.dl-horizontal dd:before,.dl-horizontal dd:after{display:table;content:" "}.dl-horizontal dd:after{clear:both}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #999}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;border-left:5px solid #eee}blockquote p{font-size:17.5px;font-weight:300;line-height:1.25}blockquote p:last-child{margin-bottom:0}blockquote small,blockquote .small{display:block;line-height:1.428571429;color:#999}blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0}blockquote.pull-right p,blockquote.pull-right small,blockquote.pull-right .small{text-align:right}blockquote.pull-right small:before,blockquote.pull-right .small:before{content:''}blockquote.pull-right small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.428571429}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;white-space:nowrap;background-color:#f9f2f4;border-radius:4px}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.428571429;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}.container:before,.container:after{display:table;content:" "}.container:after{clear:both}@media(min-width:768px){.container{width:750px}}@media(min-width:992px){.container{width:970px}}@media(min-width:1200px){.container{width:1170px}}.row{margin-right:-15px;margin-left:-15px}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666666666666%}.col-xs-10{width:83.33333333333334%}.col-xs-9{width:75%}.col-xs-8{width:66.66666666666666%}.col-xs-7{width:58.333333333333336%}.col-xs-6{width:50%}.col-xs-5{width:41.66666666666667%}.col-xs-4{width:33.33333333333333%}.col-xs-3{width:25%}.col-xs-2{width:16.666666666666664%}.col-xs-1{width:8.333333333333332%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666666666666%}.col-xs-pull-10{right:83.33333333333334%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666666666666%}.col-xs-pull-7{right:58.333333333333336%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666666666667%}.col-xs-pull-4{right:33.33333333333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.666666666666664%}.col-xs-pull-1{right:8.333333333333332%}.col-xs-pull-0{right:0}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666666666666%}.col-xs-push-10{left:83.33333333333334%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666666666666%}.col-xs-push-7{left:58.333333333333336%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666666666667%}.col-xs-push-4{left:33.33333333333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.666666666666664%}.col-xs-push-1{left:8.333333333333332%}.col-xs-push-0{left:0}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666666666666%}.col-xs-offset-10{margin-left:83.33333333333334%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666666666666%}.col-xs-offset-7{margin-left:58.333333333333336%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666666666667%}.col-xs-offset-4{margin-left:33.33333333333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.666666666666664%}.col-xs-offset-1{margin-left:8.333333333333332%}.col-xs-offset-0{margin-left:0}@media(min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666666666666%}.col-sm-10{width:83.33333333333334%}.col-sm-9{width:75%}.col-sm-8{width:66.66666666666666%}.col-sm-7{width:58.333333333333336%}.col-sm-6{width:50%}.col-sm-5{width:41.66666666666667%}.col-sm-4{width:33.33333333333333%}.col-sm-3{width:25%}.col-sm-2{width:16.666666666666664%}.col-sm-1{width:8.333333333333332%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666666666666%}.col-sm-pull-10{right:83.33333333333334%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666666666666%}.col-sm-pull-7{right:58.333333333333336%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666666666667%}.col-sm-pull-4{right:33.33333333333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.666666666666664%}.col-sm-pull-1{right:8.333333333333332%}.col-sm-pull-0{right:0}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666666666666%}.col-sm-push-10{left:83.33333333333334%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666666666666%}.col-sm-push-7{left:58.333333333333336%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666666666667%}.col-sm-push-4{left:33.33333333333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.666666666666664%}.col-sm-push-1{left:8.333333333333332%}.col-sm-push-0{left:0}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666666666666%}.col-sm-offset-10{margin-left:83.33333333333334%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666666666666%}.col-sm-offset-7{margin-left:58.333333333333336%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666666666667%}.col-sm-offset-4{margin-left:33.33333333333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.666666666666664%}.col-sm-offset-1{margin-left:8.333333333333332%}.col-sm-offset-0{margin-left:0}}@media(min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666666666666%}.col-md-10{width:83.33333333333334%}.col-md-9{width:75%}.col-md-8{width:66.66666666666666%}.col-md-7{width:58.333333333333336%}.col-md-6{width:50%}.col-md-5{width:41.66666666666667%}.col-md-4{width:33.33333333333333%}.col-md-3{width:25%}.col-md-2{width:16.666666666666664%}.col-md-1{width:8.333333333333332%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666666666666%}.col-md-pull-10{right:83.33333333333334%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666666666666%}.col-md-pull-7{right:58.333333333333336%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666666666667%}.col-md-pull-4{right:33.33333333333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.666666666666664%}.col-md-pull-1{right:8.333333333333332%}.col-md-pull-0{right:0}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666666666666%}.col-md-push-10{left:83.33333333333334%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666666666666%}.col-md-push-7{left:58.333333333333336%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666666666667%}.col-md-push-4{left:33.33333333333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.666666666666664%}.col-md-push-1{left:8.333333333333332%}.col-md-push-0{left:0}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666666666666%}.col-md-offset-10{margin-left:83.33333333333334%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666666666666%}.col-md-offset-7{margin-left:58.333333333333336%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666666666667%}.col-md-offset-4{margin-left:33.33333333333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.666666666666664%}.col-md-offset-1{margin-left:8.333333333333332%}.col-md-offset-0{margin-left:0}}@media(min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666666666666%}.col-lg-10{width:83.33333333333334%}.col-lg-9{width:75%}.col-lg-8{width:66.66666666666666%}.col-lg-7{width:58.333333333333336%}.col-lg-6{width:50%}.col-lg-5{width:41.66666666666667%}.col-lg-4{width:33.33333333333333%}.col-lg-3{width:25%}.col-lg-2{width:16.666666666666664%}.col-lg-1{width:8.333333333333332%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666666666666%}.col-lg-pull-10{right:83.33333333333334%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666666666666%}.col-lg-pull-7{right:58.333333333333336%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666666666667%}.col-lg-pull-4{right:33.33333333333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.666666666666664%}.col-lg-pull-1{right:8.333333333333332%}.col-lg-pull-0{right:0}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666666666666%}.col-lg-push-10{left:83.33333333333334%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666666666666%}.col-lg-push-7{left:58.333333333333336%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666666666667%}.col-lg-push-4{left:33.33333333333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.666666666666664%}.col-lg-push-1{left:8.333333333333332%}.col-lg-push-0{left:0}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666666666666%}.col-lg-offset-10{margin-left:83.33333333333334%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666666666666%}.col-lg-offset-7{margin-left:58.333333333333336%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666666666667%}.col-lg-offset-4{margin-left:33.33333333333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.666666666666664%}.col-lg-offset-1{margin-left:8.333333333333332%}.col-lg-offset-0{margin-left:0}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.428571429;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*="col-"]{position:static;display:table-column;float:none}table td[class*="col-"],table th[class*="col-"]{display:table-cell;float:none}.table>thead>tr>.active,.table>tbody>tr>.active,.table>tfoot>tr>.active,.table>thead>.active>td,.table>tbody>.active>td,.table>tfoot>.active>td,.table>thead>.active>th,.table>tbody>.active>th,.table>tfoot>.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>.active:hover,.table-hover>tbody>.active:hover>td,.table-hover>tbody>.active:hover>th{background-color:#e8e8e8}.table>thead>tr>.success,.table>tbody>tr>.success,.table>tfoot>tr>.success,.table>thead>.success>td,.table>tbody>.success>td,.table>tfoot>.success>td,.table>thead>.success>th,.table>tbody>.success>th,.table>tfoot>.success>th{background-color:#dff0d8}.table-hover>tbody>tr>.success:hover,.table-hover>tbody>.success:hover>td,.table-hover>tbody>.success:hover>th{background-color:#d0e9c6}.table>thead>tr>.danger,.table>tbody>tr>.danger,.table>tfoot>tr>.danger,.table>thead>.danger>td,.table>tbody>.danger>td,.table>tfoot>.danger>td,.table>thead>.danger>th,.table>tbody>.danger>th,.table>tfoot>.danger>th{background-color:#f2dede}.table-hover>tbody>tr>.danger:hover,.table-hover>tbody>.danger:hover>td,.table-hover>tbody>.danger:hover>th{background-color:#ebcccc}.table>thead>tr>.warning,.table>tbody>tr>.warning,.table>tfoot>tr>.warning,.table>thead>.warning>td,.table>tbody>.warning>td,.table>tfoot>.warning>td,.table>thead>.warning>th,.table>tbody>.warning>th,.table>tfoot>.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>.warning:hover,.table-hover>tbody>.warning:hover>td,.table-hover>tbody>.warning:hover>th{background-color:#faf2cc}@media(max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:scroll;overflow-y:hidden;border:1px solid #ddd;-ms-overflow-style:-ms-autohiding-scrollbar;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}select[multiple],select[size]{height:auto}select optgroup{font-family:inherit;font-size:inherit;font-style:inherit}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}input[type="number"]::-webkit-outer-spin-button,input[type="number"]::-webkit-inner-spin-button{height:auto}output{display:block;padding-top:7px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429;color:#555;vertical-align:middle;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.form-control:-moz-placeholder{color:#999}.form-control::-moz-placeholder{color:#999;opacity:1}.form-control:-ms-input-placeholder{color:#999}.form-control::-webkit-input-placeholder{color:#999}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee}textarea.form-control{height:auto}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:20px;padding-left:20px;margin-top:10px;margin-bottom:10px;vertical-align:middle}.radio label,.checkbox label{display:inline;margin-bottom:0;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:normal;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm{height:auto}.input-lg{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg{height:auto}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media(min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block}.form-inline select.form-control{width:auto}.form-inline .radio,.form-inline .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-group:before,.form-horizontal .form-group:after{display:table;content:" "}.form-horizontal .form-group:after{clear:both}.form-horizontal .form-control-static{padding-top:7px}@media(min-width:768px){.form-horizontal .control-label{text-align:right}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:normal;line-height:1.428571429;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;border-radius:4px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none}.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;opacity:.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#333;background-color:#ebebeb;border-color:#adadad}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#fff}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#fff;background-color:#3276b1;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#fff;background-color:#ed9c28;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#fff;background-color:#d2322d;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#fff;background-color:#47a447;border-color:#398439}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#fff;background-color:#39b3d7;border-color:#269abc}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-link{font-weight:normal;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#999;text-decoration:none}.btn-lg{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%;padding-right:0;padding-left:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;transition:height .35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';-webkit-font-smoothing:antialiased;font-style:normal;font-weight:normal;line-height:1;-moz-osx-font-smoothing:grayscale}.glyphicon:empty{width:1em}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;list-style:none;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.428571429;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#999}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.428571429;color:#999}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media(min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar:before,.btn-toolbar:after{display:table;content:" "}.btn-toolbar:after{clear:both}.btn-toolbar .btn-group{float:left}.btn-toolbar>.btn+.btn,.btn-toolbar>.btn-group+.btn,.btn-toolbar>.btn+.btn-group,.btn-toolbar>.btn-group+.btn-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after{display:table;content:" "}.btn-group-vertical>.btn-group:after{clear:both}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-bottom-left-radius:4px;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child>.btn:last-child,.btn-group-vertical>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;border-collapse:separate;table-layout:fixed}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-right:0;padding-left:0}.input-group .form-control{width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:normal;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;white-space:nowrap}.input-group-btn:first-child>.btn{margin-right:-1px}.input-group-btn:last-child>.btn{margin-left:-1px}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-4px}.input-group-btn>.btn:hover,.input-group-btn>.btn:active{z-index:2}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav:before,.nav:after{display:table;content:" "}.nav:after{clear:both}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#999}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#999;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.428571429;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media(min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media(min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}.navbar:before,.navbar:after{display:table;content:" "}.navbar:after{clear:both}@media(min-width:768px){.navbar{border-radius:4px}}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}.navbar-header:before,.navbar-header:after{display:table;content:" "}.navbar-header:after{clear:both}@media(min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;padding-right:15px;padding-left:15px;overflow-x:visible;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse:before,.navbar-collapse:after{display:table;content:" "}.navbar-collapse:after{clear:both}.navbar-collapse.in{overflow-y:auto}@media(min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.container>.navbar-header,.container>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media(min-width:768px){.container>.navbar-header,.container>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media(min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media(min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media(min-width:768px){.navbar>.container .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media(min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media(max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media(min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media(min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}@media(min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block}.navbar-form select.form-control{width:auto}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;padding-left:0;margin-top:0;margin-bottom:0}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}}@media(max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media(min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-nav.pull-right>li>.dropdown-menu,.navbar-nav>li>.dropdown-menu.pull-right{right:0;left:auto}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media(min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#ccc}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media(max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#999}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#999}.navbar-inverse .navbar-nav>li>a{color:#999}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media(max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#999}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#999}.navbar-inverse .navbar-link:hover{color:#fff}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#999}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.428571429;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:4px;border-top-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{background-color:#eee}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#999;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:6px;border-top-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:3px;border-top-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager:before,.pager:after{display:table;content:" "}.pager:after{clear:both}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#999;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#999}.label-default[href]:hover,.label-default[href]:focus{background-color:#808080}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:bold;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#999;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;font-size:21px;font-weight:200;line-height:2.1428571435;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{line-height:1;color:inherit}.jumbotron p{line-height:1.4}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.428571429;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{display:block;height:auto;max-width:100%;margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-right-radius:4px;border-top-left-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#f5f5f5}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel-body:before,.panel-body:after{display:table;content:" "}.panel-body:after{clear:both}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0}.panel>.list-group .list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.panel>.list-group .list-group-item:last-child{border-bottom:0}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child th,.panel>.table>tbody:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:last-child>th,.panel>.table-responsive>.table-bordered>thead>tr:last-child>th,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th,.panel>.table-bordered>thead>tr:last-child>td,.panel>.table-responsive>.table-bordered>thead>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:3px;border-top-left-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-group .panel{margin-bottom:0;overflow:hidden;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:#ddd}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:#428bca}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:#d6e9c6}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#d6e9c6}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:#faebcc}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:#ebccd1}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#ebccd1}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:#bce8f1}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:#bce8f1}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:bold;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;opacity:.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;display:none;overflow:auto;overflow-y:scroll}.modal.fade .modal-dialog{-webkit-transform:translate(0,-25%);-ms-transform:translate(0,-25%);transform:translate(0,-25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0,0);-ms-transform:translate(0,0);transform:translate(0,0)}.modal-dialog{position:relative;z-index:1050;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;border:1px solid #999;border:1px solid rgba(0,0,0,0.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1030;background-color:#000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:.5;filter:alpha(opacity=50)}.modal-header{min-height:16.428571429px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.428571429}.modal-body{position:relative;padding:20px}.modal-footer{padding:19px 20px 20px;margin-top:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer:before,.modal-footer:after{display:table;content:" "}.modal-footer:after{clear:both}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media screen and (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}}.tooltip{position:absolute;z-index:1030;display:block;font-size:12px;line-height:1.4;opacity:0;filter:alpha(opacity=0);visibility:visible}.tooltip.in{opacity:.9;filter:alpha(opacity=90)}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-top-color:#000;border-width:5px 5px 0}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-top-color:#000;border-width:5px 5px 0}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-right-color:#000;border-width:5px 5px 5px 0}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-left-color:#000;border-width:5px 0 5px 5px}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-bottom-color:#000;border-width:0 5px 5px}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-bottom-color:#000;border-width:0 5px 5px}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);background-clip:padding-box}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover .arrow,.popover .arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover .arrow{border-width:11px}.popover .arrow:after{border-width:10px;content:""}.popover.top .arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,0.25);border-bottom-width:0}.popover.top .arrow:after{bottom:1px;margin-left:-10px;border-top-color:#fff;border-bottom-width:0;content:" "}.popover.right .arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,0.25);border-left-width:0}.popover.right .arrow:after{bottom:-10px;left:1px;border-right-color:#fff;border-left-width:0;content:" "}.popover.bottom .arrow{top:-11px;left:50%;margin-left:-11px;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,0.25);border-top-width:0}.popover.bottom .arrow:after{top:1px;margin-left:-10px;border-bottom-color:#fff;border-top-width:0;content:" "}.popover.left .arrow{top:50%;right:-11px;margin-top:-11px;border-left-color:#999;border-left-color:rgba(0,0,0,0.25);border-right-width:0}.popover.left .arrow:after{right:1px;bottom:-10px;border-left-color:#fff;border-right-width:0;content:" "}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;height:auto;max-width:100%;line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6);opacity:.5;filter:alpha(opacity=50)}.carousel-control.left{background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.5) 0),color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.5) 0,rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000',endColorstr='#00000000',GradientType=1)}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,color-stop(rgba(0,0,0,0.0001) 0),color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right,rgba(0,0,0,0.0001) 0,rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000',endColorstr='#80000000',GradientType=1)}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;outline:0;opacity:.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicons-chevron-left,.carousel-control .glyphicons-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after{display:table;content:" "}.clearfix:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,tr.visible-xs,th.visible-xs,td.visible-xs{display:none!important}@media(max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-xs.visible-sm{display:block!important}table.visible-xs.visible-sm{display:table}tr.visible-xs.visible-sm{display:table-row!important}th.visible-xs.visible-sm,td.visible-xs.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-xs.visible-md{display:block!important}table.visible-xs.visible-md{display:table}tr.visible-xs.visible-md{display:table-row!important}th.visible-xs.visible-md,td.visible-xs.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-xs.visible-lg{display:block!important}table.visible-xs.visible-lg{display:table}tr.visible-xs.visible-lg{display:table-row!important}th.visible-xs.visible-lg,td.visible-xs.visible-lg{display:table-cell!important}}.visible-sm,tr.visible-sm,th.visible-sm,td.visible-sm{display:none!important}@media(max-width:767px){.visible-sm.visible-xs{display:block!important}table.visible-sm.visible-xs{display:table}tr.visible-sm.visible-xs{display:table-row!important}th.visible-sm.visible-xs,td.visible-sm.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-sm.visible-md{display:block!important}table.visible-sm.visible-md{display:table}tr.visible-sm.visible-md{display:table-row!important}th.visible-sm.visible-md,td.visible-sm.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-sm.visible-lg{display:block!important}table.visible-sm.visible-lg{display:table}tr.visible-sm.visible-lg{display:table-row!important}th.visible-sm.visible-lg,td.visible-sm.visible-lg{display:table-cell!important}}.visible-md,tr.visible-md,th.visible-md,td.visible-md{display:none!important}@media(max-width:767px){.visible-md.visible-xs{display:block!important}table.visible-md.visible-xs{display:table}tr.visible-md.visible-xs{display:table-row!important}th.visible-md.visible-xs,td.visible-md.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-md.visible-sm{display:block!important}table.visible-md.visible-sm{display:table}tr.visible-md.visible-sm{display:table-row!important}th.visible-md.visible-sm,td.visible-md.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-md.visible-lg{display:block!important}table.visible-md.visible-lg{display:table}tr.visible-md.visible-lg{display:table-row!important}th.visible-md.visible-lg,td.visible-md.visible-lg{display:table-cell!important}}.visible-lg,tr.visible-lg,th.visible-lg,td.visible-lg{display:none!important}@media(max-width:767px){.visible-lg.visible-xs{display:block!important}table.visible-lg.visible-xs{display:table}tr.visible-lg.visible-xs{display:table-row!important}th.visible-lg.visible-xs,td.visible-lg.visible-xs{display:table-cell!important}}@media(min-width:768px) and (max-width:991px){.visible-lg.visible-sm{display:block!important}table.visible-lg.visible-sm{display:table}tr.visible-lg.visible-sm{display:table-row!important}th.visible-lg.visible-sm,td.visible-lg.visible-sm{display:table-cell!important}}@media(min-width:992px) and (max-width:1199px){.visible-lg.visible-md{display:block!important}table.visible-lg.visible-md{display:table}tr.visible-lg.visible-md{display:table-row!important}th.visible-lg.visible-md,td.visible-lg.visible-md{display:table-cell!important}}@media(min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}.hidden-xs{display:block!important}table.hidden-xs{display:table}tr.hidden-xs{display:table-row!important}th.hidden-xs,td.hidden-xs{display:table-cell!important}@media(max-width:767px){.hidden-xs,tr.hidden-xs,th.hidden-xs,td.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-xs.hidden-sm,tr.hidden-xs.hidden-sm,th.hidden-xs.hidden-sm,td.hidden-xs.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-xs.hidden-md,tr.hidden-xs.hidden-md,th.hidden-xs.hidden-md,td.hidden-xs.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-xs.hidden-lg,tr.hidden-xs.hidden-lg,th.hidden-xs.hidden-lg,td.hidden-xs.hidden-lg{display:none!important}}.hidden-sm{display:block!important}table.hidden-sm{display:table}tr.hidden-sm{display:table-row!important}th.hidden-sm,td.hidden-sm{display:table-cell!important}@media(max-width:767px){.hidden-sm.hidden-xs,tr.hidden-sm.hidden-xs,th.hidden-sm.hidden-xs,td.hidden-sm.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-sm,tr.hidden-sm,th.hidden-sm,td.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-sm.hidden-md,tr.hidden-sm.hidden-md,th.hidden-sm.hidden-md,td.hidden-sm.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-sm.hidden-lg,tr.hidden-sm.hidden-lg,th.hidden-sm.hidden-lg,td.hidden-sm.hidden-lg{display:none!important}}.hidden-md{display:block!important}table.hidden-md{display:table}tr.hidden-md{display:table-row!important}th.hidden-md,td.hidden-md{display:table-cell!important}@media(max-width:767px){.hidden-md.hidden-xs,tr.hidden-md.hidden-xs,th.hidden-md.hidden-xs,td.hidden-md.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-md.hidden-sm,tr.hidden-md.hidden-sm,th.hidden-md.hidden-sm,td.hidden-md.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-md,tr.hidden-md,th.hidden-md,td.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-md.hidden-lg,tr.hidden-md.hidden-lg,th.hidden-md.hidden-lg,td.hidden-md.hidden-lg{display:none!important}}.hidden-lg{display:block!important}table.hidden-lg{display:table}tr.hidden-lg{display:table-row!important}th.hidden-lg,td.hidden-lg{display:table-cell!important}@media(max-width:767px){.hidden-lg.hidden-xs,tr.hidden-lg.hidden-xs,th.hidden-lg.hidden-xs,td.hidden-lg.hidden-xs{display:none!important}}@media(min-width:768px) and (max-width:991px){.hidden-lg.hidden-sm,tr.hidden-lg.hidden-sm,th.hidden-lg.hidden-sm,td.hidden-lg.hidden-sm{display:none!important}}@media(min-width:992px) and (max-width:1199px){.hidden-lg.hidden-md,tr.hidden-lg.hidden-md,th.hidden-lg.hidden-md,td.hidden-lg.hidden-md{display:none!important}}@media(min-width:1200px){.hidden-lg,tr.hidden-lg,th.hidden-lg,td.hidden-lg{display:none!important}}.visible-print,tr.visible-print,th.visible-print,td.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}.hidden-print,tr.hidden-print,th.hidden-print,td.hidden-print{display:none!important}} \ No newline at end of file diff --git a/docs/css/fig.css b/docs/css/fig.css deleted file mode 100644 index 3dc990f1..00000000 --- a/docs/css/fig.css +++ /dev/null @@ -1,187 +0,0 @@ -body { - padding-top: 20px; - padding-bottom: 60px; - font-family: 'Lato', sans-serif; - font-weight: 300; - font-size: 18px; - color: #362; -} - -h1, h2, h3, h4, h5, h6 { - font-family: 'Lato', sans-serif; - font-weight: 400; - color: #25594D; -} - -h2, h3, h4, h5, h6 { - margin-top: 1.5em; -} - -p { - margin: 20px 0; -} - -a, a:hover, a:visited { - color: #4D9900; - text-decoration: underline; -} - -pre, code { - border: none; - background: #D5E1B4; -} - -code, pre code { - color: #484F40; -} - -pre { - border-bottom: 2px solid #bec9a1; - font-size: 14px; -} - -code { - font-size: 0.84em; -} - -pre code { - background: none; -} - -img { - max-width: 100%; -} - -.container { - margin-left: 0; -} - -.logo { - font-family: 'Lilita One', sans-serif; - font-size: 64px; - margin: 20px 0 40px 0; -} - -.logo a { - color: #a41211; - text-decoration: none; -} - -.logo img { - width: 60px; - vertical-align: -8px; -} - -.mobile-logo { - text-align: center; -} - -.sidebar { - font-size: 15px; - color: #777; -} - -.sidebar a { - color: #a41211; -} - -.sidebar p { - margin: 10px 0; -} - -@media (max-width: 767px) { - .sidebar { - text-align: center; - margin-top: 40px; - } - - .sidebar .logo { - display: none; - } -} - -@media (min-width: 768px) { - .mobile-logo { - display: none; - } - - .logo { - margin-top: 30px; - margin-bottom: 30px; - } - - .content h1 { - margin: 60px 0 55px 0; - } - - .sidebar { - position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 280px; - overflow-y: auto; - padding-left: 40px; - padding-right: 10px; - border-right: 1px solid #ccc; - } - - .content { - margin-left: 320px; - max-width: 650px; - } -} - -.nav { - margin: 15px 0; -} - -.nav li a { - display: block; - padding: 5px 0; - line-height: 1.2; - text-decoration: none; -} - -.nav li a:hover, .nav li a:focus { - text-decoration: underline; - background: none; -} - -.nav ul { - padding-left: 20px; - list-style: none; -} - -.badges { - margin: 40px 0; -} - -a.btn { - background: #25594D; - color: white; - text-transform: uppercase; - text-decoration: none; -} - -a.btn:hover { - color: white; -} - -.strapline { - font-size: 30px; -} - -@media (min-width: 768px) { - .strapline { - font-size: 40px; - display: block; - line-height: 1.2; - margin-top: 25px; - margin-bottom: 35px; - } -} - -strong { - font-weight: 700; -} diff --git a/docs/fig.yml b/docs/fig.yml deleted file mode 100644 index 30e8f6d9..00000000 --- a/docs/fig.yml +++ /dev/null @@ -1,8 +0,0 @@ -jekyll: - build: . - ports: - - "4000:4000" - volumes: - - .:/code - environment: - - LANG=en_US.UTF-8 diff --git a/docs/img/favicon.ico b/docs/img/favicon.ico deleted file mode 100644 index 71c02f1136c5ab430b5c0a61db023684877cf546..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmZuwc}x>l7=Nf+7MCT=Kk(nhjG+t|_gxCK1xh)JN|_u<87K}StRRAJAcPnaflWjN zDdG|a2F6j2f`wktld}}jLOBLP0&JRT;sU$)?TLxWjCuKe-}}DzzTbD`Jwj~gvb7~t zenQS4BE*gma-4#?sOPnxDnjVlkq=*E-+qU1aEm38`IW@$WVT&W(T%LiZkeHWXmY2t zZG5+|Su-VS>d8#KcJrh3N-;@uJoZ0^^rA|VQBZO)zVJpwK~wKY*F)={ExHY)UY-L# zV+t{6Cy;yFv|G}pz9SNM9LZ~v?8kGzE+)*>0vkc$^<&{VRi(oEfoIkF6*Tle0sku_ zTp0>5V@BZ_qC`;iC@wY3?v)Q1%ekW3Ly2W~3Fkt^`=y<#bB%iQV!mX2Pf%+@eDMq- zQ^w#PD2F>^7;cOqFnCIMg~$*l(BYco@%EKYO)w$v9VqLNi-uP={%D)pLS}^pf^TLJ z6sHG+=JX3!!-FA*i~j)pB4qg7djL+}GGtfH;kt5GoO$a$r`@!$)u!3R+3#i%9IHhj zPXp#D0|F@rE5V2Wo(j(XQn&^^p!xx=L5}2$)2Pu~7c1rCT~)m+NVqVCAezrBbQs<& zB|-#7Fi&gYAJI?yAA~1+7@nb0obZsq-nj=pYz2z$&+pvUFF#Grn*ryv4(xa>d?VDf zW)*lsBN)*VxCZyZnfCAGBZ0GTFC0Dk;2S1~PnZf9f3)mX%U5<2vZld0r3Ej=0484x zzbIP&Ny>Ld19tKx*oj&?`##vabmLP;3H-v;h)J77)yOX|ZfTYt2@A~#rT0T9{JCmy zlg1E{G6_Mp8Sz;r>X*SSuos`Xbi$L_hrmb$lJcg}Xj+@7P|g;V_bmPuB^*Nlt=*ra zL|ob!qJ$F&PZ|d&!2sV#HQa)vIPNZ?96hu@1qvE0|JLX%CBmA)kAKiDv|MemBAWIb zLwS-#Cirkvbe3{(ztr!)8E=*hj$ZxvB48L9WwWT!ESZZW2K#TNCX)4&=IG6_#kxA( z+RMCZ3$D~zA-rq`D^7#pm=SOj4PXiM2VTeNTfk3(o4 zX|}GDGO6)!gZ8oa&oj%plKJ)KR?CLGeR0#Mdh(lDvAVr3S=#*je8b>#LCHNfFSq7R z{M#FTtCcvU)*m?6YB*f1wAkhk%pMhWj-SYBH;T`E-}f@9yiMf9ze@f&NXT;=Le!L? S diff --git a/docs/img/logo.png b/docs/img/logo.png deleted file mode 100644 index 13ca9bc78b18f3bb7195dec87662195508cf5a51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133640 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≺_r~!Y{H%?R0D5ge&@j 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 z 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 Date: Tue, 20 Jan 2015 21:23:01 +0000 Subject: [PATCH 0718/2154] Fix typos Signed-off-by: Aanand Prasad --- compose/cli/command.py | 2 +- compose/project.py | 2 +- script/clean | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index dc7683a3..f92f95cd 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -111,7 +111,7 @@ class Command(DocoptCommand): return os.path.join(self.base_dir, file_path) if os.path.exists(os.path.join(self.base_dir, 'docker-compose.yaml')): - log.warning("Fig just read the file 'docker-compose.yaml' on startup, rather " + log.warning("Compose just read the file 'docker-compose.yaml' on startup, rather " "than 'docker-compose.yml'") log.warning("Please be aware that .yml is the expected extension " "in most cases, and using .yaml can cause compatibility " diff --git a/compose/project.py b/compose/project.py index b707d637..028c6a96 100644 --- a/compose/project.py +++ b/compose/project.py @@ -67,7 +67,7 @@ class Project(object): dicts = [] for service_name, service in list(config.items()): if not isinstance(service, dict): - raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your compose.yml must map to a dictionary of configuration options.' % service_name) + raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your docker-compose.yml must map to a dictionary of configuration options.' % service_name) service['name'] = service_name dicts.append(service) return cls.from_dicts(name, dicts, client) diff --git a/script/clean b/script/clean index 0c845012..07a9cff1 100755 --- a/script/clean +++ b/script/clean @@ -1,3 +1,3 @@ #!/bin/sh find . -type f -name '*.pyc' -delete -rm -rf docs/_site build dist compose.egg-info +rm -rf docs/_site build dist docker-compose.egg-info From 43fdae8bc66051e040333bbe8dd1082f95ffee11 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 20 Jan 2015 18:24:10 +0000 Subject: [PATCH 0719/2154] Ship 1.1.0-rc1 Signed-off-by: Aanand Prasad --- CHANGES.md | 29 +++++++++++++++++++++++++++++ compose/__init__.py | 2 +- docs/install.md | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 57e958aa..c8199cbd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,35 @@ Change log ========== +1.1.0-rc1 (2015-01-20) +---------------------- + +Fig has been renamed to Docker Compose, or just Compose for short. This has several implications for you: + +- The command you type is now `docker-compose`, not `fig`. +- You should rename your fig.yml to docker-compose.yml. +- If you’re installing via PyPi, the package is now `docker-compose`, so install it with `pip install docker-compose`. + +Besides that, there’s a lot of new stuff in this release: + +- We’ve made a few small changes to ensure that Compose will work with Swarm, Docker’s new clustering tool (https://github.com/docker/swarm). Eventually you'll be able to point Compose at a Swarm cluster instead of a standalone Docker host and it’ll run your containers on the cluster with no extra work from you. As Swarm is still developing, integration is rough and lots of Compose features don't work yet. + +- `docker-compose run` now has a `--service-ports` flag for exposing ports on the given service. This is useful for e.g. running your webapp with an interactive debugger. + +- You can now link to containers outside your app with the `external_links` option in docker-compose.yml. + +- You can now prevent `docker-compose up` from automatically building images with the `--no-build` option. This will make fewer API calls and run faster. + +- If you don’t specify a tag when using the `image` key, Compose will default to the `latest` tag, rather than pulling all tags. + +- `docker-compose kill` now supports the `-s` flag, allowing you to specify the exact signal you want to send to a service’s containers. + +- docker-compose.yml now has an `env_file` key, analogous to `docker run --env-file`, letting you specify multiple environment variables in a separate file. This is great if you have a lot of them, or if you want to keep sensitive information out of version control. + +- docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop` and `--restart` options. + +- A number of bugs have been fixed - see the milestone for details: https://github.com/docker/fig/issues?q=milestone%3A1.1.0+ + 1.0.1 (2014-11-04) ------------------ diff --git a/compose/__init__.py b/compose/__init__.py index a7b29e0c..4ed232c6 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service # noqa:flake8 -__version__ = '1.0.1' +__version__ = '1.1.0-rc1' diff --git a/docs/install.md b/docs/install.md index e61cd0fc..4c35e12a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -18,7 +18,7 @@ There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntuli Next, install Compose: - curl -L https://github.com/docker/docker-compose/releases/download/1.0.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose + curl -L https://github.com/docker/docker-compose/releases/download/1.1.0-rc1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose Optionally, install [command completion](completion.html) for the bash shell. From f1e4fb7736a826665886158d8a9bcfd9470e7d4e Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Wed, 21 Jan 2015 09:37:07 +0100 Subject: [PATCH 0720/2154] Rebrand bash completion script Signed-off-by: Harald Albers --- .../completion/bash/{fig => docker-compose} | 118 +++++++++--------- 1 file changed, 57 insertions(+), 61 deletions(-) rename contrib/completion/bash/{fig => docker-compose} (59%) diff --git a/contrib/completion/bash/fig b/contrib/completion/bash/docker-compose similarity index 59% rename from contrib/completion/bash/fig rename to contrib/completion/bash/docker-compose index b44efa7b..53231604 100644 --- a/contrib/completion/bash/fig +++ b/contrib/completion/bash/docker-compose @@ -1,6 +1,6 @@ #!bash # -# bash completion for fig commands +# bash completion for docker-compose # # This work is based on the completion for the docker command. # @@ -12,46 +12,42 @@ # To enable the completions either: # - place this file in /etc/bash_completion.d # or -# - copy this file and add the line below to your .bashrc after -# bash completion features are loaded -# . docker.bash -# -# Note: -# Some completions require the current user to have sufficient permissions -# to execute the docker command. +# - copy this file to e.g. ~/.docker-compose-completion.sh and add the line +# below to your .bashrc after bash completion features are loaded +# . ~/.docker-compose-completion.sh -# Extracts all service names from the figfile. -___fig_all_services_in_figfile() { - awk -F: '/^[a-zA-Z0-9]/{print $1}' "${fig_file:-fig.yml}" +# Extracts all service names from docker-compose.yml. +___docker-compose_all_services_in_compose_file() { + awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-docker-compose.yml}" } # All services, even those without an existing container -__fig_services_all() { - COMPREPLY=( $(compgen -W "$(___fig_all_services_in_figfile)" -- "$cur") ) +__docker-compose_services_all() { + COMPREPLY=( $(compgen -W "$(___docker-compose_all_services_in_compose_file)" -- "$cur") ) } -# All services that have an entry with the given key in their figfile section -___fig_services_with_key() { +# All services that have an entry with the given key in their docker-compose.yml section +___docker-compose_services_with_key() { # flatten sections to one line, then filter lines containing the key and return section name. - awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' fig.yml | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' + awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-docker-compose.yml}" | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' } # All services that are defined by a Dockerfile reference -__fig_services_from_build() { - COMPREPLY=( $(compgen -W "$(___fig_services_with_key build)" -- "$cur") ) +__docker-compose_services_from_build() { + COMPREPLY=( $(compgen -W "$(___docker-compose_services_with_key build)" -- "$cur") ) } # All services that are defined by an image -__fig_services_from_image() { - COMPREPLY=( $(compgen -W "$(___fig_services_with_key image)" -- "$cur") ) +__docker-compose_services_from_image() { + COMPREPLY=( $(compgen -W "$(___docker-compose_services_with_key image)" -- "$cur") ) } # The services for which containers have been created, optionally filtered # by a boolean expression passed in as argument. -__fig_services_with() { +__docker-compose_services_with() { local containers names - containers="$(fig 2>/dev/null ${fig_file:+-f $fig_file} ${fig_project:+-p $fig_project} ps -q)" + containers="$(docker-compose 2>/dev/null ${compose_file:+-f $compose_file} ${compose_project:+-p $compose_project} ps -q)" names=( $(docker 2>/dev/null inspect --format "{{if ${1:-true}}} {{ .Name }} {{end}}" $containers) ) names=( ${names[@]%_*} ) # strip trailing numbers names=( ${names[@]#*_} ) # strip project name @@ -59,29 +55,29 @@ __fig_services_with() { } # The services for which at least one running container exists -__fig_services_running() { - __fig_services_with '.State.Running' +__docker-compose_services_running() { + __docker-compose_services_with '.State.Running' } # The services for which at least one stopped container exists -__fig_services_stopped() { - __fig_services_with 'not .State.Running' +__docker-compose_services_stopped() { + __docker-compose_services_with 'not .State.Running' } -_fig_build() { +_docker-compose_build() { case "$cur" in -*) COMPREPLY=( $( compgen -W "--no-cache" -- "$cur" ) ) ;; *) - __fig_services_from_build + __docker-compose_services_from_build ;; esac } -_fig_fig() { +_docker-compose_docker-compose() { case "$prev" in --file|-f) _filedir @@ -103,12 +99,12 @@ _fig_fig() { } -_fig_help() { +_docker-compose_help() { COMPREPLY=( $( compgen -W "${commands[*]}" -- "$cur" ) ) } -_fig_kill() { +_docker-compose_kill() { case "$prev" in -s) COMPREPLY=( $( compgen -W "SIGHUP SIGINT SIGKILL SIGUSR1 SIGUSR2" -- "$(echo $cur | tr '[:lower:]' '[:upper:]')" ) ) @@ -121,25 +117,25 @@ _fig_kill() { COMPREPLY=( $( compgen -W "-s" -- "$cur" ) ) ;; *) - __fig_services_running + __docker-compose_services_running ;; esac } -_fig_logs() { +_docker-compose_logs() { case "$cur" in -*) COMPREPLY=( $( compgen -W "--no-color" -- "$cur" ) ) ;; *) - __fig_services_all + __docker-compose_services_all ;; esac } -_fig_port() { +_docker-compose_port() { case "$prev" in --protocol) COMPREPLY=( $( compgen -W "tcp udp" -- "$cur" ) ) @@ -155,54 +151,54 @@ _fig_port() { COMPREPLY=( $( compgen -W "--protocol --index" -- "$cur" ) ) ;; *) - __fig_services_all + __docker-compose_services_all ;; esac } -_fig_ps() { +_docker-compose_ps() { case "$cur" in -*) COMPREPLY=( $( compgen -W "-q" -- "$cur" ) ) ;; *) - __fig_services_all + __docker-compose_services_all ;; esac } -_fig_pull() { +_docker-compose_pull() { case "$cur" in -*) COMPREPLY=( $( compgen -W "--allow-insecure-ssl" -- "$cur" ) ) ;; *) - __fig_services_from_image + __docker-compose_services_from_image ;; esac } -_fig_restart() { - __fig_services_running +_docker-compose_restart() { + __docker-compose_services_running } -_fig_rm() { +_docker-compose_rm() { case "$cur" in -*) COMPREPLY=( $( compgen -W "--force -v" -- "$cur" ) ) ;; *) - __fig_services_stopped + __docker-compose_services_stopped ;; esac } -_fig_run() { +_docker-compose_run() { case "$prev" in -e) COMPREPLY=( $( compgen -e -- "$cur" ) ) @@ -219,48 +215,48 @@ _fig_run() { COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm -T" -- "$cur" ) ) ;; *) - __fig_services_all + __docker-compose_services_all ;; esac } -_fig_scale() { +_docker-compose_scale() { case "$prev" in =) COMPREPLY=("$cur") ;; *) - COMPREPLY=( $(compgen -S "=" -W "$(___fig_all_services_in_figfile)" -- "$cur") ) + COMPREPLY=( $(compgen -S "=" -W "$(___docker-compose_all_services_in_compose_file)" -- "$cur") ) compopt -o nospace ;; esac } -_fig_start() { - __fig_services_stopped +_docker-compose_start() { + __docker-compose_services_stopped } -_fig_stop() { - __fig_services_running +_docker-compose_stop() { + __docker-compose_services_running } -_fig_up() { +_docker-compose_up() { case "$cur" in -*) COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --no-build --no-color --no-deps --no-recreate" -- "$cur" ) ) ;; *) - __fig_services_all + __docker-compose_services_all ;; esac } -_fig() { +_docker-compose() { local commands=( build help @@ -284,18 +280,18 @@ _fig() { # search subcommand and invoke its handler. # special treatment of some top-level options - local command='fig' + local command='docker-compose' local counter=1 - local fig_file fig_project + local compose_file compose_project while [ $counter -lt $cword ]; do case "${words[$counter]}" in -f|--file) (( counter++ )) - fig_file="${words[$counter]}" + compose_file="${words[$counter]}" ;; -p|--project-name) (( counter++ )) - fig_project="${words[$counter]}" + compose_project="${words[$counter]}" ;; -*) ;; @@ -307,10 +303,10 @@ _fig() { (( counter++ )) done - local completions_func=_fig_${command} + local completions_func=_docker-compose_${command} declare -F $completions_func >/dev/null && $completions_func return 0 } -complete -F _fig fig +complete -F _docker-compose docker-compose From bd535c76d09e4ca70695624926626b7261e1bd17 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Sun, 25 Jan 2015 09:47:30 -0800 Subject: [PATCH 0721/2154] Add --service-ports to bash completion Signed-off-by: Harald Albers --- contrib/completion/bash/docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 53231604..a8d56d06 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -212,7 +212,7 @@ _docker-compose_run() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm -T" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm --service-ports -T" -- "$cur" ) ) ;; *) __docker-compose_services_all From 0bc4a28dccd6a569623793fbea7f60202655541d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Sun, 25 Jan 2015 13:54:57 -0500 Subject: [PATCH 0722/2154] Use latest docker-py. Signed-off-by: Daniel Nephin --- requirements.txt | 2 +- setup.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2ccdf59a..a31a19ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ PyYAML==3.10 -docker-py==0.6.0 +docker-py==0.7.1 dockerpty==0.3.2 docopt==0.6.1 requests==2.2.1 diff --git a/setup.py b/setup.py index 68c11bd9..e1e29744 100644 --- a/setup.py +++ b/setup.py @@ -29,8 +29,8 @@ install_requires = [ 'PyYAML >= 3.10, < 4', 'requests >= 2.2.1, < 3', 'texttable >= 0.8.1, < 0.9', - 'websocket-client >= 0.11.0, < 0.12', - 'docker-py >= 0.6.0, < 0.7', + 'websocket-client >= 0.11.0, < 1.0', + 'docker-py >= 0.6.0, < 0.8', 'dockerpty >= 0.3.2, < 0.4', 'six >= 1.3.0, < 2', ] From 27e4f982facf4e571b4b8b333bed906265e2f6d1 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Mon, 26 Jan 2015 19:09:18 +0100 Subject: [PATCH 0723/2154] Bash completion supports fig.yml and other legacy filenames Signed-off-by: Harald Albers --- contrib/completion/bash/docker-compose | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 53231604..19eeffda 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -17,9 +17,23 @@ # . ~/.docker-compose-completion.sh -# Extracts all service names from docker-compose.yml. +# For compatibility reasons, Compose and therefore its completion supports several +# stack compositon files as listed here, in descending priority. +# Support for these filenames might be dropped in some future version. +__docker-compose_compose_file() { + local file + for file in docker-compose.y{,a}ml fig.y{,a}ml ; do + [ -e $file ] && { + echo $file + return + } + done + echo docker-compose.yml +} + +# Extracts all service names from the compose file. ___docker-compose_all_services_in_compose_file() { - awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-docker-compose.yml}" + awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null } # All services, even those without an existing container @@ -27,10 +41,10 @@ __docker-compose_services_all() { COMPREPLY=( $(compgen -W "$(___docker-compose_all_services_in_compose_file)" -- "$cur") ) } -# All services that have an entry with the given key in their docker-compose.yml section +# All services that have an entry with the given key in their compose_file section ___docker-compose_services_with_key() { # flatten sections to one line, then filter lines containing the key and return section name. - awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-docker-compose.yml}" | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' + awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-$(__docker-compose_compose_file)}" | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' } # All services that are defined by a Dockerfile reference From 147602741059544af55082c6a2a2efc97b74fcda Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 27 Jan 2015 14:46:58 -0500 Subject: [PATCH 0724/2154] Fix test for image-declared volumes A stray 'fig' got lost in the merge post-rename, meaning the containers weren't being cleaned up properly. Signed-off-by: Aanand Prasad --- tests/integration/service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index e11672bf..5904eb4e 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -189,7 +189,7 @@ class ServiceTest(DockerClientTestCase): def test_recreate_containers_with_image_declared_volume(self): service = Service( - project='figtest', + project='composetest', name='db', client=self.client, build='tests/fixtures/dockerfile-with-volume', From 6c45b6ccdb57b6a9e56aa96bb192e115f3ce067b Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 28 Jan 2015 15:58:49 -0500 Subject: [PATCH 0725/2154] Make sure we're testing blank lines and comments in env files Signed-off-by: Aanand Prasad --- tests/fixtures/env/one.env | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/fixtures/env/one.env b/tests/fixtures/env/one.env index 75a4f62f..45b59fe6 100644 --- a/tests/fixtures/env/one.env +++ b/tests/fixtures/env/one.env @@ -1,4 +1,11 @@ +# Keep the blank lines and comments in this file, please + ONE=2 TWO=1 + + # (thanks) + THREE=3 + FOO=bar +# FOO=somethingelse From dfc6206d0d3e8f31a1dcbb5c707c7b36688a9450 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 28 Jan 2015 16:13:34 -0500 Subject: [PATCH 0726/2154] Extract get_env_files() Signed-off-by: Aanand Prasad --- compose/service.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/compose/service.py b/compose/service.py index 70958405..49bccd31 100644 --- a/compose/service.py +++ b/compose/service.py @@ -617,15 +617,18 @@ def split_port(port): return internal_port, (external_ip, external_port or None) +def get_env_files(options): + env_files = options.get('env_file', []) + if not isinstance(env_files, list): + env_files = [env_files] + return env_files + + def merge_environment(options): env = {} - if 'env_file' in options: - if isinstance(options['env_file'], list): - for f in options['env_file']: - env.update(env_vars_from_file(f)) - else: - env.update(env_vars_from_file(options['env_file'])) + for f in get_env_files(options): + env.update(env_vars_from_file(f)) if 'environment' in options: if isinstance(options['environment'], list): From de07e0471ef02d85c248144959fd6932d8adaaf7 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 28 Jan 2015 16:01:48 -0500 Subject: [PATCH 0727/2154] Show a nicer error when the env file doesn't exist Closes #865 Signed-off-by: Aanand Prasad --- compose/service.py | 4 ++++ tests/unit/service_test.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/compose/service.py b/compose/service.py index 49bccd31..0c9bd357 100644 --- a/compose/service.py +++ b/compose/service.py @@ -95,6 +95,10 @@ class Service(object): if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) + for filename in get_env_files(options): + if not os.path.exists(filename): + raise ConfigError("Couldn't find env file for service %s: %s" % (name, filename)) + supported_options = DOCKER_CONFIG_KEYS + ['build', 'expose', 'external_links'] diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 0a7239b0..c7b122fc 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -378,6 +378,10 @@ class ServiceEnvironmentTest(unittest.TestCase): {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'} ) + def test_env_nonexistent_file(self): + self.assertRaises(ConfigError, lambda: Service('foo', env_file='tests/fixtures/env/nonexistent.env')) + + @mock.patch.dict(os.environ) def test_resolve_environment_from_file(self): os.environ['FILE_DEF'] = 'E1' From 9bc7604e0e47ab3debe6bf41e86ed6a4db842ca6 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 28 Jan 2015 17:19:27 -0500 Subject: [PATCH 0728/2154] Make sure we're testing uppercase directories properly (OS X is case-sensitive so we can't have fixture dirs which are identically named if you ignore case) Signed-off-by: Aanand Prasad --- tests/fixtures/UpperCaseDir/docker-compose.yml | 6 ++++++ tests/unit/cli_test.py | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/UpperCaseDir/docker-compose.yml diff --git a/tests/fixtures/UpperCaseDir/docker-compose.yml b/tests/fixtures/UpperCaseDir/docker-compose.yml new file mode 100644 index 00000000..3538ab09 --- /dev/null +++ b/tests/fixtures/UpperCaseDir/docker-compose.yml @@ -0,0 +1,6 @@ +simple: + image: busybox:latest + command: /bin/sleep 300 +another: + image: busybox:latest + command: /bin/sleep 300 diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 1154d3de..98a8f9ee 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -31,9 +31,9 @@ class CLITestCase(unittest.TestCase): def test_project_name_with_explicit_uppercase_base_dir(self): command = TopLevelCommand() - command.base_dir = 'tests/fixtures/Simple-figfile' + command.base_dir = 'tests/fixtures/UpperCaseDir' project_name = command.get_project_name(command.get_config_path()) - self.assertEquals('simplefigfile', project_name) + self.assertEquals('uppercasedir', project_name) def test_project_name_with_explicit_project_name(self): command = TopLevelCommand() From 7c087f1c07fefeeef092b010841fc36b77eda3bc Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 28 Jan 2015 17:27:53 -0500 Subject: [PATCH 0729/2154] Support fig.y(a)ml and show a deprecation warning Closes #864 Signed-off-by: Aanand Prasad --- compose/cli/command.py | 36 +++++++++++++++++++++-------- compose/cli/errors.py | 8 ++++--- tests/unit/cli_test.py | 51 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 77 insertions(+), 18 deletions(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index f92f95cd..67b77f31 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from __future__ import absolute_import from requests.exceptions import ConnectionError, SSLError -import errno import logging import os import re @@ -75,8 +74,6 @@ class Command(DocoptCommand): with open(config_path, 'r') as fh: return yaml.safe_load(fh) except IOError as e: - if e.errno == errno.ENOENT: - raise errors.ComposeFileNotFound(os.path.basename(e.filename)) raise errors.UserError(six.text_type(e)) def get_project(self, config_path, project_name=None, verbose=False): @@ -110,13 +107,34 @@ class Command(DocoptCommand): if file_path: return os.path.join(self.base_dir, file_path) - if os.path.exists(os.path.join(self.base_dir, 'docker-compose.yaml')): - log.warning("Compose just read the file 'docker-compose.yaml' on startup, rather " - "than 'docker-compose.yml'") + supported_filenames = [ + 'docker-compose.yml', + 'docker-compose.yaml', + 'fig.yml', + 'fig.yaml', + ] + + def expand(filename): + return os.path.join(self.base_dir, filename) + + candidates = [filename for filename in supported_filenames if os.path.exists(expand(filename))] + + if len(candidates) == 0: + raise errors.ComposeFileNotFound(supported_filenames) + + winner = candidates[0] + + if len(candidates) > 1: + log.warning("Found multiple config files with supported names: %s", ", ".join(candidates)) + log.warning("Using %s\n", winner) + + if winner == 'docker-compose.yaml': log.warning("Please be aware that .yml is the expected extension " "in most cases, and using .yaml can cause compatibility " - "issues in future") + "issues in future.\n") - return os.path.join(self.base_dir, 'docker-compose.yaml') + if winner.startswith("fig."): + log.warning("%s is deprecated and will not be supported in future. " + "Please rename your config file to docker-compose.yml\n" % winner) - return os.path.join(self.base_dir, 'docker-compose.yml') + return expand(winner) diff --git a/compose/cli/errors.py b/compose/cli/errors.py index d0d94724..d439aa61 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -56,7 +56,9 @@ class ConnectionErrorGeneric(UserError): class ComposeFileNotFound(UserError): - def __init__(self, filename): + def __init__(self, supported_filenames): super(ComposeFileNotFound, self).__init__(""" - Can't find %s. Are you in the right directory? - """ % filename) + Can't find a suitable configuration file. Are you in the right directory? + + Supported filenames: %s + """ % ", ".join(supported_filenames)) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 98a8f9ee..57e2f327 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -2,12 +2,15 @@ from __future__ import unicode_literals from __future__ import absolute_import import logging import os +import tempfile +import shutil from .. import unittest import mock from compose.cli import main from compose.cli.main import TopLevelCommand +from compose.cli.errors import ComposeFileNotFound from six import StringIO @@ -57,12 +60,30 @@ class CLITestCase(unittest.TestCase): project_name = command.get_project_name(None) self.assertEquals(project_name, name) - def test_yaml_filename_check(self): - command = TopLevelCommand() - command.base_dir = 'tests/fixtures/longer-filename-composefile' - with mock.patch('compose.cli.command.log', autospec=True) as mock_log: - self.assertTrue(command.get_config_path()) - self.assertEqual(mock_log.warning.call_count, 2) + def test_filename_check(self): + self.assertEqual('docker-compose.yml', get_config_filename_for_files([ + 'docker-compose.yml', + 'docker-compose.yaml', + 'fig.yml', + 'fig.yaml', + ])) + + self.assertEqual('docker-compose.yaml', get_config_filename_for_files([ + 'docker-compose.yaml', + 'fig.yml', + 'fig.yaml', + ])) + + self.assertEqual('fig.yml', get_config_filename_for_files([ + 'fig.yml', + 'fig.yaml', + ])) + + self.assertEqual('fig.yaml', get_config_filename_for_files([ + 'fig.yaml', + ])) + + self.assertRaises(ComposeFileNotFound, lambda: get_config_filename_for_files([])) def test_get_project(self): command = TopLevelCommand() @@ -81,3 +102,21 @@ class CLITestCase(unittest.TestCase): main.setup_logging() self.assertEqual(logging.getLogger().level, logging.DEBUG) self.assertEqual(logging.getLogger('requests').propagate, False) + + +def get_config_filename_for_files(filenames): + project_dir = tempfile.mkdtemp() + try: + make_files(project_dir, filenames) + command = TopLevelCommand() + command.base_dir = project_dir + return os.path.basename(command.get_config_path()) + finally: + shutil.rmtree(project_dir) + + +def make_files(dirname, filenames): + for fname in filenames: + with open(os.path.join(dirname, fname), 'w') as f: + f.write('') + From deb2de3c078d675555b071d735368fbe0a74e632 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 29 Jan 2015 13:12:51 -0500 Subject: [PATCH 0730/2154] Ship 1.1.0-rc2 Signed-off-by: Aanand Prasad --- CHANGES.md | 13 +++++++++++++ compose/__init__.py | 2 +- docs/completion.md | 2 +- docs/install.md | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c8199cbd..e361ef14 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ Change log ========== +1.1.0-rc2 (2015-01-29) +---------------------- + +On top of the changelog for 1.1.0-rc1 (see below), the following bugs have been fixed: + +- When an environment variables file specified with `env_file` doesn't exist, Compose was showing a stack trace instead of a helpful error. + +- Configuration files using the old name (`fig.yml`) were not being read unless explicitly specified with `docker-compose -f`. Compose now reads them and prints a deprecation warning. + +- Bash tab completion now reads `fig.yml` if it's present. + 1.1.0-rc1 (2015-01-20) ---------------------- @@ -28,6 +39,8 @@ Besides that, there’s a lot of new stuff in this release: - docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop` and `--restart` options. +- Compose now ships with Bash tab completion - see the installation and usage docs at https://github.com/docker/fig/blob/1.1.0-rc1/docs/completion.md + - A number of bugs have been fixed - see the milestone for details: https://github.com/docker/fig/issues?q=milestone%3A1.1.0+ 1.0.1 (2014-11-04) diff --git a/compose/__init__.py b/compose/__init__.py index 4ed232c6..0f4cc72a 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service # noqa:flake8 -__version__ = '1.1.0-rc1' +__version__ = '1.1.0-rc2' diff --git a/docs/completion.md b/docs/completion.md index 2710a635..351e5764 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -17,7 +17,7 @@ On a Mac, install with `brew install bash-completion` Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. - curl -L https://raw.githubusercontent.com/docker/docker-compose/master/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose + curl -L https://raw.githubusercontent.com/docker/fig/1.1.0-rc2/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose Completion will be available upon next login. diff --git a/docs/install.md b/docs/install.md index 4c35e12a..b8d26d38 100644 --- a/docs/install.md +++ b/docs/install.md @@ -18,7 +18,7 @@ There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntuli Next, install Compose: - curl -L https://github.com/docker/docker-compose/releases/download/1.1.0-rc1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose + curl -L https://github.com/docker/fig/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose Optionally, install [command completion](completion.html) for the bash shell. From d8d0fd6dc9d8f73b83864797fae93590156381c4 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 19 Jan 2015 15:46:23 +1000 Subject: [PATCH 0731/2154] Add Docker docs.docker.com meta-data, and reflow to 80-chars to simplify github diffs Signed-off-by: Sven Dowideit --- docs/cli.md | 63 +++++++++++++++++++++++++++++++++++-------------- docs/index.md | 47 ++++++++++++++++++++++++++---------- docs/install.md | 19 ++++++++++----- docs/yml.md | 55 +++++++++++++++++++++++++++++------------- 4 files changed, 130 insertions(+), 54 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 8fd4b800..0d39cb12 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,12 +1,15 @@ --- layout: default title: Compose CLI reference +page_title: Compose CLI reference +page_description: Compose CLI reference +page_keywords: fig, composition, compose, docker --- -CLI reference -============= +# CLI reference -Most commands are run against one or more services. If the service is omitted, it will apply to all services. +Most commands are run against one or more services. If the service is omitted, +it will apply to all services. Run `docker-compose [COMMAND] --help` for full usage. @@ -34,7 +37,9 @@ Run `docker-compose [COMMAND] --help` for full usage. Build or rebuild services. -Services are built once and then tagged as `project_service`, e.g. `composetest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `docker-compose build` to rebuild it. +Services are built once and then tagged as `project_service`, e.g.,`composetest_db`. +If you change a service's `Dockerfile` or the contents of its build directory, you +can run `docker-compose build` to rebuild it. ### help @@ -42,7 +47,8 @@ Get help on a command. ### kill -Force stop running containers by sending a `SIGKILL` signal. Optionally the signal can be passed, for example: +Force stop running containers by sending a `SIGKILL` signal. Optionally the signal +can be passed, for example: $ docker-compose kill -s SIGINT @@ -77,17 +83,24 @@ For example: By default, linked services will be started, unless they are already running. -One-off commands are started in new containers with the same configuration 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 by default no ports will be created in case they collide. +One-off commands are started in new containers with the same configuration 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 by default no ports will be created in case +they collide. -Links are also created between one-off commands and the other containers for that service so you can do stuff like this: +Links are also created between one-off commands and the other containers for that +service so you can do stuff like this: $ docker-compose run db psql -h db -U docker -If you do not want linked containers to be started when running the one-off command, specify the `--no-deps` flag: +If you do not want linked containers to be started when running the one-off command, +specify the `--no-deps` flag: $ docker-compose run --no-deps web python manage.py shell -If you want the service's ports to be created and mapped to the host, specify the `--service-ports` flag: +If you want the service's ports to be created and mapped to the host, specify the +`--service-ports` flag: $ docker-compose run --service-ports web python manage.py shell ### scale @@ -105,7 +118,8 @@ Start existing containers for a service. ### stop -Stop running containers without removing them. They can be started again with `docker-compose start`. +Stop running containers without removing them. They can be started again with +`docker-compose start`. ### up @@ -113,9 +127,15 @@ Build, (re)create, start and attach to containers for a service. Linked services will be started, unless they are already running. -By default, `docker-compose up` will aggregate the output of each container, and when it exits, all containers will be stopped. If you run `docker-compose up -d`, it'll start the containers in the background and leave them running. +By default, `docker-compose up` will aggregate the output of each container, and when +it exits, all containers will be stopped. If you run `docker-compose up -d`, it'll +start the containers in the background and leave them running. -By default if there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `docker-compose.yml` are picked up. If you do no want containers to be stopped and recreated, use `docker-compose up --no-recreate`. This will still start any stopped containers, if needed. +By default if there are existing containers for a service, `docker-compose up` will +stop and recreate them (preserving mounted volumes with [volumes-from]), so that +changes in `docker-compose.yml` are picked up. If you do not want containers to be +stopped and recreated, use `docker-compose up --no-recreate`. This will still start +any stopped containers, if needed. [volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/ @@ -124,24 +144,31 @@ By default if there are existing containers for a service, `docker-compose up` w Several environment variables can be used to configure Compose's behaviour. -Variables starting with `DOCKER_` are the same as those used to configure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values. +Variables starting with `DOCKER_` are the same as those used to configure the +Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` +will set them to their correct values. ### FIG\_PROJECT\_NAME -Set the project name, which is prepended to the name of every container started by Compose. Defaults to the `basename` of the current working directory. +Set the project name, which is prepended to the name of every container started by +Compose. Defaults to the `basename` of the current working directory. ### FIG\_FILE -Set the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` in the current working directory. +Set the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` +in the current working directory. ### DOCKER\_HOST -Set the URL to the docker daemon. Defaults to `unix:///var/run/docker.sock`, as with the docker client. +Set the URL to the docker daemon. Defaults to `unix:///var/run/docker.sock`, as +with the docker client. ### DOCKER\_TLS\_VERIFY -When set to anything other than an empty string, enables TLS communication with the daemon. +When set to anything other than an empty string, enables TLS communication with +the daemon. ### DOCKER\_CERT\_PATH -Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS verification. Defaults to `~/.docker`. +Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS +verification. Defaults to `~/.docker`. diff --git a/docs/index.md b/docs/index.md index 00f48be9..7f20624e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,13 @@ --- layout: default title: Compose: Multi-container orchestration for Docker +page_title: Compose: Multi-container orchestration for Docker +page_description: Compose: Multi-container orchestration for Docker +page_keywords: fig, composition, compose, docker --- +# Fast, isolated development environments using Docker. + Define your app's environment with a `Dockerfile` so it can be reproduced anywhere: FROM python:2.7 @@ -10,7 +15,8 @@ Define your app's environment with a `Dockerfile` so it can be reproduced anywhe WORKDIR /code RUN pip install -r requirements.txt -Define the services that make up your app in `docker-compose.yml` so they can be run together in an isolated environment: +Define the services that make up your app in `docker-compose.yml` so they can be +run together in an isolated environment: ```yaml web: @@ -36,10 +42,10 @@ There are commands to: - run a one-off command on a service -Quick start ------------ +## Quick start -Let's get a basic Python web app running on Compose. It assumes a little knowledge of Python, but the concepts should be clear if you're not familiar with it. +Let's get a basic Python web app running on Compose. It assumes a little knowledge +of Python, but the concepts should be clear if you're not familiar with it. First, [install Docker and Compose](install.html). @@ -48,7 +54,8 @@ You'll want to make a directory for the project: $ mkdir composetest $ cd composetest -Inside this directory, create `app.py`, a simple web app that uses the Flask framework and increments a value in Redis: +Inside this directory, create `app.py`, a simple web app that uses the Flask +framework and increments a value in Redis: ```python from flask import Flask @@ -71,14 +78,18 @@ We define our Python dependencies in a file called `requirements.txt`: flask redis -Next, we want to create a Docker image containing all of our app's dependencies. We specify how to build one using a file called `Dockerfile`: +Next, we want to create a Docker image containing all of our app's dependencies. +We specify how to build one using a file called `Dockerfile`: FROM python:2.7 ADD . /code WORKDIR /code RUN pip install -r requirements.txt -This tells Docker to install Python, our code and our Python dependencies inside a Docker image. For more information on how to write Dockerfiles, see the [Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). +This tells Docker to install Python, our code and our Python dependencies inside +a Docker image. For more information on how to write Dockerfiles, see the +[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) +and the [Dockerfile reference](http://docs.docker.com/reference/builder/). We then define a set of services using `docker-compose.yml`: @@ -96,7 +107,11 @@ We then define a set of services using `docker-compose.yml`: This defines two services: - - `web`, which is built from `Dockerfile` in the current directory. It also says to run the command `python app.py` inside the image, forward the exposed port 5000 on the container to port 5000 on the host machine, connect up the Redis service, and mount the current directory inside the container so we can work on code without having to rebuild the image. + - `web`, which is built from `Dockerfile` in the current directory. It also says + to run the command `python app.py` inside the image, forward the exposed port + 5000 on the container to port 5000 on the host machine, connect up the Redis + service, and mount the current directory inside the container so we can work + on code without having to rebuild the image. - `redis`, which uses the public image [redis](https://registry.hub.docker.com/_/redis/). Now if we run `docker-compose up`, it'll pull a Redis image, build an image for our own code, and start everything up: @@ -109,9 +124,11 @@ Now if we run `docker-compose up`, it'll pull a Redis image, build an image for redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 web_1 | * Running on http://0.0.0.0:5000/ -The web app should now be listening on port 5000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). +The web app should now be listening on port 5000 on your docker daemon (if you're +using boot2docker, `boot2docker ip` will tell you its address). -If you want to run your services in the background, you can pass the `-d` flag to `docker-compose up` and use `docker-compose ps` to see what is currently running: +If you want to run your services in the background, you can pass the `-d` flag to +`docker-compose up` and use `docker-compose ps` to see what is currently running: $ docker-compose up -d Starting composetest_redis_1... @@ -122,15 +139,19 @@ If you want to run your services in the background, you can pass the `-d` flag t composetest_redis_1 /usr/local/bin/run Up composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp -`docker-compose run` allows you to run one-off commands for your services. For example, to see what environment variables are available to the `web` service: +`docker-compose run` allows you to run one-off commands for your services. For +example, to see what environment variables are available to the `web` service: $ docker-compose run web env See `docker-compose --help` other commands that are available. -If you started Compose with `docker-compose up -d`, you'll probably want to stop your services once you've finished with them: +If you started Compose with `docker-compose up -d`, you'll probably want to stop +your services once you've finished with them: $ docker-compose stop -That's more-or-less how Compose works. See the reference section below for full details on the commands, configuration file and environment variables. If you have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/docker-compose). +That's more-or-less how Compose works. See the reference section below for full +details on the commands, configuration file and environment variables. If you +have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/docker-compose). diff --git a/docs/install.md b/docs/install.md index b8d26d38..4509822a 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,20 +1,26 @@ --- layout: default title: Installing Compose +page_title: Installing Compose +page_description: Installing Compose +page_keywords: fig, composition, compose, docker --- -Installing Compose -============== +# Installing Compose First, install Docker version 1.3 or greater. -If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) to install both Docker and boot2docker. Once boot2docker is running, set the environment variables that'll configure Docker and Compose to talk to it: +If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) +to install both Docker and boot2docker. Once boot2docker is running, set the environment +variables that'll configure Docker and Compose to talk to it: $(boot2docker shellinit) -To persist the environment variables across shell sessions, you can add that line to your `~/.bashrc` file. +To persist the environment variables across shell sessions, you can add that line +to your `~/.bashrc` file. -There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) and [other platforms](https://docs.docker.com/installation/) in Docker’s documentation. +There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) +and [other platforms](https://docs.docker.com/installation/) in Docker`s documentation. Next, install Compose: @@ -22,7 +28,8 @@ Next, install Compose: Optionally, install [command completion](completion.html) for the bash shell. -Releases are available for OS X and 64-bit Linux. Compose is also available as a Python package if you're on another platform (or if you prefer that sort of thing): +Releases are available for OS X and 64-bit Linux. Compose is also available as a Python +package if you're on another platform (or if you prefer that sort of thing): $ sudo pip install -U docker-compose diff --git a/docs/yml.md b/docs/yml.md index e3072d79..228fe2ce 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -1,18 +1,25 @@ --- layout: default title: docker-compose.yml reference +page_title: docker-compose.yml reference +page_description: docker-compose.yml reference +page_keywords: fig, composition, compose, docker --- -docker-compose.yml reference -================= +# docker-compose.yml reference -Each service defined in `docker-compose.yml` must specify exactly one of `image` or `build`. Other keys are optional, and are analogous to their `docker run` command-line counterparts. +Each service defined in `docker-compose.yml` must specify exactly one of +`image` or `build`. Other keys are optional, and are analogous to their +`docker run` command-line counterparts. -As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `docker-compose.yml`. +As with `docker run`, options specified in the Dockerfile (e.g., `CMD`, +`EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to +specify them again in `docker-compose.yml`. -###image +### image -Tag or partial image ID. Can be local or remote - Compose will attempt to pull if it doesn't exist locally. +Tag or partial image ID. Can be local or remote - Compose will attempt to +pull if it doesn't exist locally. ``` image: ubuntu @@ -22,7 +29,8 @@ image: a4bc65fd ### build -Path to a directory containing a Dockerfile. Compose will build and tag it with a generated name, and use that image thereafter. +Path to a directory containing a Dockerfile. Compose will build and tag it +with a generated name, and use that image thereafter. ``` build: /path/to/build/dir @@ -39,7 +47,9 @@ command: bundle exec thin -p 3000 ### links -Link to containers in another service. Either specify both the service name and the link alias (`SERVICE:ALIAS`), or just the service name (which will also be used for the alias). +Link to containers in another service. Either specify both the service name and +the link alias (`SERVICE:ALIAS`), or just the service name (which will also be +used for the alias). ``` links: @@ -48,7 +58,8 @@ links: - redis ``` -An entry with the alias' name will be created in `/etc/hosts` inside containers for this service, e.g: +An entry with the alias' name will be created in `/etc/hosts` inside containers +for this service, e.g: ``` 172.17.2.186 db @@ -56,12 +67,15 @@ An entry with the alias' name will be created in `/etc/hosts` inside containers 172.17.2.187 redis ``` -Environment variables will also be created - see the [environment variable reference](env.html) for details. +Environment variables will also be created - see the [environment variable +reference](env.html) for details. ### external_links -Link to containers started outside this `docker-compose.yml` or even outside of Compose, especially for containers that provide shared or common services. -`external_links` follow semantics similar to `links` when specifying both the container name and the link alias (`CONTAINER:ALIAS`). +Link to containers started outside this `docker-compose.yml` or even outside +of Compose, especially for containers that provide shared or common services. +`external_links` follow semantics similar to `links` when specifying both the +container name and the link alias (`CONTAINER:ALIAS`). ``` external_links: @@ -72,9 +86,13 @@ external_links: ### ports -Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen). +Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container +port (a random host port will be chosen). -**Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings. +> **Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience +> erroneous results when using a container port lower than 60, because YAML will +> parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, +> we recommend always explicitly specifying your port mappings as strings. ``` ports: @@ -86,7 +104,8 @@ ports: ### expose -Expose ports without publishing them to the host machine - they'll only be accessible to linked services. Only the internal port can be specified. +Expose ports without publishing them to the host machine - they'll only be +accessible to linked services. Only the internal port can be specified. ``` expose: @@ -120,7 +139,8 @@ volumes_from: Add environment variables. You can use either an array or a dictionary. -Environment variables with only a key are resolved to their values on the machine Compose is running on, which can be helpful for secret or host-specific values. +Environment variables with only a key are resolved to their values on the +machine Compose is running on, which can be helpful for secret or host-specific values. ``` environment: @@ -196,7 +216,8 @@ dns_search: ### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares -Each of these is a single value, analogous to its [docker run](https://docs.docker.com/reference/run/) counterpart. +Each of these is a single value, analogous to its +[docker run](https://docs.docker.com/reference/run/) counterpart. ``` cpu_shares: 73 From 461f1ad5d5a4dd02e259d6fb4699dc0f5dcf76d1 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Thu, 29 Jan 2015 18:21:49 -0800 Subject: [PATCH 0732/2154] Edit and revision of overview & quick start doc Signed-off-by: Fred Lifton --- docs/index.md | 98 ++++++++++++++++++++++++++++----------------------- 1 file changed, 54 insertions(+), 44 deletions(-) diff --git a/docs/index.md b/docs/index.md index 7f20624e..cdde1da7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,22 +1,24 @@ ---- -layout: default -title: Compose: Multi-container orchestration for Docker page_title: Compose: Multi-container orchestration for Docker -page_description: Compose: Multi-container orchestration for Docker -page_keywords: fig, composition, compose, docker ---- +page_description: Introduction and Overview of Compose +page_keywords: documentation, docs, docker, compose, orchestration, containers -# Fast, isolated development environments using Docker. -Define your app's environment with a `Dockerfile` so it can be reproduced anywhere: +## Overview + +Compose is a tool that allows you to orchestrate multiple Docker containers. With Compose, you can build clusters of containers which provide the resources (services, volumes, etc.) needed to build and run a complete distributed application. + +You can use Compose to build your app with containers hosted locally, or on a remote server, including cloud-based instances. Compose can also be used to deploy code to production. + +Using Compose is basically a three-step process. + +First, you define your app's environment with a `Dockerfile` so it can be reproduced anywhere: FROM python:2.7 ADD . /code WORKDIR /code RUN pip install -r requirements.txt -Define the services that make up your app in `docker-compose.yml` so they can be -run together in an isolated environment: +Next, you define the services that make up your app in `docker-compose.yml` so they can be run together in an isolated environment: ```yaml web: @@ -32,24 +34,25 @@ db: (No more installing Postgres on your laptop!) -Then type `docker-compose up`, and Compose will start and run your entire app. +Lastly, run `docker-compose up` and Compose will start and run your entire app. -There are commands to: +Compose includes commands to: - - start, stop and rebuild services - - view the status of running services - - tail running services' log output - - run a one-off command on a service + * Start, stop and rebuild services + * View the status of running services + * tail the log output of running services + * run a one-off command on a service ## Quick start -Let's get a basic Python web app running on Compose. It assumes a little knowledge -of Python, but the concepts should be clear if you're not familiar with it. +Let's get started with a walkthrough of getting a simple Python web app running on Compose. It assumes a little knowledge of Python, but the concepts demonstrated here should be understandable even if you're not familiar with Python. + +### Installation and set-up First, [install Docker and Compose](install.html). -You'll want to make a directory for the project: +Next, you'll want to make a directory for the project: $ mkdir composetest $ cd composetest @@ -73,25 +76,29 @@ if __name__ == "__main__": app.run(host="0.0.0.0", debug=True) ``` -We define our Python dependencies in a file called `requirements.txt`: +Next, define the Python dependencies in a file called `requirements.txt`: flask redis -Next, we want to create a Docker image containing all of our app's dependencies. -We specify how to build one using a file called `Dockerfile`: +### Create a Docker image + +Now, create a Docker image containing all of your app's dependencies. You +specify how to build the image using a file called [`Dockerfile`](http://docs.docker.com/reference/builder/): FROM python:2.7 ADD . /code WORKDIR /code RUN pip install -r requirements.txt -This tells Docker to install Python, our code and our Python dependencies inside +This tells Docker to include Python, your code, and your Python dependencies in a Docker image. For more information on how to write Dockerfiles, see the -[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) -and the [Dockerfile reference](http://docs.docker.com/reference/builder/). +[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the +[Dockerfile reference](http://docs.docker.com/reference/builder/). -We then define a set of services using `docker-compose.yml`: +### Define services + +Next, define a set of services using `docker-compose.yml`: web: build: . @@ -107,14 +114,18 @@ We then define a set of services using `docker-compose.yml`: This defines two services: - - `web`, which is built from `Dockerfile` in the current directory. It also says - to run the command `python app.py` inside the image, forward the exposed port - 5000 on the container to port 5000 on the host machine, connect up the Redis - service, and mount the current directory inside the container so we can work - on code without having to rebuild the image. - - `redis`, which uses the public image [redis](https://registry.hub.docker.com/_/redis/). + - `web`, which is built from the `Dockerfile` in the current directory. It also + says to run the command `python app.py` inside the image, forward the exposed +port 5000 on the container to port 5000 on the host machine, connect up the +Redis service, and mount the current directory inside the container so we can +work on code without having to rebuild the image. + - `redis`, which uses the public image [redis](https://registry.hub.docker.com/_/redis/), which gets pulled from the + Docker Hub registry. -Now if we run `docker-compose up`, it'll pull a Redis image, build an image for our own code, and start everything up: +### Build and run your app with Compose + +Now, when you run `docker-compose up`, Compose will pull a Redis image, build an +image for your code, and start everything up: $ docker-compose up Pulling image redis... @@ -124,11 +135,12 @@ Now if we run `docker-compose up`, it'll pull a Redis image, build an image for redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 web_1 | * Running on http://0.0.0.0:5000/ -The web app should now be listening on port 5000 on your docker daemon (if you're -using boot2docker, `boot2docker ip` will tell you its address). +The web app should now be listening on port 5000 on your docker daemon (if +you're using boot2docker, `boot2docker ip` will tell you its address). -If you want to run your services in the background, you can pass the `-d` flag to -`docker-compose up` and use `docker-compose ps` to see what is currently running: +If you want to run your services in the background, you can pass the `-d` flag +(for daemon mode) to `docker-compose up` and use `docker-compose ps` to see what +is currently running: $ docker-compose up -d Starting composetest_redis_1... @@ -139,19 +151,17 @@ If you want to run your services in the background, you can pass the `-d` flag t composetest_redis_1 /usr/local/bin/run Up composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp -`docker-compose run` allows you to run one-off commands for your services. For -example, to see what environment variables are available to the `web` service: +The `docker-compose run` command allows you to run one-off commands for your +services. For example, to see what environment variables are available to the +`web` service: $ docker-compose run web env - -See `docker-compose --help` other commands that are available. +See `docker-compose --help` to see other available commands. If you started Compose with `docker-compose up -d`, you'll probably want to stop your services once you've finished with them: $ docker-compose stop -That's more-or-less how Compose works. See the reference section below for full -details on the commands, configuration file and environment variables. If you -have any thoughts or suggestions, [open an issue on GitHub](https://github.com/docker/docker-compose). +At this point, you have seen the basics of how Compose works. See the reference section for complete details on the commands, configuration file and environment variables. From 3b7ea5c0555daaae725dd7d5403661d82459cfed Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Fri, 30 Jan 2015 21:27:57 +1000 Subject: [PATCH 0733/2154] resolve most of my comments Signed-off-by: Sven Dowideit --- docs/index.md | 57 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/docs/index.md b/docs/index.md index cdde1da7..ffe4d5b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,24 +1,32 @@ page_title: Compose: Multi-container orchestration for Docker page_description: Introduction and Overview of Compose -page_keywords: documentation, docs, docker, compose, orchestration, containers +page_keywords: documentation, docs, docker, compose, orchestration, containers ## Overview -Compose is a tool that allows you to orchestrate multiple Docker containers. With Compose, you can build clusters of containers which provide the resources (services, volumes, etc.) needed to build and run a complete distributed application. +Compose is a tool that allows you to orchestrate multiple Docker containers. +With Compose, you can build clusters of containers which provide the resources +(services, volumes, etc.) needed to build and run a complete distributed +application. -You can use Compose to build your app with containers hosted locally, or on a remote server, including cloud-based instances. Compose can also be used to deploy code to production. +You can use Compose to build your app with containers hosted locally, or on a +remote server, including cloud-based instances. Compose can also be used to +deploy code to production. Using Compose is basically a three-step process. - -First, you define your app's environment with a `Dockerfile` so it can be reproduced anywhere: + +First, you define your app's environment with a `Dockerfile` so it can be +reproduced anywhere: FROM python:2.7 - ADD . /code WORKDIR /code + ADD rements.txt /code/ RUN pip install -r requirements.txt + ADD . /code -Next, you define the services that make up your app in `docker-compose.yml` so they can be run together in an isolated environment: +Next, you define the services that make up your app in `docker-compose.yml` so +they can be run together in an isolated environment: ```yaml web: @@ -46,7 +54,10 @@ Compose includes commands to: ## Quick start -Let's get started with a walkthrough of getting a simple Python web app running on Compose. It assumes a little knowledge of Python, but the concepts demonstrated here should be understandable even if you're not familiar with Python. +Let's get started with a walkthrough of getting a simple Python web app running +on Compose. It assumes a little knowledge of Python, but the concepts +demonstrated here should be understandable even if you're not familiar with +Python. ### Installation and set-up @@ -84,7 +95,8 @@ Next, define the Python dependencies in a file called `requirements.txt`: ### Create a Docker image Now, create a Docker image containing all of your app's dependencies. You -specify how to build the image using a file called [`Dockerfile`](http://docs.docker.com/reference/builder/): +specify how to build the image using a file called +[`Dockerfile`](http://docs.docker.com/reference/builder/): FROM python:2.7 ADD . /code @@ -93,7 +105,9 @@ specify how to build the image using a file called [`Dockerfile`](http://docs.do This tells Docker to include Python, your code, and your Python dependencies in a Docker image. For more information on how to write Dockerfiles, see the -[Docker user guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the +[Docker user +guide](https://docs.docker.com/userguide/dockerimages/#building-an-image-from-a-dockerfile) +and the [Dockerfile reference](http://docs.docker.com/reference/builder/). ### Define services @@ -115,12 +129,13 @@ Next, define a set of services using `docker-compose.yml`: This defines two services: - `web`, which is built from the `Dockerfile` in the current directory. It also - says to run the command `python app.py` inside the image, forward the exposed -port 5000 on the container to port 5000 on the host machine, connect up the -Redis service, and mount the current directory inside the container so we can -work on code without having to rebuild the image. - - `redis`, which uses the public image [redis](https://registry.hub.docker.com/_/redis/), which gets pulled from the - Docker Hub registry. + says to run the command `python app.py` inside the image, forward the exposed + port 5000 on the container to port 5000 on the host machine, connect up the + Redis service, and mount the current directory inside the container so we can + work on code without having to rebuild the image. + - `redis`, which uses the public image + [redis](https://registry.hub.docker.com/_/redis/), which gets pulled from the + Docker Hub registry. ### Build and run your app with Compose @@ -135,8 +150,8 @@ image for your code, and start everything up: redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 web_1 | * Running on http://0.0.0.0:5000/ -The web app should now be listening on port 5000 on your docker daemon (if -you're using boot2docker, `boot2docker ip` will tell you its address). +The web app should now be listening on port 5000 on your Docker daemon host (if +you're using Boot2docker, `boot2docker ip` will tell you its address). If you want to run your services in the background, you can pass the `-d` flag (for daemon mode) to `docker-compose up` and use `docker-compose ps` to see what @@ -146,7 +161,7 @@ is currently running: Starting composetest_redis_1... Starting composetest_web_1... $ docker-compose ps - Name Command State Ports + Name Command State Ports ------------------------------------------------------------------- composetest_redis_1 /usr/local/bin/run Up composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp @@ -164,4 +179,6 @@ your services once you've finished with them: $ docker-compose stop -At this point, you have seen the basics of how Compose works. See the reference section for complete details on the commands, configuration file and environment variables. +At this point, you have seen the basics of how Compose works. See the reference +section for complete details on the commands, configuration file and environment +variables. From 75247e5a54736debaa72ca115ec2fffecb376da9 Mon Sep 17 00:00:00 2001 From: Ashley Penney Date: Sat, 31 Jan 2015 20:00:04 -0500 Subject: [PATCH 0734/2154] Fix a small typo. --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index ffe4d5b1..b2950320 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,7 +21,7 @@ reproduced anywhere: FROM python:2.7 WORKDIR /code - ADD rements.txt /code/ + ADD requirements.txt /code/ RUN pip install -r requirements.txt ADD . /code From 45b8d526ba94972c1d9b1f2256c575afe5b1c107 Mon Sep 17 00:00:00 2001 From: Christophe Labouisse Date: Sun, 1 Feb 2015 16:59:21 +0100 Subject: [PATCH 0735/2154] Add missing cpu_shares option for rel. 1.1.0-rc1 Signed-off-by: Christophe Labouisse --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index e361ef14..83eb94eb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -37,7 +37,7 @@ Besides that, there’s a lot of new stuff in this release: - docker-compose.yml now has an `env_file` key, analogous to `docker run --env-file`, letting you specify multiple environment variables in a separate file. This is great if you have a lot of them, or if you want to keep sensitive information out of version control. -- docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop` and `--restart` options. +- docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop`, `cpu_shares` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop`, `--cpu-shares` and `--restart` options. - Compose now ships with Bash tab completion - see the installation and usage docs at https://github.com/docker/fig/blob/1.1.0-rc1/docs/completion.md From b25ed59b1c5a119285771137b7d29fa507caf721 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 5 Feb 2015 16:16:11 -0500 Subject: [PATCH 0736/2154] Tweak intro We shouldn't yet be recommending production use - for now, let's continue to emphasise the development use case and mention staging/CI. Signed-off-by: Aanand Prasad --- docs/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index b2950320..67a93970 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,8 +11,9 @@ With Compose, you can build clusters of containers which provide the resources application. You can use Compose to build your app with containers hosted locally, or on a -remote server, including cloud-based instances. Compose can also be used to -deploy code to production. +remote server, including cloud-based instances - anywhere a Docker daemon can +run. Its primary use case is for development environments, but it can be used +just as easily for staging or CI. Using Compose is basically a three-step process. From 7ff607fb7adc28157db7d215a7f73bfe0f36811a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 5 Feb 2015 17:36:57 -0500 Subject: [PATCH 0737/2154] Add checklist item for completion script URL to CONTRIBUTING.md Signed-off-by: Aanand Prasad --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9f7e564..b03d1d40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,8 +84,9 @@ Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyIn 1. Open pull request that: - - Updates version in `compose/__init__.py` - - Updates version in `docs/install.md` + - Updates the version in `compose/__init__.py` + - Updates the binary URL in `docs/install.md` + - Updates the script URL in `docs/completion.md` - Adds release notes to `CHANGES.md` 2. Create unpublished GitHub release with release notes From 9f4775c55487b2b22bf415de57ff73d54a243c65 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Fri, 30 Jan 2015 17:16:48 -0800 Subject: [PATCH 0738/2154] Revision and edit of Compose install doc Fix rc version to latest. Signed-off-by: Fred Lifton --- docs/install.md | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/docs/install.md b/docs/install.md index 4509822a..4cc6dae7 100644 --- a/docs/install.md +++ b/docs/install.md @@ -1,36 +1,44 @@ ---- -layout: default -title: Installing Compose page_title: Installing Compose -page_description: Installing Compose -page_keywords: fig, composition, compose, docker ---- +page_description: How to intall Docker Compose +page_keywords: compose, orchestration, install, installation, docker, documentation -# Installing Compose -First, install Docker version 1.3 or greater. +## Installing Compose -If you're on OS X, you can use the [OS X installer](https://docs.docker.com/installation/mac/) -to install both Docker and boot2docker. Once boot2docker is running, set the environment -variables that'll configure Docker and Compose to talk to it: +To install Compose, you'll need to install Docker first. You'll then install +Compose with a `curl` command. + +### Install Docker + +First, you'll need to install Docker version 1.3 or greater. + +If you're on OS X, you can use the +[OS X installer](https://docs.docker.com/installation/mac/) to install both +Docker and the OSX helper app, boot2docker. Once boot2docker is running, set the +environment variables that'll configure Docker and Compose to talk to it: $(boot2docker shellinit) -To persist the environment variables across shell sessions, you can add that line +To persist the environment variables across shell sessions, add the above line to your `~/.bashrc` file. -There are also guides for [Ubuntu](https://docs.docker.com/installation/ubuntulinux/) -and [other platforms](https://docs.docker.com/installation/) in Docker`s documentation. +For complete instructions, or if you are on another platform, consult Docker's +[installation instructions](https://docs.docker.com/installation/). -Next, install Compose: +### Install Compose - curl -L https://github.com/docker/fig/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose; chmod +x /usr/local/bin/docker-compose +To install Compose, run the following commands: -Optionally, install [command completion](completion.html) for the bash shell. + curl -L https://github.com/docker/docker-compose/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose -Releases are available for OS X and 64-bit Linux. Compose is also available as a Python -package if you're on another platform (or if you prefer that sort of thing): +Optionally, you can also install [command completion](completion.html) for the +bash shell. + +Compose is available for OS X and 64-bit Linux. If you're on another platform, +Compose can also be installed as a Python package: $ sudo pip install -U docker-compose -That should be all you need! Run `docker-compose --version` to see if it worked. +No further steps are required; Compose should now be successfully installed. +You can test the installation by running `docker-compose --version`. From c39a0b0a2d16d557ed4205fd2b01f1c1bb5e5b6a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 11 Feb 2015 19:00:13 +0000 Subject: [PATCH 0739/2154] Update pep8, fix errors and freeze requirements-dev.txt Signed-off-by: Aanand Prasad --- compose/cli/log_printer.py | 5 ++++- compose/project.py | 3 ++- requirements-dev.txt | 7 ++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index 9c37ccce..77fa49ae 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -39,9 +39,12 @@ class LogPrinter(object): color_fns = cycle(colors.rainbow()) generators = [] + def no_color(text): + return text + for container in self.containers: if monochrome: - color_fn = lambda s: s + color_fn = no_color else: color_fn = color_fns.next() generators.append(self._make_log_generator(container, color_fn)) diff --git a/compose/project.py b/compose/project.py index 028c6a96..60160447 100644 --- a/compose/project.py +++ b/compose/project.py @@ -15,7 +15,8 @@ def sort_service_dicts(services): temporary_marked = set() sorted_services = [] - get_service_names = lambda links: [link.split(':')[0] for link in links] + def get_service_names(links): + return [link.split(':')[0] for link in links] def visit(n): if n['name'] in temporary_marked: diff --git a/requirements-dev.txt b/requirements-dev.txt index bd5a3949..7b529623 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ mock >= 1.0.1 -nose +nose==1.3.4 git+https://github.com/pyinstaller/pyinstaller.git@12e40471c77f588ea5be352f7219c873ddaae056#egg=pyinstaller -unittest2 -flake8 +unittest2==0.8.0 +flake8==2.3.0 +pep8==1.6.1 From 4a336867875b6bcbbd00e0b75ca54ccea1924739 Mon Sep 17 00:00:00 2001 From: Fred Lifton Date: Wed, 11 Feb 2015 18:22:36 -0800 Subject: [PATCH 0740/2154] Revises Compose cli reference --- docs/cli.md | 148 +++++++++++++++++++++++++++------------------------- 1 file changed, 77 insertions(+), 71 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 0d39cb12..39674810 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,68 +1,47 @@ ---- -layout: default -title: Compose CLI reference page_title: Compose CLI reference page_description: Compose CLI reference -page_keywords: fig, composition, compose, docker ---- +page_keywords: fig, composition, compose, docker, orchestration, cli, reference + # CLI reference -Most commands are run against one or more services. If the service is omitted, -it will apply to all services. +Most commands are run against one or more services. If the service is not +specified, the command will apply to all services. -Run `docker-compose [COMMAND] --help` for full usage. - -## Options - -### --verbose - - Show more output - -### --version - - Print version and exit - -### -f, --file FILE - - Specify an alternate compose file (default: docker-compose.yml) - -### -p, --project-name NAME - - Specify an alternate project name (default: directory name) +For full usage information, run `docker-compose [COMMAND] --help`. ## Commands ### build -Build or rebuild services. +Builds or rebuilds services. -Services are built once and then tagged as `project_service`, e.g.,`composetest_db`. -If you change a service's `Dockerfile` or the contents of its build directory, you -can run `docker-compose build` to rebuild it. +Services are built once and then tagged as `project_service`, e.g., +`composetest_db`. If you change a service's Dockerfile or the contents of its +build directory, run `docker-compose build` to rebuild it. ### help -Get help on a command. +Displays help and usage instructions for a command. ### kill -Force stop running containers by sending a `SIGKILL` signal. Optionally the signal -can be passed, for example: +Forces running containers to stop by sending a `SIGKILL` signal. Optionally the +signal can be passed, for example: $ docker-compose kill -s SIGINT ### logs -View output from services. +Displays log output from services. ### port -Print the public port for a port binding +Prints the public port for a port binding ### ps -List containers. +Lists containers. ### pull @@ -70,79 +49,103 @@ Pulls service images. ### rm -Remove stopped service containers. +Removes stopped service containers. ### run -Run a one-off command on a service. +Runs a one-off command on a service. -For example: +For example, $ docker-compose run web python manage.py shell -By default, linked services will be started, unless they are already running. +will start the `web` service and then run `manage.py shell` in python. +Note that by default, linked services will also be started, unless they are +already running. One-off commands are started in new containers with the same configuration 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 by default no ports will be created in case -they collide. +expected. When using `run`, there are two differences from bringing up a +container normally: -Links are also created between one-off commands and the other containers for that -service so you can do stuff like this: +1. the command will be overridden with the one specified. So, if you run +`docker-compose run web bash`, the container's web command (which could default +to, e.g., `python app.py`) will be overridden to `bash` + +2. by default no ports will be created in case they collide with already opened +ports. + +Links are also created between one-off commands and the other containers which +are part of that service. So, for example, you could run: $ docker-compose run db psql -h db -U docker -If you do not want linked containers to be started when running the one-off command, +This would open up an interactive PostgreSQL shell for the linked `db` container +(which would get created or started as needed). + +If you do not want linked containers to start when running the one-off command, specify the `--no-deps` flag: $ docker-compose run --no-deps web python manage.py shell -If you want the service's ports to be created and mapped to the host, specify the -`--service-ports` flag: +Similarly, if you do want the service's ports to be created and mapped to the +host, specify the `--service-ports` flag: $ docker-compose run --service-ports web python manage.py shell ### scale -Set number of containers to run for a service. +Sets the number of containers to run for a service. -Numbers are specified in the form `service=num` as arguments. -For example: +Numbers are specified as arguments in the form `service=num`. For example: $ docker-compose scale web=2 worker=3 ### start -Start existing containers for a service. +Starts existing containers for a service. ### stop -Stop running containers without removing them. They can be started again with +Stops running containers without removing them. They can be started again with `docker-compose start`. ### up -Build, (re)create, start and attach to containers for a service. +Builds, (re)creates, starts, and attaches to containers for a service. Linked services will be started, unless they are already running. -By default, `docker-compose up` will aggregate the output of each container, and when -it exits, all containers will be stopped. If you run `docker-compose up -d`, it'll -start the containers in the background and leave them running. +By default, `docker-compose up` will aggregate the output of each container and, +when it exits, all containers will be stopped. Running `docker-compose up -d`, +will start the containers in the background and leave them running. -By default if there are existing containers for a service, `docker-compose up` will -stop and recreate them (preserving mounted volumes with [volumes-from]), so that -changes in `docker-compose.yml` are picked up. If you do not want containers to be -stopped and recreated, use `docker-compose up --no-recreate`. This will still start -any stopped containers, if needed. +By default, if there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `docker-compose.yml` are picked up. If you do not want containers stopped and recreated, use `docker-compose up --no-recreate`. This will still start any stopped containers, if needed. [volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/ +## Options + +### --verbose + + Shows more output + +### --version + + Prints version and exits + +### -f, --file FILE + + Specifies an alternate Compose yaml file (default: `docker-compose.yml`) + +### -p, --project-name NAME + + Specifies an alternate project name (default: current directory name) + ## Environment Variables -Several environment variables can be used to configure Compose's behaviour. +Several environment variables are available for you to configure Compose's behaviour. Variables starting with `DOCKER_` are the same as those used to configure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` @@ -150,18 +153,15 @@ will set them to their correct values. ### FIG\_PROJECT\_NAME -Set the project name, which is prepended to the name of every container started by -Compose. Defaults to the `basename` of the current working directory. +Sets the project name, which is prepended to the name of every container started by Compose. Defaults to the `basename` of the current working directory. ### FIG\_FILE -Set the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` -in the current working directory. +Sets the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` in the current working directory. ### DOCKER\_HOST -Set the URL to the docker daemon. Defaults to `unix:///var/run/docker.sock`, as -with the docker client. +Sets the URL of the docker daemon. As with the Docker client, defaults to `unix:///var/run/docker.sock`. ### DOCKER\_TLS\_VERIFY @@ -170,5 +170,11 @@ the daemon. ### DOCKER\_CERT\_PATH -Configure the path to the `ca.pem`, `cert.pem` and `key.pem` files used for TLS -verification. Defaults to `~/.docker`. +Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`. + +## Compose Docs + +[Installing Compose](http://docs.docker.com/compose/install) +[Intro & Overview]((http://docs.docker.com/compose/userguide) +[Yaml file reference]((http://docs.docker.com/compose/yml) + From 3146fe5e4b957efc4faf76ace74c2305424c4798 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 12 Feb 2015 13:38:21 +0000 Subject: [PATCH 0741/2154] Tidy up contributing guidelines - Add intro saying we roughly follow the Docker project's guidelines - Link to Docker's signing off docs - Fix formatting at bottom Signed-off-by: Ben Firshman --- CONTRIBUTING.md | 52 +++++-------------------------------------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a9f7e564..b1ead2a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,12 +1,14 @@ # Contributing to Compose +Compose is a part of the Docker project, and follows the same rules and principles. Take a read of [Docker's contributing guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md) to get an overview. + ## TL;DR Pull requests will need: - Tests - Documentation - - [To be signed off](#sign-your-work) + - [To be signed off](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work) - A logical series of [well written commits](https://github.com/alphagov/styleguides/blob/master/git.md) ## Development environment @@ -24,50 +26,6 @@ that should get you started. $ script/test -## Sign your work - -The sign-off is a simple line at the end of the explanation for the -patch, which certifies that you wrote it or otherwise have the right to -pass it on as an open-source patch. The rules are pretty simple: if you -can certify the below (from [developercertificate.org](http://developercertificate.org/)): - - Developer's Certificate of Origin 1.1 - - By making a contribution to this project, I certify that: - - (a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - - (b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - - (c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - - (d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. - -then you just add a line saying - - Signed-off-by: Random J Developer - -using your real name (sorry, no pseudonyms or anonymous contributions.) - -The easiest way to do this is to use the `--signoff` flag when committing. E.g.: - - - $ git commit --signoff - ## Building binaries Linux: @@ -100,5 +58,5 @@ Note that this only works on Mountain Lion, not Mavericks, due to a [bug in PyIn 7. Upload PyPi package - $ git checkout $VERSION - $ python setup.py sdist upload + $ git checkout $VERSION + $ python setup.py sdist upload From 0a8f9abfae419506e5a0f14721cd975d68eaea73 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 12 Feb 2015 16:39:59 +0000 Subject: [PATCH 0742/2154] Remove @nathanleclaire as a maintainer Thanks for your help <3 Signed-off-by: Ben Firshman --- MAINTAINERS | 1 - 1 file changed, 1 deletion(-) diff --git a/MAINTAINERS b/MAINTAINERS index 8c98b04c..0fd7f81c 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2,4 +2,3 @@ Aanand Prasad (@aanand) Ben Firshman (@bfirsh) Chris Corbyn (@d11wtq) Daniel Nephin (@dnephin) -Nathan LeClaire (@nathanleclaire) From 3a8a25b9b35002368615084a9a1cbc6a6fd0e7a6 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Fri, 13 Feb 2015 10:25:18 +0100 Subject: [PATCH 0743/2154] Favour yml and yaml extensions in bash completion for -f Signed-off-by: Harald Albers --- contrib/completion/bash/docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 03721f50..a0007fcb 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -94,7 +94,7 @@ _docker-compose_build() { _docker-compose_docker-compose() { case "$prev" in --file|-f) - _filedir + _filedir y?(a)ml return ;; --project-name|-p) From 5e8bcd2d29a41322570dc9cb17101b6c07460ed6 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 16 Feb 2015 19:21:17 +0000 Subject: [PATCH 0744/2154] Rejig introduction Signed-off-by: Aanand Prasad --- README.md | 44 +++++++++++++++++++++++++++----------------- docs/index.md | 36 +++++++++++++++++------------------- 2 files changed, 44 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 6efe779b..4df3bd21 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,30 @@ Docker Compose [![wercker status](https://app.wercker.com/status/d5dbac3907301c3d5ce735e2d5e95a5b/s/master "wercker status")](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b) -Fast, isolated development environments using Docker. +Compose is a tool for defining and running complex applications with Docker. +With Compose, you define a multi-container application in a single file, then +spin your application up in a single command which does everything that needs to +be done to get it running. -Define your app's environment with Docker so it can be reproduced anywhere: +Compose is great for development environments, staging servers, and CI. We don't +recommend that you use it in production yet. - FROM python:2.7 - ADD . /code - WORKDIR /code - RUN pip install -r requirements.txt - CMD python app.py +Using Compose is basically a three-step process. -Define the services that make up your app so they can be run together in an isolated environment: +First, you define your app's environment with a `Dockerfile` so it can be +reproduced anywhere: + +```Dockerfile +FROM python:2.7 +WORKDIR /code +ADD requirements.txt /code/ +RUN pip install -r requirements.txt +ADD . /code +CMD python app.py +``` + +Next, you define the services that make up your app in `docker-compose.yml` so +they can be run together in an isolated environment: ```yaml web: @@ -22,21 +35,18 @@ web: - db ports: - "8000:8000" - - "49100:22" db: image: postgres ``` -(No more installing Postgres on your laptop!) +Lastly, run `docker-compose up` and Compose will start and run your entire app. -Then type `docker-compose up`, and Compose will start and run your entire app. +Compose has commands for managing the whole lifecycle of your application: -There are commands to: - - - start, stop and rebuild services - - view the status of running services - - tail running services' log output - - run a one-off command on a service + * Start, stop and rebuild services + * View the status of running services + * Stream the log output of running services + * Run a one-off command on a service Installation and documentation ------------------------------ diff --git a/docs/index.md b/docs/index.md index 67a93970..1800ab16 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,26 +5,27 @@ page_keywords: documentation, docs, docker, compose, orchestration, containers ## Overview -Compose is a tool that allows you to orchestrate multiple Docker containers. -With Compose, you can build clusters of containers which provide the resources -(services, volumes, etc.) needed to build and run a complete distributed -application. +Compose is a tool for defining and running complex applications with Docker. +With Compose, you define a multi-container application in a single file, then +spin your application up in a single command which does everything that needs to +be done to get it running. -You can use Compose to build your app with containers hosted locally, or on a -remote server, including cloud-based instances - anywhere a Docker daemon can -run. Its primary use case is for development environments, but it can be used -just as easily for staging or CI. +Compose is great for development environments, staging servers, and CI. We don't +recommend that you use it in production yet. Using Compose is basically a three-step process. First, you define your app's environment with a `Dockerfile` so it can be reproduced anywhere: - FROM python:2.7 - WORKDIR /code - ADD requirements.txt /code/ - RUN pip install -r requirements.txt - ADD . /code +```Dockerfile +FROM python:2.7 +WORKDIR /code +ADD requirements.txt /code/ +RUN pip install -r requirements.txt +ADD . /code +CMD python app.py +``` Next, you define the services that make up your app in `docker-compose.yml` so they can be run together in an isolated environment: @@ -32,7 +33,6 @@ they can be run together in an isolated environment: ```yaml web: build: . - command: python app.py links: - db ports: @@ -41,16 +41,14 @@ db: image: postgres ``` -(No more installing Postgres on your laptop!) - Lastly, run `docker-compose up` and Compose will start and run your entire app. -Compose includes commands to: +Compose has commands for managing the whole lifecycle of your application: * Start, stop and rebuild services * View the status of running services - * tail the log output of running services - * run a one-off command on a service + * Stream the log output of running services + * Run a one-off command on a service ## Quick start From a516d61b494f1d4655a066c55e3b1b90f73f8b90 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 18 Feb 2015 11:39:57 +0000 Subject: [PATCH 0745/2154] Update environment variable names in docs Signed-off-by: Aanand Prasad --- docs/cli.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 0d39cb12..cb428418 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -148,12 +148,12 @@ Variables starting with `DOCKER_` are the same as those used to configure the Docker command-line client. If you're using boot2docker, `$(boot2docker shellinit)` will set them to their correct values. -### FIG\_PROJECT\_NAME +### COMPOSE\_PROJECT\_NAME Set the project name, which is prepended to the name of every container started by Compose. Defaults to the `basename` of the current working directory. -### FIG\_FILE +### COMPOSE\_FILE Set the path to the `docker-compose.yml` to use. Defaults to `docker-compose.yml` in the current working directory. From d32994c250e241a9e22f133d945b22dc3995d28b Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Thu, 19 Feb 2015 14:34:08 +0000 Subject: [PATCH 0746/2154] Add title to docs index Signed-off-by: Ben Firshman --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 1800ab16..fb0e95ab 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,7 +3,7 @@ page_description: Introduction and Overview of Compose page_keywords: documentation, docs, docker, compose, orchestration, containers -## Overview +# Docker Compose Compose is a tool for defining and running complex applications with Docker. With Compose, you define a multi-container application in a single file, then From f78dfa7958e0b436cca04d1a7d0a2e86c2134d6d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 19 Feb 2015 17:42:33 +0000 Subject: [PATCH 0747/2154] Credit contributors in CHANGES.md Signed-off-by: Aanand Prasad --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 83eb94eb..6aba8f39 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,8 @@ On top of the changelog for 1.1.0-rc1 (see below), the following bugs have been - Bash tab completion now reads `fig.yml` if it's present. +Thanks, @dnephin and @albers! + 1.1.0-rc1 (2015-01-20) ---------------------- @@ -43,6 +45,8 @@ Besides that, there’s a lot of new stuff in this release: - A number of bugs have been fixed - see the milestone for details: https://github.com/docker/fig/issues?q=milestone%3A1.1.0+ +Thanks @dnephin, @squebe, @jbalonso, @raulcd, @benlangfield, @albers, @ggtools, @bersace, @dtenenba, @petercv, @drewkett, @TFenby, @paulRbr, @Aigeruth and @salehe! + 1.0.1 (2014-11-04) ------------------ From 0fa013137260a50044dfcb06d755a60baf3af721 Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 5 Nov 2014 12:07:25 +0000 Subject: [PATCH 0748/2154] Add script which runs Fig inside Docker Signed-off-by: Ben Firshman --- .dockerignore | 1 + script/dev | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100755 script/dev diff --git a/.dockerignore b/.dockerignore index 6b8710a7..f1b636b3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ .git +venv diff --git a/script/dev b/script/dev new file mode 100755 index 00000000..80b3d013 --- /dev/null +++ b/script/dev @@ -0,0 +1,21 @@ +#!/bin/bash +# This is a script for running Compose inside a Docker container. It's handy for +# development. +# +# $ ln -s `pwd`/script/dev /usr/local/bin/docker-compose +# $ cd /a/compose/project +# $ docker-compose up +# + +set -e + +# Follow symbolic links +if [ -h "$0" ]; then + DIR=$(readlink "$0") +else + DIR=$0 +fi +DIR="$(dirname "$DIR")"/.. + +docker build -t docker-compose $DIR +exec docker run -i -t -v /var/run/docker.sock:/var/run/docker.sock -v `pwd`:`pwd` -w `pwd` docker-compose $@ From bd320b19fe2211e77d362e07e3714920665df15d Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 23 Feb 2015 13:56:13 +1000 Subject: [PATCH 0749/2154] add ./script/doc to build fig documentation using the docs.docker.com tooling Signed-off-by: Sven Dowideit --- docs/Dockerfile | 15 +++++++++++++++ docs/cli.md | 8 ++++---- docs/django.md | 2 +- docs/env.md | 2 +- docs/index.md | 2 +- docs/install.md | 2 +- docs/mkdocs.yml | 7 +++++++ docs/rails.md | 2 +- docs/wordpress.md | 2 +- docs/yml.md | 2 +- script/docs | 11 +++++++++++ 11 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 docs/Dockerfile create mode 100644 docs/mkdocs.yml create mode 100755 script/docs diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..59ef66cd --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,15 @@ +FROM docs/base:latest +MAINTAINER Sven Dowideit (@SvenDowideit) + +# to get the git info for this repo +COPY . /src + +# Reset the /docs dir so we can replace the theme meta with the new repo's git info +RUN git reset --hard + +RUN grep "__version" /src/compose/__init__.py | sed "s/.*'\(.*\)'/\1/" > /docs/VERSION +COPY docs/* /docs/sources/compose/ +COPY docs/mkdocs.yml /docs/mkdocs-compose.yml + +# Then build everything together, ready for mkdocs +RUN /docs/build.sh diff --git a/docs/cli.md b/docs/cli.md index 24926bf1..9da5835e 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -172,9 +172,9 @@ the daemon. Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`. -## Compose Docs +## Compose Documentation -[Installing Compose](http://docs.docker.com/compose/install) -[Intro & Overview]((http://docs.docker.com/compose/userguide) -[Yaml file reference]((http://docs.docker.com/compose/yml) +- [Installing Compose](install.md) +- [Intro & Overview](index.md) +- [Yaml file reference](yml.md) diff --git a/docs/django.md b/docs/django.md index 93e778b3..38662223 100644 --- a/docs/django.md +++ b/docs/django.md @@ -6,7 +6,7 @@ title: Getting started with Compose and Django Getting started with Compose and Django =================================== -Let's use Compose to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.html). +Let's use Compose to set up and run a Django/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md). Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with: diff --git a/docs/env.md b/docs/env.md index 8644c8ae..27831389 100644 --- a/docs/env.md +++ b/docs/env.md @@ -6,7 +6,7 @@ title: Compose environment variables reference Environment variables reference =============================== -**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](yml.html#links) for details. +**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](yml.md#links) for details. Compose uses [Docker links] to expose services' containers to one another. Each linked container injects a set of environment variables, each of which begins with the uppercase name of the container. diff --git a/docs/index.md b/docs/index.md index fb0e95ab..76765d41 100644 --- a/docs/index.md +++ b/docs/index.md @@ -60,7 +60,7 @@ Python. ### Installation and set-up -First, [install Docker and Compose](install.html). +First, [install Docker and Compose](install.md). Next, you'll want to make a directory for the project: diff --git a/docs/install.md b/docs/install.md index 4cc6dae7..ef0da087 100644 --- a/docs/install.md +++ b/docs/install.md @@ -32,7 +32,7 @@ To install Compose, run the following commands: curl -L https://github.com/docker/docker-compose/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose -Optionally, you can also install [command completion](completion.html) for the +Optionally, you can also install [command completion](completion.md) for the bash shell. Compose is available for OS X and 64-bit Linux. If you're on another platform, diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 00000000..fc725b28 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,7 @@ + +- ['compose/index.md', 'User Guide', 'Docker Compose' ] +- ['compose/install.md', 'Installation', 'Docker Compose'] +- ['compose/cli.md', 'Reference', 'Compose command line'] +- ['compose/yml.md', 'Reference', 'Compose yml'] +- ['compose/env.md', 'Reference', 'Compose ENV variables'] +- ['compose/completion.md', 'Reference', 'Compose commandline completion'] diff --git a/docs/rails.md b/docs/rails.md index ca8f8767..be6b3133 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -6,7 +6,7 @@ title: Getting started with Compose and Rails Getting started with Compose and Rails ================================== -We're going to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.html). +We're going to use Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md). Let's set up the three files that'll get us started. First, our app is going to be running inside a Docker container which contains all of its dependencies. We can define what goes inside that Docker container using a file called `Dockerfile`. It'll contain this to start with: diff --git a/docs/wordpress.md b/docs/wordpress.md index 47808fe6..0d76668e 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -6,7 +6,7 @@ title: Getting started with Compose and Wordpress Getting started with Compose and Wordpress ====================================== -Compose makes it nice and easy to run Wordpress in an isolated environment. [Install Compose](install.html), then download Wordpress into the current directory: +Compose makes it nice and easy to run Wordpress in an isolated environment. [Install Compose](install.md), then download Wordpress into the current directory: $ curl https://wordpress.org/latest.tar.gz | tar -xvzf - diff --git a/docs/yml.md b/docs/yml.md index 228fe2ce..15d3943e 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -68,7 +68,7 @@ for this service, e.g: ``` Environment variables will also be created - see the [environment variable -reference](env.html) for details. +reference](env.md) for details. ### external_links diff --git a/script/docs b/script/docs new file mode 100755 index 00000000..31c58861 --- /dev/null +++ b/script/docs @@ -0,0 +1,11 @@ +#!/bin/sh +set -ex + +# import the existing docs build cmds from docker/docker +DOCSPORT=8000 +GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) +DOCKER_DOCS_IMAGE="compose-docs$GIT_BRANCH" +DOCKER_RUN_DOCS="docker run --rm -it -e NOCACHE" + +docker build -t "$DOCKER_DOCS_IMAGE" -f docs/Dockerfile . +$DOCKER_RUN_DOCS -p $DOCSPORT:8000 "$DOCKER_DOCS_IMAGE" mkdocs serve From c3215a1764106ac71fd0327a1fc125fa3423054f Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 23 Feb 2015 16:10:14 +1000 Subject: [PATCH 0750/2154] Add links to the main Compose references Signed-off-by: Sven Dowideit --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index fb0e95ab..f7bf8a7a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -179,5 +179,5 @@ your services once you've finished with them: $ docker-compose stop At this point, you have seen the basics of how Compose works. See the reference -section for complete details on the commands, configuration file and environment -variables. +guides for complete details on the [commands](cli.md), the [configuration +file](yml.md) and [environment variables](env.md). From 34c6920b37a22ea918abe51455ab0e51cf2a9220 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 24 Feb 2015 11:43:14 +0000 Subject: [PATCH 0751/2154] Point at official Docker install instructions rather than repeating them Signed-off-by: Aanand Prasad --- docs/install.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/install.md b/docs/install.md index ef0da087..b0c1b0e2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -10,20 +10,11 @@ Compose with a `curl` command. ### Install Docker -First, you'll need to install Docker version 1.3 or greater. +First, install Docker version 1.3 or greater: -If you're on OS X, you can use the -[OS X installer](https://docs.docker.com/installation/mac/) to install both -Docker and the OSX helper app, boot2docker. Once boot2docker is running, set the -environment variables that'll configure Docker and Compose to talk to it: - - $(boot2docker shellinit) - -To persist the environment variables across shell sessions, add the above line -to your `~/.bashrc` file. - -For complete instructions, or if you are on another platform, consult Docker's -[installation instructions](https://docs.docker.com/installation/). +- [Instructions for Mac OS X](http://docs.docker.com/installation/mac/) +- [Instructions for Ubuntu](http://docs.docker.com/installation/ubuntulinux/) +- [Instructions for other systems](http://docs.docker.com/installation/) ### Install Compose From b2425c1f1e66ba59bd9f8f7c1a8273778521654e Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 24 Feb 2015 13:39:38 +0000 Subject: [PATCH 0752/2154] Log "creating container" when scaling If an image needs pulling, it just looks like it's hanging. Signed-off-by: Ben Firshman --- compose/service.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/service.py b/compose/service.py index 0c9bd357..eb6e86fb 100644 --- a/compose/service.py +++ b/compose/service.py @@ -178,6 +178,7 @@ class Service(object): # Create enough containers containers = self.containers(stopped=True) while len(containers) < desired_num: + log.info("Creating %s..." % self._next_container_name(containers)) containers.append(self.create_container(detach=True)) running_containers = [] From ec2966222a679cda06974844bbce7874311ca822 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 10:22:24 +0000 Subject: [PATCH 0753/2154] Document Swarm integration Signed-off-by: Aanand Prasad --- ROADMAP.md | 2 ++ SWARM.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 SWARM.md diff --git a/ROADMAP.md b/ROADMAP.md index 68c85836..a74a781e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,6 +12,8 @@ Over time we will extend Compose's remit to cover test, staging and production e Compose should integrate really well with Swarm so you can take an application you've developed on your laptop and run it on a Swarm cluster. +The current state of integration is documented in [SWARM.md](SWARM.md). + ## Applications spanning multiple teams Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well. diff --git a/SWARM.md b/SWARM.md new file mode 100644 index 00000000..3e549e56 --- /dev/null +++ b/SWARM.md @@ -0,0 +1,51 @@ +Docker Compose/Swarm integration +================================ + +Eventually, Compose and Swarm aim to have full integration, meaning you can point a Compose app at a Swarm cluster and have it all just work as if you were using a single Docker host. + +However, the current extent of integration is minimal: Compose can create containers on a Swarm cluster, but the majority of Compose apps won’t work out of the box unless all containers are scheduled on one host, defeating much of the purpose of using Swarm in the first place. + +Still, Compose and Swarm can be useful in a “batch processing” scenario (where a large number of containers need to be spun up and down to do independent computation) or a “shared cluster” scenario (where multiple teams want to deploy apps on a cluster without worrying about where to put them). + +A number of things need to happen before full integration is achieved, which are documented below. + +Re-deploying containers with `docker-compose up` +------------------------------------------------ + +Repeated invocations of `docker-compose up` will not work reliably when used against a Swarm cluster because of an under-the-hood design problem; [this will be fixed](https://github.com/docker/fig/pull/972) in the next version of Compose. For now, containers must be completely removed and re-created: + + $ docker-compose kill + $ docker-compose rm --force + $ docker-compose up + +Links and networking +-------------------- + +The primary thing stopping multi-container apps from working seamlessly on Swarm is getting them to talk to one another: enabling private communication between containers on different hosts hasn’t been solved in a non-hacky way. + +Long-term, networking is [getting overhauled](https://github.com/docker/docker/issues/9983) in such a way that it’ll fit the multi-host model much better. For now, containers on different hosts cannot be linked. In the next version of Compose, linked services will be automatically scheduled on the same host; for now, this must be done manually (see “Co-scheduling containers” below). + +`volumes_from` and `net: container` +----------------------------------- + +For containers to share volumes or a network namespace, they must be scheduled on the same host - this is, after all, inherent to how both volumes and network namespaces work. In the next version of Compose, this co-scheduling will be automatic whenever `volumes_from` or `net: "container:..."` is specified; for now, containers which share volumes or a network namespace must be co-scheduled manually (see “Co-scheduling containers” below). + +Co-scheduling containers +------------------------ + +For now, containers can be manually scheduled on the same host using Swarm’s [affinity filters](https://github.com/docker/swarm/blob/master/scheduler/filter/README.md#affinity-filter). Here’s a simple example: + +```yaml +web: + image: my-web-image + links: ["db"] + environment: + - "affinity:container==myproject_db_*" +db: + image: postgres +``` + +Here, we express an affinity filter on all web containers, saying that each one must run alongside a container whose name begins with `myproject_db_`. + +- `myproject` is the common prefix Compose gives to all containers in your project, which is either generated from the name of the current directory or specified with `-p` or the `DOCKER_COMPOSE_PROJECT_NAME` environment variable. +- `*` is a wildcard, which works just like filename wildcards in a Unix shell. From 5b07c581e0b38f6929437afb6adf255d72ef839c Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Wed, 25 Feb 2015 18:43:33 +1000 Subject: [PATCH 0754/2154] Add an index to the bottom of the Compose docs as they're scattered around docs.docker.com Signed-off-by: Sven Dowideit --- docs/cli.md | 11 ++++++----- docs/completion.md | 8 ++++++++ docs/django.md | 8 ++++++++ docs/env.md | 8 ++++++++ docs/index.md | 12 +++++++++--- docs/install.md | 8 ++++++++ docs/rails.md | 9 +++++++++ docs/wordpress.md | 9 +++++++++ docs/yml.md | 14 ++++++++++++-- 9 files changed, 77 insertions(+), 10 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 9da5835e..30f82177 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -5,8 +5,8 @@ page_keywords: fig, composition, compose, docker, orchestration, cli, reference # CLI reference -Most commands are run against one or more services. If the service is not -specified, the command will apply to all services. +Most Docker Compose commands are run against one or more services. If +the service is not specified, the command will apply to all services. For full usage information, run `docker-compose [COMMAND] --help`. @@ -172,9 +172,10 @@ the daemon. Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`. -## Compose Documentation +## Compose documentation - [Installing Compose](install.md) -- [Intro & Overview](index.md) +- [User guide](index.md) - [Yaml file reference](yml.md) - +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) diff --git a/docs/completion.md b/docs/completion.md index 351e5764..01a34216 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -31,3 +31,11 @@ Depending on what you typed on the command line so far, it will complete - arguments for selected options, e.g. `docker-compose kill -s` will complete some signals like SIGHUP and SIGUSR1. Enjoy working with Compose faster and with less typos! + +## Compose documentation + +- [Installing Compose](install.md) +- [User guide](index.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) diff --git a/docs/django.md b/docs/django.md index 38662223..48d3e6d0 100644 --- a/docs/django.md +++ b/docs/django.md @@ -89,3 +89,11 @@ You can also run management commands with Docker. To set up your database, for e $ docker-compose run web python manage.py syncdb +## Compose documentation + +- [Installing Compose](install.md) +- [User guide](index.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) diff --git a/docs/env.md b/docs/env.md index 27831389..3fc7b95a 100644 --- a/docs/env.md +++ b/docs/env.md @@ -31,3 +31,11 @@ Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp` Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1` [Docker links]: http://docs.docker.com/userguide/dockerlinks/ + +## Compose documentation + +- [Installing Compose](install.md) +- [User guide](index.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose command line completion](completion.md) diff --git a/docs/index.md b/docs/index.md index a9668bdd..471b10af 100644 --- a/docs/index.md +++ b/docs/index.md @@ -178,6 +178,12 @@ your services once you've finished with them: $ docker-compose stop -At this point, you have seen the basics of how Compose works. See the reference -guides for complete details on the [commands](cli.md), the [configuration -file](yml.md) and [environment variables](env.md). +At this point, you have seen the basics of how Compose works. + +## Compose documentation + +- [Installing Compose](install.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) diff --git a/docs/install.md b/docs/install.md index ef0da087..78242062 100644 --- a/docs/install.md +++ b/docs/install.md @@ -42,3 +42,11 @@ Compose can also be installed as a Python package: No further steps are required; Compose should now be successfully installed. You can test the installation by running `docker-compose --version`. + +## Compose documentation + +- [User guide](index.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) diff --git a/docs/rails.md b/docs/rails.md index be6b3133..67682cf5 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -94,3 +94,12 @@ Finally, we just need to create the database. In another terminal, run: $ docker-compose run web rake db:create And we're rolling—your app should now be running on port 3000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). + +## Compose documentation + +- [Installing Compose](install.md) +- [User guide](index.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) diff --git a/docs/wordpress.md b/docs/wordpress.md index 0d76668e..1fa1d9e3 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -89,3 +89,12 @@ if(file_exists($root.$path)) ``` With those four files in place, run `docker-compose up` inside your Wordpress directory and it'll pull and build the images we need, and then start the web and database containers. You'll then be able to visit Wordpress at port 8000 on your docker daemon (if you're using boot2docker, `boot2docker ip` will tell you its address). + +## Compose documentation + +- [Installing Compose](install.md) +- [User guide](index.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) diff --git a/docs/yml.md b/docs/yml.md index 15d3943e..035a99e9 100644 --- a/docs/yml.md +++ b/docs/yml.md @@ -29,8 +29,10 @@ image: a4bc65fd ### build -Path to a directory containing a Dockerfile. Compose will build and tag it -with a generated name, and use that image thereafter. +Path to a directory containing a Dockerfile. This directory is also the +build context that is sent to the Docker daemon. + +Compose will build and tag it with a generated name, and use that image thereafter. ``` build: /path/to/build/dir @@ -237,3 +239,11 @@ restart: always stdin_open: true tty: true ``` + +## Compose documentation + +- [Installing Compose](install.md) +- [User guide](index.md) +- [Command line reference](cli.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) From 178c50d46fa1f1934f523e9228a7a50f645027ab Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Wed, 25 Feb 2015 14:09:30 +0000 Subject: [PATCH 0755/2154] Move docs index higher up on index page Signed-off-by: Ben Firshman --- docs/index.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index 471b10af..b44adc8e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,6 +50,13 @@ Compose has commands for managing the whole lifecycle of your application: * Stream the log output of running services * Run a one-off command on a service +## Compose documentation + +- [Installing Compose](install.md) +- [Command line reference](cli.md) +- [Yaml file reference](yml.md) +- [Compose environment variables](env.md) +- [Compose command line completion](completion.md) ## Quick start @@ -180,10 +187,4 @@ your services once you've finished with them: At this point, you have seen the basics of how Compose works. -## Compose documentation -- [Installing Compose](install.md) -- [Command line reference](cli.md) -- [Yaml file reference](yml.md) -- [Compose environment variables](env.md) -- [Compose command line completion](completion.md) From bb943d5cb5551aa00bda77830d25533a556052da Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 14:58:03 +0000 Subject: [PATCH 0756/2154] Update URLs in documentation Signed-off-by: Aanand Prasad --- docs/completion.md | 2 +- docs/install.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/completion.md b/docs/completion.md index 01a34216..a0ab8a3c 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -17,7 +17,7 @@ On a Mac, install with `brew install bash-completion` Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. - curl -L https://raw.githubusercontent.com/docker/fig/1.1.0-rc2/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose + curl -L https://raw.githubusercontent.com/docker/compose/1.1.0-rc2/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose Completion will be available upon next login. diff --git a/docs/install.md b/docs/install.md index 78242062..a84c151f 100644 --- a/docs/install.md +++ b/docs/install.md @@ -29,7 +29,7 @@ For complete instructions, or if you are on another platform, consult Docker's To install Compose, run the following commands: - curl -L https://github.com/docker/docker-compose/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose Optionally, you can also install [command completion](completion.md) for the From c82bdad6c9d6f380506bd4853ad7f59f0553ca77 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 16:26:12 +0000 Subject: [PATCH 0757/2154] update --- cli.html | 2 +- django.html | 2 +- env.html | 2 +- index.html | 2 +- install.html | 2 +- rails.html | 2 +- wordpress.html | 2 +- yml.html | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cli.html b/cli.html index 3c9a47d9..457b410c 100644 --- a/cli.html +++ b/cli.html @@ -6,7 +6,7 @@ - + diff --git a/django.html b/django.html index 93c03d6e..a4be7995 100644 --- a/django.html +++ b/django.html @@ -6,7 +6,7 @@ - + diff --git a/env.html b/env.html index 50f84b51..339461b4 100644 --- a/env.html +++ b/env.html @@ -6,7 +6,7 @@ - + diff --git a/index.html b/index.html index 98ddea81..959ef428 100644 --- a/index.html +++ b/index.html @@ -6,7 +6,7 @@ - + diff --git a/install.html b/install.html index baba2417..472aa435 100644 --- a/install.html +++ b/install.html @@ -6,7 +6,7 @@ - + diff --git a/rails.html b/rails.html index 901ff2f4..77380cfb 100644 --- a/rails.html +++ b/rails.html @@ -6,7 +6,7 @@ - + diff --git a/wordpress.html b/wordpress.html index 818d65a0..8987a745 100644 --- a/wordpress.html +++ b/wordpress.html @@ -6,7 +6,7 @@ - + diff --git a/yml.html b/yml.html index 8000ed6f..667736f9 100644 --- a/yml.html +++ b/yml.html @@ -6,7 +6,7 @@ - + From cf6b09e94b3d8b698da77050c890077a7d0c157e Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 17:34:22 +0000 Subject: [PATCH 0758/2154] Fix requests version range It was more permissive than docker-py's, resulting in an incompatible version (2.5.x) being installed. Signed-off-by: Aanand Prasad --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e1e29744..9dfe9321 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def find_version(*file_paths): install_requires = [ 'docopt >= 0.6.1, < 0.7', 'PyYAML >= 3.10, < 4', - 'requests >= 2.2.1, < 3', + 'requests >= 2.2.1, < 2.5.0', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.11.0, < 1.0', 'docker-py >= 0.6.0, < 0.8', From ea8364fd11278559055fc2b55d6cf5781fdb0413 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 15:01:40 +0000 Subject: [PATCH 0759/2154] Add 'previously known as Fig' note to README.md Signed-off-by: Aanand Prasad --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 4df3bd21..52e6f360 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,8 @@ Docker Compose [![wercker status](https://app.wercker.com/status/d5dbac3907301c3d5ce735e2d5e95a5b/s/master "wercker status")](https://app.wercker.com/project/bykey/d5dbac3907301c3d5ce735e2d5e95a5b) +*(Previously known as Fig)* + Compose is a tool for defining and running complex applications with Docker. With Compose, you define a multi-container application in a single file, then spin your application up in a single command which does everything that needs to From 35d5d1a5b1889956d72778dfc08427c628990fa9 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 14:04:30 +0000 Subject: [PATCH 0760/2154] Build and link to getting started guides Signed-off-by: Aanand Prasad --- docs/index.md | 7 +++++-- docs/mkdocs.yml | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index b44adc8e..a75e7285 100644 --- a/docs/index.md +++ b/docs/index.md @@ -185,6 +185,9 @@ your services once you've finished with them: $ docker-compose stop -At this point, you have seen the basics of how Compose works. - +At this point, you have seen the basics of how Compose works. +- Next, try the quick start guide for [Django](django.md), + [Rails](rails.md), or [Wordpress](wordpress.md). +- See the reference guides for complete details on the [commands](cli.md), the + [configuration file](yml.md) and [environment variables](env.md). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index fc725b28..14335873 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -5,3 +5,6 @@ - ['compose/yml.md', 'Reference', 'Compose yml'] - ['compose/env.md', 'Reference', 'Compose ENV variables'] - ['compose/completion.md', 'Reference', 'Compose commandline completion'] +- ['compose/django.md', 'Examples', 'Getting started with Compose and Django'] +- ['compose/rails.md', 'Examples', 'Getting started with Compose and Rails'] +- ['compose/wordpress.md', 'Examples', 'Getting started with Compose and Wordpress'] From 4ac02bfca640437be84f149a76f6dad3170d9601 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 18:16:44 +0000 Subject: [PATCH 0761/2154] Ship 1.1.0 Signed-off-by: Aanand Prasad --- CHANGES.md | 21 ++++----------------- compose/__init__.py | 2 +- docs/completion.md | 2 +- docs/install.md | 2 +- 4 files changed, 7 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6aba8f39..75c13090 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,21 +1,8 @@ Change log ========== -1.1.0-rc2 (2015-01-29) ----------------------- - -On top of the changelog for 1.1.0-rc1 (see below), the following bugs have been fixed: - -- When an environment variables file specified with `env_file` doesn't exist, Compose was showing a stack trace instead of a helpful error. - -- Configuration files using the old name (`fig.yml`) were not being read unless explicitly specified with `docker-compose -f`. Compose now reads them and prints a deprecation warning. - -- Bash tab completion now reads `fig.yml` if it's present. - -Thanks, @dnephin and @albers! - -1.1.0-rc1 (2015-01-20) ----------------------- +1.1.0 (2015-02-25) +------------------ Fig has been renamed to Docker Compose, or just Compose for short. This has several implications for you: @@ -41,9 +28,9 @@ Besides that, there’s a lot of new stuff in this release: - docker-compose.yml now supports the `dns_search`, `cap_add`, `cap_drop`, `cpu_shares` and `restart` options, analogous to `docker run`’s `--dns-search`, `--cap-add`, `--cap-drop`, `--cpu-shares` and `--restart` options. -- Compose now ships with Bash tab completion - see the installation and usage docs at https://github.com/docker/fig/blob/1.1.0-rc1/docs/completion.md +- Compose now ships with Bash tab completion - see the installation and usage docs at https://github.com/docker/compose/blob/1.1.0/docs/completion.md -- A number of bugs have been fixed - see the milestone for details: https://github.com/docker/fig/issues?q=milestone%3A1.1.0+ +- A number of bugs have been fixed - see the milestone for details: https://github.com/docker/compose/issues?q=milestone%3A1.1.0+ Thanks @dnephin, @squebe, @jbalonso, @raulcd, @benlangfield, @albers, @ggtools, @bersace, @dtenenba, @petercv, @drewkett, @TFenby, @paulRbr, @Aigeruth and @salehe! diff --git a/compose/__init__.py b/compose/__init__.py index 0f4cc72a..c770b395 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1,4 @@ from __future__ import unicode_literals from .service import Service # noqa:flake8 -__version__ = '1.1.0-rc2' +__version__ = '1.1.0' diff --git a/docs/completion.md b/docs/completion.md index a0ab8a3c..d9b94f6c 100644 --- a/docs/completion.md +++ b/docs/completion.md @@ -17,7 +17,7 @@ On a Mac, install with `brew install bash-completion` Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. - curl -L https://raw.githubusercontent.com/docker/compose/1.1.0-rc2/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose + curl -L https://raw.githubusercontent.com/docker/compose/1.1.0/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose Completion will be available upon next login. diff --git a/docs/install.md b/docs/install.md index a84c151f..e78f4b48 100644 --- a/docs/install.md +++ b/docs/install.md @@ -29,7 +29,7 @@ For complete instructions, or if you are on another platform, consult Docker's To install Compose, run the following commands: - curl -L https://github.com/docker/compose/releases/download/1.1.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose Optionally, you can also install [command completion](completion.md) for the From c41342501bef738c7f39e7dfbee0b571b9b77e33 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 18:25:15 +0000 Subject: [PATCH 0762/2154] Update README with new docs URL and IRC channel Signed-off-by: Aanand Prasad --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e6f360..c943c70d 100644 --- a/README.md +++ b/README.md @@ -53,4 +53,5 @@ Compose has commands for managing the whole lifecycle of your application: Installation and documentation ------------------------------ -Full documentation is available on [Fig's website](http://www.fig.sh/). +- Full documentation is available on [Docker's website](http://docs.docker.com/compose/). +- Hop into #docker-compose on Freenode if you have any questions. From 126590ee114be1fedd538e52c1428a9fee623225 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 19:23:52 +0000 Subject: [PATCH 0763/2154] update --- cli.html | 6 +++++- css/fig.css | 11 ++++++++++- django.html | 6 +++++- env.html | 6 +++++- index.html | 6 +++++- install.html | 6 +++++- rails.html | 6 +++++- wordpress.html | 6 +++++- yml.html | 6 +++++- 9 files changed, 50 insertions(+), 9 deletions(-) diff --git a/cli.html b/cli.html index 457b410c..5d661abc 100644 --- a/cli.html +++ b/cli.html @@ -6,11 +6,15 @@ - +
    + +