From f5533c1ed835a2578ab11afbf45570cc7e080c8d Mon Sep 17 00:00:00 2001 From: jrabbit Date: Sat, 23 Jan 2016 15:58:06 -0500 Subject: [PATCH 01/54] If an env var is passthrough but not defined on the host don't set it. This doesn't change too much code and keeps the generators. Signed-off-by: jrabbit --- compose/config/config.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index f362f1b80..a42f11a60 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -491,12 +491,18 @@ def resolve_environment(service_dict): env.update(env_vars_from_file(env_file)) env.update(parse_environment(service_dict.get('environment'))) - return dict(resolve_env_var(k, v) for k, v in six.iteritems(env)) + d = dict(resolve_env_var(k, v) for k, v in six.iteritems(env)) + if '_' in d.keys(): + del d['_'] + return d def resolve_build_args(build): args = parse_build_arguments(build.get('args')) - return dict(resolve_env_var(k, v) for k, v in six.iteritems(args)) + d = dict(resolve_env_var(k, v) for k, v in six.iteritems(args)) + if '_' in d.keys(): + del d['_'] + return d def validate_extended_service_dict(service_dict, filename, service): @@ -806,7 +812,7 @@ def resolve_env_var(key, val): elif key in os.environ: return key, os.environ[key] else: - return key, '' + return "_", None def env_vars_from_file(filename): From 34d8f9b55af621ee9f60732d973cae5d95c1525e Mon Sep 17 00:00:00 2001 From: jrabbit Date: Sat, 23 Jan 2016 16:19:17 -0500 Subject: [PATCH 02/54] Mangle the tests. They pass for better or worse! Signed-off-by: jrabbit --- tests/unit/config/config_test.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 8c9b73dc5..6cb932885 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1776,7 +1776,7 @@ class EnvTest(unittest.TestCase): } self.assertEqual( resolve_environment(service_dict), - {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}, + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3'}, ) def test_resolve_environment_from_env_file(self): @@ -1817,7 +1817,6 @@ class EnvTest(unittest.TestCase): 'FILE_DEF': u'bär', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', - 'NO_DEF': '' }, ) @@ -1836,7 +1835,7 @@ class EnvTest(unittest.TestCase): } self.assertEqual( resolve_build_args(build), - {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2', 'no_env': ''}, + {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2'}, ) @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') From c1959152631cffda875a9dfe99a5fa0900d18bf4 Mon Sep 17 00:00:00 2001 From: jrabbit Date: Sun, 24 Jan 2016 15:25:06 -0500 Subject: [PATCH 03/54] Modify service_test.py::ServiceTest::test_resolve_env to reflect new behavior Signed-off-by: jrabbit --- tests/integration/service_test.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 189eb9da9..37dc4a0e5 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -885,7 +885,6 @@ class ServiceTest(DockerClientTestCase): 'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', - 'NO_DEF': '' }.items(): self.assertEqual(env[k], v) From abec6f58910f5d670493ce23e2de14de286ba248 Mon Sep 17 00:00:00 2001 From: jrabbit Date: Sat, 6 Feb 2016 02:54:06 -0500 Subject: [PATCH 04/54] Change special case from '_', None to () Signed-off-by: jrabbit --- compose/config/config.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index a42f11a60..27f5ff6a2 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -491,18 +491,12 @@ def resolve_environment(service_dict): env.update(env_vars_from_file(env_file)) env.update(parse_environment(service_dict.get('environment'))) - d = dict(resolve_env_var(k, v) for k, v in six.iteritems(env)) - if '_' in d.keys(): - del d['_'] - return d + return dict(filter(None, (resolve_env_var(k, v) for k, v in six.iteritems(env)))) def resolve_build_args(build): args = parse_build_arguments(build.get('args')) - d = dict(resolve_env_var(k, v) for k, v in six.iteritems(args)) - if '_' in d.keys(): - del d['_'] - return d + return dict(filter(None, (resolve_env_var(k, v) for k, v in six.iteritems(args)))) def validate_extended_service_dict(service_dict, filename, service): @@ -812,7 +806,7 @@ def resolve_env_var(key, val): elif key in os.environ: return key, os.environ[key] else: - return "_", None + return () def env_vars_from_file(filename): From a716bdc4828edcb1ea140a401396624c2e3be5d1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 2 Feb 2016 17:55:21 +0000 Subject: [PATCH 05/54] Update Swarm integration guide and make it an official part of the docs Signed-off-by: Aanand Prasad --- SWARM.md | 40 +--------- docs/networking.md | 14 ++-- docs/production.md | 10 +-- docs/swarm.md | 184 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 51 deletions(-) create mode 100644 docs/swarm.md diff --git a/SWARM.md b/SWARM.md index 1ea4e25f3..c6f378a9a 100644 --- a/SWARM.md +++ b/SWARM.md @@ -1,39 +1 @@ -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, integration is currently incomplete: 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, because links between containers do not work across hosts. - -Docker networking is [getting overhauled](https://github.com/docker/libnetwork) in such a way that it’ll fit the multi-host model much better. For now, linked containers are automatically scheduled on the same host. - -Building --------- - -Swarm can build an image from a Dockerfile just like a single-host Docker instance can, but the resulting image will only live on a single node and won't be distributed to other nodes. - -If you want to use Compose to scale the service in question to multiple nodes, you'll have to build it yourself, push it to a registry (e.g. the Docker Hub) and reference it from `docker-compose.yml`: - - $ docker build -t myusername/web . - $ docker push myusername/web - - $ cat docker-compose.yml - web: - image: myusername/web - - $ docker-compose up -d - $ docker-compose scale web=3 - -Scheduling ----------- - -Swarm offers a rich set of scheduling and affinity hints, enabling you to control where containers are located. They are specified via container environment variables, so you can use Compose's `environment` option to set them. - - environment: - # Schedule containers on a node that has the 'storage' label set to 'ssd' - - "constraint:storage==ssd" - - # Schedule containers where the 'redis' image is already pulled - - "affinity:image==redis" - -For the full set of available filters and expressions, see the [Swarm documentation](https://docs.docker.com/swarm/scheduler/filter/). +This file has moved to: https://docs.docker.com/compose/swarm/ diff --git a/docs/networking.md b/docs/networking.md index d625ca194..1fd6c1161 100644 --- a/docs/networking.md +++ b/docs/networking.md @@ -76,7 +76,9 @@ See the [links reference](compose-file.md#links) for more information. ## Multi-host networking -When deploying a Compose application to a Swarm cluster, you can make use of the built-in `overlay` driver to enable multi-host communication between containers with no changes to application code. Consult the [Getting started with multi-host networking](/engine/userguide/networking/get-started-overlay.md) to see how to set up the overlay driver, and then specify `driver: overlay` in your networking config (see the sections below for how to do this). +When [deploying a Compose application to a Swarm cluster](swarm.md), you can make use of the built-in `overlay` driver to enable multi-host communication between containers with no changes to your Compose file or application code. + +Consult the [Getting started with multi-host networking](/engine/userguide/networking/get-started-overlay.md) to see how to set up a Swarm cluster. The cluster will use the `overlay` driver by default, but you can specify it explicitly if you prefer - see below for how to do this. ## Specifying custom networks @@ -105,11 +107,11 @@ Here's an example Compose file defining two custom networks. The `proxy` service networks: front: - # Use the overlay driver for multi-host communication - driver: overlay + # Use a custom driver + driver: custom-driver-1 back: # Use a custom driver which takes special options - driver: my-custom-driver + driver: custom-driver-2 driver_opts: foo: "1" bar: "2" @@ -135,8 +137,8 @@ Instead of (or as well as) specifying your own networks, you can also change the networks: default: - # Use the overlay driver for multi-host communication - driver: overlay + # Use a custom driver + driver: custom-driver-1 ## Using a pre-existing network diff --git a/docs/production.md b/docs/production.md index dc9544cab..40ce1e661 100644 --- a/docs/production.md +++ b/docs/production.md @@ -60,7 +60,7 @@ recreating any services which `web` depends on. You can use Compose to deploy an app to a remote Docker host by setting the `DOCKER_HOST`, `DOCKER_TLS_VERIFY`, and `DOCKER_CERT_PATH` environment variables appropriately. For tasks like this, -[Docker Machine](https://docs.docker.com/machine/) makes managing local and +[Docker Machine](/machine/overview) makes managing local and remote Docker hosts very easy, and is recommended even if you're not deploying remotely. @@ -69,14 +69,12 @@ commands will work with no further configuration. ### Running Compose on a Swarm cluster -[Docker Swarm](https://docs.docker.com/swarm/), a Docker-native clustering +[Docker Swarm](/swarm/overview), a Docker-native clustering system, exposes the same API as a single Docker host, which means you can use Compose against a Swarm instance and run your apps across multiple hosts. -Compose/Swarm integration is still in the experimental stage, and Swarm is still -in beta, but if you'd like to explore and experiment, check out the integration -guide. +Compose/Swarm integration is still in the experimental stage, but if you'd like +to explore and experiment, check out the [integration guide](swarm.md). ## Compose documentation diff --git a/docs/swarm.md b/docs/swarm.md new file mode 100644 index 000000000..2b609efaa --- /dev/null +++ b/docs/swarm.md @@ -0,0 +1,184 @@ + + + +# Using Compose with Swarm + +Docker Compose and [Docker Swarm](/swarm/overview) 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. + +The actual extent of integration depends on which version of the [Compose file +format](compose-file.md#versioning) you are using: + +1. If you're using version 1 along with `links`, your app will work, but Swarm + will schedule all containers on one host, because links between containers + do not work across hosts with the old networking system. + +2. If you're using version 2, your app should work with no changes: + + - subject to the [limitations](#limitations) described below, + + - as long as the Swarm cluster is configured to use the [overlay + driver](/engine/userguide/networking/dockernetworks.md#an-overlay-network), + or a custom driver which supports multi-host networking. + +Read the [Getting started with multi-host +networking](/engine/userguide/networking/get-started-overlay.md) to see how to +set up a Swarm cluster with [Docker Machine](/machine/overview) and the overlay driver. +Once you've got it running, deploying your app to it should be as simple as: + + $ eval "$(docker-machine env --swarm )" + $ docker-compose up + + +## Limitations + +### Building images + +Swarm can build an image from a Dockerfile just like a single-host Docker +instance can, but the resulting image will only live on a single node and won't +be distributed to other nodes. + +If you want to use Compose to scale the service in question to multiple nodes, +you'll have to build it yourself, push it to a registry (e.g. the Docker Hub) +and reference it from `docker-compose.yml`: + + $ docker build -t myusername/web . + $ docker push myusername/web + + $ cat docker-compose.yml + web: + image: myusername/web + + $ docker-compose up -d + $ docker-compose scale web=3 + +### Multiple dependencies + +If a service has multiple dependencies of the type which force co-scheduling +(see [Automatic scheduling](#automatic-scheduling) below), it's possible that +Swarm will schedule the dependencies on different nodes, making the dependent +service impossible to schedule. For example, here `foo` needs to be co-scheduled +with `bar` and `baz`: + + version: "2" + services: + foo: + image: foo + volumes_from: ["bar"] + network_mode: "service:baz" + bar: + image: bar + baz: + image: baz + +The problem is that Swarm might first schedule `bar` and `baz` on different +nodes (since they're not dependent on one another), making it impossible to +pick an appropriate node for `foo`. + +To work around this, use [manual scheduling](#manual-scheduling) to ensure that +all three services end up on the same node: + + version: "2" + services: + foo: + image: foo + volumes_from: ["bar"] + network_mode: "service:baz" + environment: + - "constraint:node==node-1" + bar: + image: bar + environment: + - "constraint:node==node-1" + baz: + image: baz + environment: + - "constraint:node==node-1" + +### Host ports and recreating containers + +If a service maps a port from the host, e.g. `80:8000`, then you may get an +error like this when running `docker-compose up` on it after the first time: + + docker: Error response from daemon: unable to find a node that satisfies + container==6ab2dfe36615ae786ef3fc35d641a260e3ea9663d6e69c5b70ce0ca6cb373c02. + +The usual cause of this error is that the container has a volume (defined either +in its image or in the Compose file) without an explicit mapping, and so in +order to preserve its data, Compose has directed Swarm to schedule the new +container on the same node as the old container. This results in a port clash. + +There are two viable workarounds for this problem: + +- Specify a named volume, and use a volume driver which is capable of mounting + the volume into the container regardless of what node it's scheduled on. + + Compose does not give Swarm any specific scheduling instructions if a + service uses only named volumes. + + version: "2" + + services: + web: + build: . + ports: + - "80:8000" + volumes: + - web-logs:/var/log/web + + volumes: + web-logs: + driver: custom-volume-driver + +- Remove the old container before creating the new one. You will lose any data + in the volume. + + $ docker-compose stop web + $ docker-compose rm -f web + $ docker-compose up web + + +## Scheduling containers + +### Automatic scheduling + +Some configuration options will result in containers being automatically +scheduled on the same Swarm node to ensure that they work correctly. These are: + +- `network_mode: "service:..."` and `network_mode: "container:..."` (and + `net: "container:..."` in the version 1 file format). + +- `volumes_from` + +- `links` + +### Manual scheduling + +Swarm offers a rich set of scheduling and affinity hints, enabling you to +control where containers are located. They are specified via container +environment variables, so you can use Compose's `environment` option to set +them. + + # Schedule containers on a specific node + environment: + - "constraint:node==node-1" + + # Schedule containers on a node that has the 'storage' label set to 'ssd' + environment: + - "constraint:storage==ssd" + + # Schedule containers where the 'redis' image is already pulled + environment: + - "affinity:image==redis" + +For the full set of available filters and expressions, see the [Swarm +documentation](/swarm/scheduler/filter.md). From c1be49ad53efa5c9ab79d6675a20abc737ecfafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20K=C3=A4ufl?= Date: Sat, 6 Feb 2016 22:10:22 +0100 Subject: [PATCH 06/54] Used absolute links in readme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This prevents links being broken on pypi (e.g. https://pypi.python.org/pypi/docs/index.md#features) Signed-off-by: Michael Käufl --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b60a7eee5..f88221519 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a Compose file to configure your application's services. Then, using a single command, you create and start all the services from your configuration. To learn more about all the features of Compose -see [the list of features](docs/index.md#features). +see [the list of features](https://github.com/docker/compose/blob/release/docs/overview.md#features). Compose is great for development, testing, and staging environments, as well as CI workflows. You can learn more about each case in -[Common Use Cases](docs/index.md#common-use-cases). +[Common Use Cases](https://github.com/docker/compose/blob/release/docs/overview.md#common-use-cases). Using Compose is basically a three-step process. @@ -34,7 +34,7 @@ A `docker-compose.yml` looks like this: image: redis For more information about the Compose file, see the -[Compose file reference](docs/compose-file.md) +[Compose file reference](https://github.com/docker/compose/blob/release/docs/compose-file.md) Compose has commands for managing the whole lifecycle of your application: From 8548b75582c8f87113c17b2b5996bcf1ea516e27 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Mon, 8 Feb 2016 14:29:03 +0100 Subject: [PATCH 07/54] Separate MergePortsTest from MergeListsTest and add MergeNetworksTest. Signed-off-by: Lukas Waslowski --- tests/unit/config/config_test.py | 52 ++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 6cb932885..d756b6f68 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1594,30 +1594,64 @@ class BuildOrImageMergeTest(unittest.TestCase): ) -class MergeListsTest(unittest.TestCase): +class MergeListsTest(object): + def config_name(self): + return "" + + def base_config(self): + return [] + + def override_config(self): + return [] + + def merged_config(self): + return set(self.base_config()) | set(self.override_config()) + def test_empty(self): - assert 'ports' not in config.merge_service_dicts({}, {}, DEFAULT_VERSION) + assert self.config_name() not in config.merge_service_dicts({}, {}, DEFAULT_VERSION) def test_no_override(self): service_dict = config.merge_service_dicts( - {'ports': ['10:8000', '9000']}, + {self.config_name(): self.base_config()}, {}, DEFAULT_VERSION) - assert set(service_dict['ports']) == set(['10:8000', '9000']) + assert set(service_dict[self.config_name()]) == set(self.base_config()) def test_no_base(self): service_dict = config.merge_service_dicts( {}, - {'ports': ['10:8000', '9000']}, + {self.config_name(): self.base_config()}, DEFAULT_VERSION) - assert set(service_dict['ports']) == set(['10:8000', '9000']) + assert set(service_dict[self.config_name()]) == set(self.base_config()) def test_add_item(self): service_dict = config.merge_service_dicts( - {'ports': ['10:8000', '9000']}, - {'ports': ['20:8000']}, + {self.config_name(): self.base_config()}, + {self.config_name(): self.override_config()}, DEFAULT_VERSION) - assert set(service_dict['ports']) == set(['10:8000', '9000', '20:8000']) + assert set(service_dict[self.config_name()]) == set(self.merged_config()) + + +class MergePortsTest(unittest.TestCase, MergeListsTest): + def config_name(self): + return 'ports' + + def base_config(self): + return ['10:8000', '9000'] + + def override_config(self): + return ['20:8000'] + + +class MergeNetworksTest(unittest.TestCase, MergeListsTest): + def config_name(self): + return 'networks' + + def base_config(self): + return ['frontend', 'backend'] + + def override_config(self): + return ['monitoring'] class MergeStringsOrListsTest(unittest.TestCase): From c77a8cfe3bf09d5cccd8fe77f3b7696cac2be400 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Mon, 8 Feb 2016 15:17:21 +0100 Subject: [PATCH 08/54] Correctly merge the 'services//networks' key in the case of multiple compose files. Fixes docker/compose#2839. Signed-off-by: Lukas Waslowski --- compose/config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/config/config.py b/compose/config/config.py index 27f5ff6a2..2faa12b2d 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -698,6 +698,7 @@ def merge_service_dicts(base, override, version): 'depends_on', 'expose', 'external_links', + 'networks', 'ports', 'volumes_from', ]: From ad00f3dd219cec792a2089025917daea461145d3 Mon Sep 17 00:00:00 2001 From: Lukas Waslowski Date: Mon, 8 Feb 2016 15:33:26 +0100 Subject: [PATCH 09/54] Handle the 'network_mode' key when merging multiple compose files. Fixes docker/compose#2840. Signed-off-by: Lukas Waslowski --- compose/config/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/config/config.py b/compose/config/config.py index 2faa12b2d..102758e9d 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -87,6 +87,7 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [ 'container_name', 'dockerfile', 'logging', + 'network_mode', ] DOCKER_VALID_URL_PREFIXES = ( From edcbe2eb4d30d2b2c7bcbe03d1c83ed69af5b714 Mon Sep 17 00:00:00 2001 From: cr7pt0gr4ph7 Date: Mon, 8 Feb 2016 21:57:15 +0100 Subject: [PATCH 10/54] Simplify unit tests in config/config_test.py by using class variables instead of methods for parametrizing tests. Signed-off-by: cr7pt0gr4ph7 --- tests/unit/config/config_test.py | 88 +++++++++++++------------------- 1 file changed, 35 insertions(+), 53 deletions(-) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index d756b6f68..e545aba73 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1506,57 +1506,54 @@ class VolumeConfigTest(unittest.TestCase): class MergePathMappingTest(object): - def config_name(self): - return "" + config_name = "" def test_empty(self): service_dict = config.merge_service_dicts({}, {}, DEFAULT_VERSION) - assert self.config_name() not in service_dict + assert self.config_name not in service_dict def test_no_override(self): service_dict = config.merge_service_dicts( - {self.config_name(): ['/foo:/code', '/data']}, + {self.config_name: ['/foo:/code', '/data']}, {}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(['/foo:/code', '/data']) + assert set(service_dict[self.config_name]) == set(['/foo:/code', '/data']) def test_no_base(self): service_dict = config.merge_service_dicts( {}, - {self.config_name(): ['/bar:/code']}, + {self.config_name: ['/bar:/code']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(['/bar:/code']) + assert set(service_dict[self.config_name]) == set(['/bar:/code']) def test_override_explicit_path(self): service_dict = config.merge_service_dicts( - {self.config_name(): ['/foo:/code', '/data']}, - {self.config_name(): ['/bar:/code']}, + {self.config_name: ['/foo:/code', '/data']}, + {self.config_name: ['/bar:/code']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(['/bar:/code', '/data']) + assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data']) def test_add_explicit_path(self): service_dict = config.merge_service_dicts( - {self.config_name(): ['/foo:/code', '/data']}, - {self.config_name(): ['/bar:/code', '/quux:/data']}, + {self.config_name: ['/foo:/code', '/data']}, + {self.config_name: ['/bar:/code', '/quux:/data']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(['/bar:/code', '/quux:/data']) + assert set(service_dict[self.config_name]) == set(['/bar:/code', '/quux:/data']) def test_remove_explicit_path(self): service_dict = config.merge_service_dicts( - {self.config_name(): ['/foo:/code', '/quux:/data']}, - {self.config_name(): ['/bar:/code', '/data']}, + {self.config_name: ['/foo:/code', '/quux:/data']}, + {self.config_name: ['/bar:/code', '/data']}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(['/bar:/code', '/data']) + assert set(service_dict[self.config_name]) == set(['/bar:/code', '/data']) class MergeVolumesTest(unittest.TestCase, MergePathMappingTest): - def config_name(self): - return 'volumes' + config_name = 'volumes' class MergeDevicesTest(unittest.TestCase, MergePathMappingTest): - def config_name(self): - return 'devices' + config_name = 'devices' class BuildOrImageMergeTest(unittest.TestCase): @@ -1595,63 +1592,48 @@ class BuildOrImageMergeTest(unittest.TestCase): class MergeListsTest(object): - def config_name(self): - return "" - - def base_config(self): - return [] - - def override_config(self): - return [] + config_name = "" + base_config = [] + override_config = [] def merged_config(self): - return set(self.base_config()) | set(self.override_config()) + return set(self.base_config) | set(self.override_config) def test_empty(self): - assert self.config_name() not in config.merge_service_dicts({}, {}, DEFAULT_VERSION) + assert self.config_name not in config.merge_service_dicts({}, {}, DEFAULT_VERSION) def test_no_override(self): service_dict = config.merge_service_dicts( - {self.config_name(): self.base_config()}, + {self.config_name: self.base_config}, {}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(self.base_config()) + assert set(service_dict[self.config_name]) == set(self.base_config) def test_no_base(self): service_dict = config.merge_service_dicts( {}, - {self.config_name(): self.base_config()}, + {self.config_name: self.base_config}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(self.base_config()) + assert set(service_dict[self.config_name]) == set(self.base_config) def test_add_item(self): service_dict = config.merge_service_dicts( - {self.config_name(): self.base_config()}, - {self.config_name(): self.override_config()}, + {self.config_name: self.base_config}, + {self.config_name: self.override_config}, DEFAULT_VERSION) - assert set(service_dict[self.config_name()]) == set(self.merged_config()) + assert set(service_dict[self.config_name]) == set(self.merged_config()) class MergePortsTest(unittest.TestCase, MergeListsTest): - def config_name(self): - return 'ports' - - def base_config(self): - return ['10:8000', '9000'] - - def override_config(self): - return ['20:8000'] + config_name = 'ports' + base_config = ['10:8000', '9000'] + override_config = ['20:8000'] class MergeNetworksTest(unittest.TestCase, MergeListsTest): - def config_name(self): - return 'networks' - - def base_config(self): - return ['frontend', 'backend'] - - def override_config(self): - return ['monitoring'] + config_name = 'networks' + base_config = ['frontend', 'backend'] + override_config = ['monitoring'] class MergeStringsOrListsTest(unittest.TestCase): From deeca57a0d7df41a2bac9e6277421179a2fbaece Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 8 Feb 2016 12:18:48 -0500 Subject: [PATCH 11/54] Use 12 characters for the short id to match docker and fix backwards compatibility. Signed-off-by: Daniel Nephin --- compose/container.py | 2 +- tests/unit/container_test.py | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/compose/container.py b/compose/container.py index 2565c8ffc..3a1ce0b9f 100644 --- a/compose/container.py +++ b/compose/container.py @@ -60,7 +60,7 @@ class Container(object): @property def short_id(self): - return self.id[:10] + return self.id[:12] @property def name(self): diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 886911504..189b0c992 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -12,8 +12,9 @@ from compose.container import get_container_name class ContainerTest(unittest.TestCase): def setUp(self): + self.container_id = "abcabcabcbabc12345" self.container_dict = { - "Id": "abc", + "Id": self.container_id, "Image": "busybox:latest", "Command": "top", "Created": 1387384730, @@ -41,19 +42,22 @@ class ContainerTest(unittest.TestCase): self.assertEqual( container.dictionary, { - "Id": "abc", + "Id": self.container_id, "Image": "busybox:latest", "Name": "/composetest_db_1", }) def test_from_ps_prefixed(self): - self.container_dict['Names'] = ['/swarm-host-1' + n for n in self.container_dict['Names']] + 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) + container = Container.from_ps( + None, + self.container_dict, + has_been_inspected=True) self.assertEqual(container.dictionary, { - "Id": "abc", + "Id": self.container_id, "Image": "busybox:latest", "Name": "/composetest_db_1", }) @@ -142,6 +146,10 @@ class ContainerTest(unittest.TestCase): self.assertEqual(container.get('HostConfig.VolumesFrom'), ["volume_id"]) self.assertEqual(container.get('Foo.Bar.DoesNotExist'), None) + def test_short_id(self): + container = Container(None, self.container_dict, has_been_inspected=True) + assert container.short_id == self.container_id[:12] + class GetContainerNameTestCase(unittest.TestCase): From db12794b1cf004f3d338764a75ac1eebf658ef2f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 8 Feb 2016 18:15:21 -0500 Subject: [PATCH 12/54] Fix upgrading url. Signed-off-by: Daniel Nephin --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d115f05d3..8df63c5fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Major Features: 1.6 exactly as they do today. Check the upgrade guide for full details: - https://docs.docker.com/compose/compose-file/upgrading + https://docs.docker.com/compose/compose-file#upgrading - Support for networking has exited experimental status and is the recommended way to enable communication between containers. From cea7911f56979485d44186f17ca68aa7a950c106 Mon Sep 17 00:00:00 2001 From: Yohan Graterol Date: Tue, 9 Feb 2016 18:40:44 -0500 Subject: [PATCH 13/54] Typo into the doc with `networks` in yaml Signed-off-by: Yohan Graterol --- docs/compose-file.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index ec90ddcdc..240fea1e1 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -761,14 +761,14 @@ service's containers to it. networks: - default - networks + networks: outside: external: true You can also specify the name of the network separately from the name used to refer to it within the Compose file: - networks + networks: outside: external: name: actual-name-of-network From 2ced83e3d9dd0036bc540a2dfd456abd3a305870 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 10 Feb 2016 13:32:04 -0500 Subject: [PATCH 14/54] Fix build section without context. Signed-off-by: Daniel Nephin --- compose/config/config.py | 3 +++ compose/config/service_schema_v2.0.json | 7 ++++++- compose/config/validation.py | 5 ++--- tests/unit/config/config_test.py | 11 +++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 102758e9d..37a94498a 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -878,6 +878,9 @@ def validate_paths(service_dict): build_path = build elif isinstance(build, dict) and 'context' in build: build_path = build['context'] + else: + # We have a build section but no context, so nothing to validate + return if ( not is_url(build_path) and diff --git a/compose/config/service_schema_v2.0.json b/compose/config/service_schema_v2.0.json index 8dd4faf5d..f5ebbe432 100644 --- a/compose/config/service_schema_v2.0.json +++ b/compose/config/service_schema_v2.0.json @@ -195,7 +195,12 @@ "anyOf": [ {"required": ["build"]}, {"required": ["image"]} - ] + ], + "properties": { + "build": { + "required": ["context"] + } + } } } } diff --git a/compose/config/validation.py b/compose/config/validation.py index 6b2401352..35727e2cc 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -253,10 +253,9 @@ def handle_generic_service_error(error, path): msg_format = "{path} contains an invalid type, it should be {msg}" error_msg = _parse_valid_types_from_validator(error.validator_value) - # TODO: no test case for this branch, there are no config options - # which exercise this branch elif error.validator == 'required': - msg_format = "{path} is invalid, {msg}" + error_msg = ", ".join(error.validator_value) + msg_format = "{path} is invalid, {msg} is required." elif error.validator == 'dependencies': config_key = list(error.validator_value.keys())[0] diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index e545aba73..b77aab4ff 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1136,6 +1136,17 @@ class ConfigTest(unittest.TestCase): config.load(config_details) assert "Service 'one' depends on service 'three'" in exc.exconly() + def test_load_dockerfile_without_context(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'one': {'build': {'dockerfile': 'Dockerfile.foo'}}, + }, + }) + with pytest.raises(ConfigurationError) as exc: + config.load(config_details) + assert 'one.build is invalid, context is required.' in exc.exconly() + class NetworkModeTest(unittest.TestCase): def test_network_mode_standard(self): From 155efd28fabfddd1871f8266caff613e5e1742ad Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 10 Feb 2016 13:54:40 -0500 Subject: [PATCH 15/54] Merge build.args when merging services. Signed-off-by: Daniel Nephin --- compose/config/config.py | 29 ++++++++++++---------------- tests/unit/config/config_test.py | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 37a94498a..2745ca429 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -713,29 +713,24 @@ def merge_service_dicts(base, override, version): if version == V1: legacy_v1_merge_image_or_build(md, base, override) - else: - merge_build(md, base, override) + elif md.needs_merge('build'): + md['build'] = merge_build(md, base, override) return dict(md) def merge_build(output, base, override): - build = {} + def to_dict(service): + build_config = service.get('build', {}) + if isinstance(build_config, six.string_types): + return {'context': build_config} + return build_config - if 'build' in base: - if isinstance(base['build'], six.string_types): - build['context'] = base['build'] - else: - build.update(base['build']) - - if 'build' in override: - if isinstance(override['build'], six.string_types): - build['context'] = override['build'] - else: - build.update(override['build']) - - if build: - output['build'] = build + md = MergeDict(to_dict(base), to_dict(override)) + md.merge_scalar('context') + md.merge_scalar('dockerfile') + md.merge_mapping('args', parse_build_arguments) + return dict(md) def legacy_v1_merge_image_or_build(output, base, override): diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index b77aab4ff..7fecfed37 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1079,6 +1079,39 @@ class ConfigTest(unittest.TestCase): 'extends': {'service': 'foo'} } + def test_merge_build_args(self): + base = { + 'build': { + 'context': '.', + 'args': { + 'ONE': '1', + 'TWO': '2', + }, + } + } + override = { + 'build': { + 'args': { + 'TWO': 'dos', + 'THREE': '3', + }, + } + } + actual = config.merge_service_dicts( + base, + override, + DEFAULT_VERSION) + assert actual == { + 'build': { + 'context': '.', + 'args': { + 'ONE': '1', + 'TWO': 'dos', + 'THREE': '3', + }, + } + } + def test_external_volume_config(self): config_details = build_config_details({ 'version': '2', From c7687592ff638517a49f2972988ce22bac9825b3 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Wed, 10 Feb 2016 18:58:01 -0500 Subject: [PATCH 16/54] Typo fixed Signed-off-by: Manuel Kaufmann --- docs/django.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/django.md b/docs/django.md index 573ea3d97..150d36317 100644 --- a/docs/django.md +++ b/docs/django.md @@ -129,7 +129,7 @@ In this step, you create a Django started project by building the image from the In this section, you set up the database connection for Django. -1. In your project dirctory, edit the `composeexample/settings.py` file. +1. In your project directory, edit the `composeexample/settings.py` file. 2. Replace the `DATABASES = ...` with the following: From 3eac70a9d31edf5c326ec4fc4603ff23c1df8781 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 10 Feb 2016 16:35:27 -0800 Subject: [PATCH 17/54] Detailed error message when daemon version is too old. Signed-off-by: Joffrey F --- compose/cli/main.py | 19 ++++++++++++++++++- compose/const.py | 5 +++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index deb1e9121..7413c53cf 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -19,6 +19,7 @@ from ..config import config from ..config import ConfigurationError from ..config import parse_environment from ..config.serialize import serialize_config +from ..const import API_VERSION_TO_ENGINE_VERSION from ..const import DEFAULT_TIMEOUT from ..const import HTTP_TIMEOUT from ..const import IS_WINDOWS_PLATFORM @@ -64,7 +65,7 @@ def main(): log.error("No such command: %s\n\n%s", e.command, commands) sys.exit(1) except APIError as e: - log.error(e.explanation) + log_api_error(e) sys.exit(1) except BuildError as e: log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason)) @@ -84,6 +85,22 @@ def main(): sys.exit(1) +def log_api_error(e): + if 'client is newer than server' in e.explanation: + # we need JSON formatted errors. In the meantime... + # TODO: fix this by refactoring project dispatch + # http://github.com/docker/compose/pull/2832#commitcomment-15923800 + client_version = e.explanation.split('client API version: ')[1].split(',')[0] + log.error( + "The engine version is lesser than the minimum required by " + "compose. Your current project requires a Docker Engine of " + "version {version} or superior.".format( + version=API_VERSION_TO_ENGINE_VERSION[client_version] + )) + else: + log.error(e.explanation) + + def setup_logging(): root_logger = logging.getLogger() root_logger.addHandler(console_handler) diff --git a/compose/const.py b/compose/const.py index 0e307835c..db5e2fb4f 100644 --- a/compose/const.py +++ b/compose/const.py @@ -22,3 +22,8 @@ API_VERSIONS = { COMPOSEFILE_V1: '1.21', COMPOSEFILE_V2_0: '1.22', } + +API_VERSION_TO_ENGINE_VERSION = { + API_VERSIONS[COMPOSEFILE_V1]: '1.9.0', + API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0' +} From a1d6e3b9e3ba5d9724dca434c05f0c7503f23d18 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 10 Feb 2016 16:49:50 -0800 Subject: [PATCH 18/54] Add logging when initializing a volume. Signed-off-by: Joffrey F --- compose/volume.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/compose/volume.py b/compose/volume.py index 2713fd32b..26fbda96f 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -64,12 +64,13 @@ class ProjectVolumes(object): config_volumes = config_data.volumes or {} volumes = { vol_name: Volume( - client=client, - project=name, - name=vol_name, - driver=data.get('driver'), - driver_opts=data.get('driver_opts'), - external_name=data.get('external_name')) + client=client, + project=name, + name=vol_name, + driver=data.get('driver'), + driver_opts=data.get('driver_opts'), + external_name=data.get('external_name') + ) for vol_name, data in config_volumes.items() } return cls(volumes) @@ -96,6 +97,11 @@ class ProjectVolumes(object): ) ) continue + log.info( + 'Creating volume "{0}" with {1} driver'.format( + volume.full_name, volume.driver or 'default' + ) + ) volume.create() except NotFound: raise ConfigurationError( From 4f7c950ca81a4d2428463f3aa9cacead458b5167 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 11 Feb 2016 13:10:29 -0500 Subject: [PATCH 19/54] Upgrade pyinstaller. Signed-off-by: Daniel Nephin --- requirements-build.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-build.txt b/requirements-build.txt index 20aad4208..3f1dbd75b 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1 +1 @@ -pyinstaller==3.0 +pyinstaller==3.1.1 From dd9a8d6eee7bee298a0ea540c254420ddbf4b074 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 11 Feb 2016 16:31:08 -0800 Subject: [PATCH 20/54] Bring up all dependencies when running a single service. Added test for running a depends_on service Signed-off-by: Joffrey F --- compose/cli/main.py | 2 +- tests/acceptance/cli_test.py | 9 +++++++++ tests/fixtures/v2-dependencies/docker-compose.yml | 13 +++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/v2-dependencies/docker-compose.yml diff --git a/compose/cli/main.py b/compose/cli/main.py index 7413c53cf..cc15fa051 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -703,7 +703,7 @@ def image_type_from_opt(flag, value): def run_one_off_container(container_options, project, service, options): if not options['--no-deps']: - deps = service.get_linked_service_names() + deps = service.get_dependency_names() if deps: project.up( service_names=deps, diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 032900d51..ea3d132a5 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -738,6 +738,15 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(db.containers()), 1) self.assertEqual(len(console.containers()), 0) + @v2_only() + def test_run_service_with_dependencies(self): + self.base_dir = 'tests/fixtures/v2-dependencies' + self.dispatch(['run', 'web', '/bin/true'], None) + db = self.project.get_service('db') + console = self.project.get_service('console') + self.assertEqual(len(db.containers()), 1) + self.assertEqual(len(console.containers()), 0) + def test_run_with_no_deps(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['run', '--no-deps', 'web', '/bin/true']) diff --git a/tests/fixtures/v2-dependencies/docker-compose.yml b/tests/fixtures/v2-dependencies/docker-compose.yml new file mode 100644 index 000000000..2e14b94bb --- /dev/null +++ b/tests/fixtures/v2-dependencies/docker-compose.yml @@ -0,0 +1,13 @@ +version: "2.0" +services: + db: + image: busybox:latest + command: top + web: + image: busybox:latest + command: top + depends_on: + - db + console: + image: busybox:latest + command: top From 7c95c733a9a6060dc538ad1fc2a3e78c718bfb62 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 11 Feb 2016 19:53:28 -0500 Subject: [PATCH 21/54] Only set a container affinity if there are volumes to copy over. Signed-off-by: Daniel Nephin --- compose/service.py | 32 +++++++++++--------- tests/unit/service_test.py | 60 ++++++++++++++++++++++++++++++++------ 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/compose/service.py b/compose/service.py index 652ee7944..38a305d36 100644 --- a/compose/service.py +++ b/compose/service.py @@ -591,20 +591,19 @@ class Service(object): ports.append(port) container_options['ports'] = ports - override_options['binds'] = merge_volume_bindings( - container_options.get('volumes') or [], - previous_container) - - if 'volumes' in container_options: - container_options['volumes'] = dict( - (v.internal, {}) for v in container_options['volumes']) - container_options['environment'] = merge_environment( self.options.get('environment'), override_options.get('environment')) - if previous_container: - container_options['environment']['affinity:container'] = ('=' + previous_container.id) + binds, affinity = merge_volume_bindings( + container_options.get('volumes') or [], + previous_container) + override_options['binds'] = binds + container_options['environment'].update(affinity) + + if 'volumes' in container_options: + container_options['volumes'] = dict( + (v.internal, {}) for v in container_options['volumes']) container_options['image'] = self.image_name @@ -875,18 +874,23 @@ def merge_volume_bindings(volumes, previous_container): """Return a list of volume bindings for a container. Container data volumes are replaced by those from the previous container. """ + affinity = {} + volume_bindings = dict( build_volume_binding(volume) for volume in volumes if volume.external) if previous_container: - data_volumes = get_container_data_volumes(previous_container, volumes) - warn_on_masked_volume(volumes, data_volumes, previous_container.service) + old_volumes = get_container_data_volumes(previous_container, volumes) + warn_on_masked_volume(volumes, old_volumes, previous_container.service) volume_bindings.update( - build_volume_binding(volume) for volume in data_volumes) + build_volume_binding(volume) for volume in old_volumes) - return list(volume_bindings.values()) + if old_volumes: + affinity = {'affinity:container': '=' + previous_container.id} + + return list(volume_bindings.values()), affinity def get_container_data_volumes(container, volumes_option): diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index f34de3bf1..62f7f0042 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -267,13 +267,52 @@ class ServiceTest(unittest.TestCase): self.assertEqual( opts['labels'][LABEL_CONFIG_HASH], 'f8bfa1058ad1f4231372a0b1639f0dfdb574dafff4e8d7938049ae993f7cf1fc') - self.assertEqual( - opts['environment'], - { - 'affinity:container': '=ababab', - 'also': 'real', - } + assert opts['environment'] == {'also': 'real'} + + def test_get_container_create_options_sets_affinity_with_binds(self): + service = Service( + 'foo', + image='foo', + client=self.mock_client, ) + self.mock_client.inspect_image.return_value = {'Id': 'abcd'} + prev_container = mock.Mock( + id='ababab', + image_config={'ContainerConfig': {'Volumes': ['/data']}}) + + def container_get(key): + return { + 'Mounts': [ + { + 'Destination': '/data', + 'Source': '/some/path', + 'Name': 'abab1234', + }, + ] + }.get(key, None) + + prev_container.get.side_effect = container_get + + opts = service._get_container_create_options( + {}, + 1, + previous_container=prev_container) + + assert opts['environment'] == {'affinity:container': '=ababab'} + + def test_get_container_create_options_no_affinity_without_binds(self): + service = Service('foo', image='foo', client=self.mock_client) + self.mock_client.inspect_image.return_value = {'Id': 'abcd'} + prev_container = mock.Mock( + id='ababab', + image_config={'ContainerConfig': {}}) + prev_container.get.return_value = None + + opts = service._get_container_create_options( + {}, + 1, + previous_container=prev_container) + assert opts['environment'] == {} def test_get_container_not_found(self): self.mock_client.containers.return_value = [] @@ -650,6 +689,7 @@ class ServiceVolumesTest(unittest.TestCase): '/host/volume:/host/volume:ro', '/new/volume', '/existing/volume', + 'named:/named/vol', ]] self.mock_client.inspect_image.return_value = { @@ -710,7 +750,8 @@ class ServiceVolumesTest(unittest.TestCase): 'ContainerConfig': {'Volumes': {}} } - intermediate_container = Container(self.mock_client, { + previous_container = Container(self.mock_client, { + 'Id': 'cdefab', 'Image': 'ababab', 'Mounts': [{ 'Source': '/var/lib/docker/aaaaaaaa', @@ -727,8 +768,9 @@ class ServiceVolumesTest(unittest.TestCase): '/var/lib/docker/aaaaaaaa:/existing/volume:rw', ] - binds = merge_volume_bindings(options, intermediate_container) - self.assertEqual(set(binds), set(expected)) + binds, affinity = merge_volume_bindings(options, previous_container) + assert sorted(binds) == sorted(expected) + assert affinity == {'affinity:container': '=cdefab'} def test_mount_same_host_path_to_two_volumes(self): service = Service( From d78ea85301deff1305bbff4c3d1a6e5de0341d6c Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Feb 2016 10:41:27 -0800 Subject: [PATCH 22/54] driver_opts can only be of type string Signed-off-by: Joffrey F --- compose/config/fields_schema_v2.0.json | 2 +- tests/unit/config/config_test.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/compose/config/fields_schema_v2.0.json b/compose/config/fields_schema_v2.0.json index 7703adcd0..876065e51 100644 --- a/compose/config/fields_schema_v2.0.json +++ b/compose/config/fields_schema_v2.0.json @@ -78,7 +78,7 @@ "driver_opts": { "type": "object", "patternProperties": { - "^.+$": {"type": ["string", "number"]} + "^.+$": {"type": "string"} } }, "external": { diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 7fecfed37..5f7633d90 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -231,6 +231,20 @@ class ConfigTest(unittest.TestCase): assert volumes['simple'] == {} assert volumes['other'] == {} + def test_volume_invalid_driver_opt(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'simple': {'image': 'busybox'} + }, + 'volumes': { + 'simple': {'driver_opts': {'size': 42}}, + } + }) + with pytest.raises(ConfigurationError) as exc: + config.load(config_details) + assert 'driver_opts.size contains an invalid type' in exc.exconly() + def test_load_service_with_name_version(self): with mock.patch('compose.config.config.log') as mock_logging: config_data = config.load( From 2f7a77e954a38cc227beaa829c32982d5b1dcc61 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Feb 2016 16:10:36 -0800 Subject: [PATCH 23/54] Allow user to specify custom network aliases Signed-off-by: Joffrey F --- compose/config/config.py | 3 +++ compose/config/service_schema_v2.0.json | 13 ++++++++++--- compose/config/validation.py | 13 +++++++++++++ compose/network.py | 11 ++++++++--- compose/project.py | 4 ++-- compose/service.py | 12 ++++++------ 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 2745ca429..08324c496 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -30,6 +30,7 @@ from .types import ServiceLink from .types import VolumeFromSpec from .types import VolumeSpec from .validation import match_named_volumes +from .validation import match_network_aliases from .validation import validate_against_fields_schema from .validation import validate_against_service_schema from .validation import validate_depends_on @@ -535,6 +536,8 @@ def validate_service(service_config, service_names, version): validate_network_mode(service_config, service_names) validate_depends_on(service_config, service_names) + match_network_aliases(service_config.config) + if not service_dict.get('image') and has_uppercase(service_name): raise ConfigurationError( "Service '{name}' contains uppercase characters which are not valid " diff --git a/compose/config/service_schema_v2.0.json b/compose/config/service_schema_v2.0.json index f5ebbe432..1fcfc19d3 100644 --- a/compose/config/service_schema_v2.0.json +++ b/compose/config/service_schema_v2.0.json @@ -107,9 +107,16 @@ "network_mode": {"type": "string"}, "networks": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true + "$ref": "#/definitions/list_of_strings" + }, + "network_aliases": { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/list_of_strings" + } + }, + "additionalProperties": false }, "pid": {"type": ["string", "null"]}, diff --git a/compose/config/validation.py b/compose/config/validation.py index 35727e2cc..539291509 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -91,6 +91,19 @@ def match_named_volumes(service_dict, project_volumes): ) +def match_network_aliases(service_dict): + networks = service_dict.get('networks', []) + aliased_networks = service_dict.get('network_aliases', {}).keys() + for n in aliased_networks: + if n not in networks: + raise ConfigurationError( + 'Network "{0}" is referenced in network_aliases, but is not' + 'declared in the networks list for service "{1}"'.format( + n, service_dict.get('name') + ) + ) + + def validate_top_level_service_objects(filename, service_dicts): """Perform some high level validation of the service name and value. diff --git a/compose/network.py b/compose/network.py index 82a78f3b5..99c04649b 100644 --- a/compose/network.py +++ b/compose/network.py @@ -15,7 +15,7 @@ log = logging.getLogger(__name__) class Network(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - ipam=None, external_name=None): + ipam=None, external_name=None, aliases=None): self.client = client self.project = project self.name = name @@ -23,6 +23,7 @@ class Network(object): self.driver_opts = driver_opts self.ipam = create_ipam_config_from_dict(ipam) self.external_name = external_name + self.aliases = aliases or [] def ensure(self): if self.external_name: @@ -166,14 +167,18 @@ def get_network_names_for_service(service_dict): def get_networks(service_dict, network_definitions): - networks = [] + networks = {} + aliases = service_dict.get('network_aliases', {}) for name in get_network_names_for_service(service_dict): + log.debug(name) network = network_definitions.get(name) if network: - networks.append(network.full_name) + log.debug(aliases) + networks[network.full_name] = aliases.get(name, []) else: raise ConfigurationError( 'Service "{}" uses an undefined network "{}"' .format(service_dict['name'], name)) + log.debug(networks) return networks diff --git a/compose/project.py b/compose/project.py index 62e1d2cd3..0394fa15a 100644 --- a/compose/project.py +++ b/compose/project.py @@ -69,11 +69,11 @@ class Project(object): if use_networking: service_networks = get_networks(service_dict, networks) else: - service_networks = [] + service_networks = {} service_dict.pop('networks', None) links = project.get_links(service_dict) - network_mode = project.get_network_mode(service_dict, service_networks) + network_mode = project.get_network_mode(service_dict, service_networks.keys()) volumes_from = get_volumes_from(project, service_dict) if config_data.version != V1: diff --git a/compose/service.py b/compose/service.py index 38a305d36..b2e02cc84 100644 --- a/compose/service.py +++ b/compose/service.py @@ -123,7 +123,7 @@ class Service(object): self.links = links or [] self.volumes_from = volumes_from or [] self.network_mode = network_mode or NetworkMode(None) - self.networks = networks or [] + self.networks = networks or {} self.options = options def containers(self, stopped=False, one_off=False, filters={}): @@ -431,14 +431,14 @@ class Service(object): def connect_container_to_networks(self, container): connected_networks = container.get('NetworkSettings.Networks') - for network in self.networks: + for network, aliases in self.networks.items(): if network in connected_networks: self.client.disconnect_container_from_network( container.id, network) self.client.connect_container_to_network( container.id, network, - aliases=self._get_aliases(container), + aliases=list(self._get_aliases(container).union(aliases)), links=self._get_links(False), ) @@ -472,7 +472,7 @@ class Service(object): 'image_id': self.image()['Id'], 'links': self.get_link_names(), 'net': self.network_mode.id, - 'networks': self.networks, + 'networks': self.networks.keys(), 'volumes_from': [ (v.source.name, v.mode) for v in self.volumes_from if isinstance(v.source, Service) @@ -513,9 +513,9 @@ class Service(object): def _get_aliases(self, container): if container.labels.get(LABEL_ONE_OFF) == "True": - return [] + return set() - return [self.name, container.short_id] + return set([self.name, container.short_id]) def _get_links(self, link_to_self): links = {} From c686be8fd3f5b709cd244072f6d854de7056926e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Feb 2016 16:58:29 -0800 Subject: [PATCH 24/54] Test network_aliases feature Signed-off-by: Joffrey F --- compose/config/validation.py | 2 +- compose/project.py | 4 ++- tests/acceptance/cli_test.py | 27 +++++++++++++++++++++ tests/fixtures/networks/network-aliases.yml | 18 ++++++++++++++ tests/unit/config/config_test.py | 21 ++++++++++++++++ 5 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/networks/network-aliases.yml diff --git a/compose/config/validation.py b/compose/config/validation.py index 539291509..59ce9f54e 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -97,7 +97,7 @@ def match_network_aliases(service_dict): for n in aliased_networks: if n not in networks: raise ConfigurationError( - 'Network "{0}" is referenced in network_aliases, but is not' + 'Network "{0}" is referenced in network_aliases, but is not ' 'declared in the networks list for service "{1}"'.format( n, service_dict.get('name') ) diff --git a/compose/project.py b/compose/project.py index 0394fa15a..cfb11aa05 100644 --- a/compose/project.py +++ b/compose/project.py @@ -73,7 +73,9 @@ class Project(object): service_dict.pop('networks', None) links = project.get_links(service_dict) - network_mode = project.get_network_mode(service_dict, service_networks.keys()) + network_mode = project.get_network_mode( + service_dict, list(service_networks.keys()) + ) volumes_from = get_volumes_from(project, service_dict) if config_data.version != V1: diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index ea3d132a5..49048fb79 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -445,6 +445,33 @@ class CLITestCase(DockerClientTestCase): assert networks[0]['Options']['com.docker.network.bridge.enable_icc'] == 'false' + def test_up_with_network_aliases(self): + filename = 'network-aliases.yml' + self.base_dir = 'tests/fixtures/networks' + self.dispatch(['-f', filename, 'up', '-d'], None) + back_name = '{}_back'.format(self.project.name) + front_name = '{}_front'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('{}_'.format(self.project.name)) + ] + + # Two networks were created: back and front + assert sorted(n['Name'] for n in networks) == [back_name, front_name] + web_container = self.project.get_service('web').containers()[0] + + back_aliases = web_container.get( + 'NetworkSettings.Networks.{}.Aliases'.format(back_name) + ) + assert 'web' in back_aliases + front_aliases = web_container.get( + 'NetworkSettings.Networks.{}.Aliases'.format(front_name) + ) + assert 'web' in front_aliases + assert 'forward_facing' in front_aliases + assert 'ahead' in front_aliases + @v2_only() def test_up_with_networks(self): self.base_dir = 'tests/fixtures/networks' diff --git a/tests/fixtures/networks/network-aliases.yml b/tests/fixtures/networks/network-aliases.yml new file mode 100644 index 000000000..987b0809a --- /dev/null +++ b/tests/fixtures/networks/network-aliases.yml @@ -0,0 +1,18 @@ +version: "2" + +services: + web: + image: busybox + command: top + networks: + - front + - back + + network_aliases: + front: + - forward_facing + - ahead + +networks: + front: {} + back: {} diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 5f7633d90..4061272c0 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -543,6 +543,27 @@ class ConfigTest(unittest.TestCase): assert services[1]['name'] == 'db' assert services[2]['name'] == 'web' + def test_invalid_network_alias(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'web': { + 'image': 'busybox', + 'networks': ['hello'], + 'network_aliases': { + 'world': ['planet', 'universe'] + } + } + }, + 'networks': { + 'hello': {}, + 'world': {} + } + }) + with pytest.raises(ConfigurationError) as exc: + config.load(config_details) + assert 'not declared in the networks list' in exc.exconly() + def test_config_build_configuration(self): service = config.load( build_config_details( From 353da73eaba566e78857cc7d8965eb6e68218c4b Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Feb 2016 17:17:31 -0800 Subject: [PATCH 25/54] Document network_aliases config Signed-off-by: Joffrey F --- docs/compose-file.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index 240fea1e1..a6963b6c0 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -451,9 +451,27 @@ id. net: "none" net: "container:[service name or container name/id]" +### network_aliases + +> [Version 2 file format](#version-2) only. + +Alias names for this service on each joined network. All networks referenced +here must also appear under the `networks` key. + + networks: + - some-network + - other-network + network_aliases: + some-network: + - alias1 + - alias3 + other-network: + - alias2 + - alias4 + ### network_mode -> [Version 2 file format](#version-1) only. In version 1, use [net](#net). +> [Version 2 file format](#version-2) only. In version 1, use [net](#net). Network mode. Use the same values as the docker client `--net` parameter, plus the special form `service:[service name]`. From 6ac6860dda78503ffe105a77bffaa042ebfbc1d1 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Feb 2016 17:20:18 -0800 Subject: [PATCH 26/54] Fix network list serialization in py3 Signed-off-by: Joffrey F --- compose/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/service.py b/compose/service.py index b2e02cc84..971062014 100644 --- a/compose/service.py +++ b/compose/service.py @@ -472,7 +472,7 @@ class Service(object): 'image_id': self.image()['Id'], 'links': self.get_link_names(), 'net': self.network_mode.id, - 'networks': self.networks.keys(), + 'networks': list(self.networks.keys()), 'volumes_from': [ (v.source.name, v.mode) for v in self.volumes_from if isinstance(v.source, Service) From 42cb719b528f258bd343053c5b67ea660506e393 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 12 Feb 2016 17:48:42 -0800 Subject: [PATCH 27/54] Add v2_only decorator to network aliases test Signed-off-by: Joffrey F --- tests/acceptance/cli_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 49048fb79..4ba48d45a 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -445,6 +445,7 @@ class CLITestCase(DockerClientTestCase): assert networks[0]['Options']['com.docker.network.bridge.enable_icc'] == 'false' + @v2_only() def test_up_with_network_aliases(self): filename = 'network-aliases.yml' self.base_dir = 'tests/fixtures/networks' From e5689afe4cd0343e848f0974d7bb4109cb7c5785 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Feb 2016 12:04:07 -0800 Subject: [PATCH 28/54] Network aliases are now part of the network dictionary Signed-off-by: Joffrey F --- compose/config/config.py | 3 -- compose/config/service_schema_v2.0.json | 30 +++++++++++++------- compose/config/validation.py | 13 --------- compose/network.py | 28 +++++++++++-------- docs/compose-file.md | 31 +++++++++------------ tests/fixtures/networks/network-aliases.yml | 10 +++---- tests/unit/config/config_test.py | 21 -------------- 7 files changed, 54 insertions(+), 82 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 08324c496..2745ca429 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -30,7 +30,6 @@ from .types import ServiceLink from .types import VolumeFromSpec from .types import VolumeSpec from .validation import match_named_volumes -from .validation import match_network_aliases from .validation import validate_against_fields_schema from .validation import validate_against_service_schema from .validation import validate_depends_on @@ -536,8 +535,6 @@ def validate_service(service_config, service_names, version): validate_network_mode(service_config, service_names) validate_depends_on(service_config, service_names) - match_network_aliases(service_config.config) - if not service_dict.get('image') and has_uppercase(service_name): raise ConfigurationError( "Service '{name}' contains uppercase characters which are not valid " diff --git a/compose/config/service_schema_v2.0.json b/compose/config/service_schema_v2.0.json index 1fcfc19d3..98ae90a27 100644 --- a/compose/config/service_schema_v2.0.json +++ b/compose/config/service_schema_v2.0.json @@ -107,18 +107,28 @@ "network_mode": {"type": "string"}, "networks": { - "$ref": "#/definitions/list_of_strings" - }, - "network_aliases": { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/list_of_strings" + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false } - }, - "additionalProperties": false + ] }, - "pid": {"type": ["string", "null"]}, "ports": { diff --git a/compose/config/validation.py b/compose/config/validation.py index 59ce9f54e..35727e2cc 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -91,19 +91,6 @@ def match_named_volumes(service_dict, project_volumes): ) -def match_network_aliases(service_dict): - networks = service_dict.get('networks', []) - aliased_networks = service_dict.get('network_aliases', {}).keys() - for n in aliased_networks: - if n not in networks: - raise ConfigurationError( - 'Network "{0}" is referenced in network_aliases, but is not ' - 'declared in the networks list for service "{1}"'.format( - n, service_dict.get('name') - ) - ) - - def validate_top_level_service_objects(filename, service_dicts): """Perform some high level validation of the service name and value. diff --git a/compose/network.py b/compose/network.py index 99c04649b..d17ed0805 100644 --- a/compose/network.py +++ b/compose/network.py @@ -15,7 +15,7 @@ log = logging.getLogger(__name__) class Network(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - ipam=None, external_name=None, aliases=None): + ipam=None, external_name=None): self.client = client self.project = project self.name = name @@ -23,7 +23,6 @@ class Network(object): self.driver_opts = driver_opts self.ipam = create_ipam_config_from_dict(ipam) self.external_name = external_name - self.aliases = aliases or [] def ensure(self): if self.external_name: @@ -160,25 +159,32 @@ class ProjectNetworks(object): network.ensure() -def get_network_names_for_service(service_dict): +def get_network_aliases_for_service(service_dict): if 'network_mode' in service_dict: - return [] - return service_dict.get('networks', ['default']) + return {} + networks = service_dict.get('networks', ['default']) + if isinstance(networks, list): + return dict((net, []) for net in networks) + + return dict( + (net, (config or {}).get('aliases', [])) + for net, config in networks.items() + ) + + +def get_network_names_for_service(service_dict): + return get_network_aliases_for_service(service_dict).keys() def get_networks(service_dict, network_definitions): networks = {} - aliases = service_dict.get('network_aliases', {}) - for name in get_network_names_for_service(service_dict): - log.debug(name) + for name, aliases in get_network_aliases_for_service(service_dict).items(): network = network_definitions.get(name) if network: - log.debug(aliases) - networks[network.full_name] = aliases.get(name, []) + networks[network.full_name] = aliases else: raise ConfigurationError( 'Service "{}" uses an undefined network "{}"' .format(service_dict['name'], name)) - log.debug(networks) return networks diff --git a/docs/compose-file.md b/docs/compose-file.md index a6963b6c0..38960cc72 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -451,24 +451,6 @@ id. net: "none" net: "container:[service name or container name/id]" -### network_aliases - -> [Version 2 file format](#version-2) only. - -Alias names for this service on each joined network. All networks referenced -here must also appear under the `networks` key. - - networks: - - some-network - - other-network - network_aliases: - some-network: - - alias1 - - alias3 - other-network: - - alias2 - - alias4 - ### network_mode > [Version 2 file format](#version-2) only. In version 1, use [net](#net). @@ -493,6 +475,19 @@ Networks to join, referencing entries under the - some-network - other-network +#### aliases + +Alias names for this service on the specified network. + + networks: + some-network: + aliases: + - alias1 + - alias3 + other-network: + aliases: + - alias2 + ### pid pid: "host" diff --git a/tests/fixtures/networks/network-aliases.yml b/tests/fixtures/networks/network-aliases.yml index 987b0809a..8cf7d5af9 100644 --- a/tests/fixtures/networks/network-aliases.yml +++ b/tests/fixtures/networks/network-aliases.yml @@ -5,13 +5,11 @@ services: image: busybox command: top networks: - - front - - back - - network_aliases: front: - - forward_facing - - ahead + aliases: + - forward_facing + - ahead + back: networks: front: {} diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 4061272c0..5f7633d90 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -543,27 +543,6 @@ class ConfigTest(unittest.TestCase): assert services[1]['name'] == 'db' assert services[2]['name'] == 'web' - def test_invalid_network_alias(self): - config_details = build_config_details({ - 'version': '2', - 'services': { - 'web': { - 'image': 'busybox', - 'networks': ['hello'], - 'network_aliases': { - 'world': ['planet', 'universe'] - } - } - }, - 'networks': { - 'hello': {}, - 'world': {} - } - }) - with pytest.raises(ConfigurationError) as exc: - config.load(config_details) - assert 'not declared in the networks list' in exc.exconly() - def test_config_build_configuration(self): service = config.load( build_config_details( From f4a22b94eddbe610acf214861c8b2c1690d32dfd Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Feb 2016 14:52:52 -0800 Subject: [PATCH 29/54] Handle mismatched network formats in config files Signed-off-by: Joffrey F --- compose/config/config.py | 6 ++- compose/network.py | 5 +-- docs/compose-file.md | 10 ++++- tests/acceptance/cli_test.py | 2 +- tests/integration/project_test.py | 4 +- tests/unit/config/config_test.py | 64 +++++++++++++++++++++++++++++++ tests/unit/project_test.py | 2 +- 7 files changed, 83 insertions(+), 10 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 2745ca429..2a0df9453 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -601,6 +601,9 @@ def finalize_service(service_config, service_names, version): else: service_dict['network_mode'] = network_mode + if 'networks' in service_dict: + service_dict['networks'] = parse_networks(service_dict['networks']) + if 'restart' in service_dict: service_dict['restart'] = parse_restart_spec(service_dict['restart']) @@ -690,6 +693,7 @@ def merge_service_dicts(base, override, version): md.merge_mapping('environment', parse_environment) md.merge_mapping('labels', parse_labels) md.merge_mapping('ulimits', parse_ulimits) + md.merge_mapping('networks', parse_networks) md.merge_sequence('links', ServiceLink.parse) for field in ['volumes', 'devices']: @@ -699,7 +703,6 @@ def merge_service_dicts(base, override, version): 'depends_on', 'expose', 'external_links', - 'networks', 'ports', 'volumes_from', ]: @@ -787,6 +790,7 @@ def parse_dict_or_list(split_func, type_name, arguments): parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments') parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment') parse_labels = functools.partial(parse_dict_or_list, split_label, 'labels') +parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks') def parse_ulimits(ulimits): diff --git a/compose/network.py b/compose/network.py index d17ed0805..135502cc0 100644 --- a/compose/network.py +++ b/compose/network.py @@ -162,10 +162,7 @@ class ProjectNetworks(object): def get_network_aliases_for_service(service_dict): if 'network_mode' in service_dict: return {} - networks = service_dict.get('networks', ['default']) - if isinstance(networks, list): - return dict((net, []) for net in networks) - + networks = service_dict.get('networks', {'default': None}) return dict( (net, (config or {}).get('aliases', [])) for net, config in networks.items() diff --git a/docs/compose-file.md b/docs/compose-file.md index 38960cc72..c2c0f1952 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -477,7 +477,15 @@ Networks to join, referencing entries under the #### aliases -Alias names for this service on the specified network. +Aliases (alternative hostnames) for this service on the network. Other servers +on the network can use either the service name or this alias to connect to +this service. Since `alias` is network-scoped: + + * the same service can have different aliases when connected to another + network. + * it is allowable to configure the same alias name to multiple containers + (services) on the same network. + networks: some-network: diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 4ba48d45a..318ab3d3f 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -185,7 +185,7 @@ class CLITestCase(DockerClientTestCase): 'build': { 'context': os.path.abspath(self.base_dir), }, - 'networks': ['front', 'default'], + 'networks': {'front': None, 'default': None}, 'volumes_from': ['service:other:rw'], }, 'other': { diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 6bb076a3f..6542fa18e 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -565,7 +565,7 @@ class ProjectTest(DockerClientTestCase): 'name': 'web', 'image': 'busybox:latest', 'command': 'top', - 'networks': ['foo', 'bar', 'baz'], + 'networks': {'foo': None, 'bar': None, 'baz': None}, }], volumes={}, networks={ @@ -598,7 +598,7 @@ class ProjectTest(DockerClientTestCase): services=[{ 'name': 'web', 'image': 'busybox:latest', - 'networks': ['front'], + 'networks': {'front': None}, }], volumes={}, networks={ diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 5f7633d90..59dd73648 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -608,6 +608,70 @@ class ConfigTest(unittest.TestCase): self.assertTrue('context' in service[0]['build']) self.assertEqual(service[0]['build']['dockerfile'], 'Dockerfile-alt') + def test_load_with_buildargs(self): + service = config.load( + build_config_details( + { + 'version': '2', + 'services': { + 'web': { + 'build': { + 'context': '.', + 'dockerfile': 'Dockerfile-alt', + 'args': { + 'opt1': 42, + 'opt2': 'foobar' + } + } + } + } + }, + 'tests/fixtures/extends', + 'filename.yml' + ) + ).services[0] + assert 'args' in service['build'] + assert 'opt1' in service['build']['args'] + assert isinstance(service['build']['args']['opt1'], str) + assert service['build']['args']['opt1'] == '42' + assert service['build']['args']['opt2'] == 'foobar' + + def test_load_with_multiple_files_mismatched_networks_format(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'image': 'example/web', + 'networks': { + 'foobar': {'aliases': ['foo', 'bar']} + } + } + }, + 'networks': {'foobar': {}, 'baz': {}} + } + ) + + override_file = config.ConfigFile( + 'override.yaml', + { + 'version': '2', + 'services': { + 'web': { + 'networks': ['baz'] + } + } + } + ) + + details = config.ConfigDetails('.', [base_file, override_file]) + web_service = config.load(details).services[0] + assert web_service['networks'] == { + 'foobar': {'aliases': ['foo', 'bar']}, + 'baz': None + } + def test_load_with_multiple_files_v2(self): base_file = config.ConfigFile( 'base.yaml', diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index bec238de6..c28c21523 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -438,7 +438,7 @@ class ProjectTest(unittest.TestCase): { 'name': 'foo', 'image': 'busybox:latest', - 'networks': ['custom'] + 'networks': {'custom': None} }, ], networks={'custom': {}}, From 654b3710f7833ad6034a1306e5ad5d6b95c8cf0d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Feb 2016 15:28:12 -0800 Subject: [PATCH 30/54] Use modern set notation in _get_aliases Signed-off-by: Joffrey F --- compose/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/service.py b/compose/service.py index 971062014..129c26753 100644 --- a/compose/service.py +++ b/compose/service.py @@ -515,7 +515,7 @@ class Service(object): if container.labels.get(LABEL_ONE_OFF) == "True": return set() - return set([self.name, container.short_id]) + return {self.name, container.short_id} def _get_links(self, link_to_self): links = {} From ed5fedf516effbb0b819887d84af7bc267c5998b Mon Sep 17 00:00:00 2001 From: Chia-liang Kao Date: Sun, 14 Feb 2016 01:42:51 +0800 Subject: [PATCH 31/54] Don't mount pwd if it is / Signed-off-by: Chia-liang Kao --- script/run.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/script/run.sh b/script/run.sh index 784228e15..16778cdd9 100755 --- a/script/run.sh +++ b/script/run.sh @@ -31,7 +31,9 @@ fi # Setup volume mounts for compose config and context -VOLUMES="-v $(pwd):$(pwd)" +if [ "$(pwd)" != '/' ]; then + VOLUMES="-v $(pwd):$(pwd)" +fi if [ -n "$COMPOSE_FILE" ]; then compose_dir=$(dirname $COMPOSE_FILE) fi @@ -50,4 +52,4 @@ else DOCKER_RUN_OPTIONS="-i" fi -exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w $(pwd) $IMAGE $@ +exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE $@ From 674e541cf73b999f49b33e0e8430c18600352561 Mon Sep 17 00:00:00 2001 From: Chia-liang Kao Date: Sun, 14 Feb 2016 01:43:06 +0800 Subject: [PATCH 32/54] Detect -t and -i separately Signed-off-by: Chia-liang Kao --- script/run.sh | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/script/run.sh b/script/run.sh index 16778cdd9..a6d04885d 100755 --- a/script/run.sh +++ b/script/run.sh @@ -47,9 +47,10 @@ fi # Only allocate tty if we detect one if [ -t 1 ]; then - DOCKER_RUN_OPTIONS="-ti" -else - DOCKER_RUN_OPTIONS="-i" + DOCKER_RUN_OPTIONS="-t" +fi +if [ -t 0 ]; then + DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i" fi exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE $@ From f0a8c65b0599a3a9c55fe570d28e40339d40b102 Mon Sep 17 00:00:00 2001 From: Chia-liang Kao Date: Sun, 14 Feb 2016 01:57:04 +0800 Subject: [PATCH 33/54] Quote argv as they are Signed-off-by: Chia-liang Kao --- script/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/run.sh b/script/run.sh index a6d04885d..749481f6c 100755 --- a/script/run.sh +++ b/script/run.sh @@ -53,4 +53,4 @@ if [ -t 0 ]; then DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i" fi -exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE $@ +exec docker run --rm $DOCKER_RUN_OPTIONS $DOCKER_ADDR $COMPOSE_OPTIONS $VOLUMES -w "$(pwd)" $IMAGE "$@" From f59fef09a6d0a50a62cb0562fc7d941fb9ee93ed Mon Sep 17 00:00:00 2001 From: Anthon van der Neut Date: Tue, 16 Feb 2016 14:46:47 +0100 Subject: [PATCH 34/54] reset colors after warning If a warning is shown, and you happen to have no color setting in your (bash) prompt, the \033[37m setting, stays active. With the message hardly readable (light grey on my default light yellow background), that means the prompt is barely visible and you need to do `tput reset`. Would probably be better if the background color was set as well in case you have dark on light theme by default in your terminal. Signed-off-by: Anthon van der Neut --- contrib/migration/migrate-compose-file-v1-to-v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/migration/migrate-compose-file-v1-to-v2.py b/contrib/migration/migrate-compose-file-v1-to-v2.py index 4f9be97f4..387e9ef4a 100755 --- a/contrib/migration/migrate-compose-file-v1-to-v2.py +++ b/contrib/migration/migrate-compose-file-v1-to-v2.py @@ -155,7 +155,7 @@ def parse_opts(args): def main(args): - logging.basicConfig(format='\033[33m%(levelname)s:\033[37m %(message)s\n') + logging.basicConfig(format='\033[33m%(levelname)s:\033[37m %(message)s\033[0m\n') opts = parse_opts(args) From 13ec3d0217939e375fca87ea1899d766a1b30021 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 16 Feb 2016 14:24:17 -0500 Subject: [PATCH 35/54] Fix copying of volumes by using the name of the volume instead of the host path. Signed-off-by: Daniel Nephin --- compose/service.py | 2 +- tests/integration/service_test.py | 24 ++++++++++++++++++++++++ tests/unit/service_test.py | 17 +++++++++-------- 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/compose/service.py b/compose/service.py index 129c26753..f9424e8fa 100644 --- a/compose/service.py +++ b/compose/service.py @@ -927,7 +927,7 @@ def get_container_data_volumes(container, volumes_option): continue # Copy existing volume from old container - volume = volume._replace(external=mount['Source']) + volume = volume._replace(external=mount['Name']) volumes.append(volume) return volumes diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 37dc4a0e5..647d36daa 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -266,6 +266,30 @@ class ServiceTest(DockerClientTestCase): self.client.inspect_container, old_container.id) + def test_execute_convergence_plan_recreate_twice(self): + service = self.create_service( + 'db', + volumes=[VolumeSpec.parse('/etc')], + entrypoint=['top'], + command=['-d', '1']) + + orig_container = service.create_container() + service.start_container(orig_container) + + orig_container.inspect() # reload volume data + volume_path = orig_container.get_mount('/etc')['Source'] + + # Do this twice to reproduce the bug + for _ in range(2): + new_container, = service.execute_convergence_plan( + ConvergencePlan('recreate', [orig_container])) + + assert new_container.get_mount('/etc')['Source'] == volume_path + assert ('affinity:container==%s' % orig_container.id in + new_container.get('Config.Env')) + + orig_container = new_container + def test_execute_convergence_plan_when_containers_are_stopped(self): service = self.create_service( 'db', diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 62f7f0042..ce28a9ca4 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -731,8 +731,8 @@ class ServiceVolumesTest(unittest.TestCase): }, has_been_inspected=True) expected = [ - VolumeSpec.parse('/var/lib/docker/aaaaaaaa:/existing/volume:rw'), - VolumeSpec.parse('/var/lib/docker/cccccccc:/mnt/image/data:rw'), + VolumeSpec.parse('existingvolume:/existing/volume:rw'), + VolumeSpec.parse('imagedata:/mnt/image/data:rw'), ] volumes = get_container_data_volumes(container, options) @@ -765,7 +765,7 @@ class ServiceVolumesTest(unittest.TestCase): expected = [ '/host/volume:/host/volume:ro', '/host/rw/volume:/host/rw/volume:rw', - '/var/lib/docker/aaaaaaaa:/existing/volume:rw', + 'existingvolume:/existing/volume:rw', ] binds, affinity = merge_volume_bindings(options, previous_container) @@ -803,13 +803,14 @@ class ServiceVolumesTest(unittest.TestCase): ]), ) - def test_different_host_path_in_container_json(self): + def test_get_container_create_options_with_different_host_path_in_container_json(self): service = Service( 'web', image='busybox', volumes=[VolumeSpec.parse('/host/path:/data')], client=self.mock_client, ) + volume_name = 'abcdefff1234' self.mock_client.inspect_image.return_value = { 'Id': 'ababab', @@ -830,7 +831,7 @@ class ServiceVolumesTest(unittest.TestCase): 'Mode': '', 'RW': True, 'Driver': 'local', - 'Name': 'abcdefff1234' + 'Name': volume_name, }, ] } @@ -841,9 +842,9 @@ class ServiceVolumesTest(unittest.TestCase): previous_container=Container(self.mock_client, {'Id': '123123123'}), ) - self.assertEqual( - self.mock_client.create_host_config.call_args[1]['binds'], - ['/mnt/sda1/host/path:/data:rw'], + assert ( + self.mock_client.create_host_config.call_args[1]['binds'] == + ['{}:/data:rw'.format(volume_name)] ) def test_warn_on_masked_volume_no_warning_when_no_container_volumes(self): From 6d2aa80435282890af79ff73a78854c1a904bba6 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 16 Feb 2016 16:34:31 -0500 Subject: [PATCH 36/54] Update link to docker volume create docs. Signed-off-by: Daniel Nephin --- docs/compose-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index c2c0f1952..f27457fcb 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -646,7 +646,7 @@ While it is possible to declare volumes on the fly as part of the service declaration, this section allows you to create named volumes that can be reused across multiple services (without relying on `volumes_from`), and are easily retrieved and inspected using the docker command line or API. -See the [docker volume](http://docs.docker.com/reference/commandline/volume/) +See the [docker volume](/engine/reference/commandline/volume_create.md) subcommand documentation for more information. ### driver From 8d7b1e9047fc62897560541800ce7bf76b937547 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 16 Feb 2016 16:38:31 -0500 Subject: [PATCH 37/54] Update guides to use v2 config format. Signed-off-by: Daniel Nephin --- docs/django.md | 24 +++++++++++++----------- docs/gettingstarted.md | 23 +++++++++++++---------- docs/rails.md | 24 +++++++++++++----------- docs/wordpress.md | 28 +++++++++++++++------------- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/docs/django.md b/docs/django.md index 150d36317..e616d0e12 100644 --- a/docs/django.md +++ b/docs/django.md @@ -72,17 +72,19 @@ and a `docker-compose.yml` file. 9. Add the following configuration to the file. - db: - image: postgres - web: - build: . - command: python manage.py runserver 0.0.0.0:8000 - volumes: - - .:/code - ports: - - "8000:8000" - links: - - db + version: '2' + services: + db: + image: postgres + web: + build: . + command: python manage.py runserver 0.0.0.0:8000 + volumes: + - .:/code + ports: + - "8000:8000" + depends_on: + - db This file defines two services: The `db` service and the `web` service. diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 1939500c2..36577f075 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -95,16 +95,19 @@ Define a set of services using `docker-compose.yml`: 1. Create a file called docker-compose.yml in your project directory and add the following: - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - links: - - redis - redis: - image: redis + + version: '2' + services: + web: + build: . + ports: + - "5000:5000" + volumes: + - .:/code + depends_on: + - redis + redis: + image: redis This Compose file defines two services, `web` and `redis`. The web service: diff --git a/docs/rails.md b/docs/rails.md index f7634a6d6..8b7b4fd91 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -43,17 +43,19 @@ You'll need an empty `Gemfile.lock` in order to build our `Dockerfile`. Finally, `docker-compose.yml` is where the magic happens. This file describes the services that comprise your app (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 needed to link them together and expose the web app's port. - db: - image: postgres - web: - build: . - command: bundle exec rails s -p 3000 -b '0.0.0.0' - volumes: - - .:/myapp - ports: - - "3000:3000" - links: - - db + version: '2' + services: + db: + image: postgres + web: + build: . + command: bundle exec rails s -p 3000 -b '0.0.0.0' + volumes: + - .:/myapp + ports: + - "3000:3000" + depends_on: + - db ### Build the project diff --git a/docs/wordpress.md b/docs/wordpress.md index 503622538..62aec2518 100644 --- a/docs/wordpress.md +++ b/docs/wordpress.md @@ -41,19 +41,21 @@ and WordPress. Next you'll create a `docker-compose.yml` file that will start your web service and a separate MySQL instance: - web: - build: . - command: php -S 0.0.0.0:8000 -t /code - ports: - - "8000:8000" - links: - - db - volumes: - - .:/code - db: - image: orchardup/mysql - environment: - MYSQL_DATABASE: wordpress + version: '2' + services: + web: + build: . + command: php -S 0.0.0.0:8000 -t /code + ports: + - "8000:8000" + depends_on: + - db + volumes: + - .:/code + db: + image: orchardup/mysql + environment: + MYSQL_DATABASE: wordpress A supporting file is needed to get this working. `wp-config.php` is the standard WordPress config file with a single change to point the database From fcf78fe3dea7df1dd20189dc2c168ab2a0ff62ac Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 16 Feb 2016 16:48:04 -0800 Subject: [PATCH 38/54] Constraint build argument types. Numbers are cast into strings Numerical driver_opts are also valid and typecast into strings. Additional config tests. Signed-off-by: Joffrey F --- compose/config/config.py | 13 +++++++++++-- compose/config/fields_schema_v2.0.json | 2 +- compose/config/service_schema_v2.0.json | 15 ++++++++++++++- compose/utils.py | 4 ++++ tests/unit/config/config_test.py | 15 ++++++++++++++- 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 2a0df9453..46ee2e28e 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -16,6 +16,7 @@ from cached_property import cached_property from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V2_0 as V2_0 +from ..utils import build_string_dict from .errors import CircularReference from .errors import ComposeFileNotFound from .errors import ConfigurationError @@ -291,7 +292,7 @@ def load(config_details): config_details = config_details._replace(config_files=processed_files) main_file = config_details.config_files[0] - volumes = load_mapping(config_details.config_files, 'get_volumes', 'Volume') + volumes = load_volumes(config_details.config_files) networks = load_mapping(config_details.config_files, 'get_networks', 'Network') service_dicts = load_services( config_details.working_dir, @@ -335,6 +336,14 @@ def load_mapping(config_files, get_func, entity_type): return mapping +def load_volumes(config_files): + volumes = load_mapping(config_files, 'get_volumes', 'Volume') + for volume_name, volume in volumes.items(): + if 'driver_opts' in volume: + volume['driver_opts'] = build_string_dict(volume['driver_opts']) + return volumes + + def load_services(working_dir, config_file, service_configs): def build_service(service_name, service_dict, service_names): service_config = ServiceConfig.with_abs_paths( @@ -854,7 +863,7 @@ def normalize_build(service_dict, working_dir): else: build.update(service_dict['build']) if 'args' in build: - build['args'] = resolve_build_args(build) + build['args'] = build_string_dict(resolve_build_args(build)) service_dict['build'] = build diff --git a/compose/config/fields_schema_v2.0.json b/compose/config/fields_schema_v2.0.json index 876065e51..7703adcd0 100644 --- a/compose/config/fields_schema_v2.0.json +++ b/compose/config/fields_schema_v2.0.json @@ -78,7 +78,7 @@ "driver_opts": { "type": "object", "patternProperties": { - "^.+$": {"type": "string"} + "^.+$": {"type": ["string", "number"]} } }, "external": { diff --git a/compose/config/service_schema_v2.0.json b/compose/config/service_schema_v2.0.json index 98ae90a27..4c5c40fbc 100644 --- a/compose/config/service_schema_v2.0.json +++ b/compose/config/service_schema_v2.0.json @@ -23,7 +23,20 @@ "properties": { "context": {"type": "string"}, "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} + "args": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^.+$": { + "type": ["string", "number"] + } + }, + "additionalProperties": false + } + ] + } }, "additionalProperties": false } diff --git a/compose/utils.py b/compose/utils.py index 29d8a695d..669df1d20 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -92,3 +92,7 @@ def json_hash(obj): def microseconds_from_time_nano(time_nano): return int(time_nano % 1000000000 / 1000) + + +def build_string_dict(source_dict): + return dict([(k, str(v)) for k, v in source_dict.items()]) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 59dd73648..204003bce 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -231,7 +231,7 @@ class ConfigTest(unittest.TestCase): assert volumes['simple'] == {} assert volumes['other'] == {} - def test_volume_invalid_driver_opt(self): + def test_volume_numeric_driver_opt(self): config_details = build_config_details({ 'version': '2', 'services': { @@ -241,6 +241,19 @@ class ConfigTest(unittest.TestCase): 'simple': {'driver_opts': {'size': 42}}, } }) + cfg = config.load(config_details) + assert cfg.volumes['simple']['driver_opts']['size'] == '42' + + def test_volume_invalid_driver_opt(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'simple': {'image': 'busybox'} + }, + 'volumes': { + 'simple': {'driver_opts': {'size': True}}, + } + }) with pytest.raises(ConfigurationError) as exc: config.load(config_details) assert 'driver_opts.size contains an invalid type' in exc.exconly() From 1e29ad9fc7d7e096ce15ba0c5f07ffbee9d3a651 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Feb 2016 10:53:40 -0800 Subject: [PATCH 39/54] Apply driver_opts processing to network configs Signed-off-by: Joffrey F --- compose/config/config.py | 21 +++++++++++---------- compose/utils.py | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 46ee2e28e..119e98f46 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -292,8 +292,12 @@ def load(config_details): config_details = config_details._replace(config_files=processed_files) main_file = config_details.config_files[0] - volumes = load_volumes(config_details.config_files) - networks = load_mapping(config_details.config_files, 'get_networks', 'Network') + volumes = load_mapping( + config_details.config_files, 'get_volumes', 'Volume' + ) + networks = load_mapping( + config_details.config_files, 'get_networks', 'Network' + ) service_dicts = load_services( config_details.working_dir, main_file, @@ -333,17 +337,14 @@ def load_mapping(config_files, get_func, entity_type): mapping[name] = config + if 'driver_opts' in config: + config['driver_opts'] = build_string_dict( + config['driver_opts'] + ) + return mapping -def load_volumes(config_files): - volumes = load_mapping(config_files, 'get_volumes', 'Volume') - for volume_name, volume in volumes.items(): - if 'driver_opts' in volume: - volume['driver_opts'] = build_string_dict(volume['driver_opts']) - return volumes - - def load_services(working_dir, config_file, service_configs): def build_service(service_name, service_dict, service_names): service_config = ServiceConfig.with_abs_paths( diff --git a/compose/utils.py b/compose/utils.py index 669df1d20..494beea34 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -95,4 +95,4 @@ def microseconds_from_time_nano(time_nano): def build_string_dict(source_dict): - return dict([(k, str(v)) for k, v in source_dict.items()]) + return dict((k, str(v)) for k, v in source_dict.items()) From cfda9d844ef7d09ed4ad8b55fadc7d652694412f Mon Sep 17 00:00:00 2001 From: Anthon van der Neut Date: Wed, 17 Feb 2016 09:56:49 +0100 Subject: [PATCH 40/54] for 1.6.0 the version value needs to be a string After conversion a file would immediately not load in docker-compose 1.6.0 with the message: ERROR: Version in "./converted.yml" is invalid - it should be a string. Signed-off-by: Anthon van der Neut anthon@mnt.org Signed-off-by: Anthon van der Neut --- contrib/migration/migrate-compose-file-v1-to-v2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/migration/migrate-compose-file-v1-to-v2.py b/contrib/migration/migrate-compose-file-v1-to-v2.py index 387e9ef4a..c1785b0da 100755 --- a/contrib/migration/migrate-compose-file-v1-to-v2.py +++ b/contrib/migration/migrate-compose-file-v1-to-v2.py @@ -33,7 +33,7 @@ def migrate(content): services = {name: data.pop(name) for name in data.keys()} - data['version'] = 2 + data['version'] = "2" data['services'] = services create_volumes_section(data) From 7d22809ef4bbfa26dabdb92c06331f95fccf69b9 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 16 Feb 2016 17:30:23 -0500 Subject: [PATCH 41/54] Validate that each section of the config is a mapping before running interpolation. Signed-off-by: Daniel Nephin --- compose/config/config.py | 31 +++++++++++------ compose/config/interpolation.py | 2 +- compose/config/validation.py | 59 ++++++++++++++++++++++---------- tests/acceptance/cli_test.py | 2 +- tests/unit/config/config_test.py | 34 +++++++++++++++--- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 119e98f46..1daae9818 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -33,11 +33,11 @@ from .types import VolumeSpec from .validation import match_named_volumes from .validation import validate_against_fields_schema from .validation import validate_against_service_schema +from .validation import validate_config_section from .validation import validate_depends_on from .validation import validate_extends_file_path from .validation import validate_network_mode from .validation import validate_top_level_object -from .validation import validate_top_level_service_objects from .validation import validate_ulimits @@ -387,22 +387,31 @@ def load_services(working_dir, config_file, service_configs): return build_services(service_config) -def process_config_file(config_file, service_name=None): - service_dicts = config_file.get_service_dicts() - validate_top_level_service_objects(config_file.filename, service_dicts) +def interpolate_config_section(filename, config, section): + validate_config_section(filename, config, section) + return interpolate_environment_variables(config, section) - interpolated_config = interpolate_environment_variables(service_dicts, 'service') + +def process_config_file(config_file, service_name=None): + services = interpolate_config_section( + config_file.filename, + config_file.get_service_dicts(), + 'service') if config_file.version == V2_0: processed_config = dict(config_file.config) - processed_config['services'] = services = interpolated_config - processed_config['volumes'] = interpolate_environment_variables( - config_file.get_volumes(), 'volume') - processed_config['networks'] = interpolate_environment_variables( - config_file.get_networks(), 'network') + processed_config['services'] = services + processed_config['volumes'] = interpolate_config_section( + config_file.filename, + config_file.get_volumes(), + 'volume') + processed_config['networks'] = interpolate_config_section( + config_file.filename, + config_file.get_networks(), + 'network') if config_file.version == V1: - processed_config = services = interpolated_config + processed_config = services config_file = config_file._replace(config=processed_config) validate_against_fields_schema(config_file) diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index e1c781fec..1e56ebb66 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -21,7 +21,7 @@ def interpolate_environment_variables(config, section): ) return dict( - (name, process_item(name, config_dict)) + (name, process_item(name, config_dict or {})) for name, config_dict in config.items() ) diff --git a/compose/config/validation.py b/compose/config/validation.py index 35727e2cc..557e57683 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -91,29 +91,50 @@ def match_named_volumes(service_dict, project_volumes): ) -def validate_top_level_service_objects(filename, service_dicts): - """Perform some high level validation of the service name and value. +def python_type_to_yaml_type(type_): + type_name = type(type_).__name__ + return { + 'dict': 'mapping', + 'list': 'array', + 'int': 'number', + 'float': 'number', + 'bool': 'boolean', + 'unicode': 'string', + 'str': 'string', + 'bytes': 'string', + }.get(type_name, type_name) - This validation must happen before interpolation, which must happen - before the rest of validation, which is why it's separate from the - rest of the service validation. + +def validate_config_section(filename, config, section): + """Validate the structure of a configuration section. This must be done + before interpolation so it's separate from schema validation. """ - for service_name, service_dict in service_dicts.items(): - if not isinstance(service_name, six.string_types): - raise ConfigurationError( - "In file '{}' service name: {} needs to be a string, eg '{}'".format( - filename, - service_name, - service_name)) + if not isinstance(config, dict): + raise ConfigurationError( + "In file '{filename}' {section} must be a mapping, not " + "'{type}'.".format( + filename=filename, + section=section, + type=python_type_to_yaml_type(config))) - if not isinstance(service_dict, dict): + for key, value in config.items(): + if not isinstance(key, six.string_types): raise ConfigurationError( - "In file '{}' service '{}' doesn\'t have any configuration options. " - "All top level keys in your docker-compose.yml must map " - "to a dictionary of configuration options.".format( - filename, service_name - ) - ) + "In file '{filename}' {section} name {name} needs to be a " + "string, eg '{name}'".format( + filename=filename, + section=section, + name=key)) + + if not isinstance(value, (dict, type(None))): + raise ConfigurationError( + "In file '{filename}' {section} '{name}' is the wrong type. " + "It should be a mapping of configuration options, it is a " + "'{type}'.".format( + filename=filename, + section=section, + name=key, + type=python_type_to_yaml_type(value))) def validate_top_level_object(config_file): diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 318ab3d3f..f43926939 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -159,7 +159,7 @@ class CLITestCase(DockerClientTestCase): '-f', 'tests/fixtures/invalid-composefile/invalid.yml', 'config', '-q' ], returncode=1) - assert "'notaservice' doesn't have any configuration" in result.stderr + assert "'notaservice' is the wrong type" in result.stderr # TODO: this shouldn't be v2-dependent @v2_only() diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 204003bce..c58ddc607 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -231,7 +231,7 @@ class ConfigTest(unittest.TestCase): assert volumes['simple'] == {} assert volumes['other'] == {} - def test_volume_numeric_driver_opt(self): + def test_named_volume_numeric_driver_opt(self): config_details = build_config_details({ 'version': '2', 'services': { @@ -258,6 +258,30 @@ class ConfigTest(unittest.TestCase): config.load(config_details) assert 'driver_opts.size contains an invalid type' in exc.exconly() + def test_named_volume_invalid_type_list(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'simple': {'image': 'busybox'} + }, + 'volumes': [] + }) + with pytest.raises(ConfigurationError) as exc: + config.load(config_details) + assert "volume must be a mapping, not 'array'" in exc.exconly() + + def test_networks_invalid_type_list(self): + config_details = build_config_details({ + 'version': '2', + 'services': { + 'simple': {'image': 'busybox'} + }, + 'networks': [] + }) + with pytest.raises(ConfigurationError) as exc: + config.load(config_details) + assert "network must be a mapping, not 'array'" in exc.exconly() + def test_load_service_with_name_version(self): with mock.patch('compose.config.config.log') as mock_logging: config_data = config.load( @@ -368,7 +392,7 @@ class ConfigTest(unittest.TestCase): 'filename.yml') with pytest.raises(ConfigurationError) as exc: config.load(config_details) - error_msg = "service 'web' doesn't have any configuration options" + error_msg = "service 'web' is the wrong type" assert error_msg in exc.exconly() def test_config_integer_service_name_raise_validation_error(self): @@ -381,7 +405,7 @@ class ConfigTest(unittest.TestCase): ) ) - assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \ + assert "In file 'filename.yml' service name 1 needs to be a string, eg '1'" \ in excinfo.exconly() def test_config_integer_service_name_raise_validation_error_v2(self): @@ -397,7 +421,7 @@ class ConfigTest(unittest.TestCase): ) ) - assert "In file 'filename.yml' service name: 1 needs to be a string, eg '1'" \ + assert "In file 'filename.yml' service name 1 needs to be a string, eg '1'" \ in excinfo.exconly() def test_load_with_multiple_files_v1(self): @@ -532,7 +556,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError) as exc: config.load(details) - assert "service 'bogus' doesn't have any configuration" in exc.exconly() + assert "service 'bogus' is the wrong type" in exc.exconly() assert "In file 'override.yaml'" in exc.exconly() def test_load_sorts_in_dependency_order(self): From ea8032c115ad85637edf3043d2c69edb5399368f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 Feb 2016 12:35:05 -0500 Subject: [PATCH 42/54] Make config validation error messages more consistent. Signed-off-by: Daniel Nephin --- compose/config/validation.py | 33 ++++++++++++++++---------------- tests/acceptance/cli_test.py | 2 +- tests/unit/config/config_test.py | 21 +++++++++++--------- 3 files changed, 29 insertions(+), 27 deletions(-) diff --git a/compose/config/validation.py b/compose/config/validation.py index 557e57683..6dc72f566 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -111,30 +111,29 @@ def validate_config_section(filename, config, section): """ if not isinstance(config, dict): raise ConfigurationError( - "In file '{filename}' {section} must be a mapping, not " - "'{type}'.".format( + "In file '{filename}', {section} must be a mapping, not " + "{type}.".format( filename=filename, section=section, - type=python_type_to_yaml_type(config))) + type=anglicize_json_type(python_type_to_yaml_type(config)))) for key, value in config.items(): if not isinstance(key, six.string_types): raise ConfigurationError( - "In file '{filename}' {section} name {name} needs to be a " - "string, eg '{name}'".format( + "In file '{filename}', the {section} name {name} must be a " + "quoted string, i.e. '{name}'.".format( filename=filename, section=section, name=key)) if not isinstance(value, (dict, type(None))): raise ConfigurationError( - "In file '{filename}' {section} '{name}' is the wrong type. " - "It should be a mapping of configuration options, it is a " - "'{type}'.".format( + "In file '{filename}', {section} '{name}' must be a mapping not " + "{type}.".format( filename=filename, section=section, name=key, - type=python_type_to_yaml_type(value))) + type=anglicize_json_type(python_type_to_yaml_type(value)))) def validate_top_level_object(config_file): @@ -203,10 +202,10 @@ def get_unsupported_config_msg(path, error_key): return msg -def anglicize_validator(validator): - if validator in ["array", "object"]: - return 'an ' + validator - return 'a ' + validator +def anglicize_json_type(json_type): + if json_type.startswith(('a', 'e', 'i', 'o', 'u')): + return 'an ' + json_type + return 'a ' + json_type def is_service_dict_schema(schema_id): @@ -314,14 +313,14 @@ def _parse_valid_types_from_validator(validator): a valid type. Parse the valid types and prefix with the correct article. """ if not isinstance(validator, list): - return anglicize_validator(validator) + return anglicize_json_type(validator) if len(validator) == 1: - return anglicize_validator(validator[0]) + return anglicize_json_type(validator[0]) return "{}, or {}".format( - ", ".join([anglicize_validator(validator[0])] + validator[1:-1]), - anglicize_validator(validator[-1])) + ", ".join([anglicize_json_type(validator[0])] + validator[1:-1]), + anglicize_json_type(validator[-1])) def _parse_oneof_validator(error): diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index f43926939..6c5b7818b 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -159,7 +159,7 @@ class CLITestCase(DockerClientTestCase): '-f', 'tests/fixtures/invalid-composefile/invalid.yml', 'config', '-q' ], returncode=1) - assert "'notaservice' is the wrong type" in result.stderr + assert "'notaservice' must be a mapping" in result.stderr # TODO: this shouldn't be v2-dependent @v2_only() diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index c58ddc607..1f5183d78 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -268,7 +268,7 @@ class ConfigTest(unittest.TestCase): }) with pytest.raises(ConfigurationError) as exc: config.load(config_details) - assert "volume must be a mapping, not 'array'" in exc.exconly() + assert "volume must be a mapping, not an array" in exc.exconly() def test_networks_invalid_type_list(self): config_details = build_config_details({ @@ -280,7 +280,7 @@ class ConfigTest(unittest.TestCase): }) with pytest.raises(ConfigurationError) as exc: config.load(config_details) - assert "network must be a mapping, not 'array'" in exc.exconly() + assert "network must be a mapping, not an array" in exc.exconly() def test_load_service_with_name_version(self): with mock.patch('compose.config.config.log') as mock_logging: @@ -392,8 +392,7 @@ class ConfigTest(unittest.TestCase): 'filename.yml') with pytest.raises(ConfigurationError) as exc: config.load(config_details) - error_msg = "service 'web' is the wrong type" - assert error_msg in exc.exconly() + assert "service 'web' must be a mapping not a string." in exc.exconly() def test_config_integer_service_name_raise_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: @@ -405,8 +404,10 @@ class ConfigTest(unittest.TestCase): ) ) - assert "In file 'filename.yml' service name 1 needs to be a string, eg '1'" \ - in excinfo.exconly() + assert ( + "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'" in + excinfo.exconly() + ) def test_config_integer_service_name_raise_validation_error_v2(self): with pytest.raises(ConfigurationError) as excinfo: @@ -421,8 +422,10 @@ class ConfigTest(unittest.TestCase): ) ) - assert "In file 'filename.yml' service name 1 needs to be a string, eg '1'" \ - in excinfo.exconly() + assert ( + "In file 'filename.yml', the service name 1 must be a quoted string, i.e. '1'." in + excinfo.exconly() + ) def test_load_with_multiple_files_v1(self): base_file = config.ConfigFile( @@ -556,7 +559,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError) as exc: config.load(details) - assert "service 'bogus' is the wrong type" in exc.exconly() + assert "service 'bogus' must be a mapping not a string." in exc.exconly() assert "In file 'override.yaml'" in exc.exconly() def test_load_sorts_in_dependency_order(self): From 61906ac2ff6d8f90b59e33b28b95fe50c67c254d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 18 Feb 2016 17:17:20 -0800 Subject: [PATCH 43/54] Update documentation for volume_driver option. Signed-off-by: Joffrey F --- docs/compose-file.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index f27457fcb..f218dc4f9 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -555,10 +555,11 @@ limit as an integer or soft/hard limits as a mapping. ### volumes, volume\_driver Mount paths or named volumes, optionally specifying a path on the host machine -(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`). Named volumes can -be specified with the -[top-level `volumes` key](#volume-configuration-reference), but this is -optional - the Docker Engine will create the volume if it doesn't exist. +(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`). +For [version 2 files](#version-2), named volumes need to be specified with the +[top-level `volumes` key](#volume-configuration-reference). +When using [version 1](#version-1), the Docker Engine will create the named +volume automatically if it doesn't exist. You can mount a relative path on the host, which will expand relative to the directory of the Compose configuration file being used. Relative paths @@ -580,11 +581,16 @@ should always begin with `.` or `..`. # Named volume - datavolume:/var/lib/mysql -If you use a volume name (instead of a volume path), you may also specify -a `volume_driver`. +If you do not use a host path, you may specify a `volume_driver`. volume_driver: mydriver +Note that for [version 2 files](#version-2), this driver +will not apply to named volumes (you should use the `driver` option when +[declaring the volume](#volume-configuration-reference) instead). +For [version 1](#version-1), both named volumes and container volumes will +use the specified driver. + > Note: No path expansion will be done if you have also specified a > `volume_driver`. From f7c923062d24e4eae5559b173af8c244d0ee9657 Mon Sep 17 00:00:00 2001 From: Victoria Bialas Date: Thu, 18 Feb 2016 17:49:28 -0800 Subject: [PATCH 44/54] corrected description of network aliases, added real-world example per #2907 Signed-off-by: Victoria Bialas --- docs/compose-file.md | 41 ++++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index f218dc4f9..f0b7b673b 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -477,15 +477,13 @@ Networks to join, referencing entries under the #### aliases -Aliases (alternative hostnames) for this service on the network. Other servers -on the network can use either the service name or this alias to connect to -this service. Since `alias` is network-scoped: +Aliases (alternative hostnames) for this service on the network. Other containers on the same network can use either the service name or this alias to connect to one of the service's containers. - * the same service can have different aliases when connected to another - network. - * it is allowable to configure the same alias name to multiple containers - (services) on the same network. +Since `aliases` is network-scoped, the same service can have different aliases on different networks. +> **Note**: A network-wide alias can be shared by multiple containers, and even by multiple services. If it is, then exactly which container the name will resolve to is not guaranteed. + +The general format is shown here. networks: some-network: @@ -496,6 +494,35 @@ this service. Since `alias` is network-scoped: aliases: - alias2 +In the example below, three services are provided (`web`, `worker`, and `db`), along with two networks (`new` and `legacy`). The `db` service is reachable at the hostname `db` or `database` on the `new` network, and at `db` or `mysql` on the legacy network. + + version: 2 + + services: + web: + build: ./web + networks: + - new + + worker: + build: ./worker + networks: + - legacy + + db: + image: mysql + networks: + new: + aliases: + - database + legacy: + aliases: + - mysql + + networks: + new: + legacy: + ### pid pid: "host" From f8e3c46fbb6134d694fb98f1c3bc899a2f13f357 Mon Sep 17 00:00:00 2001 From: Victoria Bialas Date: Thu, 18 Feb 2016 18:05:30 -0800 Subject: [PATCH 45/54] copyedit to make show as file format Signed-off-by: Victoria Bialas --- docs/compose-file.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index f0b7b673b..485429b51 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -494,7 +494,7 @@ The general format is shown here. aliases: - alias2 -In the example below, three services are provided (`web`, `worker`, and `db`), along with two networks (`new` and `legacy`). The `db` service is reachable at the hostname `db` or `database` on the `new` network, and at `db` or `mysql` on the legacy network. +In the example below, three services are provided (`web`, `worker`, and `db`), along with two networks (`new` and `legacy`). The `db` service is reachable at the hostname `db` or `database` on the `new` network, and at `db` or `mysql` on the `legacy` network. version: 2 From b79ad5f9663178eb3019ca53b54a818914c3039e Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 19 Feb 2016 14:22:55 -0500 Subject: [PATCH 46/54] Fix validation message when there are multiple ested oneOf validations. Signed-off-by: Daniel Nephin --- compose/config/validation.py | 4 ++++ tests/unit/config/config_test.py | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/compose/config/validation.py b/compose/config/validation.py index 6dc72f566..4e2083cbc 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -332,6 +332,10 @@ def _parse_oneof_validator(error): types = [] for context in error.context: + if context.validator == 'oneOf': + _, error_msg = _parse_oneof_validator(context) + return path_string(context.path), error_msg + if context.validator == 'required': return (None, context.message) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 1f5183d78..ce37d794f 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -394,6 +394,27 @@ class ConfigTest(unittest.TestCase): config.load(config_details) assert "service 'web' must be a mapping not a string." in exc.exconly() + def test_load_with_empty_build_args(self): + config_details = build_config_details( + { + 'version': '2', + 'services': { + 'web': { + 'build': { + 'context': '.', + 'args': None, + }, + }, + }, + } + ) + with pytest.raises(ConfigurationError) as exc: + config.load(config_details) + assert ( + "services.web.build.args contains an invalid type, it should be an " + "array, or an object" in exc.exconly() + ) + def test_config_integer_service_name_raise_validation_error(self): with pytest.raises(ConfigurationError) as excinfo: config.load( From 4aae2c3b7b009d97b200c40fabf980342e62481b Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 22 Feb 2016 12:56:54 -0800 Subject: [PATCH 47/54] Use docker-py 1.7.1 Signed-off-by: Joffrey F --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3fdd34ed0..5f55ba8ad 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PyYAML==3.11 cached-property==1.2.0 -docker-py==1.7.0 +docker-py==1.7.1 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4 From 73c2f8ee37bf5c7cf5d9277b8c99dcfda0105621 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 22 Feb 2016 14:48:56 -0800 Subject: [PATCH 48/54] Fix warning about boolean values. Signed-off-by: Daniel Nephin --- compose/config/validation.py | 12 ++++++------ tests/unit/config/config_test.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/compose/config/validation.py b/compose/config/validation.py index 4e2083cbc..60ee5c930 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -64,16 +64,16 @@ def format_expose(instance): @FormatChecker.cls_checks(format="bool-value-in-mapping") def format_boolean_in_environment(instance): - """ - Check if there is a boolean in the environment and display a warning. + """Check if there is a boolean in the mapping sections and display a warning. Always return True here so the validation won't raise an error. """ if isinstance(instance, bool): log.warn( - "There is a boolean value in the 'environment' key.\n" - "Environment variables can only be strings.\n" - "Please add quotes to any boolean values to make them string " - "(eg, 'True', 'yes', 'N').\n" + "There is a boolean value in the 'environment', 'labels', or " + "'extra_hosts' field of a service.\n" + "These sections only support string values.\n" + "Please add quotes to any boolean values to make them strings " + "(eg, 'True', 'false', 'yes', 'N', 'on', 'Off').\n" "This warning will become an error in a future release. \r\n" ) return True diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index ce37d794f..4d3bb7be7 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1100,7 +1100,7 @@ class ConfigTest(unittest.TestCase): @mock.patch('compose.config.validation.log') def test_logs_warning_for_boolean_in_environment(self, mock_logging): - expected_warning_msg = "There is a boolean value in the 'environment' key." + expected_warning_msg = "There is a boolean value in the 'environment'" config.load( build_config_details( {'web': { From 155d813606a5f1a9492a630807b12c999ef9b709 Mon Sep 17 00:00:00 2001 From: Richard Bann Date: Thu, 18 Feb 2016 12:13:16 +0100 Subject: [PATCH 49/54] Add failing test for --abort-on-container-exit Handle --abort-on-container-exit. Fixes #2940 Signed-off-by: Richard Bann --- compose/cli/log_printer.py | 11 ++++++++--- compose/cli/main.py | 3 +++ compose/cli/signals.py | 4 ++++ tests/acceptance/cli_test.py | 6 ++++++ 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index 85fef794f..b7abc007e 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -5,6 +5,7 @@ import sys from itertools import cycle from . import colors +from . import signals from .multiplexer import Multiplexer from compose import utils from compose.utils import split_buffer @@ -41,7 +42,7 @@ class LogPrinter(object): for color_func, container in zip(color_funcs, self.containers): generator_func = get_log_generator(container) prefix = color_func(build_log_prefix(container, prefix_width)) - yield generator_func(container, prefix, color_func) + yield generator_func(container, prefix, color_func, self.cascade_stop) def build_log_prefix(container, prefix_width): @@ -64,7 +65,7 @@ def get_log_generator(container): return build_no_log_generator -def build_no_log_generator(container, prefix, color_func): +def build_no_log_generator(container, prefix, color_func, cascade_stop): """Return a generator that prints a warning about logs and waits for container to exit. """ @@ -72,9 +73,11 @@ def build_no_log_generator(container, prefix, color_func): prefix, container.log_driver) yield color_func(wait_on_exit(container)) + if cascade_stop: + raise signals.CascadeStopException() -def build_log_generator(container, prefix, color_func): +def build_log_generator(container, prefix, color_func, cascade_stop): # if the container doesn't have a log_stream we need to attach to container # before log printer starts running if container.log_stream is None: @@ -86,6 +89,8 @@ def build_log_generator(container, prefix, color_func): for line in line_generator: yield prefix + line yield color_func(wait_on_exit(container)) + if cascade_stop: + raise signals.CascadeStopException() def wait_on_exit(container): diff --git a/compose/cli/main.py b/compose/cli/main.py index cc15fa051..5a7ac8d47 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -774,6 +774,9 @@ def up_shutdown_context(project, service_names, timeout, detached): except signals.ShutdownException: print("Gracefully stopping... (press Ctrl+C again to force)") project.stop(service_names=service_names, timeout=timeout) + except signals.CascadeStopException: + print("Aborting on container exit... (press Ctrl+C to force)") + project.stop(service_names=service_names, timeout=timeout) except signals.ShutdownException: project.kill(service_names=service_names) sys.exit(2) diff --git a/compose/cli/signals.py b/compose/cli/signals.py index 68a0598e1..808700df3 100644 --- a/compose/cli/signals.py +++ b/compose/cli/signals.py @@ -8,6 +8,10 @@ class ShutdownException(Exception): pass +class CascadeStopException(Exception): + pass + + def shutdown(signal, frame): raise ShutdownException() diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 6c5b7818b..28f5155aa 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -746,6 +746,12 @@ class CLITestCase(DockerClientTestCase): os.kill(proc.pid, signal.SIGTERM) wait_on_condition(ContainerCountCondition(self.project, 0)) + def test_up_handles_abort_on_container_exit(self): + start_process(self.base_dir, ['up', '--abort-on-container-exit']) + wait_on_condition(ContainerCountCondition(self.project, 2)) + self.project.stop(['simple']) + wait_on_condition(ContainerCountCondition(self.project, 0)) + def test_run_service_without_links(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['run', 'console', '/bin/true']) From daebf74d6cd888473868d98815b68f92739a7e53 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 22 Feb 2016 16:46:09 -0800 Subject: [PATCH 50/54] Stop other containers if the flag is set. Signed-off-by: Daniel Nephin --- compose/cli/log_printer.py | 11 +++-------- compose/cli/main.py | 7 ++++--- compose/cli/signals.py | 4 ---- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index b7abc007e..85fef794f 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -5,7 +5,6 @@ import sys from itertools import cycle from . import colors -from . import signals from .multiplexer import Multiplexer from compose import utils from compose.utils import split_buffer @@ -42,7 +41,7 @@ class LogPrinter(object): for color_func, container in zip(color_funcs, self.containers): generator_func = get_log_generator(container) prefix = color_func(build_log_prefix(container, prefix_width)) - yield generator_func(container, prefix, color_func, self.cascade_stop) + yield generator_func(container, prefix, color_func) def build_log_prefix(container, prefix_width): @@ -65,7 +64,7 @@ def get_log_generator(container): return build_no_log_generator -def build_no_log_generator(container, prefix, color_func, cascade_stop): +def build_no_log_generator(container, prefix, color_func): """Return a generator that prints a warning about logs and waits for container to exit. """ @@ -73,11 +72,9 @@ def build_no_log_generator(container, prefix, color_func, cascade_stop): prefix, container.log_driver) yield color_func(wait_on_exit(container)) - if cascade_stop: - raise signals.CascadeStopException() -def build_log_generator(container, prefix, color_func, cascade_stop): +def build_log_generator(container, prefix, color_func): # if the container doesn't have a log_stream we need to attach to container # before log printer starts running if container.log_stream is None: @@ -89,8 +86,6 @@ def build_log_generator(container, prefix, color_func, cascade_stop): for line in line_generator: yield prefix + line yield color_func(wait_on_exit(container)) - if cascade_stop: - raise signals.CascadeStopException() def wait_on_exit(container): diff --git a/compose/cli/main.py b/compose/cli/main.py index 5a7ac8d47..3c4b5721d 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -662,6 +662,10 @@ class TopLevelCommand(DocoptCommand): print("Attaching to", list_containers(log_printer.containers)) log_printer.run() + if cascade_stop: + print("Aborting on container exit...") + project.stop(service_names=service_names, timeout=timeout) + def version(self, project, options): """ Show version informations @@ -774,9 +778,6 @@ def up_shutdown_context(project, service_names, timeout, detached): except signals.ShutdownException: print("Gracefully stopping... (press Ctrl+C again to force)") project.stop(service_names=service_names, timeout=timeout) - except signals.CascadeStopException: - print("Aborting on container exit... (press Ctrl+C to force)") - project.stop(service_names=service_names, timeout=timeout) except signals.ShutdownException: project.kill(service_names=service_names) sys.exit(2) diff --git a/compose/cli/signals.py b/compose/cli/signals.py index 808700df3..68a0598e1 100644 --- a/compose/cli/signals.py +++ b/compose/cli/signals.py @@ -8,10 +8,6 @@ class ShutdownException(Exception): pass -class CascadeStopException(Exception): - pass - - def shutdown(signal, frame): raise ShutdownException() From bcd5286cd38a51336e151ff0640a3312095f4818 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 22 Feb 2016 17:30:42 -0800 Subject: [PATCH 51/54] Revert "Change special case from '_', None to ()" This reverts commit 677c50650c86b4b6fabbc21e18165f2117022bbe. Revert "Modify service_test.py::ServiceTest::test_resolve_env to reflect new behavior" This reverts commit 001903771260069c475738efbbcb830dd9cf8227. Revert "Mangle the tests. They pass for better or worse!" This reverts commit 7ab9509ce65167dc81dd14f34cddfb5ecff1329d. Revert "If an env var is passthrough but not defined on the host don't set it." This reverts commit 6540efb3d380e7ae50dd94493a43382f31e1e004. Signed-off-by: Daniel Nephin --- compose/config/config.py | 6 +++--- tests/integration/service_test.py | 1 + tests/unit/config/config_test.py | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 1daae9818..a05bf8ebd 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -511,12 +511,12 @@ def resolve_environment(service_dict): env.update(env_vars_from_file(env_file)) env.update(parse_environment(service_dict.get('environment'))) - return dict(filter(None, (resolve_env_var(k, v) for k, v in six.iteritems(env)))) + return dict(resolve_env_var(k, v) for k, v in six.iteritems(env)) def resolve_build_args(build): args = parse_build_arguments(build.get('args')) - return dict(filter(None, (resolve_env_var(k, v) for k, v in six.iteritems(args)))) + return dict(resolve_env_var(k, v) for k, v in six.iteritems(args)) def validate_extended_service_dict(service_dict, filename, service): @@ -826,7 +826,7 @@ def resolve_env_var(key, val): elif key in os.environ: return key, os.environ[key] else: - return () + return key, '' def env_vars_from_file(filename): diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 647d36daa..abbf1978b 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -909,6 +909,7 @@ class ServiceTest(DockerClientTestCase): 'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', + 'NO_DEF': '' }.items(): self.assertEqual(env[k], v) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 4d3bb7be7..446fc5600 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1975,7 +1975,7 @@ class EnvTest(unittest.TestCase): } self.assertEqual( resolve_environment(service_dict), - {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3'}, + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}, ) def test_resolve_environment_from_env_file(self): @@ -2016,6 +2016,7 @@ class EnvTest(unittest.TestCase): 'FILE_DEF': u'bär', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', + 'NO_DEF': '' }, ) @@ -2034,7 +2035,7 @@ class EnvTest(unittest.TestCase): } self.assertEqual( resolve_build_args(build), - {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2'}, + {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2', 'no_env': ''}, ) @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') From 9d7dbe3857ef0c5c79229747e26caa68b4f0d228 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Mon, 22 Feb 2016 17:47:51 -0800 Subject: [PATCH 52/54] Make environment variables without a value the same as docker-cli. Signed-off-by: Daniel Nephin --- compose/config/config.py | 2 +- compose/container.py | 6 +++++- compose/service.py | 11 +++++++++++ tests/integration/service_test.py | 2 +- tests/unit/cli_test.py | 7 ++++--- tests/unit/config/config_test.py | 6 +++--- tests/unit/service_test.py | 6 +++--- 7 files changed, 28 insertions(+), 12 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index a05bf8ebd..48b34318b 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -826,7 +826,7 @@ def resolve_env_var(key, val): elif key in os.environ: return key, os.environ[key] else: - return key, '' + return key, None def env_vars_from_file(filename): diff --git a/compose/container.py b/compose/container.py index 3a1ce0b9f..c96b63ef4 100644 --- a/compose/container.py +++ b/compose/container.py @@ -134,7 +134,11 @@ class Container(object): @property def environment(self): - return dict(var.split("=", 1) for var in self.get('Config.Env') or []) + def parse_env(var): + if '=' in var: + return var.split("=", 1) + return var, None + return dict(parse_env(var) for var in self.get('Config.Env') or []) @property def exit_code(self): diff --git a/compose/service.py b/compose/service.py index f9424e8fa..4e169daae 100644 --- a/compose/service.py +++ b/compose/service.py @@ -621,6 +621,8 @@ class Service(object): override_options, one_off=one_off) + container_options['environment'] = format_environment( + container_options['environment']) return container_options def _get_container_host_config(self, override_options, one_off=False): @@ -1018,3 +1020,12 @@ def get_log_config(logging_dict): type=log_driver, config=log_options ) + + +# TODO: remove once fix is available in docker-py +def format_environment(environment): + def format_env(key, value): + if value is None: + return key + return '{key}={value}'.format(key=key, value=value) + return [format_env(*item) for item in environment.items()] diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index abbf1978b..bbfcd8ec9 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -909,7 +909,7 @@ class ServiceTest(DockerClientTestCase): 'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', - 'NO_DEF': '' + 'NO_DEF': None }.items(): self.assertEqual(env[k], v) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 69236e2e1..26ae4e300 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -138,9 +138,10 @@ class CLITestCase(unittest.TestCase): }) _, _, call_kwargs = mock_client.create_container.mock_calls[0] - self.assertEqual( - call_kwargs['environment'], - {'FOO': 'ONE', 'BAR': 'NEW', 'OTHER': u'bär'}) + assert ( + sorted(call_kwargs['environment']) == + sorted(['FOO=ONE', 'BAR=NEW', 'OTHER=bär']) + ) def test_run_service_with_restart_always(self): command = TopLevelCommand() diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 446fc5600..11bc7f0b7 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1975,7 +1975,7 @@ class EnvTest(unittest.TestCase): } self.assertEqual( resolve_environment(service_dict), - {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''}, + {'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': None}, ) def test_resolve_environment_from_env_file(self): @@ -2016,7 +2016,7 @@ class EnvTest(unittest.TestCase): 'FILE_DEF': u'bär', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', - 'NO_DEF': '' + 'NO_DEF': None }, ) @@ -2035,7 +2035,7 @@ class EnvTest(unittest.TestCase): } self.assertEqual( resolve_build_args(build), - {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2', 'no_env': ''}, + {'arg1': 'value1', 'empty_arg': '', 'env_arg': 'value2', 'no_env': None}, ) @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index ce28a9ca4..321ebad05 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -267,7 +267,7 @@ class ServiceTest(unittest.TestCase): self.assertEqual( opts['labels'][LABEL_CONFIG_HASH], 'f8bfa1058ad1f4231372a0b1639f0dfdb574dafff4e8d7938049ae993f7cf1fc') - assert opts['environment'] == {'also': 'real'} + assert opts['environment'] == ['also=real'] def test_get_container_create_options_sets_affinity_with_binds(self): service = Service( @@ -298,7 +298,7 @@ class ServiceTest(unittest.TestCase): 1, previous_container=prev_container) - assert opts['environment'] == {'affinity:container': '=ababab'} + assert opts['environment'] == ['affinity:container==ababab'] def test_get_container_create_options_no_affinity_without_binds(self): service = Service('foo', image='foo', client=self.mock_client) @@ -312,7 +312,7 @@ class ServiceTest(unittest.TestCase): {}, 1, previous_container=prev_container) - assert opts['environment'] == {} + assert opts['environment'] == [] def test_get_container_not_found(self): self.mock_client.containers.return_value = [] From e08409f18d0d72f4e3201cfe664bbfa55dd7bc31 Mon Sep 17 00:00:00 2001 From: Mary Anthony Date: Wed, 10 Feb 2016 20:47:15 -0800 Subject: [PATCH 53/54] Updating Dockerfile Signed-off-by: Mary Anthony --- docs/Dockerfile | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/Dockerfile b/docs/Dockerfile index 83b656333..5f32dc4dc 100644 --- a/docs/Dockerfile +++ b/docs/Dockerfile @@ -5,9 +5,10 @@ RUN svn checkout https://github.com/docker/docker/trunk/docs /docs/content/engin RUN svn checkout https://github.com/docker/swarm/trunk/docs /docs/content/swarm RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine RUN svn checkout https://github.com/docker/distribution/trunk/docs /docs/content/registry -RUN svn checkout https://github.com/kitematic/kitematic/trunk/docs /docs/content/kitematic -RUN svn checkout https://github.com/docker/tutorials/trunk/docs /docs/content/tutorials -RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content +RUN svn checkout https://github.com/docker/notary/trunk/docs /docs/content/notary +RUN svn checkout https://github.com/docker/kitematic/trunk/docs /docs/content/kitematic +RUN svn checkout https://github.com/docker/toolbox/trunk/docs /docs/content/toolbox +RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/project ENV PROJECT=compose # To get the git info for this repo From a75c16cb1be80f2c8e94120b7a79e3ddbd901419 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 23 Feb 2016 11:31:22 -0800 Subject: [PATCH 54/54] Bump 1.6.1 Signed-off-by: Daniel Nephin --- CHANGELOG.md | 55 ++++++++++++++++++++++++++++++++++++++++++++ compose/__init__.py | 2 +- docs/compose-file.md | 2 +- docs/install.md | 6 ++--- script/run.sh | 2 +- 5 files changed, 61 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df63c5fd..7d553cc2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,61 @@ Change log ========== +1.6.1 (2016-02-23) +------------------ + +Bug Fixes + +- Fixed a bug where recreating a container multiple times would cause the + new container to be started without the previous volumes. + +- Fixed a bug where Compose would set the value of unset environment variables + to an empty string, instead of a key without a value. + +- Provide a better error message when Compose requires a more recent version + of the Docker API. + +- Add a missing config field `network.aliases` which allows setting a network + scoped alias for a service. + +- Fixed a bug where `run` would not start services listed in `depends_on`. + +- Fixed a bug where `networks` and `network_mode` where not merged when using + extends or multiple Compose files. + +- Fixed a bug with service aliases where the short container id alias was + only contained 10 characters, instead of the 12 characters used in previous + versions. + +- Added a missing log message when creating a new named volume. + +- Fixed a bug where `build.args` was not merged when using `extends` or + multiple Compose files. + +- Fixed some bugs with config validation when null values or incorrect types + were used instead of a mapping. + +- Fixed a bug where a `build` section without a `context` would show a stack + trace instead of a helpful validation message. + +- Improved compatibility with swarm by only setting a container affinity to + the previous instance of a services' container when the service uses an + anonymous container volume. Previously the affinity was always set on all + containers. + +- Fixed the validation of some `driver_opts` would cause an error if a number + was used instead of a string. + +- Some improvements to the `run.sh` script used by the Compose container install + option. + +- Fixed a bug with `up --abort-on-container-exit` where Compose would exit, + but would not stop other containers. + +- Corrected the warning message that is printed when a boolean value is used + as a value in a mapping. + + 1.6.0 (2016-01-15) ------------------ diff --git a/compose/__init__.py b/compose/__init__.py index 268bb719e..942062f51 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1,4 @@ from __future__ import absolute_import from __future__ import unicode_literals -__version__ = '1.6.0' +__version__ = '1.6.1' diff --git a/docs/compose-file.md b/docs/compose-file.md index 485429b51..97b8ba51f 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -477,7 +477,7 @@ Networks to join, referencing entries under the #### aliases -Aliases (alternative hostnames) for this service on the network. Other containers on the same network can use either the service name or this alias to connect to one of the service's containers. +Aliases (alternative hostnames) for this service on the network. Other containers on the same network can use either the service name or this alias to connect to one of the service's containers. Since `aliases` is network-scoped, the same service can have different aliases on different networks. diff --git a/docs/install.md b/docs/install.md index b8979b959..a51befca8 100644 --- a/docs/install.md +++ b/docs/install.md @@ -39,7 +39,7 @@ which the release page specifies, in your terminal. The following is an example command illustrating the format: - curl -L https://github.com/docker/compose/releases/download/1.6.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.6.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose If you have problems installing with `curl`, see [Alternative Install Options](#alternative-install-options). @@ -54,7 +54,7 @@ which the release page specifies, in your terminal. 7. Test the installation. $ docker-compose --version - docker-compose version: 1.6.0 + docker-compose version: 1.6.1 ## Alternative install options @@ -77,7 +77,7 @@ to get started. Compose can also be run inside a container, from a small bash script wrapper. To install compose as a container run: - $ curl -L https://github.com/docker/compose/releases/download/1.6.0/run.sh > /usr/local/bin/docker-compose + $ curl -L https://github.com/docker/compose/releases/download/1.6.1/run.sh > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose ## Master builds diff --git a/script/run.sh b/script/run.sh index 749481f6c..3e30dd15b 100755 --- a/script/run.sh +++ b/script/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.6.0" +VERSION="1.6.1" IMAGE="docker/compose:$VERSION"