From c838f7da1862c5d97e3cdc3530c52b34a9d50df4 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Tue, 21 Oct 2014 13:32:54 +0200 Subject: [PATCH 0001/1480] Convert project_name to lowercase Signed-off-by: Gabor Nagy --- fig/cli/command.py | 2 +- tests/unit/cli_test.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index 743c96e9..6cc7acad 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -81,7 +81,7 @@ class Command(DocoptCommand): def get_project_name(self, config_path, project_name=None): def normalize_name(name): - return re.sub(r'[^a-zA-Z0-9]', '', name) + return re.sub(r'[^a-z0-9]', '', name.lower()) project_name = project_name or os.environ.get('FIG_PROJECT_NAME') if project_name is not None: diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index c9151165..bc3daa11 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -29,6 +29,12 @@ class CLITestCase(unittest.TestCase): project_name = command.get_project_name(command.get_config_path()) self.assertEquals('simplefigfile', project_name) + def test_project_name_with_explicit_uppercase_base_dir(self): + command = TopLevelCommand() + command.base_dir = 'tests/fixtures/Simple-figfile' + project_name = command.get_project_name(command.get_config_path()) + self.assertEquals('simplefigfile', project_name) + def test_project_name_with_explicit_project_name(self): command = TopLevelCommand() name = 'explicit-project-name' From 9abdd337b5146643084205ad949c017b6ab843a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20Cumplido?= Date: Mon, 3 Nov 2014 22:46:01 +0000 Subject: [PATCH 0002/1480] Add signal in the kill CLI commando to send a specific signal to the service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raúl Cumplido --- docs/cli.md | 4 +++- fig/cli/main.py | 10 ++++++++-- fig/container.py | 4 ++-- tests/integration/cli_test.py | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index 4462575d..822f1b78 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -42,7 +42,9 @@ Get help on a command. ### kill -Force stop service containers. +Force stop running containers by sending a `SIGKILL` signal. Optionally the signal can be passed, for example: + + $ fig kill -s SIGINT ### logs diff --git a/fig/cli/main.py b/fig/cli/main.py index 2ce8cfc3..2f83c163 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -133,9 +133,15 @@ class TopLevelCommand(Command): """ Force stop service containers. - Usage: kill [SERVICE...] + Usage: kill [options] [SERVICE...] + + Options: + -s SIGNAL SIGNAL to send to the container. + Default signal is SIGKILL. """ - project.kill(service_names=options['SERVICE']) + signal = options.get('-s', 'SIGKILL') + + project.kill(service_names=options['SERVICE'], signal=signal) def logs(self, project, options): """ diff --git a/fig/container.py b/fig/container.py index 7e06bde3..0ab75512 100644 --- a/fig/container.py +++ b/fig/container.py @@ -124,8 +124,8 @@ class Container(object): def stop(self, **options): return self.client.stop(self.id, **options) - def kill(self): - return self.client.kill(self.id) + def kill(self, **options): + return self.client.kill(self.id, **options) def restart(self): return self.client.restart(self.id) diff --git a/tests/integration/cli_test.py b/tests/integration/cli_test.py index ceb93f62..76c0df0f 100644 --- a/tests/integration/cli_test.py +++ b/tests/integration/cli_test.py @@ -84,6 +84,7 @@ class CLITestCase(DockerClientTestCase): self.command.dispatch(['build', '--no-cache', 'simple'], None) output = mock_stdout.getvalue() self.assertNotIn(cache_indicator, output) + def test_up(self): self.command.dispatch(['up', '-d'], None) service = self.project.get_service('simple') @@ -244,6 +245,40 @@ class CLITestCase(DockerClientTestCase): self.command.dispatch(['rm', '--force'], None) self.assertEqual(len(service.containers(stopped=True)), 0) + def test_kill(self): + self.command.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + self.assertEqual(len(service.containers()), 1) + self.assertTrue(service.containers()[0].is_running) + + self.command.dispatch(['kill'], None) + + self.assertEqual(len(service.containers(stopped=True)), 1) + self.assertFalse(service.containers(stopped=True)[0].is_running) + + def test_kill_signal_sigint(self): + self.command.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + self.assertEqual(len(service.containers()), 1) + self.assertTrue(service.containers()[0].is_running) + + self.command.dispatch(['kill', '-s', 'SIGINT'], None) + + self.assertEqual(len(service.containers()), 1) + # The container is still running. It has been only interrupted + self.assertTrue(service.containers()[0].is_running) + + def test_kill_interrupted_service(self): + self.command.dispatch(['up', '-d'], None) + service = self.project.get_service('simple') + self.command.dispatch(['kill', '-s', 'SIGINT'], None) + self.assertTrue(service.containers()[0].is_running) + + self.command.dispatch(['kill', '-s', 'SIGKILL'], None) + + self.assertEqual(len(service.containers(stopped=True)), 1) + self.assertFalse(service.containers(stopped=True)[0].is_running) + def test_restart(self): service = self.project.get_service('simple') container = service.create_container() From f98323b79e8371df63dd448064d1255fa86dbf1c Mon Sep 17 00:00:00 2001 From: Andrew Burkett Date: Thu, 6 Nov 2014 16:19:58 -0800 Subject: [PATCH 0003/1480] 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 0004/1480] 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 0005/1480] 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 0006/1480] 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 0007/1480] 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 0008/1480] 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 0009/1480] 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 0010/1480] 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 0011/1480] 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 0012/1480] 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 0013/1480] 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 0014/1480] 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 0015/1480] 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 0016/1480] 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 0017/1480] 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 0018/1480] 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 0019/1480] 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 0020/1480] 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 0021/1480] 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 0022/1480] 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 0023/1480] 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 0024/1480] 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 0025/1480] 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 0026/1480] 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 0027/1480] 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 0028/1480] 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 0029/1480] 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 0030/1480] 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 0031/1480] 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 0032/1480] 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 0033/1480] 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 0034/1480] 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 0035/1480] 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 0036/1480] 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 0037/1480] 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 0038/1480] (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 0039/1480] 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 0040/1480] 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 0041/1480] 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 0042/1480] 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 0043/1480] 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 0044/1480] 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 0045/1480] 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 0046/1480] 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 0047/1480] 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 0048/1480] 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 0049/1480] 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 0050/1480] 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 0051/1480] 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 0052/1480] 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 0053/1480] 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 0054/1480] 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 0055/1480] 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 0056/1480] 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 0057/1480] 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 0058/1480] 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 0059/1480] 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 0060/1480] 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 0061/1480] 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 0062/1480] 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 0063/1480] 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 0064/1480] 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 0065/1480] 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 0066/1480] 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 0067/1480] 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 0068/1480] 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 0069/1480] 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 0070/1480] 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 0071/1480] 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 0072/1480] 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 0073/1480] 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 0074/1480] 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 0075/1480] 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 0076/1480] 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 0077/1480] 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 0078/1480] 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 0079/1480] 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 0080/1480] 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 0081/1480] 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 0082/1480] 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 0083/1480] 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 0084/1480] 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 0085/1480] 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 0086/1480] 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 d98ba29b62cfc5bfd06e79ab7f9791d755789f09 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 25 Feb 2015 16:40:18 +0000 Subject: [PATCH 0087/1480] Add deprecation banner to fig.sh Signed-off-by: Aanand Prasad --- docs/_layouts/default.html | 4 ++++ docs/css/fig.css | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index cc191918..98d1fc26 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -11,6 +11,10 @@
+ +