From 9ddb7f3c90b64ab801f770b54eab5be569d142f6 Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Wed, 8 Jun 2016 03:51:03 +0100 Subject: [PATCH 001/118] Convert readthedocs links for their .org -> .io migration for hosted projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. Signed-off-by: Adam Chainz --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50e58ddc..16bccf98 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,7 +35,7 @@ that should get you started. This step is optional, but recommended. Pre-commit hooks will run style checks and in some cases fix style issues for you, when you commit code. -Install the git pre-commit hooks using [tox](https://tox.readthedocs.org) by +Install the git pre-commit hooks using [tox](https://tox.readthedocs.io) by running `tox -e pre-commit` or by following the [pre-commit install guide](http://pre-commit.com/#install). From 0287486b14b7b75d0544a029f188a21e972cc8ea Mon Sep 17 00:00:00 2001 From: David Beitey Date: Thu, 9 Jun 2016 16:58:34 +1000 Subject: [PATCH 002/118] Fix minor YAML typo Signed-off-by: David Beitey --- 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 b55f250a..501269b7 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -796,7 +796,7 @@ called `data` and mount it into the `db` service's containers. You can also specify the name of the volume separately from the name used to refer to it within the Compose file: - volumes + volumes: data: external: name: actual-name-of-volume From 68d73183ebcc9db5676d15cdf61ee1494242d099 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 13 Jun 2016 23:36:34 -0400 Subject: [PATCH 003/118] Fix a typo in a test's name. --- tests/integration/service_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index df50d513..1b21e1e9 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -593,12 +593,12 @@ class ServiceTest(DockerClientTestCase): service.build() assert service.image() - def test_start_container_stays_unpriviliged(self): + def test_start_container_stays_unprivileged(self): service = self.create_service('web') container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], False) - def test_start_container_becomes_priviliged(self): + def test_start_container_becomes_privileged(self): service = self.create_service('web', privileged=True) container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], True) From ee68a51e281e8a997d97ed1efde0e2d2821f85c9 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 14 Jun 2016 12:23:04 -0700 Subject: [PATCH 004/118] Skip TLS version test if TLSv1_2 is not available on platform Signed-off-by: Joffrey F --- tests/unit/cli/command_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/cli/command_test.py b/tests/unit/cli/command_test.py index 28adff3f..50fc84e1 100644 --- a/tests/unit/cli/command_test.py +++ b/tests/unit/cli/command_test.py @@ -55,6 +55,7 @@ class TestGetTlsVersion(object): environment = {} assert get_tls_version(environment) is None + @pytest.mark.skipif(not hasattr(ssl, 'PROTOCOL_TLSv1_2'), reason='TLS v1.2 unsupported') def test_get_tls_version_upgrade(self): environment = {'COMPOSE_TLS_VERSION': 'TLSv1_2'} assert get_tls_version(environment) == ssl.PROTOCOL_TLSv1_2 From a56e44f96ea86e02d5f3e634f1e23ed9f731a608 Mon Sep 17 00:00:00 2001 From: Victoria Bialas Date: Tue, 14 Jun 2016 12:24:30 -0700 Subject: [PATCH 005/118] fixes broken links in 3 Compose docs files due to topic re-org in PR#23492 Signed-off-by: Victoria Bialas --- docs/django.md | 2 +- docs/gettingstarted.md | 2 +- docs/rails.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/django.md b/docs/django.md index b4bcee97..1cf2a567 100644 --- a/docs/django.md +++ b/docs/django.md @@ -29,7 +29,7 @@ and a `docker-compose.yml` file. The Dockerfile defines an application's image content via one or more build commands that configure that image. Once built, you can run the image in a container. For more information on `Dockerfiles`, see the [Docker user - guide](/engine/userguide/containers/dockerimages.md#building-an-image-from-a-dockerfile) + guide](/engine/tutorials/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). 3. Add the following content to the `Dockerfile`. diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md index 8c706e4f..249bff72 100644 --- a/docs/gettingstarted.md +++ b/docs/gettingstarted.md @@ -77,7 +77,7 @@ dependencies the Python application requires, including Python itself. * Install the Python dependencies. * Set the default command for the container to `python app.py` - For more information on how to write Dockerfiles, see the [Docker user guide](/engine/userguide/containers/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). + For more information on how to write Dockerfiles, see the [Docker user guide](/engine/tutorials/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). 2. Build the image. diff --git a/docs/rails.md b/docs/rails.md index f54d8286..26777687 100644 --- a/docs/rails.md +++ b/docs/rails.md @@ -32,7 +32,7 @@ Dockerfile consists of: That'll put your application code inside an image that will build a container with Ruby, Bundler and all your dependencies inside it. For more information on -how to write Dockerfiles, see the [Docker user guide](/engine/userguide/containers/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). +how to write Dockerfiles, see the [Docker user guide](/engine/tutorials/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). Next, create a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`. From 020d46ff21764a57fa21e4f3ccc1ba09a1345049 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 14 Jun 2016 16:03:56 -0700 Subject: [PATCH 006/118] Warn on missing digests, don't push/pull by default Add a --fetch-digests flag to automatically push/pull Signed-off-by: Aanand Prasad --- compose/bundle.py | 64 ++++++++++++++++++++++++++++++++++++--------- compose/cli/main.py | 31 +++++++++++++++++++--- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/compose/bundle.py b/compose/bundle.py index e93c5bd9..965d65c8 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -40,6 +40,22 @@ SUPPORTED_KEYS = { VERSION = '0.1' +class NeedsPush(Exception): + def __init__(self, image_name): + self.image_name = image_name + + +class NeedsPull(Exception): + def __init__(self, image_name): + self.image_name = image_name + + +class MissingDigests(Exception): + def __init__(self, needs_push, needs_pull): + self.needs_push = needs_push + self.needs_pull = needs_pull + + def serialize_bundle(config, image_digests): if config.networks: log.warn("Unsupported top level key 'networks' - ignoring") @@ -54,21 +70,36 @@ def serialize_bundle(config, image_digests): ) -def get_image_digests(project): - return { - service.name: get_image_digest(service) - for service in project.services - } +def get_image_digests(project, allow_fetch=False): + digests = {} + needs_push = set() + needs_pull = set() + + for service in project.services: + try: + digests[service.name] = get_image_digest( + service, + allow_fetch=allow_fetch, + ) + except NeedsPush as e: + needs_push.add(e.image_name) + except NeedsPull as e: + needs_pull.add(e.image_name) + + if needs_push or needs_pull: + raise MissingDigests(needs_push, needs_pull) + + return digests -def get_image_digest(service): +def get_image_digest(service, allow_fetch=False): if 'image' not in service.options: raise UserError( "Service '{s.name}' doesn't define an image tag. An image name is " "required to generate a proper image digest for the bundle. Specify " "an image repo and tag with the 'image' option.".format(s=service)) - repo, tag, separator = parse_repository_tag(service.options['image']) + separator = parse_repository_tag(service.options['image'])[2] # Compose file already uses a digest, no lookup required if separator == '@': return service.options['image'] @@ -87,13 +118,17 @@ def get_image_digest(service): # digests return image['RepoDigests'][0] + if not allow_fetch: + if 'build' in service.options: + raise NeedsPush(service.image_name) + else: + raise NeedsPull(service.image_name) + + return fetch_image_digest(service) + + +def fetch_image_digest(service): if 'build' not in service.options: - log.warn( - "Compose needs to pull the image for '{s.name}' in order to create " - "a bundle. This may result in a more recent image being used. " - "It is recommended that you use an image tagged with a " - "specific version to minimize the potential " - "differences.".format(s=service)) digest = service.pull() else: try: @@ -108,12 +143,15 @@ def get_image_digest(service): if not digest: raise ValueError("Failed to get digest for %s" % service.name) + repo = parse_repository_tag(service.options['image'])[0] identifier = '{repo}@{digest}'.format(repo=repo, digest=digest) # Pull by digest so that image['RepoDigests'] is populated for next time # and we don't have to pull/push again service.client.pull(identifier) + log.info("Stored digest for {}".format(service.image_name)) + return identifier diff --git a/compose/cli/main.py b/compose/cli/main.py index 3e440463..25ee9050 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -15,6 +15,7 @@ from . import errors from . import signals from .. import __version__ from ..bundle import get_image_digests +from ..bundle import MissingDigests from ..bundle import serialize_bundle from ..config import ConfigurationError from ..config import parse_environment @@ -218,12 +219,17 @@ class TopLevelCommand(object): """ Generate a Docker bundle from the Compose file. - Local images will be pushed to a Docker registry, and remote images - will be pulled to fetch an image digest. + Images must have digests stored, which requires interaction with a + Docker registry. If digests aren't stored for all images, you can pass + `--fetch-digests` to automatically fetch them. Images for services + with a `build` key will be pushed. Images for services without a + `build` key will be pulled. Usage: bundle [options] Options: + --fetch-digests Automatically fetch image digests if missing + -o, --output PATH Path to write the bundle file to. Defaults to ".dsb". """ @@ -235,7 +241,26 @@ class TopLevelCommand(object): output = "{}.dsb".format(self.project.name) with errors.handle_connection_errors(self.project.client): - image_digests = get_image_digests(self.project) + try: + image_digests = get_image_digests( + self.project, + allow_fetch=options['--fetch-digests'], + ) + except MissingDigests as e: + def list_images(images): + return "\n".join(" {}".format(name) for name in sorted(images)) + + paras = ["Some images are missing digests."] + + if e.needs_push: + paras += ["The following images need to be pushed:", list_images(e.needs_push)] + + if e.needs_pull: + paras += ["The following images need to be pulled:", list_images(e.needs_pull)] + + paras.append("If this is OK, run `docker-compose bundle --fetch-digests`.") + + raise UserError("\n\n".join(paras)) with open(output, 'w') as f: f.write(serialize_bundle(compose_config, image_digests)) From 80af26d2bb29443663b87eb7a111f0bba4e352bc Mon Sep 17 00:00:00 2001 From: Anton Backer Date: Mon, 13 Jun 2016 22:45:15 -0400 Subject: [PATCH 007/118] togather -> together Signed-off-by: Anton Backer --- compose/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 25ee9050..96ad847f 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -664,7 +664,7 @@ class TopLevelCommand(object): if options['--publish'] and options['--service-ports']: raise UserError( 'Service port mapping and manual port mapping ' - 'can not be used togather' + 'can not be used together' ) if options['COMMAND']: From 3c77db709fe6c86b3da30f84907994af7ab6bc2d Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Mon, 20 Jun 2016 14:48:45 +0200 Subject: [PATCH 008/118] Fix assertion that was always true Signed-off-by: Jonathan Giannuzzi --- tests/integration/service_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index df50d513..1801f5bf 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -397,7 +397,7 @@ class ServiceTest(DockerClientTestCase): assert not mock_log.warn.called assert ( - [mount['Destination'] for mount in new_container.get('Mounts')], + [mount['Destination'] for mount in new_container.get('Mounts')] == ['/data'] ) assert new_container.get_mount('/data')['Source'] != host_path From e5c5dc09f83fea5fc708a9a4d72cf0bb83da6154 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Wed, 22 Jun 2016 17:04:22 +0200 Subject: [PATCH 009/118] bash completion for `docker-compose bundle` Signed-off-by: Harald Albers --- contrib/completion/bash/docker-compose | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 763cafc4..de25cd57 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -109,6 +109,18 @@ _docker_compose_build() { } +_docker_compose_bundle() { + case "$prev" in + --output|-o) + _filedir + return + ;; + esac + + COMPREPLY=( $( compgen -W "--help --output -o" -- "$cur" ) ) +} + + _docker_compose_config() { COMPREPLY=( $( compgen -W "--help --quiet -q --services" -- "$cur" ) ) } @@ -455,6 +467,7 @@ _docker_compose() { local commands=( build + bundle config create down From f49b624d95c30ce457edff4e32c220cc657dbad1 Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Wed, 22 Jun 2016 17:19:20 +0200 Subject: [PATCH 010/118] bash completion for `docker-compose push` Signed-off-by: Harald Albers --- contrib/completion/bash/docker-compose | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 763cafc4..058ede10 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -304,6 +304,18 @@ _docker_compose_pull() { } +_docker_compose_push() { + case "$cur" in + -*) + COMPREPLY=( $( compgen -W "--help --ignore-push-failures" -- "$cur" ) ) + ;; + *) + __docker_compose_services_all + ;; + esac +} + + _docker_compose_restart() { case "$prev" in --timeout|-t) @@ -467,6 +479,7 @@ _docker_compose() { port ps pull + push restart rm run From f77dbc06cc6f5794e828b949b5df16d79ee7c143 Mon Sep 17 00:00:00 2001 From: James Ottaway Date: Fri, 24 Jun 2016 10:51:20 +1000 Subject: [PATCH 011/118] Document `tmpfs` being v2 only Signed-off-by: James Ottaway --- docs/compose-file.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/compose-file.md b/docs/compose-file.md index b55f250a..682dacd3 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -228,6 +228,8 @@ Custom DNS search domains. Can be a single value or a list. ### tmpfs +> [Version 2 file format](#version-2) only. + Mount a temporary file system inside the container. Can be a single value or a list. tmpfs: /run From aa7f522ab0b1e85b236b782f9e82d7f70a29b3af Mon Sep 17 00:00:00 2001 From: Ke Xu Date: Mon, 27 Jun 2016 17:48:14 +0900 Subject: [PATCH 012/118] add zsh completion support for bundle Signed-off-by: Ke Xu --- contrib/completion/zsh/_docker-compose | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 0da217dc..539482e5 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -197,6 +197,11 @@ __docker-compose_subcommand() { '--pull[Always attempt to pull a newer version of the image.]' \ '*:services:__docker-compose_services_from_build' && ret=0 ;; + (bundle) + _arguments \ + $opts_help \ + '(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to ".dsb".]:file:_files' && ret=0 + ;; (config) _arguments \ $opts_help \ From a0a90b2352d8592796097c549635a56b1bba4aa5 Mon Sep 17 00:00:00 2001 From: Ke Xu Date: Mon, 27 Jun 2016 18:00:52 +0900 Subject: [PATCH 013/118] add zsh completion for push Signed-off-by: Ke Xu --- contrib/completion/zsh/_docker-compose | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 0da217dc..42876ec9 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -278,6 +278,12 @@ __docker-compose_subcommand() { '--ignore-pull-failures[Pull what it can and ignores images with pull failures.]' \ '*:services:__docker-compose_services_from_image' && ret=0 ;; + (push) + _arguments \ + $opts_help \ + '--ignore-push-failures[Push what it can and ignores images with push failures.]' \ + '*:services:__docker-compose_services' && ret=0 + ;; (rm) _arguments \ $opts_help \ From cbb44b1a1462cefe3a1e02c75e36d527af3ff245 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Sat, 26 Mar 2016 21:41:32 -0700 Subject: [PATCH 014/118] fix broken zsh autocomplete for version 2 docker-compose files This has the added benefit of making autocompletion work when the docker-compose config file is in a parent directory. Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 0da217dc..f75d412d 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -19,26 +19,13 @@ # * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion # ------------------------------------------------------------------------- -# For compatibility reasons, Compose and therefore its completion supports several -# stack compositon files as listed here, in descending priority. -# Support for these filenames might be dropped in some future version. -__docker-compose_compose_file() { - local file - for file in docker-compose.y{,a}ml ; do - [ -e $file ] && { - echo $file - return - } - done - echo docker-compose.yml -} - # Extracts all service names from docker-compose.yml. ___docker-compose_all_services_in_compose_file() { local already_selected local -a services already_selected=$(echo $words | tr " " "|") - awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | grep -Ev "$already_selected" + docker-compose config --services 2>/dev/null \ + | grep -Ev "$already_selected" } # All services, even those without an existing container @@ -57,7 +44,12 @@ ___docker-compose_services_with_key() { local -a buildable already_selected=$(echo $words | tr " " "|") # flatten sections to one line, then filter lines containing the key and return section name. - awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' 2>/dev/null | grep -Ev "$already_selected" + docker-compose config 2>/dev/null \ + | sed -n -e '/^services:/,/^[^ ]/p' \ + | sed -n 's/^ //p' \ + | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ + | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' \ + | grep -Ev "$already_selected" } # All services that are defined by a Dockerfile reference From b3d9652cc303e20a991267cc7cea4b553739df17 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Tue, 26 Apr 2016 07:45:55 -0700 Subject: [PATCH 015/118] zsh autocomplete: add missing docker-compose base flags Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index f75d412d..1286b21c 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -358,10 +358,17 @@ _docker-compose() { _arguments -C \ '(- :)'{-h,--help}'[Get help]' \ - '--verbose[Show more output]' \ - '(- :)'{-v,--version}'[Print version and exit]' \ '(-f --file)'{-f,--file}'[Specify an alternate docker-compose file (default: docker-compose.yml)]:file:_files -g "*.yml"' \ '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \ + '--verbose[Show more output]' \ + '(- :)'{-v,--version}'[Print version and exit]' \ + '(-H --host)'{-H,--host}'[Daemon socket to connect to]:host:' \ + '--tls[Use TLS; implied by --tlsverify]' \ + '--tlscacert=[Trust certs signed only by this CA]:ca path:' \ + '--tlscert=[Path to TLS certificate file]:client cert path:' \ + '--tlskey=[Path to TLS key file]:tls key path:' \ + '--tlsverify[Use TLS and verify the remote]' \ + "--skip-hostname-check[Don't check the daemon's hostname against the name specified in the client certificate (for example if your docker host is an IP address)]" \ '(-): :->command' \ '(-)*:: :->option-or-argument' && ret=0 From 73a1b60ced0ae7974aa86484032c7122e0aa708f Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Tue, 26 Apr 2016 07:47:40 -0700 Subject: [PATCH 016/118] zsh autocomplete: add missing 'remove-orphans' flag for 'up' and 'down' Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 1286b21c..b5e44762 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -207,7 +207,8 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ "--rmi[Remove images, type may be one of: 'all' to remove all images, or 'local' to remove only images that don't have an custom name set by the 'image' field]:type:(all local)" \ - '(-v --volumes)'{-v,--volumes}"[Remove data volumes]" && ret=0 + '(-v --volumes)'{-v,--volumes}"[Remove data volumes]" \ + '--remove-orphans[Remove containers for services not defined in the Compose file]' && ret=0 ;; (events) _arguments \ @@ -329,6 +330,7 @@ __docker-compose_subcommand() { "--no-build[Don't build an image, even if it's missing]" \ "(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \ '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ + "--remove-orphans[Remove containers for services not defined in the Compose file]" \ '*:services:__docker-compose_services_all' && ret=0 ;; (version) From eb10f41d13affd1f901fb37230a6868ef58f3610 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Tue, 26 Apr 2016 07:49:48 -0700 Subject: [PATCH 017/118] zsh autocomplete: fix incorrect flag exclusions for 'create' and 'up' Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index b5e44762..2b82d4eb 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -198,9 +198,9 @@ __docker-compose_subcommand() { (create) _arguments \ $opts_help \ - "(--no-recreate --no-build)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ - "(--force-recreate)--no-build[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ - "(--force-recreate)--no-recreate[Don't build an image, even if it's missing]" \ + "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ + "(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ + "--no-build[Don't build an image, even if it's missing.]" \ '*:services:__docker-compose_services_all' && ret=0 ;; (down) @@ -325,9 +325,9 @@ __docker-compose_subcommand() { '--build[Build images before starting containers.]' \ '--no-color[Produce monochrome output.]' \ "--no-deps[Don't start linked services.]" \ - "--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ - "--no-recreate[If containers already exist, don't recreate them.]" \ - "--no-build[Don't build an image, even if it's missing]" \ + "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ + "(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ + "--no-build[Don't build an image, even if it's missing.]" \ "(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \ '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ "--remove-orphans[Remove containers for services not defined in the Compose file]" \ From 8d2fbe3a555c21c427ffd31b5a85fa77b926853f Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Tue, 26 Apr 2016 07:53:50 -0700 Subject: [PATCH 018/118] zsh autocomplete: add 'build' flag for 'create' and 'up' Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 2b82d4eb..b6cdb0a6 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -200,7 +200,8 @@ __docker-compose_subcommand() { $opts_help \ "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ "(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ - "--no-build[Don't build an image, even if it's missing.]" \ + "(--build)--no-build[Don't build an image, even if it's missing.]" \ + "(--no-build)--build[Build images before creating containers.]" \ '*:services:__docker-compose_services_all' && ret=0 ;; (down) @@ -322,12 +323,12 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ '(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names.]' \ - '--build[Build images before starting containers.]' \ '--no-color[Produce monochrome output.]' \ "--no-deps[Don't start linked services.]" \ "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ "(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ - "--no-build[Don't build an image, even if it's missing.]" \ + "(--build)--no-build[Don't build an image, even if it's missing.]" \ + "(--no-build)--build[Build images before starting containers.]" \ "(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \ '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ "--remove-orphans[Remove containers for services not defined in the Compose file]" \ From 1b5a94f4e413bd59312795302602ca98da69ce31 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Tue, 26 Apr 2016 07:56:51 -0700 Subject: [PATCH 019/118] zsh autocomplete: bring flag help texts up to date Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index b6cdb0a6..54bea10e 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -185,7 +185,7 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ '--force-rm[Always remove intermediate containers.]' \ - '--no-cache[Do not use cache when building the image]' \ + '--no-cache[Do not use cache when building the image.]' \ '--pull[Always attempt to pull a newer version of the image.]' \ '*:services:__docker-compose_services_from_build' && ret=0 ;; @@ -207,14 +207,14 @@ __docker-compose_subcommand() { (down) _arguments \ $opts_help \ - "--rmi[Remove images, type may be one of: 'all' to remove all images, or 'local' to remove only images that don't have an custom name set by the 'image' field]:type:(all local)" \ - '(-v --volumes)'{-v,--volumes}"[Remove data volumes]" \ + "--rmi[Remove images. Type must be one of: 'all': Remove all images used by any service. 'local': Remove only images that don't have a custom tag set by the \`image\` field.]:type:(all local)" \ + '(-v --volumes)'{-v,--volumes}"[Remove named volumes declared in the \`volumes\` section of the Compose file and anonymous volumes attached to containers.]" \ '--remove-orphans[Remove containers for services not defined in the Compose file]' && ret=0 ;; (events) _arguments \ $opts_help \ - '--json[Output events as a stream of json objects.]' \ + '--json[Output events as a stream of json objects]' \ '*:services:__docker-compose_services_all' && ret=0 ;; (exec) @@ -224,7 +224,7 @@ __docker-compose_subcommand() { '--privileged[Give extended privileges to the process.]' \ '--user=[Run the command as this user.]:username:_users' \ '-T[Disable pseudo-tty allocation. By default `docker-compose exec` allocates a TTY.]' \ - '--index=[Index of the container if there are multiple instances of a service (default: 1)]:index: ' \ + '--index=[Index of the container if there are multiple instances of a service \[default: 1\]]:index: ' \ '(-):running services:__docker-compose_runningservices' \ '(-):command: _command_names -e' \ '*::arguments: _normal' && ret=0 @@ -255,8 +255,8 @@ __docker-compose_subcommand() { (port) _arguments \ $opts_help \ - '--protocol=-[tcp or udap (defaults to tcp)]:protocol:(tcp udp)' \ - '--index=-[index of the container if there are mutiple instances of a service (defaults to 1)]:index: ' \ + '--protocol=[tcp or udp \[default: tcp\]]:protocol:(tcp udp)' \ + '--index=[index of the container if there are multiple instances of a service \[default: 1\]]:index: ' \ '1:running services:__docker-compose_runningservices' \ '2:port:_ports' && ret=0 ;; @@ -276,7 +276,7 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ '(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \ - '-v[Remove volumes associated with containers]' \ + '-v[Remove any anonymous volumes attached to containers]' \ '*:stopped services:__docker-compose_stoppedservices' && ret=0 ;; (run) @@ -285,14 +285,14 @@ __docker-compose_subcommand() { '-d[Detached mode: Run container in the background, print new container name.]' \ '*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \ '--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \ - '--name[Assign a name to the container]:name: ' \ + '--name=[Assign a name to the container]:name: ' \ "--no-deps[Don't start linked services.]" \ - '(-p --publish)'{-p,--publish=-}"[Run command with manually mapped container's port(s) to the host.]" \ + '(-p --publish)'{-p,--publish=}"[Publish a container's port(s) to the host]" \ '--rm[Remove container after run. Ignored in detached mode.]' \ "--service-ports[Run command with the service's ports enabled and mapped to the host.]" \ '-T[Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY.]' \ - '(-u --user)'{-u,--user=-}'[Run as specified username or uid]:username or uid:_users' \ - '(-w --workdir)'{-w=,--workdir=}'[Working directory inside the container]:workdir: ' \ + '(-u --user)'{-u,--user=}'[Run as specified username or uid]:username or uid:_users' \ + '(-w --workdir)'{-w,--workdir=}'[Working directory inside the container]:workdir: ' \ '(-):services:__docker-compose_services' \ '(-):command: _command_names -e' \ '*::arguments: _normal' && ret=0 @@ -322,7 +322,7 @@ __docker-compose_subcommand() { (up) _arguments \ $opts_help \ - '(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names.]' \ + '(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names. Incompatible with --abort-on-container-exit.]' \ '--no-color[Produce monochrome output.]' \ "--no-deps[Don't start linked services.]" \ "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ @@ -330,7 +330,7 @@ __docker-compose_subcommand() { "(--build)--no-build[Don't build an image, even if it's missing.]" \ "(--no-build)--build[Build images before starting containers.]" \ "(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \ - '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ + '(-t --timeout)'{-t,--timeout}"[Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)]:seconds: " \ "--remove-orphans[Remove containers for services not defined in the Compose file]" \ '*:services:__docker-compose_services_all' && ret=0 ;; From 97ba14c82adecef3a9538e434d85c1e213ab2e38 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Sat, 26 Mar 2016 23:17:00 -0700 Subject: [PATCH 020/118] zsh autocomplete: use two underscores for all function names Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 54bea10e..e6990b77 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -20,7 +20,7 @@ # ------------------------------------------------------------------------- # Extracts all service names from docker-compose.yml. -___docker-compose_all_services_in_compose_file() { +__docker-compose_all_services_in_compose_file() { local already_selected local -a services already_selected=$(echo $words | tr " " "|") @@ -32,14 +32,14 @@ ___docker-compose_all_services_in_compose_file() { __docker-compose_services_all() { [[ $PREFIX = -* ]] && return 1 integer ret=1 - services=$(___docker-compose_all_services_in_compose_file) + services=$(__docker-compose_all_services_in_compose_file) _alternative "args:services:($services)" && ret=0 return ret } # All services that have an entry with the given key in their docker-compose.yml section -___docker-compose_services_with_key() { +__docker-compose_services_with_key() { local already_selected local -a buildable already_selected=$(echo $words | tr " " "|") @@ -56,7 +56,7 @@ ___docker-compose_services_with_key() { __docker-compose_services_from_build() { [[ $PREFIX = -* ]] && return 1 integer ret=1 - buildable=$(___docker-compose_services_with_key build) + buildable=$(__docker-compose_services_with_key build) _alternative "args:buildable services:($buildable)" && ret=0 return ret @@ -66,7 +66,7 @@ __docker-compose_services_from_build() { __docker-compose_services_from_image() { [[ $PREFIX = -* ]] && return 1 integer ret=1 - pullable=$(___docker-compose_services_with_key image) + pullable=$(__docker-compose_services_with_key image) _alternative "args:pullable services:($pullable)" && ret=0 return ret From d990f7899c921260876d93ec42444bc8fcb08e37 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Sun, 27 Mar 2016 02:33:16 -0700 Subject: [PATCH 021/118] zsh autocomplete: pass all relevant flags to docker-compose/docker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For autocomplete to work properly, we need to pass along some flags when calling docker (--host, --tls, …) and docker-compose (--file, --tls, …). Previously flags would only be passed to docker-compose, and the only flags passed were --file and --project-name. This commit makes sure that all relevant flags are passed to both docker-compose and docker. Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 50 ++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index e6990b77..eeed07a6 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -19,12 +19,16 @@ # * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion # ------------------------------------------------------------------------- +__docker-compose_q() { + docker-compose 2>/dev/null $compose_options "$@" +} + # Extracts all service names from docker-compose.yml. __docker-compose_all_services_in_compose_file() { local already_selected local -a services already_selected=$(echo $words | tr " " "|") - docker-compose config --services 2>/dev/null \ + __docker-compose_q config --services \ | grep -Ev "$already_selected" } @@ -44,7 +48,7 @@ __docker-compose_services_with_key() { local -a buildable already_selected=$(echo $words | tr " " "|") # flatten sections to one line, then filter lines containing the key and return section name. - docker-compose config 2>/dev/null \ + __docker-compose_q config \ | sed -n -e '/^services:/,/^[^ ]/p' \ | sed -n 's/^ //p' \ | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ @@ -88,7 +92,7 @@ __docker-compose_get_services() { shift [[ $kind =~ (stopped|all) ]] && args=($args -a) - lines=(${(f)"$(_call_program commands docker ps $args)"}) + lines=(${(f)"$(_call_program commands docker $docker_options ps $args)"}) services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"}) # Parse header line to find columns @@ -375,9 +379,43 @@ _docker-compose() { '(-): :->command' \ '(-)*:: :->option-or-argument' && ret=0 - local compose_file=${opt_args[-f]}${opt_args[--file]} - local compose_project=${opt_args[-p]}${opt_args[--project-name]} - local compose_options="${compose_file:+--file $compose_file} ${compose_project:+--project-name $compose_project}" + local -a relevant_compose_flags relevant_docker_flags compose_options docker_options + + relevant_compose_flags=( + "--file" "-f" + "--host" "-H" + "--project-name" "-p" + "--tls" + "--tlscacert" + "--tlscert" + "--tlskey" + "--tlsverify" + "--skip-hostname-check" + ) + + relevant_docker_flags=( + "--host" "-H" + "--tls" + "--tlscacert" + "--tlscert" + "--tlskey" + "--tlsverify" + ) + + for k in "${(@k)opt_args}"; do + if [[ -n "${relevant_docker_flags[(r)$k]}" ]]; then + docker_options+=$k + if [[ -n "$opt_args[$k]" ]]; then + docker_options+=$opt_args[$k] + fi + fi + if [[ -n "${relevant_compose_flags[(r)$k]}" ]]; then + compose_options+=$k + if [[ -n "$opt_args[$k]" ]]; then + compose_options+=$opt_args[$k] + fi + fi + done case $state in (command) From 048408af4835134734bcb565a8c07991a8818530 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Sun, 27 Mar 2016 23:21:58 -0700 Subject: [PATCH 022/118] zsh autocomplete: fix missing services issue for build/pull commands Previously, the autocomplete for the build/pull commands would only add services for which build/image were the _first_ keys, respectively, in the docker-compose file. This commit fixes this, so the appropriate services are listed regardless of the order in which they appear Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index eeed07a6..7fc692cf 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -52,7 +52,8 @@ __docker-compose_services_with_key() { | sed -n -e '/^services:/,/^[^ ]/p' \ | sed -n 's/^ //p' \ | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ - | awk -F: -v key=": +$1:" '$0 ~ key {print $1}' \ + | grep " \+$1:" \ + | sed "s/:.*//g" \ | grep -Ev "$already_selected" } From 612d263d7481edfdcfe7c82835177e17284adb55 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Tue, 29 Mar 2016 21:31:06 -0700 Subject: [PATCH 023/118] zsh autocomplete: fix issue when filtering on already selected services Previously, the filtering on already selected services would break when one service was a substring of another. This commit fixes that. Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 7fc692cf..27b17b07 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -29,7 +29,7 @@ __docker-compose_all_services_in_compose_file() { local -a services already_selected=$(echo $words | tr " " "|") __docker-compose_q config --services \ - | grep -Ev "$already_selected" + | grep -Ev "^(${already_selected})$" } # All services, even those without an existing container @@ -54,7 +54,7 @@ __docker-compose_services_with_key() { | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ | grep " \+$1:" \ | sed "s/:.*//g" \ - | grep -Ev "$already_selected" + | grep -Ev "^(${already_selected})$" } # All services that are defined by a Dockerfile reference From 0058b4ba0ce8f76341bedc0988be09cde6ee8441 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Thu, 28 Apr 2016 19:24:44 -0700 Subject: [PATCH 024/118] zsh autocomplete: replace use of sed with cut Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 27b17b07..c7df4b44 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -53,7 +53,7 @@ __docker-compose_services_with_key() { | sed -n 's/^ //p' \ | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ | grep " \+$1:" \ - | sed "s/:.*//g" \ + | cut -d: -f1 \ | grep -Ev "^(${already_selected})$" } From b3d4e9c9d7c0a9a85aa2e1958a78ee6910d98e08 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Thu, 28 Apr 2016 19:47:41 -0700 Subject: [PATCH 025/118] zsh autocomplete: break out duplicated flag messages into variables Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 40 ++++++++++++++++---------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index c7df4b44..77348d5c 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -182,7 +182,17 @@ __docker-compose_commands() { } __docker-compose_subcommand() { - local opts_help='(: -)--help[Print usage]' + local opts_help opts_force_recreate opts_no_recreate opts_no_build opts_remove_orphans opts_timeout opts_no_color opts_no_deps + + opts_help='(: -)--help[Print usage]' + opts_force_recreate="(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" + opts_no_recreate="(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" + opts_no_build="(--build)--no-build[Don't build an image, even if it's missing.]" + opts_remove_orphans="--remove-orphans[Remove containers for services not defined in the Compose file]" + opts_timeout=('(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: ") + opts_no_color='--no-color[Produce monochrome output.]' + opts_no_deps="--no-deps[Don't start linked services.]" + integer ret=1 case "$words[1]" in @@ -203,9 +213,9 @@ __docker-compose_subcommand() { (create) _arguments \ $opts_help \ - "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ - "(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ - "(--build)--no-build[Don't build an image, even if it's missing.]" \ + $opts_force_recreate \ + $opts_no_recreate \ + $opts_no_build \ "(--no-build)--build[Build images before creating containers.]" \ '*:services:__docker-compose_services_all' && ret=0 ;; @@ -214,7 +224,7 @@ __docker-compose_subcommand() { $opts_help \ "--rmi[Remove images. Type must be one of: 'all': Remove all images used by any service. 'local': Remove only images that don't have a custom tag set by the \`image\` field.]:type:(all local)" \ '(-v --volumes)'{-v,--volumes}"[Remove named volumes declared in the \`volumes\` section of the Compose file and anonymous volumes attached to containers.]" \ - '--remove-orphans[Remove containers for services not defined in the Compose file]' && ret=0 + $opts_remove_orphans && ret=0 ;; (events) _arguments \ @@ -247,7 +257,7 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ '(-f --follow)'{-f,--follow}'[Follow log output]' \ - '--no-color[Produce monochrome output.]' \ + $opts_no_color \ '--tail=[Number of lines to show from the end of the logs for each container.]:number of lines: ' \ '(-t --timestamps)'{-t,--timestamps}'[Show timestamps]' \ '*:services:__docker-compose_services_all' && ret=0 @@ -291,7 +301,7 @@ __docker-compose_subcommand() { '*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \ '--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \ '--name=[Assign a name to the container]:name: ' \ - "--no-deps[Don't start linked services.]" \ + $opts_no_deps \ '(-p --publish)'{-p,--publish=}"[Publish a container's port(s) to the host]" \ '--rm[Remove container after run. Ignored in detached mode.]' \ "--service-ports[Run command with the service's ports enabled and mapped to the host.]" \ @@ -305,7 +315,7 @@ __docker-compose_subcommand() { (scale) _arguments \ $opts_help \ - '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ + $opts_timeout \ '*:running services:__docker-compose_runningservices' && ret=0 ;; (start) @@ -316,7 +326,7 @@ __docker-compose_subcommand() { (stop|restart) _arguments \ $opts_help \ - '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ + $opts_timeout \ '*:running services:__docker-compose_runningservices' && ret=0 ;; (unpause) @@ -328,15 +338,15 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ '(--abort-on-container-exit)-d[Detached mode: Run containers in the background, print new container names. Incompatible with --abort-on-container-exit.]' \ - '--no-color[Produce monochrome output.]' \ - "--no-deps[Don't start linked services.]" \ - "(--no-recreate)--force-recreate[Recreate containers even if their configuration and image haven't changed. Incompatible with --no-recreate.]" \ - "(--force-recreate)--no-recreate[If containers already exist, don't recreate them. Incompatible with --force-recreate.]" \ - "(--build)--no-build[Don't build an image, even if it's missing.]" \ + $opts_no_color \ + $opts_no_deps \ + $opts_force_recreate \ + $opts_no_recreate \ + $opts_no_build \ "(--no-build)--build[Build images before starting containers.]" \ "(-d)--abort-on-container-exit[Stops all containers if any container was stopped. Incompatible with -d.]" \ '(-t --timeout)'{-t,--timeout}"[Use this timeout in seconds for container shutdown when attached or when containers are already running. (default: 10)]:seconds: " \ - "--remove-orphans[Remove containers for services not defined in the Compose file]" \ + $opts_remove_orphans \ '*:services:__docker-compose_services_all' && ret=0 ;; (version) From c3247e7af8edcca113018b9e39af0282a649d6f7 Mon Sep 17 00:00:00 2001 From: Andre Eriksson Date: Mon, 27 Jun 2016 10:57:35 -0700 Subject: [PATCH 026/118] zsh autocomplete: update misleading comment Signed-off-by: Andre Eriksson --- contrib/completion/zsh/_docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 77348d5c..a59abe29 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -23,7 +23,7 @@ __docker-compose_q() { docker-compose 2>/dev/null $compose_options "$@" } -# Extracts all service names from docker-compose.yml. +# All services defined in docker-compose.yml __docker-compose_all_services_in_compose_file() { local already_selected local -a services From 058a7659ba2590dbc067fe76bb6044e0d4f9b192 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 22 Jun 2016 14:53:45 -0700 Subject: [PATCH 027/118] Update bundle extension It's now .dab, for Distributed Application Bundle Signed-off-by: Aanand Prasad --- compose/cli/main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 25ee9050..ae0175d5 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -217,7 +217,7 @@ class TopLevelCommand(object): def bundle(self, config_options, options): """ - Generate a Docker bundle from the Compose file. + Generate a Distributed Application Bundle (DAB) from the Compose file. Images must have digests stored, which requires interaction with a Docker registry. If digests aren't stored for all images, you can pass @@ -231,14 +231,14 @@ class TopLevelCommand(object): --fetch-digests Automatically fetch image digests if missing -o, --output PATH Path to write the bundle file to. - Defaults to ".dsb". + Defaults to ".dab". """ self.project = project_from_options('.', config_options) compose_config = get_config_from_options(self.project_dir, config_options) output = options["--output"] if not output: - output = "{}.dsb".format(self.project.name) + output = "{}.dab".format(self.project.name) with errors.handle_connection_errors(self.project.client): try: From 8e0458205241f654bb1d75de9babd9880afa7206 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 27 Jun 2016 16:21:19 -0700 Subject: [PATCH 028/118] Fix tests to accommodate short-id container alias Signed-off-by: Aanand Prasad --- tests/acceptance/cli_test.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 068d0efc..85d5776b 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1135,7 +1135,10 @@ class CLITestCase(DockerClientTestCase): ] for _, config in networks.items(): - assert not config['Aliases'] + # TODO: once we drop support for API <1.24, this can be changed to: + # assert config['Aliases'] == [container.short_id] + aliases = set(config['Aliases']) - set([container.short_id]) + assert not aliases @v2_only() def test_run_detached_connects_to_network(self): @@ -1152,7 +1155,10 @@ class CLITestCase(DockerClientTestCase): ] for _, config in networks.items(): - assert not config['Aliases'] + # TODO: once we drop support for API <1.24, this can be changed to: + # assert config['Aliases'] == [container.short_id] + aliases = set(config['Aliases']) - set([container.short_id]) + assert not aliases assert self.lookup(container, 'app') assert self.lookup(container, 'db') From 95207561bb510a7165fbcb1e9250e6d1baea4c09 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Tue, 14 Jun 2016 16:55:48 -0400 Subject: [PATCH 029/118] Add some unit tests for new bundle and push commands. Signed-off-by: Daniel Nephin --- compose/bundle.py | 38 +++-- tests/unit/bundle_test.py | 232 +++++++++++++++++++++++++++++ tests/unit/progress_stream_test.py | 20 +++ 3 files changed, 270 insertions(+), 20 deletions(-) create mode 100644 tests/unit/bundle_test.py diff --git a/compose/bundle.py b/compose/bundle.py index 965d65c8..8a1e859e 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -57,17 +57,7 @@ class MissingDigests(Exception): def serialize_bundle(config, image_digests): - if config.networks: - log.warn("Unsupported top level key 'networks' - ignoring") - - if config.volumes: - log.warn("Unsupported top level key 'volumes' - ignoring") - - return json.dumps( - to_bundle(config, image_digests), - indent=2, - sort_keys=True, - ) + return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True) def get_image_digests(project, allow_fetch=False): @@ -99,7 +89,7 @@ def get_image_digest(service, allow_fetch=False): "required to generate a proper image digest for the bundle. Specify " "an image repo and tag with the 'image' option.".format(s=service)) - separator = parse_repository_tag(service.options['image'])[2] + _, _, separator = parse_repository_tag(service.options['image']) # Compose file already uses a digest, no lookup required if separator == '@': return service.options['image'] @@ -143,24 +133,32 @@ def fetch_image_digest(service): if not digest: raise ValueError("Failed to get digest for %s" % service.name) - repo = parse_repository_tag(service.options['image'])[0] + repo, _, _ = parse_repository_tag(service.options['image']) identifier = '{repo}@{digest}'.format(repo=repo, digest=digest) - # Pull by digest so that image['RepoDigests'] is populated for next time - # and we don't have to pull/push again - service.client.pull(identifier) - - log.info("Stored digest for {}".format(service.image_name)) + # only do this is RepoTags isn't already populated + image = service.image() + if not image['RepoDigests']: + # Pull by digest so that image['RepoDigests'] is populated for next time + # and we don't have to pull/push again + service.client.pull(identifier) + log.info("Stored digest for {}".format(service.image_name)) return identifier def to_bundle(config, image_digests): + if config.networks: + log.warn("Unsupported top level key 'networks' - ignoring") + + if config.volumes: + log.warn("Unsupported top level key 'volumes' - ignoring") + config = denormalize_config(config) return { - 'version': VERSION, - 'services': { + 'Version': VERSION, + 'Services': { name: convert_service_to_bundle( name, service_dict, diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py new file mode 100644 index 00000000..ff4c0dce --- /dev/null +++ b/tests/unit/bundle_test.py @@ -0,0 +1,232 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import docker +import mock +import pytest + +from compose import bundle +from compose import service +from compose.cli.errors import UserError +from compose.config.config import Config + + +@pytest.fixture +def mock_service(): + return mock.create_autospec( + service.Service, + client=mock.create_autospec(docker.Client), + options={}) + + +def test_get_image_digest_exists(mock_service): + mock_service.options['image'] = 'abcd' + mock_service.image.return_value = {'RepoDigests': ['digest1']} + digest = bundle.get_image_digest(mock_service) + assert digest == 'digest1' + + +def test_get_image_digest_image_uses_digest(mock_service): + mock_service.options['image'] = image_id = 'redis@sha256:digest' + + digest = bundle.get_image_digest(mock_service) + assert digest == image_id + assert not mock_service.image.called + + +def test_get_image_digest_no_image(mock_service): + with pytest.raises(UserError) as exc: + bundle.get_image_digest(service.Service(name='theservice')) + + assert "doesn't define an image tag" in exc.exconly() + + +def test_fetch_image_digest_for_image_with_saved_digest(mock_service): + mock_service.options['image'] = image_id = 'abcd' + mock_service.pull.return_value = expected = 'sha256:thedigest' + mock_service.image.return_value = {'RepoDigests': ['digest1']} + + digest = bundle.fetch_image_digest(mock_service) + assert digest == image_id + '@' + expected + + mock_service.pull.assert_called_once_with() + assert not mock_service.push.called + assert not mock_service.client.pull.called + + +def test_fetch_image_digest_for_image(mock_service): + mock_service.options['image'] = image_id = 'abcd' + mock_service.pull.return_value = expected = 'sha256:thedigest' + mock_service.image.return_value = {'RepoDigests': []} + + digest = bundle.fetch_image_digest(mock_service) + assert digest == image_id + '@' + expected + + mock_service.pull.assert_called_once_with() + assert not mock_service.push.called + mock_service.client.pull.assert_called_once_with(digest) + + +def test_fetch_image_digest_for_build(mock_service): + mock_service.options['build'] = '.' + mock_service.options['image'] = image_id = 'abcd' + mock_service.push.return_value = expected = 'sha256:thedigest' + mock_service.image.return_value = {'RepoDigests': ['digest1']} + + digest = bundle.fetch_image_digest(mock_service) + assert digest == image_id + '@' + expected + + mock_service.push.assert_called_once_with() + assert not mock_service.pull.called + assert not mock_service.client.pull.called + + +def test_to_bundle(): + image_digests = {'a': 'aaaa', 'b': 'bbbb'} + services = [ + {'name': 'a', 'build': '.', }, + {'name': 'b', 'build': './b'}, + ] + config = Config( + version=2, + services=services, + volumes={'special': {}}, + networks={'extra': {}}) + + with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + output = bundle.to_bundle(config, image_digests) + + assert mock_log.mock_calls == [ + mock.call("Unsupported top level key 'networks' - ignoring"), + mock.call("Unsupported top level key 'volumes' - ignoring"), + ] + + assert output == { + 'Version': '0.1', + 'Services': { + 'a': {'Image': 'aaaa', 'Networks': ['default']}, + 'b': {'Image': 'bbbb', 'Networks': ['default']}, + } + } + + +def test_convert_service_to_bundle(): + name = 'theservice' + image_digest = 'thedigest' + service_dict = { + 'ports': ['80'], + 'expose': ['1234'], + 'networks': {'extra': {}}, + 'command': 'foo', + 'entrypoint': 'entry', + 'environment': {'BAZ': 'ENV'}, + 'build': '.', + 'working_dir': '/tmp', + 'user': 'root', + 'labels': {'FOO': 'LABEL'}, + 'privileged': True, + } + + with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + config = bundle.convert_service_to_bundle(name, service_dict, image_digest) + + mock_log.assert_called_once_with( + "Unsupported key 'privileged' in services.theservice - ignoring") + + assert config == { + 'Image': image_digest, + 'Ports': [ + {'Protocol': 'tcp', 'Port': 80}, + {'Protocol': 'tcp', 'Port': 1234}, + ], + 'Networks': ['extra'], + 'Command': ['entry', 'foo'], + 'Env': ['BAZ=ENV'], + 'WorkingDir': '/tmp', + 'User': 'root', + 'Labels': {'FOO': 'LABEL'}, + } + + +def test_set_command_and_args_none(): + config = {} + bundle.set_command_and_args(config, [], []) + assert config == {} + + +def test_set_command_and_args_from_command(): + config = {} + bundle.set_command_and_args(config, [], "echo ok") + assert config == {'Args': ['echo', 'ok']} + + +def test_set_command_and_args_from_entrypoint(): + config = {} + bundle.set_command_and_args(config, "echo entry", []) + assert config == {'Command': ['echo', 'entry']} + + +def test_set_command_and_args_from_both(): + config = {} + bundle.set_command_and_args(config, "echo entry", ["extra", "arg"]) + assert config == {'Command': ['echo', 'entry', "extra", "arg"]} + + +def test_make_service_networks_default(): + name = 'theservice' + service_dict = {} + + with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + networks = bundle.make_service_networks(name, service_dict) + + assert not mock_log.called + assert networks == ['default'] + + +def test_make_service_networks(): + name = 'theservice' + service_dict = { + 'networks': { + 'foo': { + 'aliases': ['one', 'two'], + }, + 'bar': {} + }, + } + + with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: + networks = bundle.make_service_networks(name, service_dict) + + mock_log.assert_called_once_with( + "Unsupported key 'aliases' in services.theservice.networks.foo - ignoring") + assert sorted(networks) == sorted(service_dict['networks']) + + +def test_make_port_specs(): + service_dict = { + 'expose': ['80', '500/udp'], + 'ports': [ + '400:80', + '222', + '127.0.0.1:8001:8001', + '127.0.0.1:5000-5001:3000-3001'], + } + port_specs = bundle.make_port_specs(service_dict) + assert port_specs == [ + {'Protocol': 'tcp', 'Port': 80}, + {'Protocol': 'tcp', 'Port': 222}, + {'Protocol': 'tcp', 'Port': 8001}, + {'Protocol': 'tcp', 'Port': 3000}, + {'Protocol': 'tcp', 'Port': 3001}, + {'Protocol': 'udp', 'Port': 500}, + ] + + +def test_make_port_spec_with_protocol(): + port_spec = bundle.make_port_spec("5000/udp") + assert port_spec == {'Protocol': 'udp', 'Port': 5000} + + +def test_make_port_spec_default_protocol(): + port_spec = bundle.make_port_spec("50000") + assert port_spec == {'Protocol': 'tcp', 'Port': 50000} diff --git a/tests/unit/progress_stream_test.py b/tests/unit/progress_stream_test.py index b01be11a..c0cb906d 100644 --- a/tests/unit/progress_stream_test.py +++ b/tests/unit/progress_stream_test.py @@ -65,3 +65,23 @@ class ProgressStreamTestCase(unittest.TestCase): events = progress_stream.stream_output(events, output) self.assertTrue(len(output.getvalue()) > 0) + + +def test_get_digest_from_push(): + digest = "sha256:abcd" + events = [ + {"status": "..."}, + {"status": "..."}, + {"progressDetail": {}, "aux": {"Digest": digest}}, + ] + assert progress_stream.get_digest_from_push(events) == digest + + +def test_get_digest_from_pull(): + digest = "sha256:abcd" + events = [ + {"status": "..."}, + {"status": "..."}, + {"status": "Digest: %s" % digest}, + ] + assert progress_stream.get_digest_from_pull(events) == digest From 5640bd42a83e7d30a2f52e919d6e4e691095b9ed Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 16 Jun 2016 15:15:17 -0400 Subject: [PATCH 030/118] Add an acceptance test for bundle. Signed-off-by: Daniel Nephin --- compose/bundle.py | 2 +- tests/acceptance/cli_test.py | 27 +++++++++++++++++++ .../bundle-with-digests/docker-compose.yml | 9 +++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/bundle-with-digests/docker-compose.yml diff --git a/compose/bundle.py b/compose/bundle.py index 8a1e859e..44f6954b 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -136,7 +136,7 @@ def fetch_image_digest(service): repo, _, _ = parse_repository_tag(service.options['image']) identifier = '{repo}@{digest}'.format(repo=repo, digest=digest) - # only do this is RepoTags isn't already populated + # only do this if RepoDigests isn't already populated image = service.image() if not image['RepoDigests']: # Pull by digest so that image['RepoDigests'] is populated for next time diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 068d0efc..7aef7b52 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -12,6 +12,7 @@ from collections import Counter from collections import namedtuple from operator import attrgetter +import py import yaml from docker import errors @@ -378,6 +379,32 @@ class CLITestCase(DockerClientTestCase): ] assert not containers + def test_bundle_with_digests(self): + self.base_dir = 'tests/fixtures/bundle-with-digests/' + tmpdir = py.test.ensuretemp('cli_test_bundle') + self.addCleanup(tmpdir.remove) + filename = str(tmpdir.join('example.dab')) + + self.dispatch(['bundle', '--output', filename]) + with open(filename, 'r') as fh: + bundle = json.load(fh) + + assert bundle == { + 'Version': '0.1', + 'Services': { + 'web': { + 'Image': ('dockercloud/hello-world@sha256:fe79a2cfbd17eefc3' + '44fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d'), + 'Networks': ['default'], + }, + 'redis': { + 'Image': ('redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d' + '374b2b7392de1e7d77be26ef8f7b'), + 'Networks': ['default'], + } + }, + } + def test_create(self): self.dispatch(['create']) service = self.project.get_service('simple') diff --git a/tests/fixtures/bundle-with-digests/docker-compose.yml b/tests/fixtures/bundle-with-digests/docker-compose.yml new file mode 100644 index 00000000..b7013512 --- /dev/null +++ b/tests/fixtures/bundle-with-digests/docker-compose.yml @@ -0,0 +1,9 @@ + +version: '2.0' + +services: + web: + image: dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d + + redis: + image: redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d374b2b7392de1e7d77be26ef8f7b From a822406eb0179ec1f6fe09637dfc3b20662a7085 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 16 Jun 2016 15:10:56 -0700 Subject: [PATCH 031/118] Update docker-py version in requirements Signed-off-by: Joffrey F --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index eb5275f4..160bd0ef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PyYAML==3.11 cached-property==1.2.0 -docker-py==1.8.1 +docker-py==1.9.0rc2 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4 diff --git a/setup.py b/setup.py index 0b37c1dd..3696adc6 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires = [ 'requests >= 2.6.1, < 2.8', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker-py >= 1.8.1, < 2', + 'docker-py == 1.9.0rc2', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', From edd28f09d1bfe16b969ae6719b4a7e66e12c82f7 Mon Sep 17 00:00:00 2001 From: Ke Xu Date: Wed, 29 Jun 2016 11:01:50 +0900 Subject: [PATCH 032/118] change dsb to dab Signed-off-by: Ke Xu --- contrib/completion/zsh/_docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 539482e5..2995932d 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -200,7 +200,7 @@ __docker-compose_subcommand() { (bundle) _arguments \ $opts_help \ - '(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to ".dsb".]:file:_files' && ret=0 + '(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to ".dab".]:file:_files' && ret=0 ;; (config) _arguments \ From 2b6ea847b91da78023b94b832eaffc65170ae74d Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Wed, 29 Jun 2016 10:25:47 -0400 Subject: [PATCH 033/118] Update requirements.txt Signed-off-by: Daniel Nephin --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index eb5275f4..d4748aa1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ PyYAML==3.11 +backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 docker-py==1.8.1 dockerpty==0.4.1 docopt==0.6.1 -enum34==1.0.4 +enum34==1.0.4; python_version < '3.4' +functools32==3.2.3.post2; python_version < '3.2' +ipaddress==1.0.16 jsonschema==2.5.1 requests==2.7.0 six==1.7.3 From 622de27c1e1cbb5f25140cf6d4bc8bb0625d9a3b Mon Sep 17 00:00:00 2001 From: Harald Albers Date: Wed, 29 Jun 2016 08:45:36 -0700 Subject: [PATCH 034/118] bash completion for `docker-compose bundle --fetch-digests` Signed-off-by: Harald Albers --- contrib/completion/bash/docker-compose | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 0adfdca8..0201bcb2 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -117,7 +117,7 @@ _docker_compose_bundle() { ;; esac - COMPREPLY=( $( compgen -W "--help --output -o" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--fetch-digests --help --output -o" -- "$cur" ) ) } From a62739b9068ac4f83580021794c736c98b3415f8 Mon Sep 17 00:00:00 2001 From: Chris Clark Date: Thu, 30 Jun 2016 15:30:22 -0700 Subject: [PATCH 035/118] Signed-off-by: Chris Clark The postgres image expects a specific volume path. The docs had a typo in that path. This can cause a lot of agony. This is not hypothetical agony, but very real agony that was very recently experienced :) --- 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 8c10b5f3..464cf271 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -789,7 +789,7 @@ called `data` and mount it into the `db` service's containers. db: image: postgres volumes: - - data:/var/lib/postgres/data + - data:/var/lib/postgresql/data volumes: data: From 5d244ef6d89f79bb9323b2ecf74a7895d7a46b8d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 22 Apr 2016 14:45:51 -0700 Subject: [PATCH 036/118] Unset env vars behavior in 'run' mirroring engine Unset env vars passed to `run` via command line options take the value of the system's var with the same name. Signed-off-by: Joffrey F --- compose/cli/main.py | 4 +++- compose/config/environment.py | 12 ++++++++++++ tests/acceptance/cli_test.py | 8 ++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index ae0175d5..5d2b4fa2 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -891,7 +891,9 @@ def build_container_options(options, detach, command): } if options['-e']: - container_options['environment'] = parse_environment(options['-e']) + container_options['environment'] = Environment.from_command_line( + parse_environment(options['-e']) + ) if options['--entrypoint']: container_options['entrypoint'] = options.get('--entrypoint') diff --git a/compose/config/environment.py b/compose/config/environment.py index ff08b771..5d6b5af6 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -60,6 +60,18 @@ class Environment(dict): instance.update(os.environ) return instance + @classmethod + def from_command_line(cls, parsed_env_opts): + result = cls() + for k, v in parsed_env_opts.items(): + # Values from the command line take priority, unless they're unset + # in which case they take the value from the system's environment + if v is None and k in os.environ: + result[k] = os.environ[k] + else: + result[k] = v + return result + def __getitem__(self, key): try: return super(Environment, self).__getitem__(key) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 7b1785ee..43fe5650 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1216,6 +1216,14 @@ class CLITestCase(DockerClientTestCase): 'simplecomposefile_simple_run_1', 'exited')) + @mock.patch.dict(os.environ) + def test_run_env_values_from_system(self): + os.environ['FOO'] = 'bar' + os.environ['BAR'] = 'baz' + result = self.dispatch(['run', '-e', 'FOO', 'simple', 'env'], None) + assert 'FOO=bar' in result.stdout + assert 'BAR=baz' not in result.stdout + def test_rm(self): service = self.project.get_service('simple') service.create_container() From 10ae81f8cf94b71bdea03bcb622d972baf39f011 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 30 Jun 2016 15:18:05 -0700 Subject: [PATCH 037/118] Post-merge fix - restore Environment import in main.py Signed-off-by: Aanand Prasad --- compose/cli/main.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/cli/main.py b/compose/cli/main.py index 5d2b4fa2..f4c17167 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -19,6 +19,7 @@ from ..bundle import MissingDigests from ..bundle import serialize_bundle from ..config import ConfigurationError from ..config import parse_environment +from ..config.environment import Environment from ..config.serialize import serialize_config from ..const import DEFAULT_TIMEOUT from ..const import IS_WINDOWS_PLATFORM From 50d5aab8adbb4463aec49e31d0b90080de3733e3 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 30 Jun 2016 16:26:12 -0700 Subject: [PATCH 038/118] Fix test: check container's Env array instead of the output of 'env' Signed-off-by: Aanand Prasad --- tests/acceptance/cli_test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 43fe5650..64662654 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1220,9 +1220,13 @@ class CLITestCase(DockerClientTestCase): def test_run_env_values_from_system(self): os.environ['FOO'] = 'bar' os.environ['BAR'] = 'baz' - result = self.dispatch(['run', '-e', 'FOO', 'simple', 'env'], None) - assert 'FOO=bar' in result.stdout - assert 'BAR=baz' not in result.stdout + + self.dispatch(['run', '-e', 'FOO', 'simple', 'true'], None) + + container = self.project.containers(one_off=OneOffFilter.only, stopped=True)[0] + environment = container.get('Config.Env') + assert 'FOO=bar' in environment + assert 'BAR=baz' not in environment def test_rm(self): service = self.project.get_service('simple') From 3d0a1de0237024ae8ac49c052edf70d811b069da Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 30 Jun 2016 18:40:39 -0400 Subject: [PATCH 039/118] Upgrade pip on osx Signed-off-by: Daniel Nephin --- script/travis/ci | 2 +- script/travis/install | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/script/travis/ci b/script/travis/ci index 4cce1bc8..cd4fcc6d 100755 --- a/script/travis/ci +++ b/script/travis/ci @@ -6,5 +6,5 @@ if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then tox -e py27,py34 -- tests/unit else # TODO: we could also install py34 and test against it - python -m tox -e py27 -- tests/unit + tox -e py27 -- tests/unit fi diff --git a/script/travis/install b/script/travis/install index a23667bf..d4b34786 100755 --- a/script/travis/install +++ b/script/travis/install @@ -5,5 +5,6 @@ set -ex if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install tox==2.1.1 else - pip install --user tox==2.1.1 + sudo pip install --upgrade pip tox==2.1.1 virtualenv + pip --version fi From 931b01acf93f44b89999f4239e91c3bc8d15dba0 Mon Sep 17 00:00:00 2001 From: allencloud Date: Sat, 2 Jul 2016 23:27:10 +0800 Subject: [PATCH 040/118] make-output-consistent-typo Signed-off-by: allencloud --- compose/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index f4c17167..ff6dba11 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -178,7 +178,7 @@ class TopLevelCommand(object): pause Pause services port Print the public port for a port binding ps List containers - pull Pulls service images + pull Pull service images push Push service images restart Restart services rm Remove stopped containers From 6fe5d2b54351143ee4eab090ccd58c5067985078 Mon Sep 17 00:00:00 2001 From: George Lester Date: Tue, 5 Jul 2016 23:43:25 -0700 Subject: [PATCH 041/118] Implemented oom_score_adj Signed-off-by: George Lester --- compose/config/config.py | 1 + compose/config/config_schema_v2.0.json | 1 + compose/service.py | 2 ++ docs/compose-file.md | 2 +- tests/integration/service_test.py | 5 +++++ tests/unit/config/config_test.py | 20 ++++++++++++++++++++ 6 files changed, 30 insertions(+), 1 deletion(-) diff --git a/compose/config/config.py b/compose/config/config.py index 7a2b3d36..d3ab1d4b 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -70,6 +70,7 @@ DOCKER_CONFIG_KEYS = [ 'mem_limit', 'memswap_limit', 'net', + 'oom_score_adj' 'pid', 'ports', 'privileged', diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index e84d1317..c08fa4d7 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -166,6 +166,7 @@ } ] }, + "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, "pid": {"type": ["string", "null"]}, "ports": { diff --git a/compose/service.py b/compose/service.py index af572e5b..73381466 100644 --- a/compose/service.py +++ b/compose/service.py @@ -53,6 +53,7 @@ DOCKER_START_KEYS = [ 'log_opt', 'mem_limit', 'memswap_limit', + 'oom_score_adj', 'pid', 'privileged', 'restart', @@ -695,6 +696,7 @@ class Service(object): cpu_quota=options.get('cpu_quota'), shm_size=options.get('shm_size'), tmpfs=options.get('tmpfs'), + oom_score_adj=options.get('oom_score_adj') ) def build(self, no_cache=False, pull=False, force_rm=False): diff --git a/docs/compose-file.md b/docs/compose-file.md index 464cf271..f7b5a931 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -715,7 +715,7 @@ then read-write will be used. > - container_name > - container_name:rw -### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, privileged, read\_only, restart, shm\_size, stdin\_open, tty, user, working\_dir +### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, oom_score_adj, privileged, read\_only, restart, shm\_size, stdin\_open, tty, user, working\_dir Each of these is a single value, analogous to its [docker run](https://docs.docker.com/engine/reference/run/) counterpart. diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 1801f5bf..02f9cc0b 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -854,6 +854,11 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.RestartPolicy.Name'), 'always') + def test_oom_score_adj_value(self): + service = self.create_service('web', oom_score_adj=500) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.OomScoreAdj'), 500) + def test_restart_on_failure_value(self): service = self.create_service('web', restart={ 'Name': 'on-failure', diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 2dad224b..1be8aefa 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1243,6 +1243,26 @@ class ConfigTest(unittest.TestCase): } ] + def test_oom_score_adj_option(self): + + actual = config.load(build_config_details({ + 'version': '2', + 'services': { + 'web': { + 'image': 'alpine', + 'oom_score_adj': 500 + } + } + })) + + assert actual.services == [ + { + 'name': 'web', + 'image': 'alpine', + 'oom_score_adj': 500 + } + ] + def test_merge_service_dicts_from_files_with_extends_in_base(self): base = { 'volumes': ['.:/app'], From 5dabc81c16d69aad6ecb114a119add5bc77bd9bf Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Tue, 5 Jul 2016 17:29:28 +0100 Subject: [PATCH 042/118] Suggest to run Docker for Mac if it isn't running Instead of suggesting docker-machine. Signed-off-by: Ben Firshman --- compose/cli/errors.py | 30 ++++++++++++++++++++---------- compose/cli/utils.py | 4 ++++ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/compose/cli/errors.py b/compose/cli/errors.py index 2c68d36d..89a7a949 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -15,6 +15,7 @@ from requests.packages.urllib3.exceptions import ReadTimeoutError from ..const import API_VERSION_TO_ENGINE_VERSION from ..const import HTTP_TIMEOUT from .utils import call_silently +from .utils import is_docker_for_mac_installed from .utils import is_mac from .utils import is_ubuntu @@ -48,16 +49,7 @@ def handle_connection_errors(client): if e.args and isinstance(e.args[0], ReadTimeoutError): log_timeout_error() raise ConnectionError() - - if call_silently(['which', 'docker']) != 0: - if is_mac(): - exit_with_error(docker_not_found_mac) - if is_ubuntu(): - exit_with_error(docker_not_found_ubuntu) - exit_with_error(docker_not_found_generic) - if call_silently(['which', 'docker-machine']) == 0: - exit_with_error(conn_error_docker_machine) - exit_with_error(conn_error_generic.format(url=client.base_url)) + exit_with_error(get_conn_error_message(client.base_url)) except APIError as e: log_api_error(e, client.api_version) raise ConnectionError() @@ -97,6 +89,20 @@ def exit_with_error(msg): raise ConnectionError() +def get_conn_error_message(url): + if call_silently(['which', 'docker']) != 0: + if is_mac(): + return docker_not_found_mac + if is_ubuntu(): + return docker_not_found_ubuntu + return docker_not_found_generic + if is_docker_for_mac_installed(): + return conn_error_docker_for_mac + if call_silently(['which', 'docker-machine']) == 0: + return conn_error_docker_machine + return conn_error_generic.format(url=url) + + docker_not_found_mac = """ Couldn't connect to Docker daemon. You might need to install Docker: @@ -122,6 +128,10 @@ conn_error_docker_machine = """ Couldn't connect to Docker daemon - you might need to run `docker-machine start default`. """ +conn_error_docker_for_mac = """ + Couldn't connect to Docker daemon. You might need to start Docker for Mac. +""" + conn_error_generic = """ Couldn't connect to Docker daemon at {url} - is it running? diff --git a/compose/cli/utils.py b/compose/cli/utils.py index cc2b680d..bf5df80c 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -103,3 +103,7 @@ def get_build_version(): with open(filename) as fh: return fh.read().strip() + + +def is_docker_for_mac_installed(): + return is_mac() and os.path.isdir('/Applications/Docker.app') From 949b88fff935dce586f9226976db105a50c17253 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 6 Jul 2016 09:56:25 -0700 Subject: [PATCH 043/118] Fix alias tests on 1.11 The fix in 8e0458205241f654bb1d75de9babd9880afa7206 caused a regression when testing against 1.11. Signed-off-by: Aanand Prasad --- tests/acceptance/cli_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 64662654..a8fd3249 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1164,7 +1164,7 @@ class CLITestCase(DockerClientTestCase): for _, config in networks.items(): # TODO: once we drop support for API <1.24, this can be changed to: # assert config['Aliases'] == [container.short_id] - aliases = set(config['Aliases']) - set([container.short_id]) + aliases = set(config['Aliases'] or []) - set([container.short_id]) assert not aliases @v2_only() @@ -1184,7 +1184,7 @@ class CLITestCase(DockerClientTestCase): for _, config in networks.items(): # TODO: once we drop support for API <1.24, this can be changed to: # assert config['Aliases'] == [container.short_id] - aliases = set(config['Aliases']) - set([container.short_id]) + aliases = set(config['Aliases'] or []) - set([container.short_id]) assert not aliases assert self.lookup(container, 'app') From 08127625a0de1a50bf4e1a1f9861c669fe778be4 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 6 Jul 2016 18:01:27 -0700 Subject: [PATCH 044/118] Pin base image to alpine:3.4 in Dockerfile.run Signed-off-by: Aanand Prasad --- Dockerfile.run | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.run b/Dockerfile.run index 792077ad..4e76d64f 100644 --- a/Dockerfile.run +++ b/Dockerfile.run @@ -1,5 +1,5 @@ -FROM alpine:edge +FROM alpine:3.4 RUN apk -U add \ python \ py-pip From 49d4fd27952433feb20bc22117aba4766c15c1c1 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 7 Jul 2016 11:41:11 -0700 Subject: [PATCH 045/118] Update install.md and CHANGELOG.md for 1.7.1 Signed-off-by: Aanand Prasad --- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++++++++ docs/install.md | 6 +++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee45386..0064a5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,47 @@ Change log ========== +1.7.1 (2016-05-04) +----------------- + +Bug Fixes + +- Fixed a bug where the output of `docker-compose config` for v1 files + would be an invalid configuration file. + +- Fixed a bug where `docker-compose config` would not check the validity + of links. + +- Fixed an issue where `docker-compose help` would not output a list of + available commands and generic options as expected. + +- Fixed an issue where filtering by service when using `docker-compose logs` + would not apply for newly created services. + +- Fixed a bug where unchanged services would sometimes be recreated in + in the up phase when using Compose with Python 3. + +- Fixed an issue where API errors encountered during the up phase would + not be recognized as a failure state by Compose. + +- Fixed a bug where Compose would raise a NameError because of an undefined + exception name on non-Windows platforms. + +- Fixed a bug where the wrong version of `docker-py` would sometimes be + installed alongside Compose. + +- Fixed a bug where the host value output by `docker-machine config default` + would not be recognized as valid options by the `docker-compose` + command line. + +- Fixed an issue where Compose would sometimes exit unexpectedly while + reading events broadcasted by a Swarm cluster. + +- Corrected a statement in the docs about the location of the `.env` file, + which is indeed read from the current directory, instead of in the same + location as the Compose file. + + 1.7.0 (2016-04-13) ------------------ diff --git a/docs/install.md b/docs/install.md index 95416e7a..76e4a868 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.2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.7.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.2 + docker-compose version: 1.7.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.2/run.sh > /usr/local/bin/docker-compose + $ curl -L https://github.com/docker/compose/releases/download/1.7.1/run.sh > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose ## Master builds From 576a2ee7aef42203fd66064a8e40d991e611f90b Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 7 Jul 2016 11:58:10 -0700 Subject: [PATCH 046/118] Stop checking the deprecated DOCKER_CLIENT_TIMEOUT variable Signed-off-by: Aanand Prasad --- compose/cli/docker_client.py | 4 ---- compose/const.py | 3 +-- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index 3e0873c4..bed6be79 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -45,10 +45,6 @@ def docker_client(environment, version=None, tls_config=None, host=None, Returns a docker-py client configured using environment variables according to the same logic as the official Docker client. """ - if 'DOCKER_CLIENT_TIMEOUT' in environment: - log.warn("The DOCKER_CLIENT_TIMEOUT environment variable is deprecated. " - "Please use COMPOSE_HTTP_TIMEOUT instead.") - try: kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version) except TLSParameterError: diff --git a/compose/const.py b/compose/const.py index 9e00d96e..b930e0bf 100644 --- a/compose/const.py +++ b/compose/const.py @@ -1,11 +1,10 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os import sys DEFAULT_TIMEOUT = 10 -HTTP_TIMEOUT = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)) +HTTP_TIMEOUT = 60 IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag'] IS_WINDOWS_PLATFORM = (sys.platform == "win32") LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' From 4207d43b85c1c6b77bad82349b17fd060fa2abf4 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 7 Jul 2016 12:08:47 -0700 Subject: [PATCH 047/118] Fix timeout value in error message Signed-off-by: Aanand Prasad --- compose/cli/errors.py | 7 +++---- tests/unit/cli/docker_client_test.py | 23 +++++++++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/compose/cli/errors.py b/compose/cli/errors.py index 89a7a949..5af3ede9 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -13,7 +13,6 @@ from requests.exceptions import SSLError from requests.packages.urllib3.exceptions import ReadTimeoutError from ..const import API_VERSION_TO_ENGINE_VERSION -from ..const import HTTP_TIMEOUT from .utils import call_silently from .utils import is_docker_for_mac_installed from .utils import is_mac @@ -47,7 +46,7 @@ def handle_connection_errors(client): raise ConnectionError() except RequestsConnectionError as e: if e.args and isinstance(e.args[0], ReadTimeoutError): - log_timeout_error() + log_timeout_error(client.timeout) raise ConnectionError() exit_with_error(get_conn_error_message(client.base_url)) except APIError as e: @@ -58,13 +57,13 @@ def handle_connection_errors(client): raise ConnectionError() -def log_timeout_error(): +def log_timeout_error(timeout): log.error( "An HTTP request took too long to complete. Retry with --verbose to " "obtain debug information.\n" "If you encounter this issue regularly because of slow network " "conditions, consider setting COMPOSE_HTTP_TIMEOUT to a higher " - "value (current value: %s)." % HTTP_TIMEOUT) + "value (current value: %s)." % timeout) def log_api_error(e, client_version): diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 5334a944..74669d4a 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -6,6 +6,7 @@ import os import docker import pytest +from compose.cli import errors from compose.cli.docker_client import docker_client from compose.cli.docker_client import tls_config_from_options from tests import mock @@ -19,11 +20,25 @@ class DockerClientTestCase(unittest.TestCase): del os.environ['HOME'] docker_client(os.environ) + @mock.patch.dict(os.environ) def test_docker_client_with_custom_timeout(self): - timeout = 300 - with mock.patch('compose.cli.docker_client.HTTP_TIMEOUT', 300): - client = docker_client(os.environ) - self.assertEqual(client.timeout, int(timeout)) + os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' + client = docker_client(os.environ) + assert client.timeout == 123 + + @mock.patch.dict(os.environ) + def test_custom_timeout_error(self): + os.environ['COMPOSE_HTTP_TIMEOUT'] = '123' + client = docker_client(os.environ) + + with mock.patch('compose.cli.errors.log') as fake_log: + with pytest.raises(errors.ConnectionError): + with errors.handle_connection_errors(client): + raise errors.RequestsConnectionError( + errors.ReadTimeoutError(None, None, None)) + + assert fake_log.error.call_count == 1 + assert '123' in fake_log.error.call_args[0][0] class TLSConfigTestCase(unittest.TestCase): From fea970dff3df60c7579eb06959444160a570927e Mon Sep 17 00:00:00 2001 From: Tomas Tomecek Date: Thu, 21 Apr 2016 14:03:02 +0200 Subject: [PATCH 048/118] service: detailed error messages for create and start Fixes: #3355 Signed-off-by: Tomas Tomecek --- compose/cli/main.py | 4 +++- compose/errors.py | 7 +++++++ compose/parallel.py | 4 ++++ compose/service.py | 12 ++++++++++-- tests/integration/service_test.py | 5 ++++- 5 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 compose/errors.py diff --git a/compose/cli/main.py b/compose/cli/main.py index c924d89d..ed15d6a5 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -32,6 +32,7 @@ from ..service import BuildError from ..service import ConvergenceStrategy from ..service import ImageType from ..service import NeedsBuildError +from ..service import OperationFailedError from .command import get_config_from_options from .command import project_from_options from .docopt_command import DocoptDispatcher @@ -61,7 +62,8 @@ def main(): except (KeyboardInterrupt, signals.ShutdownException): log.error("Aborting.") sys.exit(1) - except (UserError, NoSuchService, ConfigurationError, ProjectError) as e: + except (UserError, NoSuchService, ConfigurationError, + ProjectError, OperationFailedError) as e: log.error(e.msg) sys.exit(1) except BuildError as e: diff --git a/compose/errors.py b/compose/errors.py new file mode 100644 index 00000000..9f68760d --- /dev/null +++ b/compose/errors.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + + +class OperationFailedError(Exception): + def __init__(self, reason): + self.msg = reason diff --git a/compose/parallel.py b/compose/parallel.py index 50b2dbea..7ac66b37 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -12,6 +12,7 @@ from six.moves.queue import Empty from six.moves.queue import Queue from compose.cli.signals import ShutdownException +from compose.errors import OperationFailedError from compose.utils import get_output_stream @@ -47,6 +48,9 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None): elif isinstance(exception, APIError): errors[get_name(obj)] = exception.explanation writer.write(get_name(obj), 'error') + elif isinstance(exception, OperationFailedError): + errors[get_name(obj)] = exception.msg + writer.write(get_name(obj), 'error') elif isinstance(exception, UpstreamError): writer.write(get_name(obj), 'error') else: diff --git a/compose/service.py b/compose/service.py index 73381466..60343542 100644 --- a/compose/service.py +++ b/compose/service.py @@ -27,6 +27,7 @@ from .const import LABEL_PROJECT from .const import LABEL_SERVICE from .const import LABEL_VERSION from .container import Container +from .errors import OperationFailedError from .parallel import parallel_execute from .parallel import parallel_start from .progress_stream import stream_output @@ -278,7 +279,11 @@ class Service(object): if 'name' in container_options and not quiet: log.info("Creating %s" % container_options['name']) - return Container.create(self.client, **container_options) + try: + return Container.create(self.client, **container_options) + except APIError as ex: + raise OperationFailedError("Cannot create container for service %s: %s" % + (self.name, ex.explanation)) def ensure_image_exists(self, do_build=BuildAction.none): if self.can_be_built() and do_build == BuildAction.force: @@ -448,7 +453,10 @@ class Service(object): def start_container(self, container): self.connect_container_to_networks(container) - container.start() + try: + container.start() + except APIError as ex: + raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation)) return container def connect_container_to_networks(self, container): diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 7d2f03d3..97ad7476 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -738,7 +738,10 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(service.containers()), 1) self.assertTrue(service.containers()[0].is_running) - self.assertIn("ERROR: for composetest_web_2 Boom", mock_stderr.getvalue()) + self.assertIn( + "ERROR: for composetest_web_2 Cannot create container for service web: Boom", + mock_stderr.getvalue() + ) def test_scale_with_unexpected_exception(self): """Test that when scaling if the API returns an error, that is not of type From 83f35e132b37bf20baec264e49905c3ecc944ace Mon Sep 17 00:00:00 2001 From: Jonathan Giannuzzi Date: Mon, 11 Jul 2016 11:34:01 +0200 Subject: [PATCH 049/118] Add support for creating internal networks Signed-off-by: Jonathan Giannuzzi --- compose/config/config_schema_v2.0.json | 3 ++- compose/network.py | 5 +++- docs/compose-file.md | 6 ++++- tests/acceptance/cli_test.py | 18 +++++++++++++ tests/fixtures/networks/network-internal.yml | 13 ++++++++++ tests/integration/project_test.py | 27 ++++++++++++++++++++ tests/unit/config/config_test.py | 8 ++++++ 7 files changed, 77 insertions(+), 3 deletions(-) create mode 100755 tests/fixtures/networks/network-internal.yml diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index c08fa4d7..ac46944c 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -246,7 +246,8 @@ "name": {"type": "string"} }, "additionalProperties": false - } + }, + "internal": {"type": "boolean"} }, "additionalProperties": false }, diff --git a/compose/network.py b/compose/network.py index affba7c2..8962a892 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, internal=False): 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.internal = internal def ensure(self): if self.external_name: @@ -68,6 +69,7 @@ class Network(object): driver=self.driver, options=self.driver_opts, ipam=self.ipam, + internal=self.internal, ) def remove(self): @@ -115,6 +117,7 @@ def build_networks(name, config_data, client): driver_opts=data.get('driver_opts'), ipam=data.get('ipam'), external_name=data.get('external_name'), + internal=data.get('internal'), ) for network_name, data in network_config.items() } diff --git a/docs/compose-file.md b/docs/compose-file.md index f7b5a931..59fcf331 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -859,6 +859,10 @@ A full example: host2: 172.28.1.6 host3: 172.28.1.7 +### internal + +By default, Docker also connects a bridge network to it to provide external connectivity. If you want to create an externally isolated overlay network, you can set this option to `true`. + ### external If set to `true`, specifies that this network has been created outside of @@ -866,7 +870,7 @@ Compose. `docker-compose up` will not attempt to create it, and will raise an error if it doesn't exist. `external` cannot be used in conjunction with other network configuration keys -(`driver`, `driver_opts`, `ipam`). +(`driver`, `driver_opts`, `ipam`, `internal`). In the example below, `proxy` is the gateway to the outside world. Instead of attemping to create a network called `[projectname]_outside`, Compose will diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index a8fd3249..dad23bec 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -576,6 +576,24 @@ class CLITestCase(DockerClientTestCase): assert 'forward_facing' in front_aliases assert 'ahead' in front_aliases + @v2_only() + def test_up_with_network_internal(self): + self.require_api_version('1.23') + filename = 'network-internal.yml' + self.base_dir = 'tests/fixtures/networks' + self.dispatch(['-f', filename, 'up', '-d'], None) + internal_net = '{}_internal'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('{}_'.format(self.project.name)) + ] + + # One network was created: internal + assert sorted(n['Name'] for n in networks) == [internal_net] + + assert networks[0]['Internal'] is True + @v2_only() def test_up_with_network_static_addresses(self): filename = 'network-static-addresses.yml' diff --git a/tests/fixtures/networks/network-internal.yml b/tests/fixtures/networks/network-internal.yml new file mode 100755 index 00000000..1fa339b1 --- /dev/null +++ b/tests/fixtures/networks/network-internal.yml @@ -0,0 +1,13 @@ +version: "2" + +services: + web: + image: busybox + command: top + networks: + - internal + +networks: + internal: + driver: bridge + internal: True diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 6e82e931..80915c1a 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -756,6 +756,33 @@ class ProjectTest(DockerClientTestCase): with self.assertRaises(ProjectError): project.up() + @v2_only() + def test_project_up_with_network_internal(self): + self.require_api_version('1.23') + config_data = config.Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': {'internal': None}, + }], + volumes={}, + networks={ + 'internal': {'driver': 'bridge', 'internal': True}, + }, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + project.up() + + network = self.client.networks(names=['composetest_internal'])[0] + + assert network['Internal'] is True + @v2_only() def test_project_up_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 1be8aefa..d88c1d47 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -101,6 +101,10 @@ class ConfigTest(unittest.TestCase): {'subnet': '172.28.0.0/16'} ] } + }, + 'internal': { + 'driver': 'bridge', + 'internal': True } } }, 'working_dir', 'filename.yml') @@ -140,6 +144,10 @@ class ConfigTest(unittest.TestCase): {'subnet': '172.28.0.0/16'} ] } + }, + 'internal': { + 'driver': 'bridge', + 'internal': True } }) From 593d1aeb09a10f4c28f848c7d8d7fc62ef09f6ae Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 11 Jul 2016 16:13:16 -0400 Subject: [PATCH 050/118] Fix bugs with entrypoint/command in docker-compose run - When no command is passed but `--entrypoint` is, set Cmd to `[]` - When command is a single empty string, set Cmd to `[""]` Signed-off-by: Aanand Prasad --- compose/cli/main.py | 4 +- tests/acceptance/cli_test.py | 59 +++++++++++++++---- .../docker-compose.yml | 2 - .../entrypoint-composefile/docker-compose.yml | 6 ++ .../Dockerfile | 3 +- .../entrypoint-dockerfile/docker-compose.yml | 4 ++ 6 files changed, 63 insertions(+), 15 deletions(-) delete mode 100644 tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml create mode 100644 tests/fixtures/entrypoint-composefile/docker-compose.yml rename tests/fixtures/{dockerfile_with_entrypoint => entrypoint-dockerfile}/Dockerfile (57%) create mode 100644 tests/fixtures/entrypoint-dockerfile/docker-compose.yml diff --git a/compose/cli/main.py b/compose/cli/main.py index c924d89d..e33bbe6e 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -668,8 +668,10 @@ class TopLevelCommand(object): 'can not be used together' ) - if options['COMMAND']: + if options['COMMAND'] is not None: command = [options['COMMAND']] + options['ARGS'] + elif options['--entrypoint'] is not None: + command = [] else: command = service.options.get('command') diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index dad23bec..a0d1702c 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import datetime import json import os -import shlex import signal import subprocess import time @@ -983,16 +982,54 @@ class CLITestCase(DockerClientTestCase): [u'/bin/true'], ) - def test_run_service_with_entrypoint_overridden(self): - self.base_dir = 'tests/fixtures/dockerfile_with_entrypoint' - name = 'service' - self.dispatch(['run', '--entrypoint', '/bin/echo', name, 'helloworld']) - service = self.project.get_service(name) - container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] - self.assertEqual( - shlex.split(container.human_readable_command), - [u'/bin/echo', u'helloworld'], - ) + def test_run_service_with_dockerfile_entrypoint(self): + self.base_dir = 'tests/fixtures/entrypoint-dockerfile' + self.dispatch(['run', 'test']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['printf'] + assert container.get('Config.Cmd') == ['default', 'args'] + + def test_run_service_with_dockerfile_entrypoint_overridden(self): + self.base_dir = 'tests/fixtures/entrypoint-dockerfile' + self.dispatch(['run', '--entrypoint', 'echo', 'test']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['echo'] + assert not container.get('Config.Cmd') + + def test_run_service_with_dockerfile_entrypoint_and_command_overridden(self): + self.base_dir = 'tests/fixtures/entrypoint-dockerfile' + self.dispatch(['run', '--entrypoint', 'echo', 'test', 'foo']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['echo'] + assert container.get('Config.Cmd') == ['foo'] + + def test_run_service_with_compose_file_entrypoint(self): + self.base_dir = 'tests/fixtures/entrypoint-composefile' + self.dispatch(['run', 'test']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['printf'] + assert container.get('Config.Cmd') == ['default', 'args'] + + def test_run_service_with_compose_file_entrypoint_overridden(self): + self.base_dir = 'tests/fixtures/entrypoint-composefile' + self.dispatch(['run', '--entrypoint', 'echo', 'test']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['echo'] + assert not container.get('Config.Cmd') + + def test_run_service_with_compose_file_entrypoint_and_command_overridden(self): + self.base_dir = 'tests/fixtures/entrypoint-composefile' + self.dispatch(['run', '--entrypoint', 'echo', 'test', 'foo']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['echo'] + assert container.get('Config.Cmd') == ['foo'] + + def test_run_service_with_compose_file_entrypoint_and_empty_string_command(self): + self.base_dir = 'tests/fixtures/entrypoint-composefile' + self.dispatch(['run', '--entrypoint', 'echo', 'test', '']) + container = self.project.containers(stopped=True, one_off=OneOffFilter.only)[0] + assert container.get('Config.Entrypoint') == ['echo'] + assert container.get('Config.Cmd') == [''] def test_run_service_with_user_overridden(self): self.base_dir = 'tests/fixtures/user-composefile' diff --git a/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml b/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml deleted file mode 100644 index 78631502..00000000 --- a/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml +++ /dev/null @@ -1,2 +0,0 @@ -service: - build: . diff --git a/tests/fixtures/entrypoint-composefile/docker-compose.yml b/tests/fixtures/entrypoint-composefile/docker-compose.yml new file mode 100644 index 00000000..e9880973 --- /dev/null +++ b/tests/fixtures/entrypoint-composefile/docker-compose.yml @@ -0,0 +1,6 @@ +version: "2" +services: + test: + image: busybox + entrypoint: printf + command: default args diff --git a/tests/fixtures/dockerfile_with_entrypoint/Dockerfile b/tests/fixtures/entrypoint-dockerfile/Dockerfile similarity index 57% rename from tests/fixtures/dockerfile_with_entrypoint/Dockerfile rename to tests/fixtures/entrypoint-dockerfile/Dockerfile index e7454e59..49f4416c 100644 --- a/tests/fixtures/dockerfile_with_entrypoint/Dockerfile +++ b/tests/fixtures/entrypoint-dockerfile/Dockerfile @@ -1,3 +1,4 @@ FROM busybox:latest LABEL com.docker.compose.test_image=true -ENTRYPOINT echo "From prebuilt entrypoint" +ENTRYPOINT ["printf"] +CMD ["default", "args"] diff --git a/tests/fixtures/entrypoint-dockerfile/docker-compose.yml b/tests/fixtures/entrypoint-dockerfile/docker-compose.yml new file mode 100644 index 00000000..8318e61f --- /dev/null +++ b/tests/fixtures/entrypoint-dockerfile/docker-compose.yml @@ -0,0 +1,4 @@ +version: "2" +services: + test: + build: . From 907b0690e6f9f1882297d02eb77266910217af11 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 20 Jul 2016 12:23:51 +0100 Subject: [PATCH 051/118] Clarify environment and env_file docs Add note to say that environment variables will not be automatically made available at build time, and point to the `args` documentation. Signed-off-by: Aanand Prasad --- docs/compose-file.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/compose-file.md b/docs/compose-file.md index 59fcf331..d286257d 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -276,6 +276,11 @@ beginning with `#` (i.e. comments) are ignored, as are blank lines. # Set Rails/Rack environment RACK_ENV=development +> **Note:** If your service specifies a [build](#build) option, variables +> defined in environment files will _not_ be automatically visible during the +> build. Use the [args](#args) sub-option of `build` to define build-time +> environment variables. + ### environment Add environment variables. You can use either an array or a dictionary. Any @@ -295,6 +300,11 @@ machine Compose is running on, which can be helpful for secret or host-specific - SHOW=true - SESSION_SECRET +> **Note:** If your service specifies a [build](#build) option, variables +> defined in `environment` will _not_ be automatically visible during the +> build. Use the [args](#args) sub-option of `build` to define build-time +> environment variables. + ### expose Expose ports without publishing them to the host machine - they'll only be From 9ab1d55d06b3f8b8480c64a5c958c45a1361f286 Mon Sep 17 00:00:00 2001 From: Jarrod Pooler Date: Fri, 8 Jul 2016 15:56:15 -0400 Subject: [PATCH 052/118] Updating arg docs in the proper place Signed-off-by: Jarrod Pooler --- docs/compose-file.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/compose-file.md b/docs/compose-file.md index d286257d..fce3f1bc 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -119,6 +119,29 @@ Add build arguments. You can use either an array or a dictionary. Any boolean values; true, false, yes, no, need to be enclosed in quotes to ensure they are not converted to True or False by the YML parser. +First, specify the arguments in your Dockerfile: + + ARG buildno + ARG password + + RUN echo "Build number: $buildno" + RUN script-requiring-password.sh "$password" + +Then specify the arguments under the `build` key. You can pass either a mapping +or a list: + + build: + context: . + args: + buildno: 1 + password: secret + + build: + context: . + args: + - buildno=1 + - password=secret + Build arguments with only a key are resolved to their environment value on the machine Compose is running on. From 425303992c8a953440a16ebce4997cbf225b4d1a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 20 Jul 2016 12:47:47 +0100 Subject: [PATCH 053/118] Reorder/clarify args docs Signed-off-by: Aanand Prasad --- docs/compose-file.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/compose-file.md b/docs/compose-file.md index fce3f1bc..d6d0cadc 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -115,9 +115,8 @@ specified. > [Version 2 file format](#version-2) only. -Add build arguments. You can use either an array or a dictionary. Any -boolean values; true, false, yes, no, need to be enclosed in quotes to ensure -they are not converted to True or False by the YML parser. +Add build arguments, which are environment variables accessible only during the +build process. First, specify the arguments in your Dockerfile: @@ -142,18 +141,15 @@ or a list: - buildno=1 - password=secret -Build arguments with only a key are resolved to their environment value on the -machine Compose is running on. +You can omit the value when specifying a build argument, in which case its value +at build time is the value in the environment where Compose is running. - build: - args: - buildno: 1 - user: someuser + args: + - buildno + - password - build: - args: - - buildno=1 - - user=someuser +> **Note**: YAML boolean values (`true`, `false`, `yes`, `no`, `on`, `off`) must +> be enclosed in quotes, so that the parser interprets them as strings. ### cap_add, cap_drop From 6649e9aba3293272b99ce68232cac4bdff0dc5f3 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Wed, 20 Jul 2016 12:45:02 +0100 Subject: [PATCH 054/118] tearDown the project override at the end of each test case self._project.client is a docker.client.Client, so creating a new self._project leaks (via the embedded connection pool) a bunch of Unix socket file descriptors for each test which overrides self.project using this mechanism. In my tests I observed the test harness using 800-900 file descriptor, which is OK on Linux with the default limit of 1024 but breaks on OSX (e.g. with Docker4Mac) where the default limit is only 256. The failure can be provoked on Linux too with `ulimit -n 256`. With this fix I have observed the process ending with ~100 file descriptors open, including 83 Unix sockets, so I think there is likely at least one more leak lurking. Signed-off-by: Ian Campbell --- tests/acceptance/cli_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index dad23bec..84d401e3 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -114,6 +114,8 @@ class CLITestCase(DockerClientTestCase): for n in networks: if n['Name'].startswith('{}_'.format(self.project.name)): self.client.remove_network(n['Name']) + if hasattr(self, '_project'): + del self._project super(CLITestCase, self).tearDown() From 0483bcb472e2b765fdff4fca1545b32704f93557 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Wed, 20 Jul 2016 15:51:22 +0100 Subject: [PATCH 055/118] delete DockerClientTestCase.client class attribute on tearDownClass This is a docker.client.Client and therefore contains a connection pool, so each subclass of DockerClientTestCase can end up holding on to up to 10 Unix socket file descriptors after the tests contained in the sub-class are complete. Before this by the end of a test run I was seeing ~100 open file descriptors, ~80 of which were Unix domain sockets. By cleaning these up only 15 Unix sockets remain at the end (out of ~25 fds, the rest of which are the Python interpretter, opened libraries etc). Signed-off-by: Ian Campbell --- tests/integration/testcases.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 8d69d531..3e33a6c0 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -63,6 +63,10 @@ class DockerClientTestCase(unittest.TestCase): cls.client = docker_client(Environment(), version) + @classmethod + def tearDownClass(cls): + del cls.client + def tearDown(self): for c in self.client.containers( all=True, From 5cdf30fc12a84bbb88df390a376d3ba75066b01f Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Thu, 21 Jul 2016 12:18:33 +0100 Subject: [PATCH 056/118] Teardown project and db in ResilienceTest These hold a reference to a docker.client.Client object and therefore a connection pool which leaves fds open once the test has completed. Signed-off-by: Ian Campbell --- tests/integration/resilience_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/integration/resilience_test.py b/tests/integration/resilience_test.py index b544783a..2a2d1b56 100644 --- a/tests/integration/resilience_test.py +++ b/tests/integration/resilience_test.py @@ -20,6 +20,11 @@ class ResilienceTest(DockerClientTestCase): self.db.start_container(container) self.host_path = container.get_mount('/var/db')['Source'] + def tearDown(self): + del self.project + del self.db + super(ResilienceTest, self).tearDown() + def test_successful_recreate(self): self.project.up(strategy=ConvergenceStrategy.always) container = self.db.containers()[0] From 3124fec01a5c89b52dfcbbf837058e9e5635e631 Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Thu, 21 Jul 2016 12:21:43 +0100 Subject: [PATCH 057/118] tearDown tmp_volumes array itself in VolumeTest Each volume in the array holds a reference to a docker.client.Client object and therefore a connection pool which leaves fds open once the test has completed. Signed-off-by: Ian Campbell --- tests/integration/volume_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 706179ed..04922ccd 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -17,6 +17,7 @@ class VolumeTest(DockerClientTestCase): self.client.remove_volume(volume.full_name) except DockerException: pass + del self.tmp_volumes def create_volume(self, name, driver=None, opts=None, external=None): if external and isinstance(external, bool): From d6f70dddc7376e2193ee27778f184022dec4014e Mon Sep 17 00:00:00 2001 From: Ian Campbell Date: Thu, 21 Jul 2016 12:24:24 +0100 Subject: [PATCH 058/118] Call the superclass tearDown in VolumeTest Currently it doesn't actually seem to make any practical difference that this is missing, but it seems like good practice to do so anyway, to be robust against future test case changes which might require cleanup done in the super class. Signed-off-by: Ian Campbell --- tests/integration/volume_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index 04922ccd..a75250ac 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -18,6 +18,7 @@ class VolumeTest(DockerClientTestCase): except DockerException: pass del self.tmp_volumes + super(VolumeTest, self).tearDown() def create_volume(self, name, driver=None, opts=None, external=None): if external and isinstance(external, bool): From 07e2426d89750d8eddabe9537d527ac46197e753 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 25 Jul 2016 13:38:04 +0100 Subject: [PATCH 059/118] Remove doc on experimental networking support Signed-off-by: Aanand Prasad --- experimental/compose_swarm_networking.md | 182 +---------------------- 1 file changed, 2 insertions(+), 180 deletions(-) diff --git a/experimental/compose_swarm_networking.md b/experimental/compose_swarm_networking.md index b1fb25dc..905f52f8 100644 --- a/experimental/compose_swarm_networking.md +++ b/experimental/compose_swarm_networking.md @@ -1,183 +1,5 @@ # Experimental: Compose, Swarm and Multi-Host Networking -The [experimental build of Docker](https://github.com/docker/docker/tree/master/experimental) has an entirely new networking system, which enables secure communication between containers on multiple hosts. In combination with Docker Swarm and Docker Compose, you can now run multi-container apps on multi-host clusters with the same tooling and configuration format you use to develop them locally. +Compose now supports multi-host networking as standard. Read more here: -> Note: This functionality is in the experimental stage, and contains some hacks and workarounds which will be removed as it matures. - -## Prerequisites - -Before you start, you’ll need to install the experimental build of Docker, and the latest versions of Machine and Compose. - -- To install the experimental Docker build on a Linux machine, follow the instructions [here](https://github.com/docker/docker/tree/master/experimental#install-docker-experimental). - -- To install the experimental Docker build on a Mac, run these commands: - - $ curl -L https://experimental.docker.com/builds/Darwin/x86_64/docker-latest > /usr/local/bin/docker - $ chmod +x /usr/local/bin/docker - -- To install Machine, follow the instructions [here](https://docs.docker.com/machine/install-machine/). - -- To install Compose, follow the instructions [here](https://docs.docker.com/compose/install/). - -You’ll also need a [Docker Hub](https://hub.docker.com/account/signup/) account and a [Digital Ocean](https://www.digitalocean.com/) account. - -## Set up a swarm with multi-host networking - -Set the `DIGITALOCEAN_ACCESS_TOKEN` environment variable to a valid Digital Ocean API token, which you can generate in the [API panel](https://cloud.digitalocean.com/settings/applications). - - DIGITALOCEAN_ACCESS_TOKEN=abc12345 - -Start a consul server: - - docker-machine create -d digitalocean --engine-install-url https://experimental.docker.com consul - docker $(docker-machine config consul) run -d -p 8500:8500 -h consul progrium/consul -server -bootstrap - -(In a real world setting you’d set up a distributed consul, but that’s beyond the scope of this guide!) - -Create a Swarm token: - - SWARM_TOKEN=$(docker run swarm create) - -Create a Swarm master: - - docker-machine create -d digitalocean --swarm --swarm-master --swarm-discovery=token://$SWARM_TOKEN --engine-install-url="https://experimental.docker.com" --digitalocean-image "ubuntu-14-10-x64" --engine-opt=default-network=overlay:multihost --engine-label=com.docker.network.driver.overlay.bind_interface=eth0 --engine-opt=kv-store=consul:$(docker-machine ip consul):8500 swarm-0 - -Create a Swarm node: - - docker-machine create -d digitalocean --swarm --swarm-discovery=token://$SWARM_TOKEN --engine-install-url="https://experimental.docker.com" --digitalocean-image "ubuntu-14-10-x64" --engine-opt=default-network=overlay:multihost --engine-label=com.docker.network.driver.overlay.bind_interface=eth0 --engine-opt=kv-store=consul:$(docker-machine ip consul):8500 --engine-label com.docker.network.driver.overlay.neighbor_ip=$(docker-machine ip swarm-0) swarm-1 - -You can create more Swarm nodes if you want - it’s best to give them sensible names (swarm-2, swarm-3, etc). - -Finally, point Docker at your swarm: - - eval "$(docker-machine env --swarm swarm-0)" - -## Run containers and get them communicating - -Now that you’ve got a swarm up and running, you can create containers on it just like a single Docker instance: - - $ docker run busybox echo hello world - hello world - -If you run `docker ps -a`, you can see what node that container was started on by looking at its name (here it’s swarm-3): - - $ docker ps -a - CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES - 41f59749737b busybox "echo hello world" 15 seconds ago Exited (0) 13 seconds ago swarm-3/trusting_leakey - -As you start more containers, they’ll be placed on different nodes across the cluster, thanks to Swarm’s default “spread” scheduling strategy. - -Every container started on this swarm will use the “overlay:multihost” network by default, meaning they can all intercommunicate. Each container gets an IP address on that network, and an `/etc/hosts` file which will be updated on-the-fly with every other container’s IP address and name. That means that if you have a running container named ‘foo’, other containers can access it at the hostname ‘foo’. - -Let’s verify that multi-host networking is functioning. Start a long-running container: - - $ docker run -d --name long-running busybox top - - -If you start a new container and inspect its /etc/hosts file, you’ll see the long-running container in there: - - $ docker run busybox cat /etc/hosts - ... - 172.21.0.6 long-running - -Verify that connectivity works between containers: - - $ docker run busybox ping long-running - PING long-running (172.21.0.6): 56 data bytes - 64 bytes from 172.21.0.6: seq=0 ttl=64 time=7.975 ms - 64 bytes from 172.21.0.6: seq=1 ttl=64 time=1.378 ms - 64 bytes from 172.21.0.6: seq=2 ttl=64 time=1.348 ms - ^C - --- long-running ping statistics --- - 3 packets transmitted, 3 packets received, 0% packet loss - round-trip min/avg/max = 1.140/2.099/7.975 ms - -## Run a Compose application - -Here’s an example of a simple Python + Redis app using multi-host networking on a swarm. - -Create a directory for the app: - - $ mkdir composetest - $ cd composetest - -Inside this directory, create 2 files. - -First, create `app.py` - a simple web app that uses the Flask framework and increments a value in Redis: - - from flask import Flask - from redis import Redis - import os - app = Flask(__name__) - redis = Redis(host='composetest_redis_1', port=6379) - - @app.route('/') - def hello(): - redis.incr('hits') - return 'Hello World! I have been seen %s times.' % redis.get('hits') - - if __name__ == "__main__": - app.run(host="0.0.0.0", debug=True) - -Note that we’re connecting to a host called `composetest_redis_1` - this is the name of the Redis container that Compose will start. - -Second, create a Dockerfile for the app container: - - FROM python:2.7 - RUN pip install flask redis - ADD . /code - WORKDIR /code - CMD ["python", "app.py"] - -Build the Docker image and push it to the Hub (you’ll need a Hub account). Replace `` with your Docker Hub username: - - $ docker build -t /counter . - $ docker push /counter - -Next, create a `docker-compose.yml`, which defines the configuration for the web and redis containers. Once again, replace `` with your Hub username: - - web: - image: /counter - ports: - - "80:5000" - redis: - image: redis - -Now start the app: - - $ docker-compose up -d - Pulling web (username/counter:latest)... - swarm-0: Pulling username/counter:latest... : downloaded - swarm-2: Pulling username/counter:latest... : downloaded - swarm-1: Pulling username/counter:latest... : downloaded - swarm-3: Pulling username/counter:latest... : downloaded - swarm-4: Pulling username/counter:latest... : downloaded - Creating composetest_web_1... - Pulling redis (redis:latest)... - swarm-2: Pulling redis:latest... : downloaded - swarm-1: Pulling redis:latest... : downloaded - swarm-3: Pulling redis:latest... : downloaded - swarm-4: Pulling redis:latest... : downloaded - swarm-0: Pulling redis:latest... : downloaded - Creating composetest_redis_1... - -Swarm has created containers for both web and redis, and placed them on different nodes, which you can check with `docker ps`: - - $ docker ps - CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES - 92faad2135c9 redis "/entrypoint.sh redi 43 seconds ago Up 42 seconds swarm-2/composetest_redis_1 - adb809e5cdac username/counter "/bin/sh -c 'python 55 seconds ago Up 54 seconds 45.67.8.9:80->5000/tcp swarm-1/composetest_web_1 - -You can also see that the web container has exposed port 80 on its swarm node. If you curl that IP, you’ll get a response from the container: - - $ curl http://45.67.8.9 - Hello World! I have been seen 1 times. - -If you hit it repeatedly, the counter will increment, demonstrating that the web and redis container are communicating: - - $ curl http://45.67.8.9 - Hello World! I have been seen 2 times. - $ curl http://45.67.8.9 - Hello World! I have been seen 3 times. - $ curl http://45.67.8.9 - Hello World! I have been seen 4 times. +https://docs.docker.com/compose/networking From 2c9e46f60fbe8ddcb4562c1e118f190654d01522 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 1 Jul 2016 13:54:27 -0700 Subject: [PATCH 060/118] Show a warning when engine is in swarm mode Signed-off-by: Aanand Prasad --- compose/project.py | 16 ++++++++++++++++ tests/unit/project_test.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/compose/project.py b/compose/project.py index 676b6ae8..256fb9c0 100644 --- a/compose/project.py +++ b/compose/project.py @@ -369,6 +369,8 @@ class Project(object): detached=False, remove_orphans=False): + warn_for_swarm_mode(self.client) + self.initialize() self.find_orphan_containers(remove_orphans) @@ -533,6 +535,20 @@ def get_volumes_from(project, service_dict): return [build_volume_from(vf) for vf in volumes_from] +def warn_for_swarm_mode(client): + info = client.info() + if info.get('Swarm', {}).get('LocalNodeState') == 'active': + log.warn( + "The Docker Engine you're using is running in swarm mode.\n\n" + "Compose does not use swarm mode to deploy services to multiple nodes in a swarm. " + "All containers will be scheduled on the current node.\n\n" + "To deploy your application across the swarm, " + "use the bundle feature of the Docker experimental build.\n\n" + "More info:\n" + "https://github.com/docker/docker/tree/master/experimental\n" + ) + + class NoSuchService(Exception): def __init__(self, name): self.name = name diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index b6a52e08..9569adc9 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -510,3 +510,35 @@ class ProjectTest(unittest.TestCase): project.down(ImageType.all, True) self.mock_client.remove_image.assert_called_once_with("busybox:latest") + + def test_warning_in_swarm_mode(self): + self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}} + project = Project('composetest', [], self.mock_client) + + with mock.patch('compose.project.log') as fake_log: + project.up() + assert fake_log.warn.call_count == 1 + + def test_no_warning_on_stop(self): + self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'active'}} + project = Project('composetest', [], self.mock_client) + + with mock.patch('compose.project.log') as fake_log: + project.stop() + assert fake_log.warn.call_count == 0 + + def test_no_warning_in_normal_mode(self): + self.mock_client.info.return_value = {'Swarm': {'LocalNodeState': 'inactive'}} + project = Project('composetest', [], self.mock_client) + + with mock.patch('compose.project.log') as fake_log: + project.up() + assert fake_log.warn.call_count == 0 + + def test_no_warning_with_no_swarm_info(self): + self.mock_client.info.return_value = {} + project = Project('composetest', [], self.mock_client) + + with mock.patch('compose.project.log') as fake_log: + project.up() + assert fake_log.warn.call_count == 0 From 583bbb463561807c2983669fbae4c89b21081632 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 25 Jul 2016 13:46:50 +0100 Subject: [PATCH 061/118] Copy experimental bundle docs into Compose docs so URL is stable Signed-off-by: Aanand Prasad --- compose/project.py | 2 +- docs/bundles.md | 199 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 docs/bundles.md diff --git a/compose/project.py b/compose/project.py index 256fb9c0..f85e285f 100644 --- a/compose/project.py +++ b/compose/project.py @@ -545,7 +545,7 @@ def warn_for_swarm_mode(client): "To deploy your application across the swarm, " "use the bundle feature of the Docker experimental build.\n\n" "More info:\n" - "https://github.com/docker/docker/tree/master/experimental\n" + "https://docs.docker.com/compose/bundles\n" ) diff --git a/docs/bundles.md b/docs/bundles.md new file mode 100644 index 00000000..0958e1ef --- /dev/null +++ b/docs/bundles.md @@ -0,0 +1,199 @@ + + + +# Docker Stacks and Distributed Application Bundles (experimental) + +> **Note**: This is a copy of the [Docker Stacks and Distributed Application +> Bundles](https://github.com/docker/docker/blob/v1.12.0-rc4/experimental/docker-stacks-and-bundles.md) +> document in the [docker/docker repo](https://github.com/docker/docker). + +## Overview + +Docker Stacks and Distributed Application Bundles are experimental features +introduced in Docker 1.12 and Docker Compose 1.8, alongside the concept of +swarm mode, and Nodes and Services in the Engine API. + +A Dockerfile can be built into an image, and containers can be created from +that image. Similarly, a docker-compose.yml can be built into a **distributed +application bundle**, and **stacks** can be created from that bundle. In that +sense, the bundle is a multi-services distributable image format. + +As of Docker 1.12 and Compose 1.8, the features are experimental. Neither +Docker Engine nor the Docker Registry support distribution of bundles. + +## Producing a bundle + +The easiest way to produce a bundle is to generate it using `docker-compose` +from an existing `docker-compose.yml`. Of course, that's just *one* possible way +to proceed, in the same way that `docker build` isn't the only way to produce a +Docker image. + +From `docker-compose`: + +```bash +$ docker-compose bundle +WARNING: Unsupported key 'network_mode' in services.nsqd - ignoring +WARNING: Unsupported key 'links' in services.nsqd - ignoring +WARNING: Unsupported key 'volumes' in services.nsqd - ignoring +[...] +Wrote bundle to vossibility-stack.dab +``` + +## Creating a stack from a bundle + +A stack is created using the `docker deploy` command: + +```bash +# docker deploy --help + +Usage: docker deploy [OPTIONS] STACK + +Create and update a stack + +Options: + --file string Path to a Distributed Application Bundle file (Default: STACK.dab) + --help Print usage + --with-registry-auth Send registry authentication details to Swarm agents +``` + +Let's deploy the stack created before: + +```bash +# docker deploy vossibility-stack +Loading bundle from vossibility-stack.dab +Creating service vossibility-stack_elasticsearch +Creating service vossibility-stack_kibana +Creating service vossibility-stack_logstash +Creating service vossibility-stack_lookupd +Creating service vossibility-stack_nsqd +Creating service vossibility-stack_vossibility-collector +``` + +We can verify that services were correctly created: + +```bash +# docker service ls +ID NAME REPLICAS IMAGE +COMMAND +29bv0vnlm903 vossibility-stack_lookupd 1 nsqio/nsq@sha256:eeba05599f31eba418e96e71e0984c3dc96963ceb66924dd37a47bf7ce18a662 /nsqlookupd +4awt47624qwh vossibility-stack_nsqd 1 nsqio/nsq@sha256:eeba05599f31eba418e96e71e0984c3dc96963ceb66924dd37a47bf7ce18a662 /nsqd --data-path=/data --lookupd-tcp-address=lookupd:4160 +4tjx9biia6fs vossibility-stack_elasticsearch 1 elasticsearch@sha256:12ac7c6af55d001f71800b83ba91a04f716e58d82e748fa6e5a7359eed2301aa +7563uuzr9eys vossibility-stack_kibana 1 kibana@sha256:6995a2d25709a62694a937b8a529ff36da92ebee74bafd7bf00e6caf6db2eb03 +9gc5m4met4he vossibility-stack_logstash 1 logstash@sha256:2dc8bddd1bb4a5a34e8ebaf73749f6413c101b2edef6617f2f7713926d2141fe logstash -f /etc/logstash/conf.d/logstash.conf +axqh55ipl40h vossibility-stack_vossibility-collector 1 icecrime/vossibility-collector@sha256:f03f2977203ba6253988c18d04061c5ec7aab46bca9dfd89a9a1fa4500989fba --config /config/config.toml --debug +``` + +## Managing stacks + +Stacks are managed using the `docker stack` command: + +```bash +# docker stack --help + +Usage: docker stack COMMAND + +Manage Docker stacks + +Options: + --help Print usage + +Commands: + config Print the stack configuration + deploy Create and update a stack + rm Remove the stack + services List the services in the stack + tasks List the tasks in the stack + +Run 'docker stack COMMAND --help' for more information on a command. +``` + +## Bundle file format + +Distributed application bundles are described in a JSON format. When bundles +are persisted as files, the file extension is `.dab` (Docker 1.12RC2 tools use +`.dsb` for the file extension—this will be updated in the next release client). + +A bundle has two top-level fields: `version` and `services`. The version used +by Docker 1.12 tools is `0.1`. + +`services` in the bundle are the services that comprise the app. They +correspond to the new `Service` object introduced in the 1.12 Docker Engine API. + +A service has the following fields: + +
+
+ Image (required) string +
+
+ The image that the service will run. Docker images should be referenced + with full content hash to fully specify the deployment artifact for the + service. Example: + postgres@sha256:f76245b04ddbcebab5bb6c28e76947f49222c99fec4aadb0bb + 1c24821a 9e83ef +
+
+ Command []string +
+
+ Command to run in service containers. +
+
+ Args []string +
+
+ Arguments passed to the service containers. +
+
+ Env []string +
+
+ Environment variables. +
+
+ Labels map[string]string +
+
+ Labels used for setting meta data on services. +
+
+ Ports []Port +
+
+ Service ports (composed of Port (int) and + Protocol (string). A service description can + only specify the container port to be exposed. These ports can be + mapped on runtime hosts at the operator's discretion. +
+ +
+ WorkingDir string +
+
+ Working directory inside the service containers. +
+ +
+ User string +
+
+ Username or UID (format: <name|uid>[:<group|gid>]). +
+ +
+ Networks []string +
+
+ Networks that the service containers should be connected to. An entity + deploying a bundle should create networks as needed. +
+
From 887ed8d1b650ac18fac3f58213cd7cf897f5d885 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 25 Jul 2016 16:48:28 +0100 Subject: [PATCH 062/118] Rename --fetch-digests to --push-images and remove auto-pull Signed-off-by: Aanand Prasad --- compose/bundle.py | 43 +++++++++++++++++------------------- compose/cli/main.py | 35 +++++++++++++++++++++-------- tests/unit/bundle_test.py | 46 ++++++++++++++------------------------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/compose/bundle.py b/compose/bundle.py index 44f6954b..afbdabfa 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -60,7 +60,7 @@ def serialize_bundle(config, image_digests): return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True) -def get_image_digests(project, allow_fetch=False): +def get_image_digests(project, allow_push=False): digests = {} needs_push = set() needs_pull = set() @@ -69,7 +69,7 @@ def get_image_digests(project, allow_fetch=False): try: digests[service.name] = get_image_digest( service, - allow_fetch=allow_fetch, + allow_push=allow_push, ) except NeedsPush as e: needs_push.add(e.image_name) @@ -82,7 +82,7 @@ def get_image_digests(project, allow_fetch=False): return digests -def get_image_digest(service, allow_fetch=False): +def get_image_digest(service, allow_push=False): if 'image' not in service.options: raise UserError( "Service '{s.name}' doesn't define an image tag. An image name is " @@ -108,27 +108,24 @@ def get_image_digest(service, allow_fetch=False): # digests return image['RepoDigests'][0] - if not allow_fetch: - if 'build' in service.options: - raise NeedsPush(service.image_name) - else: - raise NeedsPull(service.image_name) - - return fetch_image_digest(service) - - -def fetch_image_digest(service): if 'build' not in service.options: - digest = service.pull() - else: - try: - digest = service.push() - except: - log.error( - "Failed to push image for service '{s.name}'. Please use an " - "image tag that can be pushed to a Docker " - "registry.".format(s=service)) - raise + raise NeedsPull(service.image_name) + + if not allow_push: + raise NeedsPush(service.image_name) + + return push_image(service) + + +def push_image(service): + try: + digest = service.push() + except: + log.error( + "Failed to push image for service '{s.name}'. Please use an " + "image tag that can be pushed to a Docker " + "registry.".format(s=service)) + raise if not digest: raise ValueError("Failed to get digest for %s" % service.name) diff --git a/compose/cli/main.py b/compose/cli/main.py index 3f153d0d..db06a5e1 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -223,15 +223,16 @@ class TopLevelCommand(object): Generate a Distributed Application Bundle (DAB) from the Compose file. Images must have digests stored, which requires interaction with a - Docker registry. If digests aren't stored for all images, you can pass - `--fetch-digests` to automatically fetch them. Images for services - with a `build` key will be pushed. Images for services without a - `build` key will be pulled. + Docker registry. If digests aren't stored for all images, you can fetch + them with `docker-compose pull` or `docker-compose push`. To push images + automatically when bundling, pass `--push-images`. Only services with + a `build` option specified will have their images pushed. Usage: bundle [options] Options: - --fetch-digests Automatically fetch image digests if missing + --push-images Automatically push images for any services + which have a `build` option specified. -o, --output PATH Path to write the bundle file to. Defaults to ".dab". @@ -247,7 +248,7 @@ class TopLevelCommand(object): try: image_digests = get_image_digests( self.project, - allow_fetch=options['--fetch-digests'], + allow_push=options['--push-images'], ) except MissingDigests as e: def list_images(images): @@ -256,12 +257,28 @@ class TopLevelCommand(object): paras = ["Some images are missing digests."] if e.needs_push: - paras += ["The following images need to be pushed:", list_images(e.needs_push)] + command_hint = ( + "Use `docker-compose push {}` to push them. " + "You can do this automatically with `docker-compose bundle --push-images`." + .format(" ".join(sorted(e.needs_push))) + ) + paras += [ + "The following images can be pushed:", + list_images(e.needs_push), + command_hint, + ] if e.needs_pull: - paras += ["The following images need to be pulled:", list_images(e.needs_pull)] + command_hint = ( + "Use `docker-compose pull {}` to pull them. " + .format(" ".join(sorted(e.needs_pull))) + ) - paras.append("If this is OK, run `docker-compose bundle --fetch-digests`.") + paras += [ + "The following images need to be pulled:", + list_images(e.needs_pull), + command_hint, + ] raise UserError("\n\n".join(paras)) diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py index ff4c0dce..223b3b07 100644 --- a/tests/unit/bundle_test.py +++ b/tests/unit/bundle_test.py @@ -41,44 +41,30 @@ def test_get_image_digest_no_image(mock_service): assert "doesn't define an image tag" in exc.exconly() -def test_fetch_image_digest_for_image_with_saved_digest(mock_service): - mock_service.options['image'] = image_id = 'abcd' - mock_service.pull.return_value = expected = 'sha256:thedigest' - mock_service.image.return_value = {'RepoDigests': ['digest1']} - - digest = bundle.fetch_image_digest(mock_service) - assert digest == image_id + '@' + expected - - mock_service.pull.assert_called_once_with() - assert not mock_service.push.called - assert not mock_service.client.pull.called - - -def test_fetch_image_digest_for_image(mock_service): - mock_service.options['image'] = image_id = 'abcd' - mock_service.pull.return_value = expected = 'sha256:thedigest' - mock_service.image.return_value = {'RepoDigests': []} - - digest = bundle.fetch_image_digest(mock_service) - assert digest == image_id + '@' + expected - - mock_service.pull.assert_called_once_with() - assert not mock_service.push.called - mock_service.client.pull.assert_called_once_with(digest) - - -def test_fetch_image_digest_for_build(mock_service): +def test_push_image_with_saved_digest(mock_service): mock_service.options['build'] = '.' mock_service.options['image'] = image_id = 'abcd' mock_service.push.return_value = expected = 'sha256:thedigest' mock_service.image.return_value = {'RepoDigests': ['digest1']} - digest = bundle.fetch_image_digest(mock_service) + digest = bundle.push_image(mock_service) assert digest == image_id + '@' + expected mock_service.push.assert_called_once_with() - assert not mock_service.pull.called - assert not mock_service.client.pull.called + assert not mock_service.client.push.called + + +def test_push_image(mock_service): + mock_service.options['build'] = '.' + mock_service.options['image'] = image_id = 'abcd' + mock_service.push.return_value = expected = 'sha256:thedigest' + mock_service.image.return_value = {'RepoDigests': []} + + digest = bundle.push_image(mock_service) + assert digest == image_id + '@' + expected + + mock_service.push.assert_called_once_with() + mock_service.client.pull.assert_called_once_with(digest) def test_to_bundle(): From 8924f6c05ccd69468777dfabf23cbe9e21b0ed4a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 26 Jul 2016 11:51:16 +0100 Subject: [PATCH 063/118] Fix example image hash Signed-off-by: Aanand Prasad --- docs/bundles.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/bundles.md b/docs/bundles.md index 0958e1ef..a56adb02 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -138,8 +138,7 @@ A service has the following fields: The image that the service will run. Docker images should be referenced with full content hash to fully specify the deployment artifact for the service. Example: - postgres@sha256:f76245b04ddbcebab5bb6c28e76947f49222c99fec4aadb0bb - 1c24821a 9e83ef + postgres@sha256:e0a230a9f5b4e1b8b03bb3e8cf7322b0e42b7838c5c87f4545edb48f5eb8f077
Command []string From 28e6508f4a781bcc1b12841e9ed9e26f7ff1ba55 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 26 Jul 2016 11:51:30 +0100 Subject: [PATCH 064/118] Add note about missing volume mount support Signed-off-by: Aanand Prasad --- docs/bundles.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/bundles.md b/docs/bundles.md index a56adb02..19322824 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -196,3 +196,6 @@ A service has the following fields: deploying a bundle should create networks as needed. + +> **Note:** Some configuration options are not yet supported in the DAB format, +> including volume mounts. From 8ffbe8e0834697640239134575b460ea931b417d Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 26 Jul 2016 11:59:04 +0100 Subject: [PATCH 065/118] Remove note about .dsb Signed-off-by: Aanand Prasad --- docs/bundles.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/bundles.md b/docs/bundles.md index 19322824..5ca2c1ec 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -119,8 +119,7 @@ Run 'docker stack COMMAND --help' for more information on a command. ## Bundle file format Distributed application bundles are described in a JSON format. When bundles -are persisted as files, the file extension is `.dab` (Docker 1.12RC2 tools use -`.dsb` for the file extension—this will be updated in the next release client). +are persisted as files, the file extension is `.dab`. A bundle has two top-level fields: `version` and `services`. The version used by Docker 1.12 tools is `0.1`. From 2fec6966d4c7cf72693fff73c06a52fbe743a042 Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Fri, 24 Jun 2016 11:10:18 -0400 Subject: [PATCH 066/118] Add reference docs for push and bundle. Signed-off-by: Daniel Nephin --- docs/reference/bundle.md | 31 +++++++++++++++++++++++++++++++ docs/reference/push.md | 21 +++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/reference/bundle.md create mode 100644 docs/reference/push.md diff --git a/docs/reference/bundle.md b/docs/reference/bundle.md new file mode 100644 index 00000000..fca93a8a --- /dev/null +++ b/docs/reference/bundle.md @@ -0,0 +1,31 @@ + + +# bundle + +``` +Usage: bundle [options] + +Options: + --push-images Automatically push images for any services + which have a `build` option specified. + + -o, --output PATH Path to write the bundle file to. + Defaults to ".dab". +``` + +Generate a Distributed Application Bundle (DAB) from the Compose file. + +Images must have digests stored, which requires interaction with a +Docker registry. If digests aren't stored for all images, you can fetch +them with `docker-compose pull` or `docker-compose push`. To push images +automatically when bundling, pass `--push-images`. Only services with +a `build` option specified will have their images pushed. diff --git a/docs/reference/push.md b/docs/reference/push.md new file mode 100644 index 00000000..bdc3112e --- /dev/null +++ b/docs/reference/push.md @@ -0,0 +1,21 @@ + + +# push + +``` +Usage: push [options] [SERVICE...] + +Options: + --ignore-push-failures Push what it can and ignores images with push failures. +``` + +Pushes images for services. From 7f3375c2ce79a21a3665ccea51564a0c36b01a45 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 26 Jul 2016 13:07:38 -0700 Subject: [PATCH 067/118] Update docker-py requirement to the latest release Signed-off-by: Joffrey F --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 60260e1c..831ed65a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyYAML==3.11 backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 -docker-py==1.9.0rc2 +docker-py==1.9.0 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4; python_version < '3.4' diff --git a/setup.py b/setup.py index 3696adc6..5cb52dae 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires = [ 'requests >= 2.6.1, < 2.8', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker-py == 1.9.0rc2', + 'docker-py >= 1.9.0, < 2.0', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', From 1877a41b92eb887ace32579815278f607e95759a Mon Sep 17 00:00:00 2001 From: Ben Firshman Date: Sun, 24 Jul 2016 18:57:36 +0100 Subject: [PATCH 068/118] Add user agent to API calls Signed-off-by: Ben Firshman --- compose/cli/docker_client.py | 3 +++ compose/cli/utils.py | 15 +++++++++++++++ tests/unit/cli/docker_client_test.py | 12 ++++++++++++ 3 files changed, 30 insertions(+) diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index bed6be79..ce191fbf 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -10,6 +10,7 @@ from docker.utils import kwargs_from_env from ..const import HTTP_TIMEOUT from .errors import UserError +from .utils import generate_user_agent log = logging.getLogger(__name__) @@ -67,4 +68,6 @@ def docker_client(environment, version=None, tls_config=None, host=None, else: kwargs['timeout'] = HTTP_TIMEOUT + kwargs['user_agent'] = generate_user_agent() + return Client(**kwargs) diff --git a/compose/cli/utils.py b/compose/cli/utils.py index bf5df80c..f60f61cd 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -107,3 +107,18 @@ def get_build_version(): def is_docker_for_mac_installed(): return is_mac() and os.path.isdir('/Applications/Docker.app') + + +def generate_user_agent(): + parts = [ + "docker-compose/{}".format(compose.__version__), + "docker-py/{}".format(docker.__version__), + ] + try: + p_system = platform.system() + p_release = platform.release() + except IOError: + pass + else: + parts.append("{}/{}".format(p_system, p_release)) + return " ".join(parts) diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 74669d4a..fc914791 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -2,10 +2,12 @@ from __future__ import absolute_import from __future__ import unicode_literals import os +import platform import docker import pytest +import compose from compose.cli import errors from compose.cli.docker_client import docker_client from compose.cli.docker_client import tls_config_from_options @@ -40,6 +42,16 @@ class DockerClientTestCase(unittest.TestCase): assert fake_log.error.call_count == 1 assert '123' in fake_log.error.call_args[0][0] + def test_user_agent(self): + client = docker_client(os.environ) + expected = "docker-compose/{0} docker-py/{1} {2}/{3}".format( + compose.__version__, + docker.__version__, + platform.system(), + platform.release() + ) + self.assertEqual(client.headers['User-Agent'], expected) + class TLSConfigTestCase(unittest.TestCase): ca_cert = 'tests/fixtures/tls/ca.pem' From 6633f1962cba18e597f218eaa937e8b3d54dbf80 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 27 Jul 2016 16:00:53 +0100 Subject: [PATCH 069/118] Shell completion for --push-images Signed-off-by: Aanand Prasad --- contrib/completion/bash/docker-compose | 2 +- contrib/completion/zsh/_docker-compose | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 0201bcb2..991f6572 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -117,7 +117,7 @@ _docker_compose_bundle() { ;; esac - COMPREPLY=( $( compgen -W "--fetch-digests --help --output -o" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--push-images --help --output -o" -- "$cur" ) ) } diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 2947cef3..928e28de 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -207,6 +207,7 @@ __docker-compose_subcommand() { (bundle) _arguments \ $opts_help \ + '--push-images[Automatically push images for any services which have a `build` option specified.]' \ '(--output -o)'{--output,-o}'[Path to write the bundle file to. Defaults to ".dab".]:file:_files' && ret=0 ;; (config) From ec825af3d336a55297250b3a2af63b48fabed177 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 27 Jul 2016 18:26:40 +0100 Subject: [PATCH 070/118] Fix error message for unrecognised TLS version Signed-off-by: Aanand Prasad --- compose/cli/command.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compose/cli/command.py b/compose/cli/command.py index 09a9ced8..2c70d31a 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -65,8 +65,9 @@ def get_tls_version(environment): tls_attr_name = "PROTOCOL_{}".format(compose_tls_version) if not hasattr(ssl, tls_attr_name): log.warn( - 'The {} protocol is unavailable. You may need to update your ' + 'The "{}" protocol is unavailable. You may need to update your ' 'version of Python or OpenSSL. Falling back to TLSv1 (default).' + .format(compose_tls_version) ) return None From 22c0779a498ee701c22b857669d3f43a0d404f27 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 14 Jun 2016 11:33:45 -0700 Subject: [PATCH 071/118] Bump 1.8.0-rc1 Signed-off-by: Joffrey F --- CHANGELOG.md | 50 +++++++++++++++++++++++++++++++++++++++++++++ compose/__init__.py | 2 +- docs/install.md | 6 +++--- script/run/run.sh | 2 +- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0064a5cc..39ac8698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,56 @@ Change log ========== +1.8.0 (2016-06-14) +----------------- + +New Features + +- Added `docker-compose bundle`, a command that builds a bundle file + to be consumed by the new *Docker Stack* commands in Docker 1.12. + This command automatically pushes and pulls images as needed. + +- Added `docker-compose push`, a command that pushes service images + to a registry. + +- As announced in 1.7.0, `docker-compose rm` now removes containers + created by `docker-compose run` by default. + +- Compose now supports specifying a custom TLS version for + interaction with the Docker Engine using the `COMPOSE_TLS_VERSION` + environment variable. + +Bug Fixes + +- Fixed a bug where Compose would erroneously try to read `.env` + at the project's root when it is a directory. + +- Improved config merging when multiple compose files are involved + for several service sub-keys. + +- Fixed a bug where volume mappings containing Windows drives would + sometimes be parsed incorrectly. + +- Fixed a bug in Windows environment where volume mappings of the + host's root directory would be parsed incorrectly. + +- Fixed a bug where `docker-compose config` would ouput an invalid + Compose file if external networks were specified. + +- Fixed an issue where unset buildargs would be assigned a string + containing `'None'` instead of the expected empty value. + +- Fixed a bug where yes/no prompts on Windows would not show before + receiving input. + +- Fixed a bug where trying to `docker-compose exec` on Windows + without the `-d` option would exit with a stacktrace. This will + still fail for the time being, but should do so gracefully. + +- Fixed a bug where errors during `docker-compose up` would show + an unrelated stacktrace at the end of the process. + + 1.7.1 (2016-05-04) ----------------- diff --git a/compose/__init__.py b/compose/__init__.py index 1052c067..1dd11e79 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.8.0dev' +__version__ = '1.8.0-rc1' diff --git a/docs/install.md b/docs/install.md index 76e4a868..5191a4b5 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.7.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.8.0-rc1/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.7.1 + docker-compose version: 1.8.0-rc1 ## 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.7.1/run.sh > /usr/local/bin/docker-compose + $ curl -L https://github.com/docker/compose/releases/download/1.8.0-rc1/run.sh > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose ## Master builds diff --git a/script/run/run.sh b/script/run/run.sh index 98d32c5f..f9199ce1 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.7.0" +VERSION="1.8.0-rc1" IMAGE="docker/compose:$VERSION" From 60622026fa54453d6c49c7a1bbfd3ed93692e0c5 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 6 Jul 2016 16:08:07 -0700 Subject: [PATCH 072/118] Bump 1.8.0-rc2 Signed-off-by: Aanand Prasad --- CHANGELOG.md | 8 +++++--- compose/__init__.py | 2 +- docs/install.md | 6 +++--- script/run/run.sh | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39ac8698..afa35820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Change log 1.8.0 (2016-06-14) ----------------- +**Breaking Changes** + +- As announced in 1.7.0, `docker-compose rm` now removes containers + created by `docker-compose run` by default. + New Features - Added `docker-compose bundle`, a command that builds a bundle file @@ -13,9 +18,6 @@ New Features - Added `docker-compose push`, a command that pushes service images to a registry. -- As announced in 1.7.0, `docker-compose rm` now removes containers - created by `docker-compose run` by default. - - Compose now supports specifying a custom TLS version for interaction with the Docker Engine using the `COMPOSE_TLS_VERSION` environment variable. diff --git a/compose/__init__.py b/compose/__init__.py index 1dd11e79..bf8a6f30 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.8.0-rc1' +__version__ = '1.8.0-rc2' diff --git a/docs/install.md b/docs/install.md index 5191a4b5..d1a11ab5 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.8.0-rc1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.8.0-rc2/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.8.0-rc1 + docker-compose version: 1.8.0-rc2 ## 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.8.0-rc1/run.sh > /usr/local/bin/docker-compose + $ curl -L https://github.com/docker/compose/releases/download/1.8.0-rc2/run.sh > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose ## Master builds diff --git a/script/run/run.sh b/script/run/run.sh index f9199ce1..caf6ed11 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.8.0-rc1" +VERSION="1.8.0-rc2" IMAGE="docker/compose:$VERSION" From 7fafd72c1e3497ad3e8265ffbac06dfce449d762 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Wed, 27 Jul 2016 15:55:47 +0100 Subject: [PATCH 073/118] Bump 1.8.0-rc3 Signed-off-by: Aanand Prasad --- CHANGELOG.md | 12 +++++++++++- compose/__init__.py | 2 +- docs/install.md | 6 +++--- script/run/run.sh | 2 +- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afa35820..8ec7d5b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,15 @@ Change log - As announced in 1.7.0, `docker-compose rm` now removes containers created by `docker-compose run` by default. +- Setting `entrypoint` on a service now empties out any default + command that was set on the image (i.e. any `CMD` instruction in the + Dockerfile used to build it). This makes it consistent with + the `--entrypoint` flag to `docker run`. + New Features - Added `docker-compose bundle`, a command that builds a bundle file to be consumed by the new *Docker Stack* commands in Docker 1.12. - This command automatically pushes and pulls images as needed. - Added `docker-compose push`, a command that pushes service images to a registry. @@ -27,6 +31,9 @@ Bug Fixes - Fixed a bug where Compose would erroneously try to read `.env` at the project's root when it is a directory. +- `docker-compose run -e VAR` now passes `VAR` through from the shell + to the container, as with `docker run -e VAR`. + - Improved config merging when multiple compose files are involved for several service sub-keys. @@ -52,6 +59,9 @@ Bug Fixes - Fixed a bug where errors during `docker-compose up` would show an unrelated stacktrace at the end of the process. +- `docker-compose create` and `docker-compose start` show more + descriptive error messages when something goes wrong. + 1.7.1 (2016-05-04) ----------------- diff --git a/compose/__init__.py b/compose/__init__.py index bf8a6f30..f9f0e6a6 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.8.0-rc2' +__version__ = '1.8.0-rc3' diff --git a/docs/install.md b/docs/install.md index d1a11ab5..2099b71c 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.8.0-rc2/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.8.0-rc3/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.8.0-rc2 + docker-compose version: 1.8.0-rc3 ## 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.8.0-rc2/run.sh > /usr/local/bin/docker-compose + $ curl -L https://github.com/docker/compose/releases/download/1.8.0-rc3/run.sh > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose ## Master builds diff --git a/script/run/run.sh b/script/run/run.sh index caf6ed11..c2c01db6 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.8.0-rc2" +VERSION="1.8.0-rc3" IMAGE="docker/compose:$VERSION" From 1110af1bae8382ebce0a553da46cf8f95e46ed72 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 27 Jul 2016 11:55:49 -0700 Subject: [PATCH 074/118] Bump 1.8.0 Signed-off-by: Joffrey F --- compose/__init__.py | 2 +- docs/install.md | 6 +++--- script/run/run.sh | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compose/__init__.py b/compose/__init__.py index f9f0e6a6..c550f990 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.8.0-rc3' +__version__ = '1.8.0' diff --git a/docs/install.md b/docs/install.md index 2099b71c..bb7f07b3 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.8.0-rc3/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + curl -L https://github.com/docker/compose/releases/download/1.8.0/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.8.0-rc3 + docker-compose version: 1.8.0 ## 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.8.0-rc3/run.sh > /usr/local/bin/docker-compose + $ curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose $ chmod +x /usr/local/bin/docker-compose ## Master builds diff --git a/script/run/run.sh b/script/run/run.sh index c2c01db6..6205747a 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.8.0-rc3" +VERSION="1.8.0" IMAGE="docker/compose:$VERSION" From 6ab0607e6182f7c4dec55b6318ab07af746e7c89 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 27 Jul 2016 13:30:52 -0700 Subject: [PATCH 075/118] Switch back to dev version Signed-off-by: Joffrey F --- compose/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose/__init__.py b/compose/__init__.py index c550f990..6e610652 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.8.0' +__version__ = '1.9.0dev' From 5aeeecb6f2044860f09be0bada7f4d75f489cb47 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 29 Jul 2016 17:12:40 +0100 Subject: [PATCH 076/118] Fix stacktrace when handling timeout error Signed-off-by: Aanand Prasad --- compose/cli/errors.py | 2 +- tests/unit/cli/docker_client_test.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/compose/cli/errors.py b/compose/cli/errors.py index 5af3ede9..f9a20b9e 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -53,7 +53,7 @@ def handle_connection_errors(client): log_api_error(e, client.api_version) raise ConnectionError() except (ReadTimeout, socket.timeout) as e: - log_timeout_error() + log_timeout_error(client.timeout) raise ConnectionError() diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index fc914791..3430c25c 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -42,6 +42,14 @@ class DockerClientTestCase(unittest.TestCase): assert fake_log.error.call_count == 1 assert '123' in fake_log.error.call_args[0][0] + with mock.patch('compose.cli.errors.log') as fake_log: + with pytest.raises(errors.ConnectionError): + with errors.handle_connection_errors(client): + raise errors.ReadTimeout() + + assert fake_log.error.call_count == 1 + assert '123' in fake_log.error.call_args[0][0] + def test_user_agent(self): client = docker_client(os.environ) expected = "docker-compose/{0} docker-py/{1} {2}/{3}".format( From 6f4be1cffc43d97d8070506286e0f15aec4c6b51 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 29 Jul 2016 14:05:59 -0700 Subject: [PATCH 077/118] json_splitter: Don't break when buffer contains leading whitespace. Add error logging with detailed output for decode errors Signed-off-by: Joffrey F --- compose/utils.py | 12 +++++++++++- tests/unit/utils_test.py | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/compose/utils.py b/compose/utils.py index 925a8e79..eea73be1 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -5,11 +5,13 @@ import codecs import hashlib import json import json.decoder +import logging import six json_decoder = json.JSONDecoder() +log = logging.getLogger(__name__) def get_output_stream(stream): @@ -60,13 +62,21 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): yield item if buffered: - yield decoder(buffered) + try: + yield decoder(buffered) + except ValueError: + log.error( + 'Compose tried parsing the following chunk as a JSON object, ' + 'but failed:\n%s' % repr(buffered) + ) + raise def json_splitter(buffer): """Attempt to parse a json object from a buffer. If there is at least one object, return it and the rest of the buffer, otherwise return None. """ + buffer = buffer.strip() try: obj, index = json_decoder.raw_decode(buffer) rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():] diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 8ee37b07..85231957 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -15,6 +15,10 @@ class TestJsonSplitter(object): data = '{"foo": "bar"}\n \n{"next": "obj"}' assert utils.json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') + def test_json_splitter_leading_whitespace(self): + data = '\n \r{"foo": "bar"}\n\n {"next": "obj"}' + assert utils.json_splitter(data) == ({'foo': 'bar'}, '{"next": "obj"}') + class TestStreamAsText(object): @@ -43,3 +47,16 @@ class TestJsonStream(object): [1, 2, 3], [], ] + + def test_with_leading_whitespace(self): + stream = [ + '\n \r\n {"one": "two"}{"x": 1}', + ' {"three": "four"}\t\t{"x": 2}' + ] + output = list(utils.json_stream(stream)) + assert output == [ + {'one': 'two'}, + {'x': 1}, + {'three': 'four'}, + {'x': 2} + ] From 48258e2b4668a7a4917a3c3f102ff97ba86e0908 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 1 Aug 2016 12:19:20 +0100 Subject: [PATCH 078/118] Add note to bundle docs about requiring an experimental Engine build Signed-off-by: Aanand Prasad --- docs/bundles.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/bundles.md b/docs/bundles.md index 5ca2c1ec..096c9ec3 100644 --- a/docs/bundles.md +++ b/docs/bundles.md @@ -50,6 +50,15 @@ Wrote bundle to vossibility-stack.dab ## Creating a stack from a bundle +> **Note**: Because support for stacks and bundles is in the experimental stage, +> you need to install an experimental build of Docker Engine to use it. +> +> If you're on Mac or Windows, download the “Beta channel” version of +> [Docker for Mac](https://docs.docker.com/docker-for-mac/) or +> [Docker for Windows](https://docs.docker.com/docker-for-windows/) to install +> it. If you're on Linux, follow the instructions in the +> [experimental build README](https://github.com/docker/docker/blob/master/experimental/README.md). + A stack is created using the `docker deploy` command: ```bash From b3a4d76d4faf172efcb1c0fc691cd1b50c786447 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 2 Aug 2016 11:44:25 +0100 Subject: [PATCH 079/118] Handle connection errors on project initialization Signed-off-by: Aanand Prasad --- compose/cli/command.py | 4 +++- tests/acceptance/cli_test.py | 13 +++++++++++++ .../volumes-from-container/docker-compose.yml | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/volumes-from-container/docker-compose.yml diff --git a/compose/cli/command.py b/compose/cli/command.py index 2c70d31a..02035428 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -8,6 +8,7 @@ import ssl import six +from . import errors from . import verbose_proxy from .. import config from ..config.environment import Environment @@ -110,7 +111,8 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, host=host, environment=environment ) - return Project.from_config(project_name, config_data, client) + with errors.handle_connection_errors(client): + return Project.from_config(project_name, config_data, client) def get_project_name(working_dir, project_name=None, environment=None): diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 7641870b..3939a97b 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -154,6 +154,19 @@ class CLITestCase(DockerClientTestCase): returncode=0 ) + def test_host_not_reachable(self): + result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) + assert "Couldn't connect to Docker daemon" in result.stderr + + def test_host_not_reachable_volumes_from_container(self): + self.base_dir = 'tests/fixtures/volumes-from-container' + + container = self.client.create_container('busybox', 'true', name='composetest_data_container') + self.addCleanup(self.client.remove_container, container) + + result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) + assert "Couldn't connect to Docker daemon" in result.stderr + def test_config_list_services(self): self.base_dir = 'tests/fixtures/v2-full' result = self.dispatch(['config', '--services']) diff --git a/tests/fixtures/volumes-from-container/docker-compose.yml b/tests/fixtures/volumes-from-container/docker-compose.yml new file mode 100644 index 00000000..495fcaae --- /dev/null +++ b/tests/fixtures/volumes-from-container/docker-compose.yml @@ -0,0 +1,5 @@ +version: "2" +services: + test: + image: busybox + volumes_from: ["container:composetest_data_container"] From 9abbe1b7f8cf4e83fea7b115204d50384c9723ed Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 1 Aug 2016 11:50:57 -0700 Subject: [PATCH 080/118] Catchable error for parse failures in split_buffer Signed-off-by: Joffrey F --- compose/cli/main.py | 3 ++- compose/errors.py | 5 +++++ compose/utils.py | 10 ++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index db06a5e1..20200b09 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -23,6 +23,7 @@ from ..config.environment import Environment from ..config.serialize import serialize_config from ..const import DEFAULT_TIMEOUT from ..const import IS_WINDOWS_PLATFORM +from ..errors import StreamParseError from ..progress_stream import StreamOutputError from ..project import NoSuchService from ..project import OneOffFilter @@ -75,7 +76,7 @@ def main(): except NeedsBuildError as e: log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name) sys.exit(1) - except errors.ConnectionError: + except (errors.ConnectionError, StreamParseError): sys.exit(1) diff --git a/compose/errors.py b/compose/errors.py index 9f68760d..376cc555 100644 --- a/compose/errors.py +++ b/compose/errors.py @@ -5,3 +5,8 @@ from __future__ import unicode_literals class OperationFailedError(Exception): def __init__(self, reason): self.msg = reason + + +class StreamParseError(RuntimeError): + def __init__(self, reason): + self.msg = reason diff --git a/compose/utils.py b/compose/utils.py index eea73be1..6d9a9fdc 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -9,6 +9,8 @@ import logging import six +from .errors import StreamParseError + json_decoder = json.JSONDecoder() log = logging.getLogger(__name__) @@ -64,12 +66,12 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): if buffered: try: yield decoder(buffered) - except ValueError: + except Exception as e: log.error( - 'Compose tried parsing the following chunk as a JSON object, ' - 'but failed:\n%s' % repr(buffered) + 'Compose tried decoding the following data chunk, but failed:' + '\n%s' % repr(buffered) ) - raise + raise StreamParseError(e) def json_splitter(buffer): From 4cba653eeb3c054557a02b23b905da8a11bbc8e5 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Thu, 4 Aug 2016 15:19:02 +0100 Subject: [PATCH 081/118] Disambiguate 'Swarm' in integration doc Signed-off-by: Aanand Prasad --- docs/swarm.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/swarm.md b/docs/swarm.md index bbab6908..f956f8c2 100644 --- a/docs/swarm.md +++ b/docs/swarm.md @@ -11,6 +11,10 @@ parent="workw_compose" # Using Compose with Swarm +> **Note:** “Swarm” here refers to [Docker Swarm](/swarm/overview.md), a product separate from Docker Engine. It does _not_ refer to [swarm mode](/engine/swarm), which is a built-in feature of Docker Engine introduced in version 1.12. +> +> Integration between Compose and swarm mode is at the experimental stage. See [Docker Stacks and Bundles](bundles.md) for details. + Docker Compose and [Docker Swarm](/swarm/overview.md) 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. From 17f46f8999e66e3dbb8f8438002caf17fe6065a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JG=C2=B2?= Date: Wed, 3 Aug 2016 11:17:54 +0200 Subject: [PATCH 082/118] Update rm.md Receiving this message when using the -a flag : `--all flag is obsolete. This is now the default behavior of `docker-compose rm`, I proposed to mark it in the docs but I don't know which way is the best Signed-off-by: jgsqware --- compose/cli/main.py | 3 +-- docs/reference/rm.md | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index db06a5e1..7655fe9c 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -615,8 +615,7 @@ class TopLevelCommand(object): Options: -f, --force Don't ask to confirm removal -v Remove any anonymous volumes attached to containers - -a, --all Obsolete. Also remove one-off containers created by - docker-compose run + -a, --all Deprecated - no effect. """ if options.get('--all'): log.warn( diff --git a/docs/reference/rm.md b/docs/reference/rm.md index 8285a4ae..6351e6cf 100644 --- a/docs/reference/rm.md +++ b/docs/reference/rm.md @@ -17,8 +17,7 @@ Usage: rm [options] [SERVICE...] Options: -f, --force Don't ask to confirm removal -v Remove any anonymous volumes attached to containers - -a, --all Also remove one-off containers created by - docker-compose run + -a, --all Deprecated - no effect. ``` Removes stopped service containers. From c0305024f53c07607c34ff253860af386b004fbe Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 9 Aug 2016 15:13:01 -0700 Subject: [PATCH 083/118] Remove surrounding quotes from TLS paths, if present Signed-off-by: Joffrey F --- compose/cli/docker_client.py | 7 ++++--- compose/cli/utils.py | 8 ++++++++ tests/unit/cli/docker_client_test.py | 13 +++++++++++++ tests/unit/cli/utils_test.py | 23 +++++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 tests/unit/cli/utils_test.py diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index ce191fbf..b196d303 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -11,15 +11,16 @@ from docker.utils import kwargs_from_env from ..const import HTTP_TIMEOUT from .errors import UserError from .utils import generate_user_agent +from .utils import unquote_path log = logging.getLogger(__name__) def tls_config_from_options(options): tls = options.get('--tls', False) - ca_cert = options.get('--tlscacert') - cert = options.get('--tlscert') - key = options.get('--tlskey') + ca_cert = unquote_path(options.get('--tlscacert')) + cert = unquote_path(options.get('--tlscert')) + key = unquote_path(options.get('--tlskey')) verify = options.get('--tlsverify') skip_hostname_check = options.get('--skip-hostname-check', False) diff --git a/compose/cli/utils.py b/compose/cli/utils.py index f60f61cd..e10a3674 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -122,3 +122,11 @@ def generate_user_agent(): else: parts.append("{}/{}".format(p_system, p_release)) return " ".join(parts) + + +def unquote_path(s): + if not s: + return s + if s[0] == '"' and s[-1] == '"': + return s[1:-1] + return s diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 3430c25c..aaa935af 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -144,3 +144,16 @@ class TLSConfigTestCase(unittest.TestCase): result = tls_config_from_options(options) assert isinstance(result, docker.tls.TLSConfig) assert result.assert_hostname is False + + def test_tls_client_and_ca_quoted_paths(self): + options = { + '--tlscacert': '"{0}"'.format(self.ca_cert), + '--tlscert': '"{0}"'.format(self.client_cert), + '--tlskey': '"{0}"'.format(self.key), + '--tlsverify': True + } + result = tls_config_from_options(options) + assert isinstance(result, docker.tls.TLSConfig) + assert result.cert == (self.client_cert, self.key) + assert result.ca_cert == self.ca_cert + assert result.verify is True diff --git a/tests/unit/cli/utils_test.py b/tests/unit/cli/utils_test.py new file mode 100644 index 00000000..066fb359 --- /dev/null +++ b/tests/unit/cli/utils_test.py @@ -0,0 +1,23 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +import unittest + +from compose.cli.utils import unquote_path + + +class UnquotePathTest(unittest.TestCase): + def test_no_quotes(self): + assert unquote_path('hello') == 'hello' + + def test_simple_quotes(self): + assert unquote_path('"hello"') == 'hello' + + def test_uneven_quotes(self): + assert unquote_path('"hello') == '"hello' + assert unquote_path('hello"') == 'hello"' + + def test_nested_quotes(self): + assert unquote_path('""hello""') == '"hello"' + assert unquote_path('"hel"lo"') == 'hel"lo' + assert unquote_path('"hello""') == 'hello"' From d824cb9b0678ec2ad460b034231c00c05df8c0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Wed, 1 Jun 2016 21:15:12 +0200 Subject: [PATCH 084/118] Add support for swappiness constraint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run a service using `docker run --memory-swappiness=0` (see https://docs.docker.com/engine/reference/run/) refs #2383 Signed-off-by: Jean-François Roche --- compose/config/config.py | 1 + compose/config/config_schema_v1.json | 1 + compose/config/config_schema_v2.0.json | 1 + compose/service.py | 4 +++- docs/compose-file.md | 3 ++- tests/integration/service_test.py | 5 +++++ tests/unit/config/config_test.py | 18 ++++++++++++++++++ 7 files changed, 31 insertions(+), 2 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index d3ab1d4b..91c2f6a6 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -69,6 +69,7 @@ DOCKER_CONFIG_KEYS = [ 'mac_address', 'mem_limit', 'memswap_limit', + 'mem_swappiness', 'net', 'oom_score_adj' 'pid', diff --git a/compose/config/config_schema_v1.json b/compose/config/config_schema_v1.json index 36a93793..94354cda 100644 --- a/compose/config/config_schema_v1.json +++ b/compose/config/config_schema_v1.json @@ -85,6 +85,7 @@ "mac_address": {"type": "string"}, "mem_limit": {"type": ["number", "string"]}, "memswap_limit": {"type": ["number", "string"]}, + "mem_swappiness": {"type": "integer"}, "net": {"type": "string"}, "pid": {"type": ["string", "null"]}, diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index ac46944c..59caac9b 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -139,6 +139,7 @@ "mac_address": {"type": "string"}, "mem_limit": {"type": ["number", "string"]}, "memswap_limit": {"type": ["number", "string"]}, + "mem_swappiness": {"type": "integer"}, "network_mode": {"type": "string"}, "networks": { diff --git a/compose/service.py b/compose/service.py index 60343542..d0cdeeb3 100644 --- a/compose/service.py +++ b/compose/service.py @@ -55,6 +55,7 @@ DOCKER_START_KEYS = [ 'mem_limit', 'memswap_limit', 'oom_score_adj', + 'mem_swappiness', 'pid', 'privileged', 'restart', @@ -704,7 +705,8 @@ class Service(object): cpu_quota=options.get('cpu_quota'), shm_size=options.get('shm_size'), tmpfs=options.get('tmpfs'), - oom_score_adj=options.get('oom_score_adj') + oom_score_adj=options.get('oom_score_adj'), + mem_swappiness=options.get('mem_swappiness') ) def build(self, no_cache=False, pull=False, force_rm=False): diff --git a/docs/compose-file.md b/docs/compose-file.md index d6d0cadc..9f4bd121 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -744,7 +744,7 @@ then read-write will be used. > - container_name > - container_name:rw -### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, oom_score_adj, privileged, read\_only, restart, shm\_size, stdin\_open, tty, user, working\_dir +### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, mem\_swappiness, oom\_score\_adj, privileged, read\_only, restart, shm\_size, stdin\_open, tty, user, working\_dir Each of these is a single value, analogous to its [docker run](https://docs.docker.com/engine/reference/run/) counterpart. @@ -763,6 +763,7 @@ Each of these is a single value, analogous to its mem_limit: 1000000000 memswap_limit: 2000000000 + mem_swappiness: 10 privileged: true restart: always diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 97ad7476..24dec983 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -852,6 +852,11 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.Dns'), ['8.8.8.8', '9.9.9.9']) + def test_mem_swappiness(self): + service = self.create_service('web', mem_swappiness=11) + container = create_and_start_container(service) + self.assertEqual(container.get('HostConfig.MemorySwappiness'), 11) + def test_restart_always_value(self): service = self.create_service('web', restart={'Name': 'always'}) container = create_and_start_container(service) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index d88c1d47..02810d2b 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1271,6 +1271,24 @@ class ConfigTest(unittest.TestCase): } ] + def test_swappiness_option(self): + actual = config.load(build_config_details({ + 'version': '2', + 'services': { + 'web': { + 'image': 'alpine', + 'mem_swappiness': 10, + } + } + })) + assert actual.services == [ + { + 'name': 'web', + 'image': 'alpine', + 'mem_swappiness': 10, + } + ] + def test_merge_service_dicts_from_files_with_extends_in_base(self): base = { 'volumes': ['.:/app'], From bf91c64983a8598f9170d3f298b9abed100aacd0 Mon Sep 17 00:00:00 2001 From: Sven Dowideit Date: Mon, 22 Aug 2016 12:18:07 +1000 Subject: [PATCH 085/118] Add docs checking Jenkinsfile Signed-off-by: Sven Dowideit --- Jenkinsfile | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Jenkinsfile diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 00000000..fa29520b --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,8 @@ +// Only run on Linux atm +wrappedNode(label: 'docker') { + deleteDir() + stage "checkout" + checkout scm + + documentationChecker("docs") +} From 817c76c8e9ebbeb864ef69e8e03819fefb1a784d Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 8 Sep 2016 11:17:39 -0700 Subject: [PATCH 086/118] Fix command hint in bundle to pull services instead of images Signed-off-by: Joffrey F --- compose/bundle.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/compose/bundle.py b/compose/bundle.py index afbdabfa..854cc799 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -46,8 +46,9 @@ class NeedsPush(Exception): class NeedsPull(Exception): - def __init__(self, image_name): + def __init__(self, image_name, service_name): self.image_name = image_name + self.service_name = service_name class MissingDigests(Exception): @@ -74,7 +75,7 @@ def get_image_digests(project, allow_push=False): except NeedsPush as e: needs_push.add(e.image_name) except NeedsPull as e: - needs_pull.add(e.image_name) + needs_pull.add(e.service_name) if needs_push or needs_pull: raise MissingDigests(needs_push, needs_pull) @@ -109,7 +110,7 @@ def get_image_digest(service, allow_push=False): return image['RepoDigests'][0] if 'build' not in service.options: - raise NeedsPull(service.image_name) + raise NeedsPull(service.image_name, service.name) if not allow_push: raise NeedsPush(service.image_name) From dada36f732ed7f7fc05c0f9841a432c490c7ff9c Mon Sep 17 00:00:00 2001 From: George Lester Date: Fri, 8 Jul 2016 00:29:13 -0700 Subject: [PATCH 087/118] Supported group_add Signed-off-by: George Lester --- compose/config/config.py | 1 + compose/config/config_schema_v2.0.json | 7 +++++++ compose/service.py | 4 +++- docs/compose-file.md | 14 ++++++++++++++ tests/integration/service_test.py | 8 ++++++++ tests/unit/config/config_test.py | 20 ++++++++++++++++++++ 6 files changed, 53 insertions(+), 1 deletion(-) diff --git a/compose/config/config.py b/compose/config/config.py index 91c2f6a6..d36aefa5 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -61,6 +61,7 @@ DOCKER_CONFIG_KEYS = [ 'env_file', 'environment', 'extra_hosts', + 'group_add', 'hostname', 'image', 'ipc', diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index 59caac9b..76688916 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -168,6 +168,13 @@ ] }, "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, + "group_add": { + "type": "array", + "items": { + "type": ["string", "number"] + }, + "uniqueItems": true + }, "pid": {"type": ["string", "null"]}, "ports": { diff --git a/compose/service.py b/compose/service.py index d0cdeeb3..b5a35b7e 100644 --- a/compose/service.py +++ b/compose/service.py @@ -48,6 +48,7 @@ DOCKER_START_KEYS = [ 'dns_search', 'env_file', 'extra_hosts', + 'group_add', 'ipc', 'read_only', 'log_driver', @@ -706,7 +707,8 @@ class Service(object): shm_size=options.get('shm_size'), tmpfs=options.get('tmpfs'), oom_score_adj=options.get('oom_score_adj'), - mem_swappiness=options.get('mem_swappiness') + mem_swappiness=options.get('mem_swappiness'), + group_add=options.get('group_add') ) def build(self, no_cache=False, pull=False, force_rm=False): diff --git a/docs/compose-file.md b/docs/compose-file.md index 9f4bd121..384649b1 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -889,6 +889,20 @@ A full example: host2: 172.28.1.6 host3: 172.28.1.7 +### group_add + +Specify additional groups (by name or number) which the user inside the container will be a member of. Groups must exist in both the container and the host system to be added. An example of where this is useful is when multiple containers (running as different users) need to all read or write the same file on the host system. That file can be owned by a group shared by all the containers, and specified in `group_add`. See the [Docker documentation](https://docs.docker.com/engine/reference/run/#/additional-groups) for more details. + +A full example: + + version: '2' + services: + image: alpine + group_add: + - mail + +Running `id` inside the created container will show that the user belongs to the `mail` group, which would not have been the case if `group_add` were not used. + ### internal By default, Docker also connects a bridge network to it to provide external connectivity. If you want to create an externally isolated overlay network, you can set this option to `true`. diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 24dec983..a5ca81ee 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -867,6 +867,14 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.OomScoreAdj'), 500) + def test_group_add_value(self): + service = self.create_service('web', group_add=["root", "1"]) + container = create_and_start_container(service) + + host_container_groupadd = container.get('HostConfig.GroupAdd') + self.assertTrue("root" in host_container_groupadd) + self.assertTrue("1" in host_container_groupadd) + def test_restart_on_failure_value(self): service = self.create_service('web', restart={ 'Name': 'on-failure', diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 02810d2b..837630c1 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1289,6 +1289,26 @@ class ConfigTest(unittest.TestCase): } ] + def test_group_add_option(self): + + actual = config.load(build_config_details({ + 'version': '2', + 'services': { + 'web': { + 'image': 'alpine', + 'group_add': ["docker", 777] + } + } + })) + + assert actual.services == [ + { + 'name': 'web', + 'image': 'alpine', + 'group_add': ["docker", 777] + } + ] + def test_merge_service_dicts_from_files_with_extends_in_base(self): base = { 'volumes': ['.:/app'], From b64bd07f225dd8af9f2b8fd096dfd957022c3dda Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 9 Sep 2016 17:19:14 -0700 Subject: [PATCH 088/118] Update docker-py dependency to latest release Signed-off-by: Joffrey F --- requirements.txt | 5 +++-- setup.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 831ed65a..7f28514b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,15 @@ PyYAML==3.11 backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 -docker-py==1.9.0 +docker-py==1.10.2 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4; python_version < '3.4' functools32==3.2.3.post2; python_version < '3.2' ipaddress==1.0.16 jsonschema==2.5.1 +pypiwin32==219; sys_platform == 'win32' requests==2.7.0 -six==1.7.3 +six==1.10.0 texttable==0.8.4 websocket-client==0.32.0 diff --git a/setup.py b/setup.py index 5cb52dae..34b40273 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires = [ 'requests >= 2.6.1, < 2.8', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker-py >= 1.9.0, < 2.0', + 'docker-py >= 1.10.2, < 2.0', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', From 9759f27fa6a970ee9e01d1edd2d8b4a96eb510fa Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Tue, 13 Sep 2016 14:50:37 +0100 Subject: [PATCH 089/118] Fix integration test on Docker for Mac Signed-off-by: Aanand Prasad --- tests/unit/cli/errors_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cli/errors_test.py b/tests/unit/cli/errors_test.py index 71fa9dee..1d454a08 100644 --- a/tests/unit/cli/errors_test.py +++ b/tests/unit/cli/errors_test.py @@ -32,7 +32,7 @@ class TestHandleConnectionErrors(object): raise ConnectionError() _, args, _ = mock_logging.error.mock_calls[0] - assert "Couldn't connect to Docker daemon at" in args[0] + assert "Couldn't connect to Docker daemon" in args[0] def test_api_error_version_mismatch(self, mock_logging): with pytest.raises(errors.ConnectionError): From 7dd2e33057f8dab7bb63ceedc8ea598f993463c4 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 12 Sep 2016 16:02:58 -0700 Subject: [PATCH 090/118] Only allow log streaming if logdriver is json-file or journald Signed-off-by: Joffrey F --- compose/container.py | 2 +- tests/unit/container_test.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/compose/container.py b/compose/container.py index 2c16863d..bda4e659 100644 --- a/compose/container.py +++ b/compose/container.py @@ -163,7 +163,7 @@ class Container(object): @property def has_api_logs(self): log_type = self.log_driver - return not log_type or log_type != 'none' + return not log_type or log_type in ('json-file', 'journald') def attach_log_stream(self): """A log stream can only be attached if the container uses a json-file diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 47f60de8..62e3aa2c 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -150,6 +150,34 @@ class ContainerTest(unittest.TestCase): container = Container(None, self.container_dict, has_been_inspected=True) assert container.short_id == self.container_id[:12] + def test_has_api_logs(self): + container_dict = { + 'HostConfig': { + 'LogConfig': { + 'Type': 'json-file' + } + } + } + + container = Container(None, container_dict, has_been_inspected=True) + assert container.has_api_logs is True + + container_dict['HostConfig']['LogConfig']['Type'] = 'none' + container = Container(None, container_dict, has_been_inspected=True) + assert container.has_api_logs is False + + container_dict['HostConfig']['LogConfig']['Type'] = 'syslog' + container = Container(None, container_dict, has_been_inspected=True) + assert container.has_api_logs is False + + container_dict['HostConfig']['LogConfig']['Type'] = 'journald' + container = Container(None, container_dict, has_been_inspected=True) + assert container.has_api_logs is True + + container_dict['HostConfig']['LogConfig']['Type'] = 'foobar' + container = Container(None, container_dict, has_been_inspected=True) + assert container.has_api_logs is False + class GetContainerNameTestCase(unittest.TestCase): From 3fcd648ba2e63191f72c7bdb53cadc387c90769f Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 12 Sep 2016 15:49:54 -0700 Subject: [PATCH 091/118] Catch APIError while printing container logs Signed-off-by: Joffrey F --- compose/cli/log_printer.py | 11 +++++++++-- tests/unit/cli/log_printer_test.py | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index b48462ff..299ddea4 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -6,6 +6,7 @@ from collections import namedtuple from itertools import cycle from threading import Thread +from docker.errors import APIError from six.moves import _thread as thread from six.moves.queue import Empty from six.moves.queue import Queue @@ -176,8 +177,14 @@ def build_log_generator(container, log_args): def wait_on_exit(container): - exit_code = container.wait() - return "%s exited with code %s\n" % (container.name, exit_code) + try: + exit_code = container.wait() + return "%s exited with code %s\n" % (container.name, exit_code) + except APIError as e: + return "Unexpected API error for %s (HTTP code %s)\nResponse body:\n%s\n" % ( + container.name, e.response.status_code, + e.response.text or '[empty]' + ) def start_producer_thread(thread_args): diff --git a/tests/unit/cli/log_printer_test.py b/tests/unit/cli/log_printer_test.py index ab48eefc..b908eb68 100644 --- a/tests/unit/cli/log_printer_test.py +++ b/tests/unit/cli/log_printer_test.py @@ -4,7 +4,9 @@ from __future__ import unicode_literals import itertools import pytest +import requests import six +from docker.errors import APIError from six.moves.queue import Queue from compose.cli.log_printer import build_log_generator @@ -56,6 +58,26 @@ def test_wait_on_exit(): assert expected == wait_on_exit(mock_container) +def test_wait_on_exit_raises(): + status_code = 500 + + def mock_wait(): + resp = requests.Response() + resp.status_code = status_code + raise APIError('Bad server', resp) + + mock_container = mock.Mock( + spec=Container, + name='cname', + wait=mock_wait + ) + + expected = 'Unexpected API error for {} (HTTP code {})\n'.format( + mock_container.name, status_code, + ) + assert expected in wait_on_exit(mock_container) + + def test_build_no_log_generator(mock_container): mock_container.has_api_logs = False mock_container.log_driver = 'none' From fd254caa681ac697b9aad03d4b8c5e2e04d4f437 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 27 Jun 2016 17:27:31 -0700 Subject: [PATCH 092/118] Add support for link-local IPs in service.networks definition Signed-off-by: Joffrey F --- compose/config/config.py | 5 +- compose/config/config_schema_v2.1.json | 319 +++++++++++++++++++++++++ compose/config/serialize.py | 7 +- compose/const.py | 5 +- compose/service.py | 4 +- docs/compose-file.md | 34 +++ tests/integration/project_test.py | 27 +++ tests/integration/testcases.py | 32 ++- tests/unit/config/config_test.py | 34 ++- 9 files changed, 452 insertions(+), 15 deletions(-) create mode 100644 compose/config/config_schema_v2.1.json diff --git a/compose/config/config.py b/compose/config/config.py index 91c2f6a6..32eda81f 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -15,6 +15,7 @@ from cached_property import cached_property from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V2_0 as V2_0 +from ..const import COMPOSEFILE_V2_1 as V2_1 from ..utils import build_string_dict from .environment import env_vars_from_file from .environment import Environment @@ -173,7 +174,7 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')): if version == '2': version = V2_0 - if version != V2_0: + if version not in (V2_0, V2_1): raise ConfigurationError( 'Version in "{}" is unsupported. {}' .format(self.filename, VERSION_EXPLANATION)) @@ -423,7 +424,7 @@ def process_config_file(config_file, environment, service_name=None): 'service', environment,) - if config_file.version == V2_0: + if config_file.version in (V2_0, V2_1): processed_config = dict(config_file.config) processed_config['services'] = services processed_config['volumes'] = interpolate_config_section( diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json new file mode 100644 index 00000000..de4ddf25 --- /dev/null +++ b/compose/config/config_schema_v2.1.json @@ -0,0 +1,319 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "config_schema_v2.1.json", + "type": "object", + + "properties": { + "version": { + "type": "string" + }, + + "services": { + "id": "#/properties/services", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/service" + } + }, + "additionalProperties": false + }, + + "networks": { + "id": "#/properties/networks", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/network" + } + } + }, + + "volumes": { + "id": "#/properties/volumes", + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "$ref": "#/definitions/volume" + } + }, + "additionalProperties": false + } + }, + + "additionalProperties": false, + + "definitions": { + + "service": { + "id": "#/definitions/service", + "type": "object", + + "properties": { + "build": { + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "properties": { + "context": {"type": "string"}, + "dockerfile": {"type": "string"}, + "args": {"$ref": "#/definitions/list_or_dict"} + }, + "additionalProperties": false + } + ] + }, + "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "cgroup_parent": {"type": "string"}, + "command": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "container_name": {"type": "string"}, + "cpu_shares": {"type": ["number", "string"]}, + "cpu_quota": {"type": ["number", "string"]}, + "cpuset": {"type": "string"}, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, + "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "dns": {"$ref": "#/definitions/string_or_list"}, + "dns_search": {"$ref": "#/definitions/string_or_list"}, + "domainname": {"type": "string"}, + "entrypoint": { + "oneOf": [ + {"type": "string"}, + {"type": "array", "items": {"type": "string"}} + ] + }, + "env_file": {"$ref": "#/definitions/string_or_list"}, + "environment": {"$ref": "#/definitions/list_or_dict"}, + + "expose": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "expose" + }, + "uniqueItems": true + }, + + "extends": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + + "properties": { + "service": {"type": "string"}, + "file": {"type": "string"} + }, + "required": ["service"], + "additionalProperties": false + } + ] + }, + + "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, + "hostname": {"type": "string"}, + "image": {"type": "string"}, + "ipc": {"type": "string"}, + "labels": {"$ref": "#/definitions/list_or_dict"}, + "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + + "logging": { + "type": "object", + + "properties": { + "driver": {"type": "string"}, + "options": {"type": "object"} + }, + "additionalProperties": false + }, + + "mac_address": {"type": "string"}, + "mem_limit": {"type": ["number", "string"]}, + "memswap_limit": {"type": ["number", "string"]}, + "network_mode": {"type": "string"}, + + "networks": { + "oneOf": [ + {"$ref": "#/definitions/list_of_strings"}, + { + "type": "object", + "patternProperties": { + "^[a-zA-Z0-9._-]+$": { + "oneOf": [ + { + "type": "object", + "properties": { + "aliases": {"$ref": "#/definitions/list_of_strings"}, + "ipv4_address": {"type": "string"}, + "ipv6_address": {"type": "string"}, + "link_local_ips": {"$ref": "#/definitions/list_of_strings"} + }, + "additionalProperties": false + }, + {"type": "null"} + ] + } + }, + "additionalProperties": false + } + ] + }, + "pid": {"type": ["string", "null"]}, + + "ports": { + "type": "array", + "items": { + "type": ["string", "number"], + "format": "ports" + }, + "uniqueItems": true + }, + + "privileged": {"type": "boolean"}, + "read_only": {"type": "boolean"}, + "restart": {"type": "string"}, + "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "shm_size": {"type": ["number", "string"]}, + "stdin_open": {"type": "boolean"}, + "stop_signal": {"type": "string"}, + "tmpfs": {"$ref": "#/definitions/string_or_list"}, + "tty": {"type": "boolean"}, + "ulimits": { + "type": "object", + "patternProperties": { + "^[a-z]+$": { + "oneOf": [ + {"type": "integer"}, + { + "type":"object", + "properties": { + "hard": {"type": "integer"}, + "soft": {"type": "integer"} + }, + "required": ["soft", "hard"], + "additionalProperties": false + } + ] + } + } + }, + "user": {"type": "string"}, + "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "volume_driver": {"type": "string"}, + "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, + "working_dir": {"type": "string"} + }, + + "dependencies": { + "memswap_limit": ["mem_limit"] + }, + "additionalProperties": false + }, + + "network": { + "id": "#/definitions/network", + "type": "object", + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "ipam": { + "type": "object", + "properties": { + "driver": {"type": "string"}, + "config": { + "type": "array" + } + }, + "additionalProperties": false + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + + "volume": { + "id": "#/definitions/volume", + "type": ["object", "null"], + "properties": { + "driver": {"type": "string"}, + "driver_opts": { + "type": "object", + "patternProperties": { + "^.+$": {"type": ["string", "number"]} + } + }, + "external": { + "type": ["boolean", "object"], + "properties": { + "name": {"type": "string"} + } + }, + "additionalProperties": false + }, + "additionalProperties": false + }, + + "string_or_list": { + "oneOf": [ + {"type": "string"}, + {"$ref": "#/definitions/list_of_strings"} + ] + }, + + "list_of_strings": { + "type": "array", + "items": {"type": "string"}, + "uniqueItems": true + }, + + "list_or_dict": { + "oneOf": [ + { + "type": "object", + "patternProperties": { + ".+": { + "type": ["string", "number", "null"] + } + }, + "additionalProperties": false + }, + {"type": "array", "items": {"type": "string"}, "uniqueItems": true} + ] + }, + + "constraints": { + "service": { + "id": "#/definitions/constraints/service", + "anyOf": [ + {"required": ["build"]}, + {"required": ["image"]} + ], + "properties": { + "build": { + "required": ["context"] + } + } + } + } + } +} diff --git a/compose/config/serialize.py b/compose/config/serialize.py index b788a55d..0e6efbdf 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -7,6 +7,7 @@ import yaml from compose.config import types from compose.config.config import V1 from compose.config.config import V2_0 +from compose.config.config import V2_1 def serialize_config_type(dumper, data): @@ -32,8 +33,12 @@ def denormalize_config(config): if 'external_name' in net_conf: del net_conf['external_name'] + version = config.version + if version not in (V2_0, V2_1): + version = V2_0 + return { - 'version': V2_0, + 'version': version, 'services': services, 'networks': networks, 'volumes': config.volumes, diff --git a/compose/const.py b/compose/const.py index b930e0bf..e7b1ae97 100644 --- a/compose/const.py +++ b/compose/const.py @@ -16,13 +16,16 @@ LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' COMPOSEFILE_V1 = '1' COMPOSEFILE_V2_0 = '2.0' +COMPOSEFILE_V2_1 = '2.1' API_VERSIONS = { COMPOSEFILE_V1: '1.21', COMPOSEFILE_V2_0: '1.22', + COMPOSEFILE_V2_1: '1.24', } API_VERSION_TO_ENGINE_VERSION = { API_VERSIONS[COMPOSEFILE_V1]: '1.9.0', - API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0' + API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0', + API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', } diff --git a/compose/service.py b/compose/service.py index d0cdeeb3..31ea9e0f 100644 --- a/compose/service.py +++ b/compose/service.py @@ -477,7 +477,9 @@ class Service(object): aliases=self._get_aliases(netdefs, container), ipv4_address=netdefs.get('ipv4_address', None), ipv6_address=netdefs.get('ipv6_address', None), - links=self._get_links(False)) + links=self._get_links(False), + link_local_ips=netdefs.get('link_local_ips', None), + ) def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT): for c in self.duplicate_containers(): diff --git a/docs/compose-file.md b/docs/compose-file.md index 9f4bd121..c8fa112d 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -621,6 +621,31 @@ An example: - subnet: 2001:3984:3989::/64 gateway: 2001:3984:3989::1 +#### link_local_ips + +> [Version 2.1 file format](#version-2.1) only. + +Specify a list of link-local IPs. Link-local IPs are special IPs which belong +to a well known subnet and are purely managed by the operator, usually +dependent on the architecture where they are deployed. Therefore they are not +managed by docker (IPAM driver). + +Example usage: + + version: '2.1' + services: + app: + image: busybox + command: top + networks: + app_net: + link_local_ips: + - 57.123.22.11 + - 57.123.22.13 + networks: + app_net: + driver: bridge + ### pid pid: "host" @@ -1040,6 +1065,15 @@ A more extended example, defining volumes and networks: back-tier: driver: bridge +### Version 2.1 + +An upgrade of [version 2](#version-2) that introduces new parameters only +available with Docker Engine version **1.12.0+** + +Introduces: + +- [`link_local_ips`](#link_local_ips) +- ... ### Upgrading diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 80915c1a..2241f70f 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -13,6 +13,7 @@ from .testcases import DockerClientTestCase from compose.config import config from compose.config import ConfigurationError from compose.config.config import V2_0 +from compose.config.config import V2_1 from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec from compose.const import LABEL_PROJECT @@ -21,6 +22,7 @@ from compose.container import Container from compose.project import Project from compose.project import ProjectError from compose.service import ConvergenceStrategy +from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only @@ -756,6 +758,31 @@ class ProjectTest(DockerClientTestCase): with self.assertRaises(ProjectError): project.up() + @v2_1_only() + def test_up_with_network_link_local_ips(self): + config_data = config.Config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': { + 'linklocaltest': { + 'link_local_ips': ['169.254.8.8'] + } + } + }], + volumes={}, + networks={ + 'linklocaltest': {'driver': 'bridge'} + } + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + project.up() + @v2_only() def test_project_up_with_network_internal(self): self.require_api_version('1.23') diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index 3e33a6c0..c7743fb8 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -12,6 +12,7 @@ from compose.cli.docker_client import docker_client from compose.config.config import resolve_environment from compose.config.config import V1 from compose.config.config import V2_0 +from compose.config.config import V2_1 from compose.config.environment import Environment from compose.const import API_VERSIONS from compose.const import LABEL_PROJECT @@ -33,18 +34,22 @@ def get_links(container): return [format_link(link) for link in links] -def engine_version_too_low_for_v2(): +def engine_max_version(): if 'DOCKER_VERSION' not in os.environ: - return False + return V2_1 version = os.environ['DOCKER_VERSION'].partition('-')[0] - return version_lt(version, '1.10') + if version_lt(version, '1.10'): + return V1 + elif version_lt(version, '1.12'): + return V2_0 + return V2_1 def v2_only(): def decorator(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): - if engine_version_too_low_for_v2(): + if engine_max_version() == V1: skip("Engine version is too low") return return f(self, *args, **kwargs) @@ -53,14 +58,23 @@ def v2_only(): return decorator +def v2_1_only(): + def decorator(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if engine_max_version() in (V1, V2_0): + skip('Engine version is too low') + return + return f(self, *args, **kwargs) + return wrapper + + return decorator + + class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - if engine_version_too_low_for_v2(): - version = API_VERSIONS[V1] - else: - version = API_VERSIONS[V2_0] - + version = API_VERSIONS[engine_max_version()] cls.client = docker_client(Environment(), version) @classmethod diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 02810d2b..8087c773 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -17,6 +17,7 @@ from compose.config.config import resolve_build_args from compose.config.config import resolve_environment from compose.config.config import V1 from compose.config.config import V2_0 +from compose.config.config import V2_1 from compose.config.environment import Environment from compose.config.errors import ConfigurationError from compose.config.errors import VERSION_EXPLANATION @@ -155,6 +156,8 @@ class ConfigTest(unittest.TestCase): for version in ['2', '2.0']: cfg = config.load(build_config_details({'version': version})) assert cfg.version == V2_0 + cfg = config.load(build_config_details({'version': '2.1'})) + assert cfg.version == V2_1 def test_v1_file_version(self): cfg = config.load(build_config_details({'web': {'image': 'busybox'}})) @@ -182,7 +185,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( - {'version': '2.1'}, + {'version': '2.18'}, filename='filename.yml', ) ) @@ -344,6 +347,35 @@ class ConfigTest(unittest.TestCase): }, 'working_dir', 'filename.yml') ) + def test_load_config_link_local_ips_network(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': '2.1', + 'services': { + 'web': { + 'image': 'example/web', + 'networks': { + 'foobar': { + 'aliases': ['foo', 'bar'], + 'link_local_ips': ['169.254.8.8'] + } + } + } + }, + 'networks': {'foobar': {}} + } + ) + + details = config.ConfigDetails('.', [base_file]) + web_service = config.load(details).services[0] + assert web_service['networks'] == { + 'foobar': { + 'aliases': ['foo', 'bar'], + 'link_local_ips': ['169.254.8.8'] + } + } + def test_load_config_invalid_service_names(self): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: with pytest.raises(ConfigurationError) as exc: From 66b395d950d333e945333717b525485ec9f664fe Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 25 Jul 2016 15:21:27 -0700 Subject: [PATCH 093/118] Include docker-py link-local fix and improve integration test Signed-off-by: Joffrey F --- tests/integration/project_test.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 2241f70f..4427fe6b 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -783,6 +783,17 @@ class ProjectTest(DockerClientTestCase): ) project.up() + service_container = project.get_service('web').containers()[0] + ipam_config = service_container.inspect().get( + 'NetworkSettings', {} + ).get( + 'Networks', {} + ).get( + 'composetest_linklocaltest', {} + ).get('IPAMConfig', {}) + assert 'LinkLocalIPs' in ipam_config + assert ipam_config['LinkLocalIPs'] == ['169.254.8.8'] + @v2_only() def test_project_up_with_network_internal(self): self.require_api_version('1.23') From 64517e31fce5293f58295c567bd5487e07b069f4 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 15 Sep 2016 17:59:20 -0700 Subject: [PATCH 094/118] Force default host on windows to the default TCP host (instead of npipe) Signed-off-by: Joffrey F --- compose/cli/docker_client.py | 6 ++++++ tests/unit/cli/docker_client_test.py | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index b196d303..7950c242 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -9,6 +9,7 @@ from docker.tls import TLSConfig from docker.utils import kwargs_from_env from ..const import HTTP_TIMEOUT +from ..const import IS_WINDOWS_PLATFORM from .errors import UserError from .utils import generate_user_agent from .utils import unquote_path @@ -71,4 +72,9 @@ def docker_client(environment, version=None, tls_config=None, host=None, kwargs['user_agent'] = generate_user_agent() + if 'base_url' not in kwargs and IS_WINDOWS_PLATFORM: + # docker-py 1.10 defaults to using npipes, but we don't want that + # change in compose yet - use the default TCP connection instead. + kwargs['base_url'] = 'tcp://127.0.0.1:2375' + return Client(**kwargs) diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index aaa935af..6cdb7da5 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -60,6 +60,14 @@ class DockerClientTestCase(unittest.TestCase): ) self.assertEqual(client.headers['User-Agent'], expected) + @mock.patch.dict(os.environ) + def test_docker_client_default_windows_host(self): + with mock.patch('compose.cli.docker_client.IS_WINDOWS_PLATFORM', True): + if 'DOCKER_HOST' in os.environ: + del os.environ['DOCKER_HOST'] + client = docker_client(os.environ) + assert client.base_url == 'http://127.0.0.1:2375' + class TLSConfigTestCase(unittest.TestCase): ca_cert = 'tests/fixtures/tls/ca.pem' From fc6791f3f0e3e8ae23d9b380398f39bea2e83101 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Fri, 16 Sep 2016 14:12:15 -0700 Subject: [PATCH 095/118] Bump docker-py dependency Signed-off-by: Joffrey F --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7f28514b..7acdd130 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyYAML==3.11 backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 -docker-py==1.10.2 +docker-py==1.10.3 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4; python_version < '3.4' diff --git a/setup.py b/setup.py index 34b40273..80258fbd 100644 --- a/setup.py +++ b/setup.py @@ -34,7 +34,7 @@ install_requires = [ 'requests >= 2.6.1, < 2.8', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker-py >= 1.10.2, < 2.0', + 'docker-py >= 1.10.3, < 2.0', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', From 79116592668be2c22d0bc188e1f1d92ff8be104c Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 8 Sep 2016 16:39:30 -0700 Subject: [PATCH 096/118] Improve volumespec parsing on windows platforms Signed-off-by: Joffrey F --- compose/config/config.py | 10 +--- compose/config/types.py | 85 +++++++++++++++++++++------------ compose/utils.py | 9 ++++ tests/unit/config/types_test.py | 28 +++++++++-- 4 files changed, 90 insertions(+), 42 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 91c2f6a6..2789e9ed 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import functools import logging -import ntpath import os import string import sys @@ -16,6 +15,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 ..utils import splitdrive from .environment import env_vars_from_file from .environment import Environment from .environment import split_env @@ -942,13 +942,7 @@ def split_path_mapping(volume_path): path. Using splitdrive so windows absolute paths won't cause issues with splitting on ':'. """ - # splitdrive is very naive, so handle special cases where we can be sure - # the first character is not a drive. - if (volume_path.startswith('.') or volume_path.startswith('~') or - volume_path.startswith('/')): - drive, volume_config = '', volume_path - else: - drive, volume_config = ntpath.splitdrive(volume_path) + drive, volume_config = splitdrive(volume_path) if ':' in volume_config: (host, container) = volume_config.split(':', 1) diff --git a/compose/config/types.py b/compose/config/types.py index e6a3dea0..9664b580 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -12,6 +12,7 @@ import six from compose.config.config import V1 from compose.config.errors import ConfigurationError from compose.const import IS_WINDOWS_PLATFORM +from compose.utils import splitdrive class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')): @@ -114,41 +115,23 @@ def parse_extra_hosts(extra_hosts_config): return extra_hosts_dict -def normalize_paths_for_engine(external_path, internal_path): +def normalize_path_for_engine(path): """Windows paths, c:\my\path\shiny, need to be changed to be compatible with the Engine. Volume paths are expected to be linux style /c/my/path/shiny/ """ - if not IS_WINDOWS_PLATFORM: - return external_path, internal_path + drive, tail = splitdrive(path) - if external_path: - drive, tail = os.path.splitdrive(external_path) + if drive: + path = '/' + drive.lower().rstrip(':') + tail - if drive: - external_path = '/' + drive.lower().rstrip(':') + tail - - external_path = external_path.replace('\\', '/') - - return external_path, internal_path.replace('\\', '/') + return path.replace('\\', '/') class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): @classmethod - def parse(cls, volume_config): - """Parse a volume_config path and split it into external:internal[:mode] - parts to be returned as a valid VolumeSpec. - """ - if IS_WINDOWS_PLATFORM: - # relative paths in windows expand to include the drive, eg C:\ - # so we join the first 2 parts back together to count as one - drive, tail = os.path.splitdrive(volume_config) - parts = tail.split(":") - - if drive: - parts[0] = drive + parts[0] - else: - parts = volume_config.split(':') + def _parse_unix(cls, volume_config): + parts = volume_config.split(':') if len(parts) > 3: raise ConfigurationError( @@ -156,13 +139,11 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): "external:internal[:mode]" % volume_config) if len(parts) == 1: - external, internal = normalize_paths_for_engine( - None, - os.path.normpath(parts[0])) + external = None + internal = os.path.normpath(parts[0]) else: - external, internal = normalize_paths_for_engine( - os.path.normpath(parts[0]), - os.path.normpath(parts[1])) + external = os.path.normpath(parts[0]) + internal = os.path.normpath(parts[1]) mode = 'rw' if len(parts) == 3: @@ -170,6 +151,48 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): return cls(external, internal, mode) + @classmethod + def _parse_win32(cls, volume_config): + # relative paths in windows expand to include the drive, eg C:\ + # so we join the first 2 parts back together to count as one + mode = 'rw' + + def separate_next_section(volume_config): + drive, tail = splitdrive(volume_config) + parts = tail.split(':', 1) + if drive: + parts[0] = drive + parts[0] + return parts + + parts = separate_next_section(volume_config) + if len(parts) == 1: + internal = normalize_path_for_engine(os.path.normpath(parts[0])) + external = None + else: + external = parts[0] + parts = separate_next_section(parts[1]) + external = normalize_path_for_engine(os.path.normpath(external)) + internal = normalize_path_for_engine(os.path.normpath(parts[0])) + if len(parts) > 1: + if ':' in parts[1]: + raise ConfigurationError( + "Volume %s has incorrect format, should be " + "external:internal[:mode]" % volume_config + ) + mode = parts[1] + + return cls(external, internal, mode) + + @classmethod + def parse(cls, volume_config): + """Parse a volume_config path and split it into external:internal[:mode] + parts to be returned as a valid VolumeSpec. + """ + if IS_WINDOWS_PLATFORM: + return cls._parse_win32(volume_config) + else: + return cls._parse_unix(volume_config) + def repr(self): external = self.external + ':' if self.external else '' return '{ext}{v.internal}:{v.mode}'.format(ext=external, v=self) diff --git a/compose/utils.py b/compose/utils.py index 6d9a9fdc..8f05e308 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -6,6 +6,7 @@ import hashlib import json import json.decoder import logging +import ntpath import six @@ -108,3 +109,11 @@ def microseconds_from_time_nano(time_nano): def build_string_dict(source_dict): return dict((k, str(v if v is not None else '')) for k, v in source_dict.items()) + + +def splitdrive(path): + if len(path) == 0: + return ('', '') + if path[0] in ['.', '\\', '/', '~']: + return ('', path) + return ntpath.splitdrive(path) diff --git a/tests/unit/config/types_test.py b/tests/unit/config/types_test.py index c741a339..8dfa65d5 100644 --- a/tests/unit/config/types_test.py +++ b/tests/unit/config/types_test.py @@ -9,7 +9,6 @@ from compose.config.errors import ConfigurationError from compose.config.types import parse_extra_hosts from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec -from compose.const import IS_WINDOWS_PLATFORM def test_parse_extra_hosts_list(): @@ -64,15 +63,38 @@ class TestVolumeSpec(object): VolumeSpec.parse('one:two:three:four') assert 'has incorrect format' in exc.exconly() - @pytest.mark.xfail((not IS_WINDOWS_PLATFORM), reason='does not have a drive') def test_parse_volume_windows_absolute_path(self): windows_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro" - assert VolumeSpec.parse(windows_path) == ( + assert VolumeSpec._parse_win32(windows_path) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro" ) + def test_parse_volume_windows_internal_path(self): + windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' + assert VolumeSpec._parse_win32(windows_path) == ( + '/c/Users/reimu/scarlet', + '/c/scarlet/app', + 'ro' + ) + + def test_parse_volume_windows_just_drives(self): + windows_path = 'E:\\:C:\\:ro' + assert VolumeSpec._parse_win32(windows_path) == ( + '/e/', + '/c/', + 'ro' + ) + + def test_parse_volume_windows_mixed_notations(self): + windows_path = '/c/Foo:C:\\bar' + assert VolumeSpec._parse_win32(windows_path) == ( + '/c/Foo', + '/c/bar', + 'rw' + ) + class TestVolumesFromSpec(object): From 33424189d43ce7ec0a55abdd709d08af8840e7e5 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 19 Sep 2016 17:13:53 -0700 Subject: [PATCH 097/118] Denormalize function defaults to latest version Minor docs fix Signed-off-by: Joffrey F --- compose/config/serialize.py | 2 +- docs/compose-file.md | 2 +- tests/acceptance/cli_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 0e6efbdf..95b1387f 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -35,7 +35,7 @@ def denormalize_config(config): version = config.version if version not in (V2_0, V2_1): - version = V2_0 + version = V2_1 return { 'version': version, diff --git a/docs/compose-file.md b/docs/compose-file.md index c8fa112d..625e5bf6 100644 --- a/docs/compose-file.md +++ b/docs/compose-file.md @@ -623,7 +623,7 @@ An example: #### link_local_ips -> [Version 2.1 file format](#version-2.1) only. +> [Added in version 2.1 file format](#version-21). Specify a list of link-local IPs. Link-local IPs are special IPs which belong to a well known subnet and are purely managed by the operator, usually diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 3939a97b..2247ffff 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -257,7 +257,7 @@ class CLITestCase(DockerClientTestCase): self.base_dir = 'tests/fixtures/v1-config' result = self.dispatch(['config']) assert yaml.load(result.stdout) == { - 'version': '2.0', + 'version': '2.1', 'services': { 'net': { 'image': 'busybox', From 53fa44c01e7b50b571c48101d6ca59ac0fd94ace Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 20 Sep 2016 18:05:59 -0700 Subject: [PATCH 098/118] Don't break when interpolating environment with unicode characters Signed-off-by: Joffrey F --- compose/service.py | 2 ++ tests/acceptance/cli_test.py | 19 +++++++++++++++++++ .../unicode-environment/docker-compose.yml | 7 +++++++ 3 files changed, 28 insertions(+) create mode 100644 tests/fixtures/unicode-environment/docker-compose.yml diff --git a/compose/service.py b/compose/service.py index b5a35b7e..1759bf7d 100644 --- a/compose/service.py +++ b/compose/service.py @@ -1109,6 +1109,8 @@ def format_environment(environment): def format_env(key, value): if value is None: return key + if isinstance(value, six.binary_type): + value = value.decode('utf-8') return '{key}={value}'.format(key=key, value=value) return [format_env(*item) for item in environment.items()] diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 3939a97b..67cca8c7 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals @@ -12,6 +13,7 @@ from collections import namedtuple from operator import attrgetter import py +import six import yaml from docker import errors @@ -1286,6 +1288,23 @@ class CLITestCase(DockerClientTestCase): 'simplecomposefile_simple_run_1', 'exited')) + @mock.patch.dict(os.environ) + def test_run_unicode_env_values_from_system(self): + value = 'ą, ć, ę, ł, ń, ó, ś, ź, ż' + if six.PY2: # os.environ doesn't support unicode values in Py2 + os.environ['BAR'] = value.encode('utf-8') + else: # ... and doesn't support byte values in Py3 + os.environ['BAR'] = value + self.base_dir = 'tests/fixtures/unicode-environment' + result = self.dispatch(['run', 'simple']) + + if six.PY2: # Can't retrieve output on Py3. See issue #3670 + assert value == result.stdout.strip() + + container = self.project.containers(one_off=OneOffFilter.only, stopped=True)[0] + environment = container.get('Config.Env') + assert 'FOO={}'.format(value) in environment + @mock.patch.dict(os.environ) def test_run_env_values_from_system(self): os.environ['FOO'] = 'bar' diff --git a/tests/fixtures/unicode-environment/docker-compose.yml b/tests/fixtures/unicode-environment/docker-compose.yml new file mode 100644 index 00000000..a41af4f0 --- /dev/null +++ b/tests/fixtures/unicode-environment/docker-compose.yml @@ -0,0 +1,7 @@ +version: '2' +services: + simple: + image: busybox:latest + command: sh -c 'echo $$FOO' + environment: + FOO: ${BAR} From 5667de87e88e0abcc876647ff7e73510de8b878a Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 22 Sep 2016 12:34:59 -0700 Subject: [PATCH 099/118] Fix the contributors script to show only contributors on the current branch Signed-off-by: Joffrey F --- script/release/contributors | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/release/contributors b/script/release/contributors index 1e69b143..4657dd80 100755 --- a/script/release/contributors +++ b/script/release/contributors @@ -15,10 +15,10 @@ EOM [[ -n "$1" ]] || usage PREV_RELEASE=$1 -VERSION=HEAD +BRANCH="$(git rev-parse --abbrev-ref HEAD)" URL="https://api.github.com/repos/docker/compose/compare" -contribs=$(curl -sf "$URL/$PREV_RELEASE...$VERSION" | \ +contribs=$(curl -sf "$URL/$PREV_RELEASE...$BRANCH" | \ jq -r '.commits[].author.login' | \ sort | \ uniq -c | \ From dc8a39f70d66948bcd220d1693ed8ab167fa3513 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Sep 2016 15:31:58 -0700 Subject: [PATCH 100/118] Add support for "isolation" in config Signed-off-by: Joffrey F --- compose/config/config_schema_v2.1.json | 1 + compose/service.py | 8 ++++- tests/integration/project_test.py | 43 ++++++++++++++++++++++++++ tests/unit/config/config_test.py | 23 ++++++++++++-- 4 files changed, 72 insertions(+), 3 deletions(-) diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index de4ddf25..243759fa 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -123,6 +123,7 @@ "hostname": {"type": "string"}, "image": {"type": "string"}, "ipc": {"type": "string"}, + "isolation": {"type": "string"}, "labels": {"$ref": "#/definitions/list_or_dict"}, "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, diff --git a/compose/service.py b/compose/service.py index c461220f..f4f4b90d 100644 --- a/compose/service.py +++ b/compose/service.py @@ -682,7 +682,7 @@ class Service(object): logging_dict = options.get('logging', None) log_config = get_log_config(logging_dict) - return self.client.create_host_config( + host_config = self.client.create_host_config( links=self._get_links(link_to_self=one_off), port_bindings=build_port_bindings(options.get('ports') or []), binds=options.get('binds'), @@ -713,6 +713,12 @@ class Service(object): group_add=options.get('group_add') ) + # TODO: Add as an argument to create_host_config once it's supported + # in docker-py + host_config['Isolation'] = options.get('isolation') + + return host_config + def build(self, no_cache=False, pull=False, force_rm=False): log.info('Building %s' % self.name) diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 4427fe6b..8588c6b1 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -794,6 +794,49 @@ class ProjectTest(DockerClientTestCase): assert 'LinkLocalIPs' in ipam_config assert ipam_config['LinkLocalIPs'] == ['169.254.8.8'] + @v2_1_only() + def test_up_with_isolation(self): + self.require_api_version('1.24') + config_data = config.Config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'isolation': 'default' + }], + volumes={}, + networks={} + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + project.up() + service_container = project.get_service('web').containers()[0] + assert service_container.inspect()['HostConfig']['Isolation'] == 'default' + + @v2_1_only() + def test_up_with_invalid_isolation(self): + self.require_api_version('1.24') + config_data = config.Config( + version=V2_1, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'isolation': 'foobar' + }], + volumes={}, + networks={} + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + with self.assertRaises(ProjectError): + project.up() + @v2_only() def test_project_up_with_network_internal(self): self.require_api_version('1.23') diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 88b990e5..d9bc5764 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -351,7 +351,7 @@ class ConfigTest(unittest.TestCase): base_file = config.ConfigFile( 'base.yaml', { - 'version': '2.1', + 'version': V2_1, 'services': { 'web': { 'image': 'example/web', @@ -1330,7 +1330,7 @@ class ConfigTest(unittest.TestCase): 'image': 'alpine', 'group_add': ["docker", 777] } - } + } })) assert actual.services == [ @@ -1341,6 +1341,25 @@ class ConfigTest(unittest.TestCase): } ] + def test_isolation_option(self): + actual = config.load(build_config_details({ + 'version': V2_1, + 'services': { + 'web': { + 'image': 'win10', + 'isolation': 'hyperv' + } + } + })) + + assert actual.services == [ + { + 'name': 'web', + 'image': 'win10', + 'isolation': 'hyperv', + } + ] + def test_merge_service_dicts_from_files_with_extends_in_base(self): base = { 'volumes': ['.:/app'], From 007cf96452a28941fde58f8b775f7cd48d86c1c9 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Sep 2016 15:36:57 -0700 Subject: [PATCH 101/118] Use docker-py's default behavior when no explicit host on Windows Signed-off-by: Joffrey F --- compose/cli/docker_client.py | 6 ------ tests/unit/cli/docker_client_test.py | 8 -------- 2 files changed, 14 deletions(-) diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index 7950c242..b196d303 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -9,7 +9,6 @@ from docker.tls import TLSConfig from docker.utils import kwargs_from_env from ..const import HTTP_TIMEOUT -from ..const import IS_WINDOWS_PLATFORM from .errors import UserError from .utils import generate_user_agent from .utils import unquote_path @@ -72,9 +71,4 @@ def docker_client(environment, version=None, tls_config=None, host=None, kwargs['user_agent'] = generate_user_agent() - if 'base_url' not in kwargs and IS_WINDOWS_PLATFORM: - # docker-py 1.10 defaults to using npipes, but we don't want that - # change in compose yet - use the default TCP connection instead. - kwargs['base_url'] = 'tcp://127.0.0.1:2375' - return Client(**kwargs) diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index 6cdb7da5..aaa935af 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -60,14 +60,6 @@ class DockerClientTestCase(unittest.TestCase): ) self.assertEqual(client.headers['User-Agent'], expected) - @mock.patch.dict(os.environ) - def test_docker_client_default_windows_host(self): - with mock.patch('compose.cli.docker_client.IS_WINDOWS_PLATFORM', True): - if 'DOCKER_HOST' in os.environ: - del os.environ['DOCKER_HOST'] - client = docker_client(os.environ) - assert client.base_url == 'http://127.0.0.1:2375' - class TLSConfigTestCase(unittest.TestCase): ca_cert = 'tests/fixtures/tls/ca.pem' From fe08be698d36d42a66839ce284989947220931cd Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 10 Mar 2016 17:33:01 -0500 Subject: [PATCH 102/118] Support inline default values. Signed-off-by: Daniel Nephin --- compose/config/config.py | 22 +++++--- compose/config/interpolation.py | 74 +++++++++++++++++++------ tests/unit/config/interpolation_test.py | 74 ++++++++++++++++++++----- tests/unit/interpolation_test.py | 36 ------------ 4 files changed, 130 insertions(+), 76 deletions(-) delete mode 100644 tests/unit/interpolation_test.py diff --git a/compose/config/config.py b/compose/config/config.py index aea1e094..4d32b50c 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -413,31 +413,35 @@ def load_services(config_details, config_file): return build_services(service_config) -def interpolate_config_section(filename, config, section, environment): - validate_config_section(filename, config, section) - return interpolate_environment_variables(config, section, environment) +def interpolate_config_section(config_file, config, section, environment): + validate_config_section(config_file.filename, config, section) + return interpolate_environment_variables( + config_file.version, + config, + section, + environment) def process_config_file(config_file, environment, service_name=None): services = interpolate_config_section( - config_file.filename, + config_file, config_file.get_service_dicts(), 'service', - environment,) + environment) if config_file.version in (V2_0, V2_1): processed_config = dict(config_file.config) processed_config['services'] = services processed_config['volumes'] = interpolate_config_section( - config_file.filename, + config_file, config_file.get_volumes(), 'volume', - environment,) + environment) processed_config['networks'] = interpolate_config_section( - config_file.filename, + config_file, config_file.get_networks(), 'network', - environment,) + environment) if config_file.version == V1: processed_config = services diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 63020d91..cb841437 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -7,14 +7,35 @@ from string import Template import six from .errors import ConfigurationError +from compose.const import COMPOSEFILE_V1 as V1 +from compose.const import COMPOSEFILE_V2_0 as V2_0 + + log = logging.getLogger(__name__) -def interpolate_environment_variables(config, section, environment): +class Interpolator(object): + + def __init__(self, templater, mapping): + self.templater = templater + self.mapping = mapping + + def interpolate(self, string): + try: + return self.templater(string).substitute(self.mapping) + except ValueError: + raise InvalidInterpolation(string) + + +def interpolate_environment_variables(version, config, section, environment): + if version in (V2_0, V1): + interpolator = Interpolator(Template, environment) + else: + interpolator = Interpolator(TemplateWithDefaults, environment) def process_item(name, config_dict): return dict( - (key, interpolate_value(name, key, val, section, environment)) + (key, interpolate_value(name, key, val, section, interpolator)) for key, val in (config_dict or {}).items() ) @@ -24,9 +45,9 @@ def interpolate_environment_variables(config, section, environment): ) -def interpolate_value(name, config_key, value, section, mapping): +def interpolate_value(name, config_key, value, section, interpolator): try: - return recursive_interpolate(value, mapping) + return recursive_interpolate(value, interpolator) except InvalidInterpolation as e: raise ConfigurationError( 'Invalid interpolation format for "{config_key}" option ' @@ -37,25 +58,44 @@ def interpolate_value(name, config_key, value, section, mapping): string=e.string)) -def recursive_interpolate(obj, mapping): +def recursive_interpolate(obj, interpolator): if isinstance(obj, six.string_types): - return interpolate(obj, mapping) - elif isinstance(obj, dict): + return interpolator.interpolate(obj) + if isinstance(obj, dict): return dict( - (key, recursive_interpolate(val, mapping)) + (key, recursive_interpolate(val, interpolator)) for (key, val) in obj.items() ) - elif isinstance(obj, list): - return [recursive_interpolate(val, mapping) for val in obj] - else: - return obj + if isinstance(obj, list): + return [recursive_interpolate(val, interpolator) for val in obj] + return obj -def interpolate(string, mapping): - try: - return Template(string).substitute(mapping) - except ValueError: - raise InvalidInterpolation(string) +class TemplateWithDefaults(Template): + idpattern = r'[_a-z][_a-z0-9]*(?::?-[_a-z0-9]+)?' + + # Modified from python2.7/string.py + def substitute(self, mapping): + # Helper function for .sub() + def convert(mo): + # Check the most common path first. + named = mo.group('named') or mo.group('braced') + if named is not None: + if ':-' in named: + var, _, default = named.partition(':-') + return mapping.get(var) or default + if '-' in named: + var, _, default = named.partition('-') + return mapping.get(var, default) + val = mapping[named] + return '%s' % (val,) + if mo.group('escaped') is not None: + return self.delimiter + if mo.group('invalid') is not None: + self._invalid(mo) + raise ValueError('Unrecognized named group in pattern', + self.pattern) + return self.pattern.sub(convert, self.template) class InvalidInterpolation(Exception): diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index 42b5db6e..22444495 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -1,21 +1,28 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os - -import mock import pytest from compose.config.environment import Environment from compose.config.interpolation import interpolate_environment_variables +from compose.config.interpolation import Interpolator +from compose.config.interpolation import InvalidInterpolation +from compose.config.interpolation import TemplateWithDefaults -@pytest.yield_fixture +@pytest.fixture def mock_env(): - with mock.patch.dict(os.environ): - os.environ['USER'] = 'jenny' - os.environ['FOO'] = 'bar' - yield + return Environment({'USER': 'jenny', 'FOO': 'bar'}) + + +@pytest.fixture +def variable_mapping(): + return Environment({'FOO': 'first', 'BAR': ''}) + + +@pytest.fixture +def defaults_interpolator(variable_mapping): + return Interpolator(TemplateWithDefaults, variable_mapping).interpolate def test_interpolate_environment_variables_in_services(mock_env): @@ -43,9 +50,8 @@ def test_interpolate_environment_variables_in_services(mock_env): } } } - assert interpolate_environment_variables( - services, 'service', Environment.from_env_file(None) - ) == expected + value = interpolate_environment_variables("2.0", services, 'service', mock_env) + assert value == expected def test_interpolate_environment_variables_in_volumes(mock_env): @@ -69,6 +75,46 @@ def test_interpolate_environment_variables_in_volumes(mock_env): }, 'other': {}, } - assert interpolate_environment_variables( - volumes, 'volume', Environment.from_env_file(None) - ) == expected + value = interpolate_environment_variables("2.0", volumes, 'volume', mock_env) + assert value == expected + + +def test_escaped_interpolation(defaults_interpolator): + assert defaults_interpolator('$${foo}') == '${foo}' + + +def test_invalid_interpolation(defaults_interpolator): + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${') + with pytest.raises(InvalidInterpolation): + defaults_interpolator('$}') + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${}') + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${ }') + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${ foo}') + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${foo }') + with pytest.raises(InvalidInterpolation): + defaults_interpolator('${foo!}') + + +def test_interpolate_missing_no_default(defaults_interpolator): + assert defaults_interpolator("This ${missing} var") == "This var" + assert defaults_interpolator("This ${BAR} var") == "This var" + + +def test_interpolate_with_value(defaults_interpolator): + assert defaults_interpolator("This $FOO var") == "This first var" + assert defaults_interpolator("This ${FOO} var") == "This first var" + + +def test_interpolate_missing_with_default(defaults_interpolator): + assert defaults_interpolator("ok ${missing:-def}") == "ok def" + assert defaults_interpolator("ok ${missing-def}") == "ok def" + + +def test_interpolate_with_empty_and_default_value(defaults_interpolator): + assert defaults_interpolator("ok ${BAR:-def}") == "ok def" + assert defaults_interpolator("ok ${BAR-def}") == "ok " diff --git a/tests/unit/interpolation_test.py b/tests/unit/interpolation_test.py deleted file mode 100644 index c3050c2c..00000000 --- a/tests/unit/interpolation_test.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import unittest - -from compose.config.environment import Environment as bddict -from compose.config.interpolation import interpolate -from compose.config.interpolation import InvalidInterpolation - - -class InterpolationTest(unittest.TestCase): - def test_valid_interpolations(self): - self.assertEqual(interpolate('$foo', bddict(foo='hi')), 'hi') - self.assertEqual(interpolate('${foo}', bddict(foo='hi')), 'hi') - - self.assertEqual(interpolate('${subject} love you', bddict(subject='i')), 'i love you') - self.assertEqual(interpolate('i ${verb} you', bddict(verb='love')), 'i love you') - self.assertEqual(interpolate('i love ${object}', bddict(object='you')), 'i love you') - - def test_empty_value(self): - self.assertEqual(interpolate('${foo}', bddict(foo='')), '') - - def test_unset_value(self): - self.assertEqual(interpolate('${foo}', bddict()), '') - - def test_escaped_interpolation(self): - self.assertEqual(interpolate('$${foo}', bddict(foo='hi')), '${foo}') - - def test_invalid_strings(self): - self.assertRaises(InvalidInterpolation, lambda: interpolate('${', bddict())) - self.assertRaises(InvalidInterpolation, lambda: interpolate('$}', bddict())) - self.assertRaises(InvalidInterpolation, lambda: interpolate('${}', bddict())) - self.assertRaises(InvalidInterpolation, lambda: interpolate('${ }', bddict())) - self.assertRaises(InvalidInterpolation, lambda: interpolate('${ foo}', bddict())) - self.assertRaises(InvalidInterpolation, lambda: interpolate('${foo }', bddict())) - self.assertRaises(InvalidInterpolation, lambda: interpolate('${foo!}', bddict())) From 1a4e81920b31a2241fb37c2737f183939ad32058 Mon Sep 17 00:00:00 2001 From: John Mulhausen Date: Tue, 4 Oct 2016 17:56:18 -0700 Subject: [PATCH 103/118] Remove old documentation source, add README on migration Signed-off-by: John Mulhausen --- docs/Dockerfile | 8 - docs/Makefile | 38 - docs/README.md | 90 +-- docs/bundles.md | 209 ----- docs/completion.md | 68 -- docs/compose-file.md | 1223 ----------------------------- docs/django.md | 194 ----- docs/env-file.md | 43 - docs/environment-variables.md | 107 --- docs/extends.md | 354 --------- docs/faq.md | 128 --- docs/gettingstarted.md | 191 ----- docs/images/django-it-worked.png | Bin 28446 -> 0 bytes docs/images/rails-welcome.png | Bin 71034 -> 0 bytes docs/images/wordpress-files.png | Bin 70823 -> 0 bytes docs/images/wordpress-lang.png | Bin 30149 -> 0 bytes docs/images/wordpress-welcome.png | Bin 62063 -> 0 bytes docs/index.md | 30 - docs/install.md | 136 ---- docs/link-env-deprecated.md | 48 -- docs/networking.md | 154 ---- docs/overview.md | 188 ----- docs/production.md | 88 --- docs/rails.md | 174 ---- docs/reference/build.md | 25 - docs/reference/bundle.md | 31 - docs/reference/config.md | 23 - docs/reference/create.md | 26 - docs/reference/down.md | 38 - docs/reference/envvars.md | 92 --- docs/reference/events.md | 34 - docs/reference/exec.md | 29 - docs/reference/help.md | 18 - docs/reference/index.md | 42 - docs/reference/kill.md | 24 - docs/reference/logs.md | 25 - docs/reference/overview.md | 127 --- docs/reference/pause.md | 18 - docs/reference/port.md | 23 - docs/reference/ps.md | 21 - docs/reference/pull.md | 21 - docs/reference/push.md | 21 - docs/reference/restart.md | 21 - docs/reference/rm.md | 28 - docs/reference/run.md | 56 -- docs/reference/scale.md | 21 - docs/reference/start.md | 18 - docs/reference/stop.md | 22 - docs/reference/unpause.md | 18 - docs/reference/up.md | 55 -- docs/startup-order.md | 88 --- docs/swarm.md | 185 ----- docs/wordpress.md | 112 --- 53 files changed, 7 insertions(+), 4726 deletions(-) delete mode 100644 docs/Dockerfile delete mode 100644 docs/Makefile delete mode 100644 docs/bundles.md delete mode 100644 docs/completion.md delete mode 100644 docs/compose-file.md delete mode 100644 docs/django.md delete mode 100644 docs/env-file.md delete mode 100644 docs/environment-variables.md delete mode 100644 docs/extends.md delete mode 100644 docs/faq.md delete mode 100644 docs/gettingstarted.md delete mode 100644 docs/images/django-it-worked.png delete mode 100644 docs/images/rails-welcome.png delete mode 100644 docs/images/wordpress-files.png delete mode 100644 docs/images/wordpress-lang.png delete mode 100644 docs/images/wordpress-welcome.png delete mode 100644 docs/index.md delete mode 100644 docs/install.md delete mode 100644 docs/link-env-deprecated.md delete mode 100644 docs/networking.md delete mode 100644 docs/overview.md delete mode 100644 docs/production.md delete mode 100644 docs/rails.md delete mode 100644 docs/reference/build.md delete mode 100644 docs/reference/bundle.md delete mode 100644 docs/reference/config.md delete mode 100644 docs/reference/create.md delete mode 100644 docs/reference/down.md delete mode 100644 docs/reference/envvars.md delete mode 100644 docs/reference/events.md delete mode 100644 docs/reference/exec.md delete mode 100644 docs/reference/help.md delete mode 100644 docs/reference/index.md delete mode 100644 docs/reference/kill.md delete mode 100644 docs/reference/logs.md delete mode 100644 docs/reference/overview.md delete mode 100644 docs/reference/pause.md delete mode 100644 docs/reference/port.md delete mode 100644 docs/reference/ps.md delete mode 100644 docs/reference/pull.md delete mode 100644 docs/reference/push.md delete mode 100644 docs/reference/restart.md delete mode 100644 docs/reference/rm.md delete mode 100644 docs/reference/run.md delete mode 100644 docs/reference/scale.md delete mode 100644 docs/reference/start.md delete mode 100644 docs/reference/stop.md delete mode 100644 docs/reference/unpause.md delete mode 100644 docs/reference/up.md delete mode 100644 docs/startup-order.md delete mode 100644 docs/swarm.md delete mode 100644 docs/wordpress.md diff --git a/docs/Dockerfile b/docs/Dockerfile deleted file mode 100644 index 7b5a3b24..00000000 --- a/docs/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -FROM docs/base:oss -MAINTAINER Docker Docs - -ENV PROJECT=compose -# To get the git info for this repo -COPY . /src -RUN rm -rf /docs/content/$PROJECT/ -COPY . /docs/content/$PROJECT/ diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index e6629289..00000000 --- a/docs/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -.PHONY: all default docs docs-build docs-shell shell test - -# to allow `make DOCSDIR=1 docs-shell` (to create a bind mount in docs) -DOCS_MOUNT := $(if $(DOCSDIR),-v $(CURDIR):/docs/content/compose) - -# to allow `make DOCSPORT=9000 docs` -DOCSPORT := 8000 - -# Get the IP ADDRESS -DOCKER_IP=$(shell python -c "import urlparse ; print urlparse.urlparse('$(DOCKER_HOST)').hostname or ''") -HUGO_BASE_URL=$(shell test -z "$(DOCKER_IP)" && echo localhost || echo "$(DOCKER_IP)") -HUGO_BIND_IP=0.0.0.0 - -GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) -GIT_BRANCH_CLEAN := $(shell echo $(GIT_BRANCH) | sed -e "s/[^[:alnum:]]/-/g") -DOCKER_DOCS_IMAGE := docker-docs$(if $(GIT_BRANCH_CLEAN),:$(GIT_BRANCH_CLEAN)) - -DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e AWS_S3_BUCKET -e NOCACHE - -# for some docs workarounds (see below in "docs-build" target) -GITCOMMIT := $(shell git rev-parse --short HEAD 2>/dev/null) - -default: docs - -docs: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) --watch - -docs-draft: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" hugo server --buildDrafts="true" --port=$(DOCSPORT) --baseUrl=$(HUGO_BASE_URL) --bind=$(HUGO_BIND_IP) - -docs-shell: docs-build - $(DOCKER_RUN_DOCS) -p $(if $(DOCSPORT),$(DOCSPORT):)8000 "$(DOCKER_DOCS_IMAGE)" bash - -test: docs-build - $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" - -docs-build: - docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/README.md b/docs/README.md index e60fa48c..03d2e3a7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,86 +1,10 @@ - +# The docs have been moved! -# Contributing to the Docker Compose documentation +The documentation for Compose has been merged into +[the general documentation repo](https://github.com/docker/docker.github.io). -The documentation in this directory is part of the [https://docs.docker.com](https://docs.docker.com) website. Docker uses [the Hugo static generator](http://gohugo.io/overview/introduction/) to convert project Markdown files to a static HTML site. +The docs for Compose are now here: +https://github.com/docker/docker.github.io/tree/master/compose -You don't need to be a Hugo expert to contribute to the compose documentation. If you are familiar with Markdown, you can modify the content in the `docs` files. - -If you want to add a new file or change the location of the document in the menu, you do need to know a little more. - -## Documentation contributing workflow - -1. Edit a Markdown file in the tree. - -2. Save your changes. - -3. Make sure you are in the `docs` subdirectory. - -4. Build the documentation. - - $ make docs - ---> ffcf3f6c4e97 - Removing intermediate container a676414185e8 - Successfully built ffcf3f6c4e97 - docker run --rm -it -e AWS_S3_BUCKET -e NOCACHE -p 8000:8000 -e DOCKERHOST "docs-base:test-tooling" hugo server --port=8000 --baseUrl=192.168.59.103 --bind=0.0.0.0 - ERROR: 2015/06/13 MenuEntry's .Url is deprecated and will be removed in Hugo 0.15. Use .URL instead. - 0 of 4 drafts rendered - 0 future content - 12 pages created - 0 paginator pages created - 0 tags created - 0 categories created - in 55 ms - Serving pages from /docs/public - Web Server is available at http://0.0.0.0:8000/ - Press Ctrl+C to stop - -5. Open the available server in your browser. - - The documentation server has the complete menu but only the Docker Compose - documentation resolves. You can't access the other project docs from this - localized build. - -## Tips on Hugo metadata and menu positioning - -The top of each Docker Compose documentation file contains TOML metadata. The metadata is commented out to prevent it from appearing in GitHub. - - - -The metadata alone has this structure: - - +++ - title = "Extending services in Compose" - description = "How to use Docker Compose's extends keyword to share configuration between files and projects" - keywords = ["fig, composition, compose, docker, orchestration, documentation, docs"] - [menu.main] - parent="workw_compose" - weight=2 - +++ - -The `[menu.main]` section refers to navigation defined [in the main Docker menu](https://github.com/docker/docs-base/blob/hugo/config.toml). This metadata says *add a menu item called* Extending services in Compose *to the menu with the* `smn_workdw_compose` *identifier*. If you locate the menu in the configuration, you'll find *Create multi-container applications* is the menu title. - -You can move an article in the tree by specifying a new parent. You can shift the location of the item by changing its weight. Higher numbers are heavier and shift the item to the bottom of menu. Low or no numbers shift it up. - - -## Other key documentation repositories - -The `docker/docs-base` repository contains [the Hugo theme and menu configuration](https://github.com/docker/docs-base). If you open the `Dockerfile` you'll see the `make docs` relies on this as a base image for building the Compose documentation. - -The `docker/docs.docker.com` repository contains [build system for building the Docker documentation site](https://github.com/docker/docs.docker.com). Fork this repository to build the entire documentation site. +As always, the docs remain open-source and we appreciate your feedback and +pull requests! diff --git a/docs/bundles.md b/docs/bundles.md deleted file mode 100644 index 096c9ec3..00000000 --- a/docs/bundles.md +++ /dev/null @@ -1,209 +0,0 @@ - - - -# Docker Stacks and Distributed Application Bundles (experimental) - -> **Note**: This is a copy of the [Docker Stacks and Distributed Application -> Bundles](https://github.com/docker/docker/blob/v1.12.0-rc4/experimental/docker-stacks-and-bundles.md) -> document in the [docker/docker repo](https://github.com/docker/docker). - -## Overview - -Docker Stacks and Distributed Application Bundles are experimental features -introduced in Docker 1.12 and Docker Compose 1.8, alongside the concept of -swarm mode, and Nodes and Services in the Engine API. - -A Dockerfile can be built into an image, and containers can be created from -that image. Similarly, a docker-compose.yml can be built into a **distributed -application bundle**, and **stacks** can be created from that bundle. In that -sense, the bundle is a multi-services distributable image format. - -As of Docker 1.12 and Compose 1.8, the features are experimental. Neither -Docker Engine nor the Docker Registry support distribution of bundles. - -## Producing a bundle - -The easiest way to produce a bundle is to generate it using `docker-compose` -from an existing `docker-compose.yml`. Of course, that's just *one* possible way -to proceed, in the same way that `docker build` isn't the only way to produce a -Docker image. - -From `docker-compose`: - -```bash -$ docker-compose bundle -WARNING: Unsupported key 'network_mode' in services.nsqd - ignoring -WARNING: Unsupported key 'links' in services.nsqd - ignoring -WARNING: Unsupported key 'volumes' in services.nsqd - ignoring -[...] -Wrote bundle to vossibility-stack.dab -``` - -## Creating a stack from a bundle - -> **Note**: Because support for stacks and bundles is in the experimental stage, -> you need to install an experimental build of Docker Engine to use it. -> -> If you're on Mac or Windows, download the “Beta channel” version of -> [Docker for Mac](https://docs.docker.com/docker-for-mac/) or -> [Docker for Windows](https://docs.docker.com/docker-for-windows/) to install -> it. If you're on Linux, follow the instructions in the -> [experimental build README](https://github.com/docker/docker/blob/master/experimental/README.md). - -A stack is created using the `docker deploy` command: - -```bash -# docker deploy --help - -Usage: docker deploy [OPTIONS] STACK - -Create and update a stack - -Options: - --file string Path to a Distributed Application Bundle file (Default: STACK.dab) - --help Print usage - --with-registry-auth Send registry authentication details to Swarm agents -``` - -Let's deploy the stack created before: - -```bash -# docker deploy vossibility-stack -Loading bundle from vossibility-stack.dab -Creating service vossibility-stack_elasticsearch -Creating service vossibility-stack_kibana -Creating service vossibility-stack_logstash -Creating service vossibility-stack_lookupd -Creating service vossibility-stack_nsqd -Creating service vossibility-stack_vossibility-collector -``` - -We can verify that services were correctly created: - -```bash -# docker service ls -ID NAME REPLICAS IMAGE -COMMAND -29bv0vnlm903 vossibility-stack_lookupd 1 nsqio/nsq@sha256:eeba05599f31eba418e96e71e0984c3dc96963ceb66924dd37a47bf7ce18a662 /nsqlookupd -4awt47624qwh vossibility-stack_nsqd 1 nsqio/nsq@sha256:eeba05599f31eba418e96e71e0984c3dc96963ceb66924dd37a47bf7ce18a662 /nsqd --data-path=/data --lookupd-tcp-address=lookupd:4160 -4tjx9biia6fs vossibility-stack_elasticsearch 1 elasticsearch@sha256:12ac7c6af55d001f71800b83ba91a04f716e58d82e748fa6e5a7359eed2301aa -7563uuzr9eys vossibility-stack_kibana 1 kibana@sha256:6995a2d25709a62694a937b8a529ff36da92ebee74bafd7bf00e6caf6db2eb03 -9gc5m4met4he vossibility-stack_logstash 1 logstash@sha256:2dc8bddd1bb4a5a34e8ebaf73749f6413c101b2edef6617f2f7713926d2141fe logstash -f /etc/logstash/conf.d/logstash.conf -axqh55ipl40h vossibility-stack_vossibility-collector 1 icecrime/vossibility-collector@sha256:f03f2977203ba6253988c18d04061c5ec7aab46bca9dfd89a9a1fa4500989fba --config /config/config.toml --debug -``` - -## Managing stacks - -Stacks are managed using the `docker stack` command: - -```bash -# docker stack --help - -Usage: docker stack COMMAND - -Manage Docker stacks - -Options: - --help Print usage - -Commands: - config Print the stack configuration - deploy Create and update a stack - rm Remove the stack - services List the services in the stack - tasks List the tasks in the stack - -Run 'docker stack COMMAND --help' for more information on a command. -``` - -## Bundle file format - -Distributed application bundles are described in a JSON format. When bundles -are persisted as files, the file extension is `.dab`. - -A bundle has two top-level fields: `version` and `services`. The version used -by Docker 1.12 tools is `0.1`. - -`services` in the bundle are the services that comprise the app. They -correspond to the new `Service` object introduced in the 1.12 Docker Engine API. - -A service has the following fields: - -
-
- Image (required) string -
-
- The image that the service will run. Docker images should be referenced - with full content hash to fully specify the deployment artifact for the - service. Example: - postgres@sha256:e0a230a9f5b4e1b8b03bb3e8cf7322b0e42b7838c5c87f4545edb48f5eb8f077 -
-
- Command []string -
-
- Command to run in service containers. -
-
- Args []string -
-
- Arguments passed to the service containers. -
-
- Env []string -
-
- Environment variables. -
-
- Labels map[string]string -
-
- Labels used for setting meta data on services. -
-
- Ports []Port -
-
- Service ports (composed of Port (int) and - Protocol (string). A service description can - only specify the container port to be exposed. These ports can be - mapped on runtime hosts at the operator's discretion. -
- -
- WorkingDir string -
-
- Working directory inside the service containers. -
- -
- User string -
-
- Username or UID (format: <name|uid>[:<group|gid>]). -
- -
- Networks []string -
-
- Networks that the service containers should be connected to. An entity - deploying a bundle should create networks as needed. -
-
- -> **Note:** Some configuration options are not yet supported in the DAB format, -> including volume mounts. diff --git a/docs/completion.md b/docs/completion.md deleted file mode 100644 index 2076d512..00000000 --- a/docs/completion.md +++ /dev/null @@ -1,68 +0,0 @@ - - -# Command-line Completion - -Compose comes with [command completion](http://en.wikipedia.org/wiki/Command-line_completion) -for the bash and zsh shell. - -## Installing Command Completion - -### Bash - -Make sure bash completion is installed. If you use a current Linux in a non-minimal installation, bash completion should be available. -On a Mac, install with `brew install bash-completion` - -Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g. - - curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose - -Completion will be available upon next login. - -### Zsh - -Place the completion script in your `/path/to/zsh/completion`, using e.g. `~/.zsh/completion/` - - mkdir -p ~/.zsh/completion - curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/zsh/_docker-compose > ~/.zsh/completion/_docker-compose - -Include the directory in your `$fpath`, e.g. by adding in `~/.zshrc` - - fpath=(~/.zsh/completion $fpath) - -Make sure `compinit` is loaded or do it by adding in `~/.zshrc` - - autoload -Uz compinit && compinit -i - -Then reload your shell - - exec $SHELL -l - -## Available completions - -Depending on what you typed on the command line so far, it will complete - - - available docker-compose commands - - options that are available for a particular command - - service names that make sense in a given context (e.g. services with running or stopped instances or services based on images vs. services based on Dockerfiles). For `docker-compose scale`, completed service names will automatically have "=" appended. - - arguments for selected options, e.g. `docker-compose kill -s` will complete some signals like SIGHUP and SIGUSR1. - -Enjoy working with Compose faster and with less typos! - -## Compose documentation - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/compose-file.md b/docs/compose-file.md deleted file mode 100644 index cfc242ce..00000000 --- a/docs/compose-file.md +++ /dev/null @@ -1,1223 +0,0 @@ - - - -# Compose file reference - -The Compose file is a [YAML](http://yaml.org/) file defining -[services](#service-configuration-reference), -[networks](#network-configuration-reference) and -[volumes](#volume-configuration-reference). -The default path for a Compose file is `./docker-compose.yml`. - -A service definition contains configuration which will be applied to each -container started for that service, much like passing command-line parameters to -`docker run`. Likewise, network and volume definitions are analogous to -`docker network create` and `docker volume create`. - -As with `docker run`, options specified in the Dockerfile (e.g., `CMD`, -`EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to -specify them again in `docker-compose.yml`. - -You can use environment variables in configuration values with a Bash-like -`${VARIABLE}` syntax - see [variable substitution](#variable-substitution) for -full details. - - -## Service configuration reference - -> **Note:** There are two versions of the Compose file format – version 1 (the -> legacy format, which does not support volumes or networks) and version 2 (the -> most up-to-date). For more information, see the [Versioning](#versioning) -> section. - -This section contains a list of all configuration options supported by a service -definition. - -### build - -Configuration options that are applied at build time. - -`build` can be specified either as a string containing a path to the build -context, or an object with the path specified under [context](#context) and -optionally [dockerfile](#dockerfile) and [args](#args). - - build: ./dir - - build: - context: ./dir - dockerfile: Dockerfile-alternate - args: - buildno: 1 - -If you specify `image` as well as `build`, then Compose names the built image -with the `webapp` and optional `tag` specified in `image`: - - build: ./dir - image: webapp:tag - -This will result in an image named `webapp` and tagged `tag`, built from `./dir`. - -> **Note**: In the [version 1 file format](#version-1), `build` is different in -> two ways: -> -> - Only the string form (`build: .`) is allowed - not the object form. -> - Using `build` together with `image` is not allowed. Attempting to do so -> results in an error. - -#### context - -> [Version 2 file format](#version-2) only. In version 1, just use -> [build](#build). - -Either a path to a directory containing a Dockerfile, or a url to a git repository. - -When the value supplied is a relative path, it is interpreted as relative to the -location of the Compose file. This directory is also the build context that is -sent to the Docker daemon. - -Compose will build and tag it with a generated name, and use that image thereafter. - - build: - context: ./dir - -#### dockerfile - -Alternate Dockerfile. - -Compose will use an alternate file to build with. A build path must also be -specified. - - build: - context: . - dockerfile: Dockerfile-alternate - -> **Note**: In the [version 1 file format](#version-1), `dockerfile` is -> different in two ways: - - * It appears alongside `build`, not as a sub-option: - - build: . - dockerfile: Dockerfile-alternate - - * Using `dockerfile` together with `image` is not allowed. Attempting to do so results in an error. - -#### args - -> [Version 2 file format](#version-2) only. - -Add build arguments, which are environment variables accessible only during the -build process. - -First, specify the arguments in your Dockerfile: - - ARG buildno - ARG password - - RUN echo "Build number: $buildno" - RUN script-requiring-password.sh "$password" - -Then specify the arguments under the `build` key. You can pass either a mapping -or a list: - - build: - context: . - args: - buildno: 1 - password: secret - - build: - context: . - args: - - buildno=1 - - password=secret - -You can omit the value when specifying a build argument, in which case its value -at build time is the value in the environment where Compose is running. - - args: - - buildno - - password - -> **Note**: YAML boolean values (`true`, `false`, `yes`, `no`, `on`, `off`) must -> be enclosed in quotes, so that the parser interprets them as strings. - -### cap_add, cap_drop - -Add or drop container capabilities. -See `man 7 capabilities` for a full list. - - cap_add: - - ALL - - cap_drop: - - NET_ADMIN - - SYS_ADMIN - -### command - -Override the default command. - - command: bundle exec thin -p 3000 - -The command can also be a list, in a manner similar to [dockerfile](https://docs.docker.com/engine/reference/builder/#cmd): - - command: [bundle, exec, thin, -p, 3000] - -### cgroup_parent - -Specify an optional parent cgroup for the container. - - cgroup_parent: m-executor-abcd - -### container_name - -Specify a custom container name, rather than a generated default name. - - container_name: my-web-container - -Because Docker container names must be unique, you cannot scale a service -beyond 1 container if you have specified a custom name. Attempting to do so -results in an error. - -### devices - -List of device mappings. Uses the same format as the `--device` docker -client create option. - - devices: - - "/dev/ttyUSB0:/dev/ttyUSB0" - -### depends_on - -Express dependency between services, which has two effects: - -- `docker-compose up` will start services in dependency order. In the following - example, `db` and `redis` will be started before `web`. - -- `docker-compose up SERVICE` will automatically include `SERVICE`'s - dependencies. In the following example, `docker-compose up web` will also - create and start `db` and `redis`. - -Simple example: - - version: '2' - services: - web: - build: . - depends_on: - - db - - redis - redis: - image: redis - db: - image: postgres - -> **Note:** `depends_on` will not wait for `db` and `redis` to be "ready" before -> starting `web` - only until they have been started. If you need to wait -> for a service to be ready, see [Controlling startup order](startup-order.md) -> for more on this problem and strategies for solving it. - -### dns - -Custom DNS servers. Can be a single value or a list. - - dns: 8.8.8.8 - dns: - - 8.8.8.8 - - 9.9.9.9 - -### dns_search - -Custom DNS search domains. Can be a single value or a list. - - dns_search: example.com - dns_search: - - dc1.example.com - - dc2.example.com - -### tmpfs - -> [Version 2 file format](#version-2) only. - -Mount a temporary file system inside the container. Can be a single value or a list. - - tmpfs: /run - tmpfs: - - /run - - /tmp - -### entrypoint - -Override the default entrypoint. - - entrypoint: /code/entrypoint.sh - -The entrypoint can also be a list, in a manner similar to [dockerfile](https://docs.docker.com/engine/reference/builder/#entrypoint): - - entrypoint: - - php - - -d - - zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20100525/xdebug.so - - -d - - memory_limit=-1 - - vendor/bin/phpunit - - -### env_file - -Add environment variables from a file. Can be a single value or a list. - -If you have specified a Compose file with `docker-compose -f FILE`, paths in -`env_file` are relative to the directory that file is in. - -Environment variables specified in `environment` override these values. - - env_file: .env - - env_file: - - ./common.env - - ./apps/web.env - - /opt/secrets.env - -Compose expects each line in an env file to be in `VAR=VAL` format. Lines -beginning with `#` (i.e. comments) are ignored, as are blank lines. - - # Set Rails/Rack environment - RACK_ENV=development - -> **Note:** If your service specifies a [build](#build) option, variables -> defined in environment files will _not_ be automatically visible during the -> build. Use the [args](#args) sub-option of `build` to define build-time -> environment variables. - -### environment - -Add environment variables. You can use either an array or a dictionary. Any -boolean values; true, false, yes no, need to be enclosed in quotes to ensure -they are not converted to True or False by the YML parser. - -Environment variables with only a key are resolved to their values on the -machine Compose is running on, which can be helpful for secret or host-specific values. - - environment: - RACK_ENV: development - SHOW: 'true' - SESSION_SECRET: - - environment: - - RACK_ENV=development - - SHOW=true - - SESSION_SECRET - -> **Note:** If your service specifies a [build](#build) option, variables -> defined in `environment` will _not_ be automatically visible during the -> build. Use the [args](#args) sub-option of `build` to define build-time -> environment variables. - -### expose - -Expose ports without publishing them to the host machine - they'll only be -accessible to linked services. Only the internal port can be specified. - - expose: - - "3000" - - "8000" - -### extends - -Extend another service, in the current file or another, optionally overriding -configuration. - -You can use `extends` on any service together with other configuration keys. -The `extends` value must be a dictionary defined with a required `service` -and an optional `file` key. - - extends: - file: common.yml - service: webapp - -The `service` the name of the service being extended, for example -`web` or `database`. The `file` is the location of a Compose configuration -file defining that service. - -If you omit the `file` Compose looks for the service configuration in the -current file. The `file` value can be an absolute or relative path. If you -specify a relative path, Compose treats it as relative to the location of the -current file. - -You can extend a service that itself extends another. You can extend -indefinitely. Compose does not support circular references and `docker-compose` -returns an error if it encounters one. - -For more on `extends`, see the -[the extends documentation](extends.md#extending-services). - -### external_links - -Link to containers started outside this `docker-compose.yml` or even outside -of Compose, especially for containers that provide shared or common services. -`external_links` follow semantics similar to `links` when specifying both the -container name and the link alias (`CONTAINER:ALIAS`). - - external_links: - - redis_1 - - project_db_1:mysql - - project_db_1:postgresql - -> **Note:** If you're using the [version 2 file format](#version-2), the -> externally-created containers must be connected to at least one of the same -> networks as the service which is linking to them. - -### extra_hosts - -Add hostname mappings. Use the same values as the docker client `--add-host` parameter. - - extra_hosts: - - "somehost:162.242.195.82" - - "otherhost:50.31.209.229" - -An entry with the ip address and hostname will be created in `/etc/hosts` inside containers for this service, e.g: - - 162.242.195.82 somehost - 50.31.209.229 otherhost - -### image - -Specify the image to start the container from. Can either be a repository/tag or -a partial image ID. - - image: redis - image: ubuntu:14.04 - image: tutum/influxdb - image: example-registry.com:4000/postgresql - image: a4bc65fd - -If the image does not exist, Compose attempts to pull it, unless you have also -specified [build](#build), in which case it builds it using the specified -options and tags it with the specified tag. - -> **Note**: In the [version 1 file format](#version-1), using `build` together -> with `image` is not allowed. Attempting to do so results in an error. - -### labels - -Add metadata to containers using [Docker labels](https://docs.docker.com/engine/userguide/labels-custom-metadata/). You can use either an array or a dictionary. - -It's recommended that you use reverse-DNS notation to prevent your labels from conflicting with those used by other software. - - labels: - com.example.description: "Accounting webapp" - com.example.department: "Finance" - com.example.label-with-empty-value: "" - - labels: - - "com.example.description=Accounting webapp" - - "com.example.department=Finance" - - "com.example.label-with-empty-value" - -### links - -Link to containers in another service. Either specify both the service name and -a link alias (`SERVICE:ALIAS`), or just the service name. - - web: - links: - - db - - db:database - - redis - -Containers for the linked service will be reachable at a hostname identical to -the alias, or the service name if no alias was specified. - -Links also express dependency between services in the same way as -[depends_on](#depends-on), so they determine the order of service startup. - -> **Note:** If you define both links and [networks](#networks), services with -> links between them must share at least one network in common in order to -> communicate. - -### logging - -> [Version 2 file format](#version-2) only. In version 1, use -> [log_driver](#log_driver) and [log_opt](#log_opt). - -Logging configuration for the service. - - logging: - driver: syslog - options: - syslog-address: "tcp://192.168.0.42:123" - -The `driver` name specifies a logging driver for the service's -containers, as with the ``--log-driver`` option for docker run -([documented here](https://docs.docker.com/engine/reference/logging/overview/)). - -The default value is json-file. - - driver: "json-file" - driver: "syslog" - driver: "none" - -> **Note:** Only the `json-file` driver makes the logs available directly from -> `docker-compose up` and `docker-compose logs`. Using any other driver will not -> print any logs. - -Specify logging options for the logging driver with the ``options`` key, as with the ``--log-opt`` option for `docker run`. - -Logging options are key-value pairs. An example of `syslog` options: - - driver: "syslog" - options: - syslog-address: "tcp://192.168.0.42:123" - -### log_driver - -> [Version 1 file format](#version-1) only. In version 2, use -> [logging](#logging). - -Specify a log driver. The default is `json-file`. - - log_driver: syslog - -### log_opt - -> [Version 1 file format](#version-1) only. In version 2, use -> [logging](#logging). - -Specify logging options as key-value pairs. An example of `syslog` options: - - log_opt: - syslog-address: "tcp://192.168.0.42:123" - -### net - -> [Version 1 file format](#version-1) only. In version 2, use -> [network_mode](#network_mode). - -Network mode. Use the same values as the docker client `--net` parameter. -The `container:...` form can take a service name instead of a container name or -id. - - net: "bridge" - net: "host" - net: "none" - net: "container:[service name or container name/id]" - -### network_mode - -> [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]`. - - network_mode: "bridge" - network_mode: "host" - network_mode: "none" - network_mode: "service:[service name]" - network_mode: "container:[container name/id]" - -### networks - -> [Version 2 file format](#version-2) only. In version 1, use [net](#net). - -Networks to join, referencing entries under the -[top-level `networks` key](#network-configuration-reference). - - services: - some-service: - networks: - - some-network - - other-network - -#### 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. - -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. - - services: - some-service: - networks: - some-network: - aliases: - - alias1 - - alias3 - other-network: - 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: - -#### ipv4_address, ipv6_address - -Specify a static IP address for containers for this service when joining the network. - -The corresponding network configuration in the [top-level networks section](#network-configuration-reference) must have an `ipam` block with subnet and gateway configurations covering each static address. If IPv6 addressing is desired, the `com.docker.network.enable_ipv6` driver option must be set to `true`. - -An example: - - version: '2' - - services: - app: - image: busybox - command: ifconfig - networks: - app_net: - ipv4_address: 172.16.238.10 - ipv6_address: 2001:3984:3989::10 - - networks: - app_net: - driver: bridge - driver_opts: - com.docker.network.enable_ipv6: "true" - ipam: - driver: default - config: - - subnet: 172.16.238.0/24 - gateway: 172.16.238.1 - - subnet: 2001:3984:3989::/64 - gateway: 2001:3984:3989::1 - -#### link_local_ips - -> [Added in version 2.1 file format](#version-21). - -Specify a list of link-local IPs. Link-local IPs are special IPs which belong -to a well known subnet and are purely managed by the operator, usually -dependent on the architecture where they are deployed. Therefore they are not -managed by docker (IPAM driver). - -Example usage: - - version: '2.1' - services: - app: - image: busybox - command: top - networks: - app_net: - link_local_ips: - - 57.123.22.11 - - 57.123.22.13 - networks: - app_net: - driver: bridge - -### pid - - pid: "host" - -Sets the PID mode to the host PID mode. This turns on sharing between -container and the host operating system the PID address space. Containers -launched with this flag will be able to access and manipulate other -containers in the bare-metal machine's namespace and vise-versa. - -### ports - -Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container -port (a random host port will be chosen). - -> **Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience -> erroneous results when using a container port lower than 60, because YAML will -> parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, -> we recommend always explicitly specifying your port mappings as strings. - - ports: - - "3000" - - "3000-3005" - - "8000:8000" - - "9090-9091:8080-8081" - - "49100:22" - - "127.0.0.1:8001:8001" - - "127.0.0.1:5000-5010:5000-5010" - -### security_opt - -Override the default labeling scheme for each container. - - security_opt: - - label:user:USER - - label:role:ROLE - -### stop_signal - -Sets an alternative signal to stop the container. By default `stop` uses -SIGTERM. Setting an alternative signal using `stop_signal` will cause -`stop` to send that signal instead. - - stop_signal: SIGUSR1 - -### ulimits - -Override the default ulimits for a container. You can either specify a single -limit as an integer or soft/hard limits as a mapping. - - - ulimits: - nproc: 65535 - nofile: - soft: 20000 - hard: 40000 - -### 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`). -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 -should always begin with `.` or `..`. - - volumes: - # Just specify a path and let the Engine create a volume - - /var/lib/mysql - - # Specify an absolute path mapping - - /opt/data:/var/lib/mysql - - # Path on the host, relative to the Compose file - - ./cache:/tmp/cache - - # User-relative path - - ~/configs:/etc/configs/:ro - - # Named volume - - datavolume:/var/lib/mysql - -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`. - -See [Docker Volumes](https://docs.docker.com/engine/userguide/dockervolumes/) and -[Volume Plugins](https://docs.docker.com/engine/extend/plugins_volume/) for more -information. - -### volumes_from - -Mount all of the volumes from another service or container, optionally -specifying read-only access (``ro``) or read-write (``rw``). If no access level is specified, -then read-write will be used. - - volumes_from: - - service_name - - service_name:ro - - container:container_name - - container:container_name:rw - -> **Note:** The `container:...` formats are only supported in the -> [version 2 file format](#version-2). In [version 1](#version-1), you can use -> container names without marking them as such: -> -> - service_name -> - service_name:ro -> - container_name -> - container_name:rw - -### cpu\_shares, cpu\_quota, cpuset, domainname, hostname, ipc, mac\_address, mem\_limit, memswap\_limit, mem\_swappiness, oom\_score\_adj, privileged, read\_only, restart, shm\_size, stdin\_open, tty, user, working\_dir - -Each of these is a single value, analogous to its -[docker run](https://docs.docker.com/engine/reference/run/) counterpart. - - cpu_shares: 73 - cpu_quota: 50000 - cpuset: 0,1 - - user: postgresql - working_dir: /code - - domainname: foo.com - hostname: foo - ipc: host - mac_address: 02:42:ac:11:65:43 - - mem_limit: 1000000000 - memswap_limit: 2000000000 - mem_swappiness: 10 - privileged: true - - restart: always - - read_only: true - shm_size: 64M - stdin_open: true - tty: true - - -## Volume configuration reference - -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](https://docs.docker.com/engine/reference/commandline/volume_create/) -subcommand documentation for more information. - -### driver - -Specify which volume driver should be used for this volume. Defaults to -`local`. The Docker Engine will return an error if the driver is not available. - - driver: foobar - -### driver_opts - -Specify a list of options as key-value pairs to pass to the driver for this -volume. Those options are driver-dependent - consult the driver's -documentation for more information. Optional. - - driver_opts: - foo: "bar" - baz: 1 - -### external - -If set to `true`, specifies that this volume has been created outside of -Compose. `docker-compose up` will not attempt to create it, and will raise -an error if it doesn't exist. - -`external` cannot be used in conjunction with other volume configuration keys -(`driver`, `driver_opts`). - -In the example below, instead of attemping to create a volume called -`[projectname]_data`, Compose will look for an existing volume simply -called `data` and mount it into the `db` service's containers. - - version: '2' - - services: - db: - image: postgres - volumes: - - data:/var/lib/postgresql/data - - volumes: - data: - external: true - -You can also specify the name of the volume separately from the name used to -refer to it within the Compose file: - - volumes: - data: - external: - name: actual-name-of-volume - - -## Network configuration reference - -The top-level `networks` key lets you specify networks to be created. For a full -explanation of Compose's use of Docker networking features, see the -[Networking guide](networking.md). - -### driver - -Specify which driver should be used for this network. - -The default driver depends on how the Docker Engine you're using is configured, -but in most instances it will be `bridge` on a single host and `overlay` on a -Swarm. - -The Docker Engine will return an error if the driver is not available. - - driver: overlay - -### driver_opts - -Specify a list of options as key-value pairs to pass to the driver for this -network. Those options are driver-dependent - consult the driver's -documentation for more information. Optional. - - driver_opts: - foo: "bar" - baz: 1 - -### ipam - -Specify custom IPAM config. This is an object with several properties, each of -which is optional: - -- `driver`: Custom IPAM driver, instead of the default. -- `config`: A list with zero or more config blocks, each containing any of - the following keys: - - `subnet`: Subnet in CIDR format that represents a network segment - - `ip_range`: Range of IPs from which to allocate container IPs - - `gateway`: IPv4 or IPv6 gateway for the master subnet - - `aux_addresses`: Auxiliary IPv4 or IPv6 addresses used by Network driver, - as a mapping from hostname to IP - -A full example: - - ipam: - driver: default - config: - - subnet: 172.28.0.0/16 - ip_range: 172.28.5.0/24 - gateway: 172.28.5.254 - aux_addresses: - host1: 172.28.1.5 - host2: 172.28.1.6 - host3: 172.28.1.7 - -### group_add - -Specify additional groups (by name or number) which the user inside the container will be a member of. Groups must exist in both the container and the host system to be added. An example of where this is useful is when multiple containers (running as different users) need to all read or write the same file on the host system. That file can be owned by a group shared by all the containers, and specified in `group_add`. See the [Docker documentation](https://docs.docker.com/engine/reference/run/#/additional-groups) for more details. - -A full example: - - version: '2' - services: - image: alpine - group_add: - - mail - -Running `id` inside the created container will show that the user belongs to the `mail` group, which would not have been the case if `group_add` were not used. - -### internal - -By default, Docker also connects a bridge network to it to provide external connectivity. If you want to create an externally isolated overlay network, you can set this option to `true`. - -### external - -If set to `true`, specifies that this network has been created outside of -Compose. `docker-compose up` will not attempt to create it, and will raise -an error if it doesn't exist. - -`external` cannot be used in conjunction with other network configuration keys -(`driver`, `driver_opts`, `ipam`, `internal`). - -In the example below, `proxy` is the gateway to the outside world. Instead of -attemping to create a network called `[projectname]_outside`, Compose will -look for an existing network simply called `outside` and connect the `proxy` -service's containers to it. - - version: '2' - - services: - proxy: - build: ./proxy - networks: - - outside - - default - app: - build: ./app - networks: - - default - - 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: - outside: - external: - name: actual-name-of-network - - -## Versioning - -There are two versions of the Compose file format: - -- Version 1, the legacy format. This is specified by omitting a `version` key at - the root of the YAML. -- Version 2, the recommended format. This is specified with a `version: '2'` entry - at the root of the YAML. - -To move your project from version 1 to 2, see the [Upgrading](#upgrading) -section. - -> **Note:** If you're using -> [multiple Compose files](extends.md#different-environments) or -> [extending services](extends.md#extending-services), each file must be of the -> same version - you cannot mix version 1 and 2 in a single project. - -Several things differ depending on which version you use: - -- The structure and permitted configuration keys -- The minimum Docker Engine version you must be running -- Compose's behaviour with regards to networking - -These differences are explained below. - - -### Version 1 - -Compose files that do not declare a version are considered "version 1". In -those files, all the [services](#service-configuration-reference) are declared -at the root of the document. - -Version 1 is supported by **Compose up to 1.6.x**. It will be deprecated in a -future Compose release. - -Version 1 files cannot declare named -[volumes](#volume-configuration-reference), [networks](networking.md) or -[build arguments](#args). - -Example: - - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - links: - - redis - redis: - image: redis - - -### Version 2 - -Compose files using the version 2 syntax must indicate the version number at -the root of the document. All [services](#service-configuration-reference) -must be declared under the `services` key. - -Version 2 files are supported by **Compose 1.6.0+** and require a Docker Engine -of version **1.10.0+**. - -Named [volumes](#volume-configuration-reference) can be declared under the -`volumes` key, and [networks](#network-configuration-reference) can be declared -under the `networks` key. - -Simple example: - - version: '2' - services: - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - redis: - image: redis - -A more extended example, defining volumes and networks: - - version: '2' - services: - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - networks: - - front-tier - - back-tier - redis: - image: redis - volumes: - - redis-data:/var/lib/redis - networks: - - back-tier - volumes: - redis-data: - driver: local - networks: - front-tier: - driver: bridge - back-tier: - driver: bridge - -### Version 2.1 - -An upgrade of [version 2](#version-2) that introduces new parameters only -available with Docker Engine version **1.12.0+** - -Introduces: - -- [`link_local_ips`](#link_local_ips) -- ... - -### Upgrading - -In the majority of cases, moving from version 1 to 2 is a very simple process: - -1. Indent the whole file by one level and put a `services:` key at the top. -2. Add a `version: '2'` line at the top of the file. - -It's more complicated if you're using particular configuration features: - -- `dockerfile`: This now lives under the `build` key: - - build: - context: . - dockerfile: Dockerfile-alternate - -- `log_driver`, `log_opt`: These now live under the `logging` key: - - logging: - driver: syslog - options: - syslog-address: "tcp://192.168.0.42:123" - -- `links` with environment variables: As documented in the - [environment variables reference](link-env-deprecated.md), environment variables - created by - links have been deprecated for some time. In the new Docker network system, - they have been removed. You should either connect directly to the - appropriate hostname or set the relevant environment variable yourself, - using the link hostname: - - web: - links: - - db - environment: - - DB_PORT=tcp://db:5432 - -- `external_links`: Compose uses Docker networks when running version 2 - projects, so links behave slightly differently. In particular, two - containers must be connected to at least one network in common in order to - communicate, even if explicitly linked together. - - Either connect the external container to your app's - [default network](networking.md), or connect both the external container and - your service's containers to an - [external network](networking.md#using-a-pre-existing-network). - -- `net`: This is now replaced by [network_mode](#network_mode): - - net: host -> network_mode: host - net: bridge -> network_mode: bridge - net: none -> network_mode: none - - If you're using `net: "container:[service name]"`, you must now use - `network_mode: "service:[service name]"` instead. - - net: "container:web" -> network_mode: "service:web" - - If you're using `net: "container:[container name/id]"`, the value does not - need to change. - - net: "container:cont-name" -> network_mode: "container:cont-name" - net: "container:abc12345" -> network_mode: "container:abc12345" - -- `volumes` with named volumes: these must now be explicitly declared in a - top-level `volumes` section of your Compose file. If a service mounts a - named volume called `data`, you must declare a `data` volume in your - top-level `volumes` section. The whole file might look like this: - - version: '2' - services: - db: - image: postgres - volumes: - - data:/var/lib/postgresql/data - volumes: - data: {} - - By default, Compose creates a volume whose name is prefixed with your - project name. If you want it to just be called `data`, declare it as - external: - - volumes: - data: - external: true - -## Variable substitution - -Your configuration options can contain environment variables. Compose uses the -variable values from the shell environment in which `docker-compose` is run. -For example, suppose the shell contains `EXTERNAL_PORT=8000` and you supply -this configuration: - - web: - build: . - ports: - - "${EXTERNAL_PORT}:5000" - -When you run `docker-compose up` with this configuration, Compose looks for -the `EXTERNAL_PORT` environment variable in the shell and substitutes its -value in. In this example, Compose resolves the port mapping to `"8000:5000"` -before creating the `web` container. - -If an environment variable is not set, Compose substitutes with an empty -string. In the example above, if `EXTERNAL_PORT` is not set, the value for the -port mapping is `:5000` (which is of course an invalid port mapping, and will -result in an error when attempting to create the container). - -Both `$VARIABLE` and `${VARIABLE}` syntax are supported. Extended shell-style -features, such as `${VARIABLE-default}` and `${VARIABLE/foo/bar}`, are not -supported. - -You can use a `$$` (double-dollar sign) when your configuration needs a literal -dollar sign. This also prevents Compose from interpolating a value, so a `$$` -allows you to refer to environment variables that you don't want processed by -Compose. - - web: - build: . - command: "$$VAR_NOT_INTERPOLATED_BY_COMPOSE" - -If you forget and use a single dollar sign (`$`), Compose interprets the value as an environment variable and will warn you: - - The VAR_NOT_INTERPOLATED_BY_COMPOSE is not set. Substituting an empty string. - -## Compose documentation - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) diff --git a/docs/django.md b/docs/django.md deleted file mode 100644 index 1cf2a567..00000000 --- a/docs/django.md +++ /dev/null @@ -1,194 +0,0 @@ - - - -# Quickstart: Docker Compose and Django - -This quick-start guide demonstrates how to use Docker Compose to set up and run a simple Django/PostgreSQL app. Before starting, you'll need to have -[Compose installed](install.md). - -### Define the project components - -For this project, you need to create a Dockerfile, a Python dependencies file, -and a `docker-compose.yml` file. - -1. Create an empty project directory. - - You can name the directory something easy for you to remember. This directory is the context for your application image. The directory should only contain resources to build that image. - -2. Create a new file called `Dockerfile` in your project directory. - - The Dockerfile defines an application's image content via one or more build - commands that configure that image. Once built, you can run the image in a - container. For more information on `Dockerfiles`, see the [Docker user - guide](/engine/tutorials/dockerimages.md#building-an-image-from-a-dockerfile) - and the [Dockerfile reference](/engine/reference/builder.md). - -3. Add the following content to the `Dockerfile`. - - FROM python:2.7 - ENV PYTHONUNBUFFERED 1 - RUN mkdir /code - WORKDIR /code - ADD requirements.txt /code/ - RUN pip install -r requirements.txt - ADD . /code/ - - This `Dockerfile` starts with a Python 2.7 base image. The base image is - modified by adding a new `code` directory. The base image is further modified - by installing the Python requirements defined in the `requirements.txt` file. - -4. Save and close the `Dockerfile`. - -5. Create a `requirements.txt` in your project directory. - - This file is used by the `RUN pip install -r requirements.txt` command in your `Dockerfile`. - -6. Add the required software in the file. - - Django - psycopg2 - -7. Save and close the `requirements.txt` file. - -8. Create a file called `docker-compose.yml` in your project directory. - - The `docker-compose.yml` file describes the services that make your app. In - this example those services are a web server and database. The compose file - also describes which Docker images these services use, how they link - together, any volumes they might need mounted inside the containers. - Finally, the `docker-compose.yml` file describes which ports these services - expose. See the [`docker-compose.yml` reference](compose-file.md) for more - information on how this file works. - -9. Add the following configuration to the file. - - 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. - -10. Save and close the `docker-compose.yml` file. - -### Create a Django project - -In this step, you create a Django started project by building the image from the build context defined in the previous procedure. - -1. Change to the root of your project directory. - -2. Create the Django project using the `docker-compose` command. - - $ docker-compose run web django-admin.py startproject composeexample . - - This instructs Compose to run `django-admin.py startproject composeeexample` - in a container, using the `web` service's image and configuration. Because - the `web` image doesn't exist yet, Compose builds it from the current - directory, as specified by the `build: .` line in `docker-compose.yml`. - - Once the `web` service image is built, Compose runs it and executes the - `django-admin.py startproject` command in the container. This command - instructs Django to create a set of files and directories representing a - Django project. - -3. After the `docker-compose` command completes, list the contents of your project. - - $ ls -l - drwxr-xr-x 2 root root composeexample - -rw-rw-r-- 1 user user docker-compose.yml - -rw-rw-r-- 1 user user Dockerfile - -rwxr-xr-x 1 root root manage.py - -rw-rw-r-- 1 user user requirements.txt - - If you are running Docker on Linux, the files `django-admin` created are owned - by root. This happens because the container runs as the root user. Change the - ownership of the the new files. - - sudo chown -R $USER:$USER . - - If you are running Docker on Mac or Windows, you should already have ownership - of all files, including those generated by `django-admin`. List the files just - verify this. - - $ ls -l - total 32 - -rw-r--r-- 1 user staff 145 Feb 13 23:00 Dockerfile - drwxr-xr-x 6 user staff 204 Feb 13 23:07 composeexample - -rw-r--r-- 1 user staff 159 Feb 13 23:02 docker-compose.yml - -rwxr-xr-x 1 user staff 257 Feb 13 23:07 manage.py - -rw-r--r-- 1 user staff 16 Feb 13 23:01 requirements.txt - - -### Connect the database - -In this section, you set up the database connection for Django. - -1. In your project directory, edit the `composeexample/settings.py` file. - -2. Replace the `DATABASES = ...` with the following: - - DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', - 'NAME': 'postgres', - 'USER': 'postgres', - 'HOST': 'db', - 'PORT': 5432, - } - } - - These settings are determined by the - [postgres](https://hub.docker.com/_/postgres/) Docker image - specified in `docker-compose.yml`. - -3. Save and close the file. - -4. Run the `docker-compose up` command. - - $ docker-compose up - Starting composepractice_db_1... - Starting composepractice_web_1... - Attaching to composepractice_db_1, composepractice_web_1 - ... - db_1 | PostgreSQL init process complete; ready for start up. - ... - db_1 | LOG: database system is ready to accept connections - db_1 | LOG: autovacuum launcher started - .. - web_1 | Django version 1.8.4, using settings 'composeexample.settings' - web_1 | Starting development server at http://0.0.0.0:8000/ - web_1 | Quit the server with CONTROL-C. - - At this point, your Django app should be running at port `8000` on your - Docker host. If you are using a Docker Machine VM, you can use the - `docker-machine ip MACHINE_NAME` to get the IP address. - - ![Django example](images/django-it-worked.png) - -## More Compose documentation - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Getting Started](gettingstarted.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/env-file.md b/docs/env-file.md deleted file mode 100644 index be2625f8..00000000 --- a/docs/env-file.md +++ /dev/null @@ -1,43 +0,0 @@ - - - -# Environment file - -Compose supports declaring default environment variables in an environment -file named `.env` placed in the folder `docker-compose` command is executed from -*(current working directory)*. - -Compose expects each line in an env file to be in `VAR=VAL` format. Lines -beginning with `#` (i.e. comments) are ignored, as are blank lines. - -> Note: Values present in the environment at runtime will always override -> those defined inside the `.env` file. Similarly, values passed via -> command-line arguments take precedence as well. - -Those environment variables will be used for -[variable substitution](compose-file.md#variable-substitution) in your Compose -file, but can also be used to define the following -[CLI variables](reference/envvars.md): - -- `COMPOSE_API_VERSION` -- `COMPOSE_FILE` -- `COMPOSE_HTTP_TIMEOUT` -- `COMPOSE_PROJECT_NAME` -- `DOCKER_CERT_PATH` -- `DOCKER_HOST` -- `DOCKER_TLS_VERIFY` - -## More Compose documentation - -- [User guide](index.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/environment-variables.md b/docs/environment-variables.md deleted file mode 100644 index a2e74f0a..00000000 --- a/docs/environment-variables.md +++ /dev/null @@ -1,107 +0,0 @@ - - -# Environment variables in Compose - -There are multiple parts of Compose that deal with environment variables in one sense or another. This page should help you find the information you need. - - -## Substituting environment variables in Compose files - -It's possible to use environment variables in your shell to populate values inside a Compose file: - - web: - image: "webapp:${TAG}" - -For more information, see the [Variable substitution](compose-file.md#variable-substitution) section in the Compose file reference. - - -## Setting environment variables in containers - -You can set environment variables in a service's containers with the ['environment' key](compose-file.md#environment), just like with `docker run -e VARIABLE=VALUE ...`: - - web: - environment: - - DEBUG=1 - - -## Passing environment variables through to containers - -You can pass environment variables from your shell straight through to a service's containers with the ['environment' key](compose-file.md#environment) by not giving them a value, just like with `docker run -e VARIABLE ...`: - - web: - environment: - - DEBUG - -The value of the `DEBUG` variable in the container will be taken from the value for the same variable in the shell in which Compose is run. - - -## The “env_file” configuration option - -You can pass multiple environment variables from an external file through to a service's containers with the ['env_file' option](compose-file.md#env-file), just like with `docker run --env-file=FILE ...`: - - web: - env_file: - - web-variables.env - - -## Setting environment variables with 'docker-compose run' - -Just like with `docker run -e`, you can set environment variables on a one-off container with `docker-compose run -e`: - - $ docker-compose run -e DEBUG=1 web python console.py - -You can also pass a variable through from the shell by not giving it a value: - - $ docker-compose run -e DEBUG web python console.py - -The value of the `DEBUG` variable in the container will be taken from the value for the same variable in the shell in which Compose is run. - - -## The “.env” file - -You can set default values for any environment variables referenced in the Compose file, or used to configure Compose, in an [environment file](env-file.md) named `.env`: - - $ cat .env - TAG=v1.5 - - $ cat docker-compose.yml - version: '2.0' - services: - web: - image: "webapp:${TAG}" - -When you run `docker-compose up`, the `web` service defined above uses the image `webapp:v1.5`. You can verify this with the [config command](reference/config.md), which prints your resolved application config to the terminal: - - $ docker-compose config - version: '2.0' - services: - web: - image: 'webapp:v1.5' - -Values in the shell take precedence over those specified in the `.env` file. If you set `TAG` to a different value in your shell, the substitution in `image` uses that instead: - - $ export TAG=v2.0 - - $ docker-compose config - version: '2.0' - services: - web: - image: 'webapp:v2.0' - -## Configuring Compose using environment variables - -Several environment variables are available for you to configure the Docker Compose command-line behaviour. They begin with `COMPOSE_` or `DOCKER_`, and are documented in [CLI Environment Variables](reference/envvars.md). - - -## Environment variables created by links - -When using the ['links' option](compose-file.md#links) in a [v1 Compose file](compose-file.md#version-1), environment variables will be created for each link. They are documented in the [Link environment variables reference](link-env-deprecated.md). Please note, however, that these variables are deprecated - you should just use the link alias as a hostname instead. diff --git a/docs/extends.md b/docs/extends.md deleted file mode 100644 index 6f457391..00000000 --- a/docs/extends.md +++ /dev/null @@ -1,354 +0,0 @@ - - - -# Extending services and Compose files - -Compose supports two methods of sharing common configuration: - -1. Extending an entire Compose file by - [using multiple Compose files](#multiple-compose-files) -2. Extending individual services with [the `extends` field](#extending-services) - - -## Multiple Compose files - -Using multiple Compose files enables you to customize a Compose application -for different environments or different workflows. - -### Understanding multiple Compose files - -By default, Compose reads two files, a `docker-compose.yml` and an optional -`docker-compose.override.yml` file. By convention, the `docker-compose.yml` -contains your base configuration. The override file, as its name implies, can -contain configuration overrides for existing services or entirely new -services. - -If a service is defined in both files Compose merges the configurations using -the rules described in [Adding and overriding -configuration](#adding-and-overriding-configuration). - -To use multiple override files, or an override file with a different name, you -can use the `-f` option to specify the list of files. Compose merges files in -the order they're specified on the command line. See the [`docker-compose` -command reference](./reference/overview.md) for more information about -using `-f`. - -When you use multiple configuration files, you must make sure all paths in the -files are relative to the base Compose file (the first Compose file specified -with `-f`). This is required because override files need not be valid -Compose files. Override files can contain small fragments of configuration. -Tracking which fragment of a service is relative to which path is difficult and -confusing, so to keep paths easier to understand, all paths must be defined -relative to the base file. - -### Example use case - -In this section are two common use cases for multiple compose files: changing a -Compose app for different environments, and running administrative tasks -against a Compose app. - -#### Different environments - -A common use case for multiple files is changing a development Compose app -for a production-like environment (which may be production, staging or CI). -To support these differences, you can split your Compose configuration into -a few different files: - -Start with a base file that defines the canonical configuration for the -services. - -**docker-compose.yml** - - web: - image: example/my_web_app:latest - links: - - db - - cache - - db: - image: postgres:latest - - cache: - image: redis:latest - -In this example the development configuration exposes some ports to the -host, mounts our code as a volume, and builds the web image. - -**docker-compose.override.yml** - - - web: - build: . - volumes: - - '.:/code' - ports: - - 8883:80 - environment: - DEBUG: 'true' - - db: - command: '-d' - ports: - - 5432:5432 - - cache: - ports: - - 6379:6379 - -When you run `docker-compose up` it reads the overrides automatically. - -Now, it would be nice to use this Compose app in a production environment. So, -create another override file (which might be stored in a different git -repo or managed by a different team). - -**docker-compose.prod.yml** - - web: - ports: - - 80:80 - environment: - PRODUCTION: 'true' - - cache: - environment: - TTL: '500' - -To deploy with this production Compose file you can run - - docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d - -This deploys all three services using the configuration in -`docker-compose.yml` and `docker-compose.prod.yml` (but not the -dev configuration in `docker-compose.override.yml`). - - -See [production](production.md) for more information about Compose in -production. - -#### Administrative tasks - -Another common use case is running adhoc or administrative tasks against one -or more services in a Compose app. This example demonstrates running a -database backup. - -Start with a **docker-compose.yml**. - - web: - image: example/my_web_app:latest - links: - - db - - db: - image: postgres:latest - -In a **docker-compose.admin.yml** add a new service to run the database -export or backup. - - dbadmin: - build: database_admin/ - links: - - db - -To start a normal environment run `docker-compose up -d`. To run a database -backup, include the `docker-compose.admin.yml` as well. - - docker-compose -f docker-compose.yml -f docker-compose.admin.yml \ - run dbadmin db-backup - - -## Extending services - -Docker Compose's `extends` keyword enables sharing of common configurations -among different files, or even different projects entirely. Extending services -is useful if you have several services that reuse a common set of configuration -options. Using `extends` you can define a common set of service options in one -place and refer to it from anywhere. - -> **Note:** `links`, `volumes_from`, and `depends_on` are never shared between -> services using >`extends`. These exceptions exist to avoid -> implicit dependencies—you always define `links` and `volumes_from` -> locally. This ensures dependencies between services are clearly visible when -> reading the current file. Defining these locally also ensures changes to the -> referenced file don't result in breakage. - -### Understand the extends configuration - -When defining any service in `docker-compose.yml`, you can declare that you are -extending another service like this: - - web: - extends: - file: common-services.yml - service: webapp - -This instructs Compose to re-use the configuration for the `webapp` service -defined in the `common-services.yml` file. Suppose that `common-services.yml` -looks like this: - - webapp: - build: . - ports: - - "8000:8000" - volumes: - - "/data" - -In this case, you'll get exactly the same result as if you wrote -`docker-compose.yml` with the same `build`, `ports` and `volumes` configuration -values defined directly under `web`. - -You can go further and define (or re-define) configuration locally in -`docker-compose.yml`: - - web: - extends: - file: common-services.yml - service: webapp - environment: - - DEBUG=1 - cpu_shares: 5 - - important_web: - extends: web - cpu_shares: 10 - -You can also write other services and link your `web` service to them: - - web: - extends: - file: common-services.yml - service: webapp - environment: - - DEBUG=1 - cpu_shares: 5 - links: - - db - db: - image: postgres - -### Example use case - -Extending an individual service is useful when you have multiple services that -have a common configuration. The example below is a Compose app with -two services: a web application and a queue worker. Both services use the same -codebase and share many configuration options. - -In a **common.yml** we define the common configuration: - - app: - build: . - environment: - CONFIG_FILE_PATH: /code/config - API_KEY: xxxyyy - cpu_shares: 5 - -In a **docker-compose.yml** we define the concrete services which use the -common configuration: - - webapp: - extends: - file: common.yml - service: app - command: /code/run_web_app - ports: - - 8080:8080 - links: - - queue - - db - - queue_worker: - extends: - file: common.yml - service: app - command: /code/run_worker - links: - - queue - -## Adding and overriding configuration - -Compose copies configurations from the original service over to the local one. -If a configuration option is defined in both the original service the local -service, the local value *replaces* or *extends* the original value. - -For single-value options like `image`, `command` or `mem_limit`, the new value -replaces the old value. - - # original service - command: python app.py - - # local service - command: python otherapp.py - - # result - command: python otherapp.py - -> **Note:** In the case of `build` and `image`, when using -> [version 1 of the Compose file format](compose-file.md#version-1), using one -> option in the local service causes Compose to discard the other option if it -> was defined in the original service. -> -> For example, if the original service defines `image: webapp` and the -> local service defines `build: .` then the resulting service will have -> `build: .` and no `image` option. -> -> This is because `build` and `image` cannot be used together in a version 1 -> file. - -For the **multi-value options** `ports`, `expose`, `external_links`, `dns`, -`dns_search`, and `tmpfs`, Compose concatenates both sets of values: - - # original service - expose: - - "3000" - - # local service - expose: - - "4000" - - "5000" - - # result - expose: - - "3000" - - "4000" - - "5000" - -In the case of `environment`, `labels`, `volumes` and `devices`, Compose -"merges" entries together with locally-defined values taking precedence: - - # original service - environment: - - FOO=original - - BAR=original - - # local service - environment: - - BAR=local - - BAZ=local - - # result - environment: - - FOO=original - - BAR=local - - BAZ=local - - - - -## Compose documentation - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Getting Started](gettingstarted.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index 45885255..00000000 --- a/docs/faq.md +++ /dev/null @@ -1,128 +0,0 @@ - - -# Frequently asked questions - -If you don’t see your question here, feel free to drop by `#docker-compose` on -freenode IRC and ask the community. - - -## Can I control service startup order? - -Yes - see [Controlling startup order](startup-order.md). - - -## Why do my services take 10 seconds to recreate or stop? - -Compose stop attempts to stop a container by sending a `SIGTERM`. It then waits -for a [default timeout of 10 seconds](./reference/stop.md). After the timeout, -a `SIGKILL` is sent to the container to forcefully kill it. If you -are waiting for this timeout, it means that your containers aren't shutting down -when they receive the `SIGTERM` signal. - -There has already been a lot written about this problem of -[processes handling signals](https://medium.com/@gchudnov/trapping-signals-in-docker-containers-7a57fdda7d86) -in containers. - -To fix this problem, try the following: - -* Make sure you're using the JSON form of `CMD` and `ENTRYPOINT` -in your Dockerfile. - - For example use `["program", "arg1", "arg2"]` not `"program arg1 arg2"`. - Using the string form causes Docker to run your process using `bash` which - doesn't handle signals properly. Compose always uses the JSON form, so don't - worry if you override the command or entrypoint in your Compose file. - -* If you are able, modify the application that you're running to -add an explicit signal handler for `SIGTERM`. - -* Set the `stop_signal` to a signal which the application knows how to handle: - - web: - build: . - stop_signal: SIGINT - -* If you can't modify the application, wrap the application in a lightweight init -system (like [s6](http://skarnet.org/software/s6/)) or a signal proxy (like -[dumb-init](https://github.com/Yelp/dumb-init) or -[tini](https://github.com/krallin/tini)). Either of these wrappers take care of -handling `SIGTERM` properly. - -## How do I run multiple copies of a Compose file on the same host? - -Compose uses the project name to create unique identifiers for all of a -project's containers and other resources. To run multiple copies of a project, -set a custom project name using the [`-p` command line -option](./reference/overview.md) or the [`COMPOSE_PROJECT_NAME` -environment variable](./reference/envvars.md#compose-project-name). - -## What's the difference between `up`, `run`, and `start`? - -Typically, you want `docker-compose up`. Use `up` to start or restart all the -services defined in a `docker-compose.yml`. In the default "attached" -mode, you'll see all the logs from all the containers. In "detached" mode (`-d`), -Compose exits after starting the containers, but the containers continue to run -in the background. - -The `docker-compose run` command is for running "one-off" or "adhoc" tasks. It -requires the service name you want to run and only starts containers for services -that the running service depends on. Use `run` to run tests or perform -an administrative task such as removing or adding data to a data volume -container. The `run` command acts like `docker run -ti` in that it opens an -interactive terminal to the container and returns an exit status matching the -exit status of the process in the container. - -The `docker-compose start` command is useful only to restart containers -that were previously created, but were stopped. It never creates new -containers. - -## Can I use json instead of yaml for my Compose file? - -Yes. [Yaml is a superset of json](http://stackoverflow.com/a/1729545/444646) so -any JSON file should be valid Yaml. To use a JSON file with Compose, -specify the filename to use, for example: - -```bash -docker-compose -f docker-compose.json up -``` - -## Should I include my code with `COPY`/`ADD` or a volume? - -You can add your code to the image using `COPY` or `ADD` directive in a -`Dockerfile`. This is useful if you need to relocate your code along with the -Docker image, for example when you're sending code to another environment -(production, CI, etc). - -You should use a `volume` if you want to make changes to your code and see them -reflected immediately, for example when you're developing code and your server -supports hot code reloading or live-reload. - -There may be cases where you'll want to use both. You can have the image -include the code using a `COPY`, and use a `volume` in your Compose file to -include the code from the host during development. The volume overrides -the directory contents of the image. - -## Where can I find example compose files? - -There are [many examples of Compose files on -github](https://github.com/search?q=in%3Apath+docker-compose.yml+extension%3Ayml&type=Code). - - -## Compose documentation - -- [Installing Compose](install.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md deleted file mode 100644 index 249bff72..00000000 --- a/docs/gettingstarted.md +++ /dev/null @@ -1,191 +0,0 @@ - - - -# Getting Started - -On this page you build a simple Python web application running on Docker Compose. The -application uses the Flask framework and increments a value in Redis. While the -sample uses Python, the concepts demonstrated here should be understandable even -if you're not familiar with it. - -## Prerequisites - -Make sure you have already -[installed both Docker Engine and Docker Compose](install.md). You -don't need to install Python, it is provided by a Docker image. - -## Step 1: Setup - -1. Create a directory for the project: - - $ mkdir composetest - $ cd composetest - -2. With your favorite text editor create a file called `app.py` in your project - directory. - - from flask import Flask - from redis import Redis - - app = Flask(__name__) - redis = Redis(host='redis', port=6379) - - @app.route('/') - def hello(): - redis.incr('hits') - return 'Hello World! I have been seen %s times.' % redis.get('hits') - - if __name__ == "__main__": - app.run(host="0.0.0.0", debug=True) - -3. Create another file called `requirements.txt` in your project directory and - add the following: - - flask - redis - - These define the applications dependencies. - -## Step 2: Create a Docker image - -In this step, you build a new Docker image. The image contains all the -dependencies the Python application requires, including Python itself. - -1. In your project directory create a file named `Dockerfile` and add the - following: - - FROM python:2.7 - ADD . /code - WORKDIR /code - RUN pip install -r requirements.txt - CMD python app.py - - This tells Docker to: - - * Build an image starting with the Python 2.7 image. - * Add the current directory `.` into the path `/code` in the image. - * Set the working directory to `/code`. - * Install the Python dependencies. - * Set the default command for the container to `python app.py` - - For more information on how to write Dockerfiles, see the [Docker user guide](/engine/tutorials/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). - -2. Build the image. - - $ docker build -t web . - - This command builds an image named `web` from the contents of the current - directory. The command automatically locates the `Dockerfile`, `app.py`, and - `requirements.txt` files. - - -## Step 3: Define services - -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: - - - 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: - -* Builds from the `Dockerfile` in the current directory. -* Forwards the exposed port 5000 on the container to port 5000 on the host machine. -* Mounts the project directory on the host to `/code` inside the container allowing you to modify the code without having to rebuild the image. -* Links the web service to the Redis service. - -The `redis` service uses the latest public [Redis](https://registry.hub.docker.com/_/redis/) image pulled from the Docker Hub registry. - -## Step 4: Build and run your app with Compose - -1. From your project directory, start up your application. - - $ docker-compose up - Pulling image redis... - Building web... - Starting composetest_redis_1... - Starting composetest_web_1... - redis_1 | [8] 02 Jan 18:43:35.576 # Server started, Redis version 2.8.3 - web_1 | * Running on http://0.0.0.0:5000/ - web_1 | * Restarting with stat - - Compose pulls a Redis image, builds an image for your code, and start the - services you defined. - -2. Enter `http://0.0.0.0:5000/` in a browser to see the application running. - - If you're using Docker on Linux natively, then the web app should now be - listening on port 5000 on your Docker daemon host. If `http://0.0.0.0:5000` - doesn't resolve, you can also try `http://localhost:5000`. - - If you're using Docker Machine on a Mac, use `docker-machine ip MACHINE_VM` to get - the IP address of your Docker host. Then, `open http://MACHINE_VM_IP:5000` in a - browser. - - You should see a message in your browser saying: - - `Hello World! I have been seen 1 times.` - -3. Refresh the page. - - The number should increment. - -## Step 5: Experiment with some other commands - -If you want to run your services in the background, you can pass the `-d` flag -(for "detached" mode) to `docker-compose up` and use `docker-compose ps` to -see what is currently running: - - $ docker-compose up -d - Starting composetest_redis_1... - Starting composetest_web_1... - $ docker-compose ps - Name Command State Ports - ------------------------------------------------------------------- - composetest_redis_1 /usr/local/bin/run Up - composetest_web_1 /bin/sh -c python app.py Up 5000->5000/tcp - -The `docker-compose run` command allows you to run one-off commands for your -services. For example, to see what environment variables are available to the -`web` service: - - $ docker-compose run web env - -See `docker-compose --help` to see other available commands. You can also install [command completion](completion.md) for the bash and zsh shell, which will also show you available commands. - -If you started Compose with `docker-compose up -d`, you'll probably want to stop -your services once you've finished with them: - - $ docker-compose stop - -At this point, you have seen the basics of how Compose works. - - -## Where to go next - -- Next, try the quick start guide for [Django](django.md), - [Rails](rails.md), or [WordPress](wordpress.md). -- [Explore the full list of Compose commands](./reference/index.md) -- [Compose configuration file reference](compose-file.md) diff --git a/docs/images/django-it-worked.png b/docs/images/django-it-worked.png deleted file mode 100644 index 75769754b975748dea24a9896083e6e9447d121f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28446 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 a6Y}y^58ih#ifY@Kcf`{7Q?LtYPUU~rH#6%1 diff --git a/docs/images/wordpress-files.png b/docs/images/wordpress-files.png deleted file mode 100644 index 4762935baeb03568530e962231687bbcd38d075b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70823 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 diff --git a/docs/images/wordpress-lang.png b/docs/images/wordpress-lang.png deleted file mode 100644 index f0bd864ef0a72930a28dd8e58685437ecbd947b3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30149 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 diff --git a/docs/images/wordpress-welcome.png b/docs/images/wordpress-welcome.png deleted file mode 100644 index c9ba20368c55c34b18cc6a37ed1e5d3d37aa1a60..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 62063 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

O2I>c(i=A{R+)A=E;y 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 diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index f1b71079..00000000 --- a/docs/index.md +++ /dev/null @@ -1,30 +0,0 @@ - - - -# Docker Compose - -Compose is a tool for defining and running multi-container Docker applications. To learn more about Compose refer to the following documentation: - -- [Compose Overview](overview.md) -- [Install Compose](install.md) -- [Getting Started](gettingstarted.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Frequently asked questions](faq.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) -- [Environment file](env-file.md) - -To see a detailed list of changes for past and current releases of Docker -Compose, please refer to the -[CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md). diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index bb7f07b3..00000000 --- a/docs/install.md +++ /dev/null @@ -1,136 +0,0 @@ - - - -# Install Docker Compose - -You can run Compose on OS X, Windows and 64-bit Linux. To install it, you'll need to install Docker first. - -To install Compose, do the following: - -1. Install Docker Engine: - - * Mac OS X installation - - * Windows installation - - * Ubuntu installation - - * other system installations - -2. The Docker Toolbox installation includes both Engine and Compose, so Mac and Windows users are done installing. Others should continue to the next step. - -3. Go to the Compose repository release page on GitHub. - -4. Follow the instructions from the release page and run the `curl` command, -which the release page specifies, in your terminal. - - > Note: If you get a "Permission denied" error, your `/usr/local/bin` directory - probably isn't writable and you'll need to install Compose as the superuser. Run - `sudo -i`, then the two commands below, then `exit`. - - The following is an example command illustrating the format: - - curl -L https://github.com/docker/compose/releases/download/1.8.0/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). - -5. Apply executable permissions to the binary: - - $ chmod +x /usr/local/bin/docker-compose - -6. Optionally, install [command completion](completion.md) for the -`bash` and `zsh` shell. - -7. Test the installation. - - $ docker-compose --version - docker-compose version: 1.8.0 - - -## Alternative install options - -### Install using pip - -Compose can be installed from [pypi](https://pypi.python.org/pypi/docker-compose) -using `pip`. If you install using `pip` it is highly recommended that you use a -[virtualenv](https://virtualenv.pypa.io/en/latest/) because many operating systems -have python system packages that conflict with docker-compose dependencies. See -the [virtualenv tutorial](http://docs.python-guide.org/en/latest/dev/virtualenvs/) -to get started. - - $ pip install docker-compose - -> **Note:** pip version 6.0 or greater is required - -### Install as a container - -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.8.0/run.sh > /usr/local/bin/docker-compose - $ chmod +x /usr/local/bin/docker-compose - -## Master builds - -If you're interested in trying out a pre-release build you can download a -binary from https://dl.bintray.com/docker-compose/master/. Pre-release -builds allow you to try out new features before they are released, but may -be less stable. - - -## Upgrading - -If you're upgrading from Compose 1.2 or earlier, you'll need to remove or migrate -your existing containers after upgrading Compose. This is because, as of version -1.3, Compose uses Docker labels to keep track of containers, and so they need to -be recreated with labels added. - -If Compose detects containers that were created without labels, it will refuse -to run so that you don't end up with two sets of them. If you want to keep using -your existing containers (for example, because they have data volumes you want -to preserve) you can use compose 1.5.x to migrate them with the following command: - - $ docker-compose migrate-to-labels - -Alternatively, if you're not worried about keeping them, you can remove them. -Compose will just create new ones. - - $ docker rm -f -v myapp_web_1 myapp_db_1 ... - - -## Uninstallation - -To uninstall Docker Compose if you installed using `curl`: - - $ rm /usr/local/bin/docker-compose - - -To uninstall Docker Compose if you installed using `pip`: - - $ pip uninstall docker-compose - ->**Note**: If you get a "Permission denied" error using either of the above ->methods, you probably do not have the proper permissions to remove ->`docker-compose`. To force the removal, prepend `sudo` to either of the above ->commands and run again. - - -## Where to go next - -- [User guide](index.md) -- [Getting Started](gettingstarted.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/link-env-deprecated.md b/docs/link-env-deprecated.md deleted file mode 100644 index b1f01b3b..00000000 --- a/docs/link-env-deprecated.md +++ /dev/null @@ -1,48 +0,0 @@ - - -# Link environment variables reference - -> **Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](compose-file.md#links) for details. -> -> Environment variables will only be populated if you're using the [legacy version 1 Compose file format](compose-file.md#versioning). - -Compose uses [Docker links](/engine/userguide/networking/default_network/dockerlinks.md) -to expose services' containers to one another. Each linked container injects a set of -environment variables, each of which begins with the uppercase name of the container. - -To see what environment variables are available to a service, run `docker-compose run SERVICE env`. - -name\_PORT
-Full URL, e.g. `DB_PORT=tcp://172.17.0.5:5432` - -name\_PORT\_num\_protocol
-Full URL, e.g. `DB_PORT_5432_TCP=tcp://172.17.0.5:5432` - -name\_PORT\_num\_protocol\_ADDR
-Container's IP address, e.g. `DB_PORT_5432_TCP_ADDR=172.17.0.5` - -name\_PORT\_num\_protocol\_PORT
-Exposed port number, e.g. `DB_PORT_5432_TCP_PORT=5432` - -name\_PORT\_num\_protocol\_PROTO
-Protocol (tcp or udp), e.g. `DB_PORT_5432_TCP_PROTO=tcp` - -name\_NAME
-Fully qualified container name, e.g. `DB_1_NAME=/myapp_web_1/myapp_db_1` - -## Related Information - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/networking.md b/docs/networking.md deleted file mode 100644 index 9739a088..00000000 --- a/docs/networking.md +++ /dev/null @@ -1,154 +0,0 @@ - - - -# Networking in Compose - -> **Note:** This document only applies if you're using [version 2 of the Compose file format](compose-file.md#versioning). Networking features are not supported for version 1 (legacy) Compose files. - -By default Compose sets up a single -[network](https://docs.docker.com/engine/reference/commandline/network_create/) for your app. Each -container for a service joins the default network and is both *reachable* by -other containers on that network, and *discoverable* by them at a hostname -identical to the container name. - -> **Note:** Your app's network is given a name based on the "project name", -> which is based on the name of the directory it lives in. You can override the -> project name with either the [`--project-name` -> flag](reference/overview.md) or the [`COMPOSE_PROJECT_NAME` environment -> variable](reference/envvars.md#compose-project-name). - -For example, suppose your app is in a directory called `myapp`, and your `docker-compose.yml` looks like this: - - version: '2' - - services: - web: - build: . - ports: - - "8000:8000" - db: - image: postgres - -When you run `docker-compose up`, the following happens: - -1. A network called `myapp_default` is created. -2. A container is created using `web`'s configuration. It joins the network - `myapp_default` under the name `web`. -3. A container is created using `db`'s configuration. It joins the network - `myapp_default` under the name `db`. - -Each container can now look up the hostname `web` or `db` and -get back the appropriate container's IP address. For example, `web`'s -application code could connect to the URL `postgres://db:5432` and start -using the Postgres database. - -Because `web` explicitly maps a port, it's also accessible from the outside world via port 8000 on your Docker host's network interface. - -## Updating containers - -If you make a configuration change to a service and run `docker-compose up` to update it, the old container will be removed and the new one will join the network under a different IP address but the same name. Running containers will be able to look up that name and connect to the new address, but the old address will stop working. - -If any containers have connections open to the old container, they will be closed. It is a container's responsibility to detect this condition, look up the name again and reconnect. - -## Links - -Links allow you to define extra aliases by which a service is reachable from another service. They are not required to enable services to communicate - by default, any service can reach any other service at that service's name. In the following example, `db` is reachable from `web` at the hostnames `db` and `database`: - - version: '2' - services: - web: - build: . - links: - - "db:database" - db: - image: postgres - -See the [links reference](compose-file.md#links) for more information. - -## Multi-host networking - -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](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) 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 - -Instead of just using the default app network, you can specify your own networks with the top-level `networks` key. This lets you create more complex topologies and specify [custom network drivers](https://docs.docker.com/engine/extend/plugins_network/) and options. You can also use it to connect services to externally-created networks which aren't managed by Compose. - -Each service can specify what networks to connect to with the *service-level* `networks` key, which is a list of names referencing entries under the *top-level* `networks` key. - -Here's an example Compose file defining two custom networks. The `proxy` service is isolated from the `db` service, because they do not share a network in common - only `app` can talk to both. - - version: '2' - - services: - proxy: - build: ./proxy - networks: - - front - app: - build: ./app - networks: - - front - - back - db: - image: postgres - networks: - - back - - networks: - front: - # Use a custom driver - driver: custom-driver-1 - back: - # Use a custom driver which takes special options - driver: custom-driver-2 - driver_opts: - foo: "1" - bar: "2" - -Networks can be configured with static IP addresses by setting the [ipv4_address and/or ipv6_address](compose-file.md#ipv4-address-ipv6-address) for each attached network. - -For full details of the network configuration options available, see the following references: - -- [Top-level `networks` key](compose-file.md#network-configuration-reference) -- [Service-level `networks` key](compose-file.md#networks) - -## Configuring the default network - -Instead of (or as well as) specifying your own networks, you can also change the settings of the app-wide default network by defining an entry under `networks` named `default`: - - version: '2' - - services: - web: - build: . - ports: - - "8000:8000" - db: - image: postgres - - networks: - default: - # Use a custom driver - driver: custom-driver-1 - -## Using a pre-existing network - -If you want your containers to join a pre-existing network, use the [`external` option](compose-file.md#network-configuration-reference): - - networks: - default: - external: - name: my-pre-existing-network - -Instead of attemping to create a network called `[projectname]_default`, Compose will look for a network called `my-pre-existing-network` and connect your app's containers to it. diff --git a/docs/overview.md b/docs/overview.md deleted file mode 100644 index ef07a45b..00000000 --- a/docs/overview.md +++ /dev/null @@ -1,188 +0,0 @@ - - - -# Overview of Docker Compose - -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](#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](#common-use-cases). - -Using Compose is basically a three-step process. - -1. Define your app's environment with a `Dockerfile` so it can be reproduced -anywhere. - -2. Define the services that make up your app in `docker-compose.yml` -so they can be run together in an isolated environment. - -3. Lastly, run -`docker-compose up` and Compose will start and run your entire app. - -A `docker-compose.yml` looks like this: - - version: '2' - services: - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - - logvolume01:/var/log - links: - - redis - redis: - image: redis - volumes: - logvolume01: {} - -For more information about the Compose file, see the -[Compose file reference](compose-file.md) - -Compose has commands for managing the whole lifecycle of your application: - - * Start, stop and rebuild services - * View the status of running services - * Stream the log output of running services - * Run a one-off command on a service - -## Compose documentation - -- [Installing Compose](install.md) -- [Getting Started](gettingstarted.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Frequently asked questions](faq.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) - -## Features - -The features of Compose that make it effective are: - -* [Multiple isolated environments on a single host](#Multiple-isolated-environments-on-a-single-host) -* [Preserve volume data when containers are created](#preserve-volume-data-when-containers-are-created) -* [Only recreate containers that have changed](#only-recreate-containers-that-have-changed) -* [Variables and moving a composition between environments](#variables-and-moving-a-composition-between-environments) - -### Multiple isolated environments on a single host - -Compose uses a project name to isolate environments from each other. You can make use of this project name in several different contexts: - -* on a dev host, to create multiple copies of a single environment (e.g., you want to run a stable copy for each feature branch of a project) -* on a CI server, to keep builds from interfering with each other, you can set - the project name to a unique build number -* on a shared host or dev host, to prevent different projects, which may use the - same service names, from interfering with each other - -The default project name is the basename of the project directory. You can set -a custom project name by using the -[`-p` command line option](./reference/overview.md) or the -[`COMPOSE_PROJECT_NAME` environment variable](./reference/envvars.md#compose-project-name). - -### Preserve volume data when containers are created - -Compose preserves all volumes used by your services. When `docker-compose up` -runs, if it finds any containers from previous runs, it copies the volumes from -the old container to the new container. This process ensures that any data -you've created in volumes isn't lost. - - -### Only recreate containers that have changed - -Compose caches the configuration used to create a container. When you -restart a service that has not changed, Compose re-uses the existing -containers. Re-using containers means that you can make changes to your -environment very quickly. - - -### Variables and moving a composition between environments - -Compose supports variables in the Compose file. You can use these variables -to customize your composition for different environments, or different users. -See [Variable substitution](compose-file.md#variable-substitution) for more -details. - -You can extend a Compose file using the `extends` field or by creating multiple -Compose files. See [extends](extends.md) for more details. - - -## Common Use Cases - -Compose can be used in many different ways. Some common use cases are outlined -below. - -### Development environments - -When you're developing software, the ability to run an application in an -isolated environment and interact with it is crucial. The Compose command -line tool can be used to create the environment and interact with it. - -The [Compose file](compose-file.md) provides a way to document and configure -all of the application's service dependencies (databases, queues, caches, -web service APIs, etc). Using the Compose command line tool you can create -and start one or more containers for each dependency with a single command -(`docker-compose up`). - -Together, these features provide a convenient way for developers to get -started on a project. Compose can reduce a multi-page "developer getting -started guide" to a single machine readable Compose file and a few commands. - -### Automated testing environments - -An important part of any Continuous Deployment or Continuous Integration process -is the automated test suite. Automated end-to-end testing requires an -environment in which to run tests. Compose provides a convenient way to create -and destroy isolated testing environments for your test suite. By defining the full environment in a [Compose file](compose-file.md) you can create and destroy these environments in just a few commands: - - $ docker-compose up -d - $ ./run_tests - $ docker-compose down - -### Single host deployments - -Compose has traditionally been focused on development and testing workflows, -but with each release we're making progress on more production-oriented features. You can use Compose to deploy to a remote Docker Engine. The Docker Engine may be a single instance provisioned with -[Docker Machine](/machine/overview.md) or an entire -[Docker Swarm](/swarm/overview.md) cluster. - -For details on using production-oriented features, see -[compose in production](production.md) in this documentation. - - -## Release Notes - -To see a detailed list of changes for past and current releases of Docker -Compose, please refer to the -[CHANGELOG](https://github.com/docker/compose/blob/master/CHANGELOG.md). - -## Getting help - -Docker Compose is under active development. If you need help, would like to -contribute, or simply want to talk about the project with like-minded -individuals, we have a number of open channels for communication. - -* To report bugs or file feature requests: please use the [issue tracker on Github](https://github.com/docker/compose/issues). - -* To talk about the project with people in real time: please join the - `#docker-compose` channel on freenode IRC. - -* To contribute code or documentation changes: please submit a [pull request on Github](https://github.com/docker/compose/pulls). - -For more information and resources, please visit the [Getting Help project page](https://docs.docker.com/opensource/get-help/). diff --git a/docs/production.md b/docs/production.md deleted file mode 100644 index cfb87293..00000000 --- a/docs/production.md +++ /dev/null @@ -1,88 +0,0 @@ - - - -## Using Compose in production - -When you define your app with Compose in development, you can use this -definition to run your application in different environments such as CI, -staging, and production. - -The easiest way to deploy an application is to run it on a single server, -similar to how you would run your development environment. If you want to scale -up your application, you can run Compose apps on a Swarm cluster. - -### Modify your Compose file for production - -You'll almost certainly want to make changes to your app configuration that are -more appropriate to a live environment. These changes may include: - -- Removing any volume bindings for application code, so that code stays inside - the container and can't be changed from outside -- Binding to different ports on the host -- Setting environment variables differently (e.g., to decrease the verbosity of - logging, or to enable email sending) -- Specifying a restart policy (e.g., `restart: always`) to avoid downtime -- Adding extra services (e.g., a log aggregator) - -For this reason, you'll probably want to define an additional Compose file, say -`production.yml`, which specifies production-appropriate -configuration. This configuration file only needs to include the changes you'd -like to make from the original Compose file. The additional Compose file -can be applied over the original `docker-compose.yml` to create a new configuration. - -Once you've got a second configuration file, tell Compose to use it with the -`-f` option: - - $ docker-compose -f docker-compose.yml -f production.yml up -d - -See [Using multiple compose files](extends.md#different-environments) for a more -complete example. - -### Deploying changes - -When you make changes to your app code, you'll need to rebuild your image and -recreate your app's containers. To redeploy a service called -`web`, you would use: - - $ docker-compose build web - $ docker-compose up --no-deps -d web - -This will first rebuild the image for `web` and then stop, destroy, and recreate -*just* the `web` service. The `--no-deps` flag prevents Compose from also -recreating any services which `web` depends on. - -### Running Compose on a single server - -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](/machine/overview.md) makes managing local and -remote Docker hosts very easy, and is recommended even if you're not deploying -remotely. - -Once you've set up your environment variables, all the normal `docker-compose` -commands will work with no further configuration. - -### Running Compose on a Swarm cluster - -[Docker Swarm](/swarm/overview.md), 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. - -Read more about the Compose/Swarm integration in the -[integration guide](swarm.md). - -## Compose documentation - -- [Installing Compose](install.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/rails.md b/docs/rails.md deleted file mode 100644 index 26777687..00000000 --- a/docs/rails.md +++ /dev/null @@ -1,174 +0,0 @@ - - -## Quickstart: Docker Compose and Rails - -This Quickstart guide will show you how to use Docker Compose to set up and run a Rails/PostgreSQL app. Before starting, you'll need to have [Compose installed](install.md). - -### Define the project - -Start by setting up the three files you'll need to build the app. First, since -your app is going to run inside a Docker container containing all of its -dependencies, you'll need to define exactly what needs to be included in the -container. This is done using a file called `Dockerfile`. To begin with, the -Dockerfile consists of: - - FROM ruby:2.2.0 - RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs - RUN mkdir /myapp - WORKDIR /myapp - ADD Gemfile /myapp/Gemfile - ADD Gemfile.lock /myapp/Gemfile.lock - RUN bundle install - ADD . /myapp - -That'll put your application code inside an image that will build a container -with Ruby, Bundler and all your dependencies inside it. For more information on -how to write Dockerfiles, see the [Docker user guide](/engine/tutorials/dockerimages.md#building-an-image-from-a-dockerfile) and the [Dockerfile reference](/engine/reference/builder.md). - -Next, create a bootstrap `Gemfile` which just loads Rails. It'll be overwritten in a moment by `rails new`. - - source 'https://rubygems.org' - gem 'rails', '4.2.0' - -You'll need an empty `Gemfile.lock` in order to build our `Dockerfile`. - - $ touch Gemfile.lock - -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. - - 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 - -With those three files in place, you can now generate the Rails skeleton app -using `docker-compose run`: - - $ docker-compose run web rails new . --force --database=postgresql --skip-bundle - -First, Compose will build the image for the `web` service using the `Dockerfile`. Then it'll run `rails new` inside a new container, using that image. Once it's done, you should have generated a fresh app: - - $ ls -l - total 56 - -rw-r--r-- 1 user staff 215 Feb 13 23:33 Dockerfile - -rw-r--r-- 1 user staff 1480 Feb 13 23:43 Gemfile - -rw-r--r-- 1 user staff 2535 Feb 13 23:43 Gemfile.lock - -rw-r--r-- 1 root root 478 Feb 13 23:43 README.rdoc - -rw-r--r-- 1 root root 249 Feb 13 23:43 Rakefile - drwxr-xr-x 8 root root 272 Feb 13 23:43 app - drwxr-xr-x 6 root root 204 Feb 13 23:43 bin - drwxr-xr-x 11 root root 374 Feb 13 23:43 config - -rw-r--r-- 1 root root 153 Feb 13 23:43 config.ru - drwxr-xr-x 3 root root 102 Feb 13 23:43 db - -rw-r--r-- 1 user staff 161 Feb 13 23:35 docker-compose.yml - drwxr-xr-x 4 root root 136 Feb 13 23:43 lib - drwxr-xr-x 3 root root 102 Feb 13 23:43 log - drwxr-xr-x 7 root root 238 Feb 13 23:43 public - drwxr-xr-x 9 root root 306 Feb 13 23:43 test - drwxr-xr-x 3 root root 102 Feb 13 23:43 tmp - drwxr-xr-x 3 root root 102 Feb 13 23:43 vendor - - -If you are running Docker on Linux, the files `rails new` created are owned by -root. This happens because the container runs as the root user. Change the -ownership of the the new files. - - sudo chown -R $USER:$USER . - -If you are running Docker on Mac or Windows, you should already have ownership -of all files, including those generated by `rails new`. List the files just to -verify this. - -Uncomment the line in your new `Gemfile` which loads `therubyracer`, so you've -got a Javascript runtime: - - gem 'therubyracer', platforms: :ruby - -Now that you've got a new `Gemfile`, you need to build the image again. (This, -and changes to the Dockerfile itself, should be the only times you'll need to -rebuild.) - - $ docker-compose build - - -### Connect the database - -The app is now bootable, but you're not quite there yet. By default, Rails -expects a database to be running on `localhost` - so you need to point it at the -`db` container instead. You also need to change the database and username to -align with the defaults set by the `postgres` image. - -Replace the contents of `config/database.yml` with the following: - - development: &default - adapter: postgresql - encoding: unicode - database: postgres - pool: 5 - username: postgres - password: - host: db - - test: - <<: *default - database: myapp_test - -You can now boot the app with: - - $ docker-compose up - -If all's well, you should see some PostgreSQL output, and then—after a few -seconds—the familiar refrain: - - myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick 1.3.1 - myapp_web_1 | [2014-01-17 17:16:29] INFO ruby 2.2.0 (2014-12-25) [x86_64-linux-gnu] - myapp_web_1 | [2014-01-17 17:16:29] INFO WEBrick::HTTPServer#start: pid=1 port=3000 - -Finally, you need to create the database. In another terminal, run: - - $ docker-compose run web rake db:create - -That's it. Your app should now be running on port 3000 on your Docker daemon. If you're using [Docker Machine](/machine/overview.md), then `docker-machine ip MACHINE_VM` returns the Docker host IP address. - -![Rails example](images/rails-welcome.png) - ->**Note**: If you stop the example application and attempt to restart it, you might get the -following error: `web_1 | A server is already running. Check -/myapp/tmp/pids/server.pid.` One way to resolve this is to delete the file -`tmp/pids/server.pid`, and then re-start the application with `docker-compose -up`. - - -## More Compose documentation - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Getting Started](gettingstarted.md) -- [Get started with Django](django.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/reference/build.md b/docs/reference/build.md deleted file mode 100644 index 84aefc25..00000000 --- a/docs/reference/build.md +++ /dev/null @@ -1,25 +0,0 @@ - - -# build - -``` -Usage: build [options] [SERVICE...] - -Options: ---force-rm Always remove intermediate containers. ---no-cache Do not use cache when building the image. ---pull Always attempt to pull a newer version of the image. -``` - -Services are built once and then tagged as `project_service`, e.g., -`composetest_db`. If you change a service's Dockerfile or the contents of its -build directory, run `docker-compose build` to rebuild it. diff --git a/docs/reference/bundle.md b/docs/reference/bundle.md deleted file mode 100644 index fca93a8a..00000000 --- a/docs/reference/bundle.md +++ /dev/null @@ -1,31 +0,0 @@ - - -# bundle - -``` -Usage: bundle [options] - -Options: - --push-images Automatically push images for any services - which have a `build` option specified. - - -o, --output PATH Path to write the bundle file to. - Defaults to ".dab". -``` - -Generate a Distributed Application Bundle (DAB) from the Compose file. - -Images must have digests stored, which requires interaction with a -Docker registry. If digests aren't stored for all images, you can fetch -them with `docker-compose pull` or `docker-compose push`. To push images -automatically when bundling, pass `--push-images`. Only services with -a `build` option specified will have their images pushed. diff --git a/docs/reference/config.md b/docs/reference/config.md deleted file mode 100644 index 1a9706f4..00000000 --- a/docs/reference/config.md +++ /dev/null @@ -1,23 +0,0 @@ - - -# config - -```: -Usage: config [options] - -Options: --q, --quiet Only validate the configuration, don't print - anything. ---services Print the service names, one per line. -``` - -Validate and view the compose file. diff --git a/docs/reference/create.md b/docs/reference/create.md deleted file mode 100644 index 5065e8be..00000000 --- a/docs/reference/create.md +++ /dev/null @@ -1,26 +0,0 @@ - - -# create - -``` -Creates containers for a service. - -Usage: create [options] [SERVICE...] - -Options: - --force-recreate Recreate containers even if their configuration and - image haven't changed. Incompatible with --no-recreate. - --no-recreate If containers already exist, don't recreate them. - Incompatible with --force-recreate. - --no-build Don't build an image, even if it's missing. - --build Build images before creating containers. -``` diff --git a/docs/reference/down.md b/docs/reference/down.md deleted file mode 100644 index ffe88b4e..00000000 --- a/docs/reference/down.md +++ /dev/null @@ -1,38 +0,0 @@ - - -# down - -``` -Usage: down [options] - -Options: - --rmi type Remove images. Type must be one of: - 'all': Remove all images used by any service. - 'local': Remove only images that don't have a custom tag - set by the `image` field. - -v, --volumes Remove named volumes declared in the `volumes` section - of the Compose file and anonymous volumes - attached to containers. - --remove-orphans Remove containers for services not defined in the - Compose file -``` - -Stops containers and removes containers, networks, volumes, and images -created by `up`. - -By default, the only things removed are: - -- Containers for services defined in the Compose file -- Networks defined in the `networks` section of the Compose file -- The default network, if one is used - -Networks and volumes defined as `external` are never removed. diff --git a/docs/reference/envvars.md b/docs/reference/envvars.md deleted file mode 100644 index 22516deb..00000000 --- a/docs/reference/envvars.md +++ /dev/null @@ -1,92 +0,0 @@ - - - -# CLI Environment Variables - -Several environment variables are available for you to configure the Docker Compose command-line behaviour. - -Variables starting with `DOCKER_` are the same as those used to configure the -Docker command-line client. If you're using `docker-machine`, then the `eval "$(docker-machine env my-docker-vm)"` command should set them to their correct values. (In this example, `my-docker-vm` is the name of a machine you created.) - -> Note: Some of these variables can also be provided using an -> [environment file](../env-file.md) - -## COMPOSE\_PROJECT\_NAME - -Sets the project name. This value is prepended along with the service name to the container container on start up. For example, if you project name is `myapp` and it includes two services `db` and `web` then compose starts containers named `myapp_db_1` and `myapp_web_1` respectively. - -Setting this is optional. If you do not set this, the `COMPOSE_PROJECT_NAME` -defaults to the `basename` of the project directory. See also the `-p` -[command-line option](overview.md). - -## COMPOSE\_FILE - -Specify the path to a Compose file. If not provided, Compose looks for a file named -`docker-compose.yml` in the current directory and then each parent directory in -succession until a file by that name is found. - -This variable supports multiple compose files separate by a path separator (on -Linux and OSX the path separator is `:`, on Windows it is `;`). For example: -`COMPOSE_FILE=docker-compose.yml:docker-compose.prod.yml` - -See also the `-f` [command-line option](overview.md). - -## COMPOSE\_API\_VERSION - -The Docker API only supports requests from clients which report a specific -version. If you receive a `client and server don't have same version error` using -`docker-compose`, you can workaround this error by setting this environment -variable. Set the version value to match the server version. - -Setting this variable is intended as a workaround for situations where you need -to run temporarily with a mismatch between the client and server version. For -example, if you can upgrade the client but need to wait to upgrade the server. - -Running with this variable set and a known mismatch does prevent some Docker -features from working properly. The exact features that fail would depend on the -Docker client and server versions. For this reason, running with this variable -set is only intended as a workaround and it is not officially supported. - -If you run into problems running with this set, resolve the mismatch through -upgrade and remove this setting to see if your problems resolve before notifying -support. - -## DOCKER\_HOST - -Sets the URL of the `docker` daemon. As with the Docker client, defaults to `unix:///var/run/docker.sock`. - -## DOCKER\_TLS\_VERIFY - -When set to anything other than an empty string, enables TLS communication with -the `docker` daemon. - -## DOCKER\_CERT\_PATH - -Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TLS verification. Defaults to `~/.docker`. - -## COMPOSE\_HTTP\_TIMEOUT - -Configures the time (in seconds) a request to the Docker daemon is allowed to hang before Compose considers -it failed. Defaults to 60 seconds. - -## COMPOSE\_TLS\_VERSION - -Configure which TLS version is used for TLS communication with the `docker` -daemon. Defaults to `TLSv1`. -Supported values are: `TLSv1`, `TLSv1_1`, `TLSv1_2`. - -## Related Information - -- [User guide](../index.md) -- [Installing Compose](../install.md) -- [Compose file reference](../compose-file.md) -- [Environment file](../env-file.md) diff --git a/docs/reference/events.md b/docs/reference/events.md deleted file mode 100644 index 827258f2..00000000 --- a/docs/reference/events.md +++ /dev/null @@ -1,34 +0,0 @@ - - -# events - -``` -Usage: events [options] [SERVICE...] - -Options: - --json Output events as a stream of json objects -``` - -Stream container events for every container in the project. - -With the `--json` flag, a json object will be printed one per line with the -format: - -``` -{ - "service": "web", - "event": "create", - "container": "213cf75fc39a", - "image": "alpine:edge", - "time": "2015-11-20T18:01:03.615550", -} -``` diff --git a/docs/reference/exec.md b/docs/reference/exec.md deleted file mode 100644 index 6c0eeb04..00000000 --- a/docs/reference/exec.md +++ /dev/null @@ -1,29 +0,0 @@ - - -# exec - -``` -Usage: exec [options] SERVICE COMMAND [ARGS...] - -Options: --d Detached mode: Run command in the background. ---privileged Give extended privileges to the process. ---user USER Run the command as this user. --T Disable pseudo-tty allocation. By default `docker-compose exec` - allocates a TTY. ---index=index index of the container if there are multiple - instances of a service [default: 1] -``` - -This is equivalent of `docker exec`. With this subcommand you can run arbitrary -commands in your services. Commands are by default allocating a TTY, so you can -do e.g. `docker-compose exec web sh` to get an interactive prompt. diff --git a/docs/reference/help.md b/docs/reference/help.md deleted file mode 100644 index 613708ed..00000000 --- a/docs/reference/help.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# help - -``` -Usage: help COMMAND -``` - -Displays help and usage instructions for a command. diff --git a/docs/reference/index.md b/docs/reference/index.md deleted file mode 100644 index 2ac3676a..00000000 --- a/docs/reference/index.md +++ /dev/null @@ -1,42 +0,0 @@ - - -## Compose command-line reference - -The following pages describe the usage information for the [docker-compose](overview.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line. - -* [docker-compose](overview.md) -* [build](build.md) -* [config](config.md) -* [create](create.md) -* [down](down.md) -* [events](events.md) -* [help](help.md) -* [kill](kill.md) -* [logs](logs.md) -* [pause](pause.md) -* [port](port.md) -* [ps](ps.md) -* [pull](pull.md) -* [restart](restart.md) -* [rm](rm.md) -* [run](run.md) -* [scale](scale.md) -* [start](start.md) -* [stop](stop.md) -* [unpause](unpause.md) -* [up](up.md) - -## Where to go next - -* [CLI environment variables](envvars.md) -* [docker-compose Command](overview.md) diff --git a/docs/reference/kill.md b/docs/reference/kill.md deleted file mode 100644 index dc4bf23a..00000000 --- a/docs/reference/kill.md +++ /dev/null @@ -1,24 +0,0 @@ - - -# kill - -``` -Usage: kill [options] [SERVICE...] - -Options: --s SIGNAL SIGNAL to send to the container. Default signal is SIGKILL. -``` - -Forces running containers to stop by sending a `SIGKILL` signal. Optionally the -signal can be passed, for example: - - $ docker-compose kill -s SIGINT diff --git a/docs/reference/logs.md b/docs/reference/logs.md deleted file mode 100644 index 745d24f7..00000000 --- a/docs/reference/logs.md +++ /dev/null @@ -1,25 +0,0 @@ - - -# logs - -``` -Usage: logs [options] [SERVICE...] - -Options: ---no-color Produce monochrome output. --f, --follow Follow log output --t, --timestamps Show timestamps ---tail Number of lines to show from the end of the logs - for each container. -``` - -Displays log output from services. diff --git a/docs/reference/overview.md b/docs/reference/overview.md deleted file mode 100644 index d59fa565..00000000 --- a/docs/reference/overview.md +++ /dev/null @@ -1,127 +0,0 @@ - - - -# Overview of docker-compose CLI - -This page provides the usage information for the `docker-compose` Command. -You can also see this information by running `docker-compose --help` from the -command line. - -``` -Define and run multi-container applications with Docker. - -Usage: - docker-compose [-f=...] [options] [COMMAND] [ARGS...] - docker-compose -h|--help - -Options: - -f, --file FILE Specify an alternate compose file (default: docker-compose.yml) - -p, --project-name NAME Specify an alternate project name (default: directory name) - --verbose Show more output - -v, --version Print version and exit - -H, --host HOST Daemon socket to connect to - - --tls Use TLS; implied by --tlsverify - --tlscacert CA_PATH Trust certs signed only by this CA - --tlscert CLIENT_CERT_PATH Path to TLS certificate file - --tlskey TLS_KEY_PATH Path to TLS key file - --tlsverify Use TLS and verify the remote - --skip-hostname-check Don't check the daemon's hostname against the name specified - in the client certificate (for example if your docker host - is an IP address) - -Commands: - build Build or rebuild services - config Validate and view the compose file - create Create services - down Stop and remove containers, networks, images, and volumes - events Receive real time events from containers - help Get help on a command - kill Kill containers - logs View output from containers - pause Pause services - port Print the public port for a port binding - ps List containers - pull Pulls service images - restart Restart services - rm Remove stopped containers - run Run a one-off command - scale Set number of containers for a service - start Start services - stop Stop services - unpause Unpause services - up Create and start containers - version Show the Docker-Compose version information - -``` - -The Docker Compose binary. You use this command to build and manage multiple -services in Docker containers. - -Use the `-f` flag to specify the location of a Compose configuration file. You -can supply multiple `-f` configuration files. When you supply multiple files, -Compose combines them into a single configuration. Compose builds the -configuration in the order you supply the files. Subsequent files override and -add to their successors. - -For example, consider this command line: - -``` -$ docker-compose -f docker-compose.yml -f docker-compose.admin.yml run backup_db` -``` - -The `docker-compose.yml` file might specify a `webapp` service. - -``` -webapp: - image: examples/web - ports: - - "8000:8000" - volumes: - - "/data" -``` - -If the `docker-compose.admin.yml` also specifies this same service, any matching -fields will override the previous file. New values, add to the `webapp` service -configuration. - -``` -webapp: - build: . - environment: - - DEBUG=1 -``` - -Use a `-f` with `-` (dash) as the filename to read the configuration from -stdin. When stdin is used all paths in the configuration are -relative to the current working directory. - -The `-f` flag is optional. If you don't provide this flag on the command line, -Compose traverses the working directory and its parent directories looking for a -`docker-compose.yml` and a `docker-compose.override.yml` file. You must -supply at least the `docker-compose.yml` file. If both files are present on the -same directory level, Compose combines the two files into a single configuration. -The configuration in the `docker-compose.override.yml` file is applied over and -in addition to the values in the `docker-compose.yml` file. - -See also the `COMPOSE_FILE` [environment variable](envvars.md#compose-file). - -Each configuration has a project name. If you supply a `-p` flag, you can -specify a project name. If you don't specify the flag, Compose uses the current -directory name. See also the `COMPOSE_PROJECT_NAME` [environment variable]( -envvars.md#compose-project-name) - - -## Where to go next - -* [CLI environment variables](envvars.md) diff --git a/docs/reference/pause.md b/docs/reference/pause.md deleted file mode 100644 index a0ffab03..00000000 --- a/docs/reference/pause.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# pause - -``` -Usage: pause [SERVICE...] -``` - -Pauses running containers of a service. They can be unpaused with `docker-compose unpause`. diff --git a/docs/reference/port.md b/docs/reference/port.md deleted file mode 100644 index c946a97d..00000000 --- a/docs/reference/port.md +++ /dev/null @@ -1,23 +0,0 @@ - - -# port - -``` -Usage: port [options] SERVICE PRIVATE_PORT - -Options: ---protocol=proto tcp or udp [default: tcp] ---index=index index of the container if there are multiple - instances of a service [default: 1] -``` - -Prints the public port for a port binding. diff --git a/docs/reference/ps.md b/docs/reference/ps.md deleted file mode 100644 index 546d68e7..00000000 --- a/docs/reference/ps.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# ps - -``` -Usage: ps [options] [SERVICE...] - -Options: --q Only display IDs -``` - -Lists containers. diff --git a/docs/reference/pull.md b/docs/reference/pull.md deleted file mode 100644 index 5ec184b7..00000000 --- a/docs/reference/pull.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# pull - -``` -Usage: pull [options] [SERVICE...] - -Options: ---ignore-pull-failures Pull what it can and ignores images with pull failures. -``` - -Pulls service images. diff --git a/docs/reference/push.md b/docs/reference/push.md deleted file mode 100644 index bdc3112e..00000000 --- a/docs/reference/push.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# push - -``` -Usage: push [options] [SERVICE...] - -Options: - --ignore-push-failures Push what it can and ignores images with push failures. -``` - -Pushes images for services. diff --git a/docs/reference/restart.md b/docs/reference/restart.md deleted file mode 100644 index bbd4a68b..00000000 --- a/docs/reference/restart.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# restart - -``` -Usage: restart [options] [SERVICE...] - -Options: --t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) -``` - -Restarts services. diff --git a/docs/reference/rm.md b/docs/reference/rm.md deleted file mode 100644 index 6351e6cf..00000000 --- a/docs/reference/rm.md +++ /dev/null @@ -1,28 +0,0 @@ - - -# rm - -``` -Usage: rm [options] [SERVICE...] - -Options: - -f, --force Don't ask to confirm removal - -v Remove any anonymous volumes attached to containers - -a, --all Deprecated - no effect. -``` - -Removes stopped service containers. - -By default, anonymous volumes attached to containers will not be removed. You -can override this with `-v`. To list all volumes, use `docker volume ls`. - -Any data which is not in a volume will be lost. diff --git a/docs/reference/run.md b/docs/reference/run.md deleted file mode 100644 index 86354424..00000000 --- a/docs/reference/run.md +++ /dev/null @@ -1,56 +0,0 @@ - - -# run - -``` -Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...] - -Options: --d Detached mode: Run container in the background, print - new container name. ---name NAME Assign a name to the container ---entrypoint CMD Override the entrypoint of the image. --e KEY=VAL Set an environment variable (can be used multiple times) --u, --user="" Run as specified username or uid ---no-deps Don't start linked services. ---rm Remove container after run. Ignored in detached mode. --p, --publish=[] Publish a container's port(s) to the host ---service-ports Run command with the service's ports enabled and mapped to the host. --T Disable pseudo-tty allocation. By default `docker-compose run` allocates a TTY. --w, --workdir="" Working directory inside the container -``` - -Runs a one-time command against a service. For example, the following command starts the `web` service and runs `bash` as its command. - - $ docker-compose run web bash - -Commands you use with `run` start in new containers with the same configuration as defined by the service' configuration. This means the container has the same volumes, links, as defined in the configuration file. There two differences though. - -First, the command passed by `run` overrides the command defined in the service configuration. For example, if the `web` service configuration is started with `bash`, then `docker-compose run web python app.py` overrides it with `python app.py`. - -The second difference is the `docker-compose run` command does not create any of the ports specified in the service configuration. This prevents the port collisions with already open ports. If you *do want* the service's ports created and mapped to the host, specify the `--service-ports` flag: - - $ docker-compose run --service-ports web python manage.py shell - -Alternatively manual port mapping can be specified. Same as when running Docker's `run` command - using `--publish` or `-p` options: - - $ docker-compose run --publish 8080:80 -p 2022:22 -p 127.0.0.1:2021:21 web python manage.py shell - -If you start a service configured with links, the `run` command first checks to see if the linked service is running and starts the service if it is stopped. Once all the linked services are running, the `run` executes the command you passed it. So, for example, you could run: - - $ docker-compose run db psql -h db -U docker - -This would open up an interactive PostgreSQL shell for the linked `db` container. - -If you do not want the `run` command to start linked containers, specify the `--no-deps` flag: - - $ docker-compose run --no-deps web python manage.py shell diff --git a/docs/reference/scale.md b/docs/reference/scale.md deleted file mode 100644 index 75140ee9..00000000 --- a/docs/reference/scale.md +++ /dev/null @@ -1,21 +0,0 @@ - - -# scale - -``` -Usage: scale [SERVICE=NUM...] -``` - -Sets the number of containers to run for a service. - -Numbers are specified as arguments in the form `service=num`. For example: - - $ docker-compose scale web=2 worker=3 diff --git a/docs/reference/start.md b/docs/reference/start.md deleted file mode 100644 index f0bdd5a9..00000000 --- a/docs/reference/start.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# start - -``` -Usage: start [SERVICE...] -``` - -Starts existing containers for a service. diff --git a/docs/reference/stop.md b/docs/reference/stop.md deleted file mode 100644 index ec7e6688..00000000 --- a/docs/reference/stop.md +++ /dev/null @@ -1,22 +0,0 @@ - - -# stop - -``` -Usage: stop [options] [SERVICE...] - -Options: --t, --timeout TIMEOUT Specify a shutdown timeout in seconds (default: 10). -``` - -Stops running containers without removing them. They can be started again with -`docker-compose start`. diff --git a/docs/reference/unpause.md b/docs/reference/unpause.md deleted file mode 100644 index 846b229e..00000000 --- a/docs/reference/unpause.md +++ /dev/null @@ -1,18 +0,0 @@ - - -# unpause - -``` -Usage: unpause [SERVICE...] -``` - -Unpauses paused containers of a service. diff --git a/docs/reference/up.md b/docs/reference/up.md deleted file mode 100644 index 3951f879..00000000 --- a/docs/reference/up.md +++ /dev/null @@ -1,55 +0,0 @@ - - -# up - -``` -Usage: up [options] [SERVICE...] - -Options: - -d Detached mode: Run containers in the background, - print new container names. - Incompatible with --abort-on-container-exit. - --no-color Produce monochrome output. - --no-deps Don't start linked services. - --force-recreate Recreate containers even if their configuration - and image haven't changed. - Incompatible with --no-recreate. - --no-recreate If containers already exist, don't recreate them. - Incompatible with --force-recreate. - --no-build Don't build an image, even if it's missing. - --build Build images before starting containers. - --abort-on-container-exit Stops all containers if any container was stopped. - Incompatible with -d. - -t, --timeout TIMEOUT Use this timeout in seconds for container shutdown - when attached or when containers are already - running. (default: 10) - --remove-orphans Remove containers for services not defined in - the Compose file - -``` - -Builds, (re)creates, starts, and attaches to containers for a service. - -Unless they are already running, this command also starts any linked services. - -The `docker-compose up` command aggregates the output of each container. When -the command exits, all containers are stopped. Running `docker-compose up -d` -starts the containers in the background and leaves them running. - -If there are existing containers for a service, and the service's configuration -or image was changed after the container's creation, `docker-compose up` picks -up the changes by stopping and recreating the containers (preserving mounted -volumes). To prevent Compose from picking up changes, use the `--no-recreate` -flag. - -If you want to force Compose to stop and recreate all containers, use the -`--force-recreate` flag. diff --git a/docs/startup-order.md b/docs/startup-order.md deleted file mode 100644 index c67e1829..00000000 --- a/docs/startup-order.md +++ /dev/null @@ -1,88 +0,0 @@ - - -# Controlling startup order in Compose - -You can control the order of service startup with the -[depends_on](compose-file.md#depends-on) option. Compose always starts -containers in dependency order, where dependencies are determined by -`depends_on`, `links`, `volumes_from` and `network_mode: "service:..."`. - -However, Compose will not wait until a container is "ready" (whatever that means -for your particular application) - only until it's running. There's a good -reason for this. - -The problem of waiting for a database (for example) to be ready is really just -a subset of a much larger problem of distributed systems. In production, your -database could become unavailable or move hosts at any time. Your application -needs to be resilient to these types of failures. - -To handle this, your application should attempt to re-establish a connection to -the database after a failure. If the application retries the connection, -it should eventually be able to connect to the database. - -The best solution is to perform this check in your application code, both at -startup and whenever a connection is lost for any reason. However, if you don't -need this level of resilience, you can work around the problem with a wrapper -script: - -- Use a tool such as [wait-for-it](https://github.com/vishnubob/wait-for-it) - or [dockerize](https://github.com/jwilder/dockerize). These are small - wrapper scripts which you can include in your application's image and will - poll a given host and port until it's accepting TCP connections. - - Supposing your application's image has a `CMD` set in its Dockerfile, you - can wrap it by setting the entrypoint in `docker-compose.yml`: - - version: "2" - services: - web: - build: . - ports: - - "80:8000" - depends_on: - - "db" - entrypoint: ./wait-for-it.sh db:5432 - db: - image: postgres - -- Write your own wrapper script to perform a more application-specific health - check. For example, you might want to wait until Postgres is definitely - ready to accept commands: - - #!/bin/bash - - set -e - - host="$1" - shift - cmd="$@" - - until psql -h "$host" -U "postgres" -c '\l'; do - >&2 echo "Postgres is unavailable - sleeping" - sleep 1 - done - - >&2 echo "Postgres is up - executing command" - exec $cmd - - You can use this as a wrapper script as in the previous example, by setting - `entrypoint: ./wait-for-postgres.sh db`. - - -## Compose documentation - -- [Installing Compose](install.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Get started with WordPress](wordpress.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) diff --git a/docs/swarm.md b/docs/swarm.md deleted file mode 100644 index f956f8c2..00000000 --- a/docs/swarm.md +++ /dev/null @@ -1,185 +0,0 @@ - - - -# Using Compose with Swarm - -> **Note:** “Swarm” here refers to [Docker Swarm](/swarm/overview.md), a product separate from Docker Engine. It does _not_ refer to [swarm mode](/engine/swarm), which is a built-in feature of Docker Engine introduced in version 1.12. -> -> Integration between Compose and swarm mode is at the experimental stage. See [Docker Stacks and Bundles](bundles.md) for details. - -Docker Compose and [Docker Swarm](/swarm/overview.md) 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](https://docs.docker.com/engine/userguide/networking/dockernetworks/#an-overlay-network), - or a custom driver which supports multi-host networking. - -Read [Get started with multi-host networking](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) to see how to -set up a Swarm cluster with [Docker Machine](/machine/overview.md) 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). diff --git a/docs/wordpress.md b/docs/wordpress.md deleted file mode 100644 index b39a8bbb..00000000 --- a/docs/wordpress.md +++ /dev/null @@ -1,112 +0,0 @@ - - - -# Quickstart: Docker Compose and WordPress - -You can use Docker Compose to easily run WordPress in an isolated environment built -with Docker containers. This quick-start guide demonstrates how to use Compose to set up and run WordPress. Before starting, you'll need to have -[Compose installed](install.md). - -### Define the project - -1. Create an empty project directory. - - You can name the directory something easy for you to remember. This directory is the context for your application image. The directory should only contain resources to build that image. - - This project directory will contain a `docker-compose.yaml` file which will be complete in itself for a good starter wordpress project. - -2. Change directories into your project directory. - - For example, if you named your directory `my_wordpress`: - - $ cd my-wordpress/ - -3. Create a `docker-compose.yml` file that will start your `Wordpress` blog and a separate `MySQL` instance with a volume mount for data persistence: - - version: '2' - services: - db: - image: mysql:5.7 - volumes: - - "./.data/db:/var/lib/mysql" - restart: always - environment: - MYSQL_ROOT_PASSWORD: wordpress - MYSQL_DATABASE: wordpress - MYSQL_USER: wordpress - MYSQL_PASSWORD: wordpress - - wordpress: - depends_on: - - db - image: wordpress:latest - links: - - db - ports: - - "8000:80" - restart: always - environment: - WORDPRESS_DB_HOST: db:3306 - WORDPRESS_DB_PASSWORD: wordpress - - **NOTE**: The folder `./.data/db` will be automatically created in the project directory - alongside the `docker-compose.yml` which will persist any updates made by wordpress to the - database. - -### Build the project - -Now, run `docker-compose up -d` from your project directory. - -This pulls the needed images, and starts the wordpress and database containers, as shown in the example below. - - $ docker-compose up -d - Creating network "my_wordpress_default" with the default driver - Pulling db (mysql:5.7)... - 5.7: Pulling from library/mysql - efd26ecc9548: Pull complete - a3ed95caeb02: Pull complete - ... - Digest: sha256:34a0aca88e85f2efa5edff1cea77cf5d3147ad93545dbec99cfe705b03c520de - Status: Downloaded newer image for mysql:5.7 - Pulling wordpress (wordpress:latest)... - latest: Pulling from library/wordpress - efd26ecc9548: Already exists - a3ed95caeb02: Pull complete - 589a9d9a7c64: Pull complete - ... - Digest: sha256:ed28506ae44d5def89075fd5c01456610cd6c64006addfe5210b8c675881aff6 - Status: Downloaded newer image for wordpress:latest - Creating my_wordpress_db_1 - Creating my_wordpress_wordpress_1 - -### Bring up WordPress in a web browser - -If you're using [Docker Machine](https://docs.docker.com/machine/), then `docker-machine ip MACHINE_VM` gives you the machine address and you can open `http://MACHINE_VM_IP:8000` in a browser. - -At this point, WordPress should be running on port `8000` of your Docker Host, and you can complete the "famous five-minute installation" as a WordPress administrator. - -**NOTE**: The Wordpress site will not be immediately available on port `8000` because the containers are still being initialized and may take a couple of minutes before the first load. - -![Choose language for WordPress install](images/wordpress-lang.png) - -![WordPress Welcome](images/wordpress-welcome.png) - - -## More Compose documentation - -- [User guide](index.md) -- [Installing Compose](install.md) -- [Getting Started](gettingstarted.md) -- [Get started with Django](django.md) -- [Get started with Rails](rails.md) -- [Command line reference](./reference/index.md) -- [Compose file reference](compose-file.md) From a8ff4285d1bdcafb0a4c1bd176c052a9898ec1e8 Mon Sep 17 00:00:00 2001 From: Victoria Bialas Date: Wed, 5 Oct 2016 16:19:09 -0700 Subject: [PATCH 104/118] updated README per vnext branch plan Signed-off-by: Victoria Bialas --- docs/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/README.md b/docs/README.md index 03d2e3a7..50c91d20 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,5 +6,11 @@ The documentation for Compose has been merged into The docs for Compose are now here: https://github.com/docker/docker.github.io/tree/master/compose +Please submit pull requests for unpublished features on the `vnext-compose` branch (https://github.com/docker/docker.github.io/tree/vnext-compose). + +If you submit a PR to this codebase that has a docs impact, create a second docs PR on `docker.github.io`. Use the docs PR template provided (coming soon - watch this space). + +PRs for typos, additional information, etc. for already-published features should be labeled as `okay-to-publish` (we are still settling on a naming convention, will provide a label soon). You can submit these PRs either to `vnext-compose` or directly to `master` on `docker.github.io` + As always, the docs remain open-source and we appreciate your feedback and pull requests! From b50b14f937455889fa0d62c451941d58b3cec124 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 28 Sep 2016 13:13:04 -0700 Subject: [PATCH 105/118] Fix openssl dependency in OSX binary build Signed-off-by: Joffrey F --- script/setup/osx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/script/setup/osx b/script/setup/osx index 39941de2..e6ab62a8 100755 --- a/script/setup/osx +++ b/script/setup/osx @@ -10,12 +10,12 @@ openssl_version() { python -c "import ssl; print ssl.OPENSSL_VERSION" } -desired_python_version="2.7.9" -desired_python_brew_version="2.7.9" -python_formula="https://raw.githubusercontent.com/Homebrew/homebrew/1681e193e4d91c9620c4901efd4458d9b6fcda8e/Library/Formula/python.rb" +desired_python_version="2.7.12" +desired_python_brew_version="2.7.12" +python_formula="https://raw.githubusercontent.com/Homebrew/homebrew-core/737a2e34a89b213c1f0a2a24fc1a3c06635eed04/Formula/python.rb" -desired_openssl_version="1.0.2h" -desired_openssl_brew_version="1.0.2h" +desired_openssl_version="1.0.2j" +desired_openssl_brew_version="1.0.2j" openssl_formula="https://raw.githubusercontent.com/Homebrew/homebrew-core/30d3766453347f6e22b3ed6c74bb926d6def2eb5/Formula/openssl.rb" PATH="/usr/local/bin:$PATH" From 2bce81508effe82c0bca3603769bdd2cc1b8d00f Mon Sep 17 00:00:00 2001 From: Daniel Nephin Date: Thu, 6 Oct 2016 17:04:19 -0400 Subject: [PATCH 106/118] Support non-alphanumeric default values. Signed-off-by: Daniel Nephin --- compose/config/interpolation.py | 2 +- tests/unit/config/interpolation_test.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index cb841437..1b270b9e 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -72,7 +72,7 @@ def recursive_interpolate(obj, interpolator): class TemplateWithDefaults(Template): - idpattern = r'[_a-z][_a-z0-9]*(?::?-[_a-z0-9]+)?' + idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?' # Modified from python2.7/string.py def substitute(self, mapping): diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index 22444495..fd40153d 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -113,6 +113,7 @@ def test_interpolate_with_value(defaults_interpolator): def test_interpolate_missing_with_default(defaults_interpolator): assert defaults_interpolator("ok ${missing:-def}") == "ok def" assert defaults_interpolator("ok ${missing-def}") == "ok def" + assert defaults_interpolator("ok ${BAR:-/non:-alphanumeric}") == "ok /non:-alphanumeric" def test_interpolate_with_empty_and_default_value(defaults_interpolator): From 17c5b45641a4fd7a210d1b73a96d6e6357a8cb12 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 6 Oct 2016 15:39:47 -0700 Subject: [PATCH 107/118] Handle formatter case where logrecord message is binary containing unicode characters. Signed-off-by: Joffrey F --- compose/cli/formatter.py | 5 ++++- tests/unit/cli/formatter_test.py | 28 +++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/compose/cli/formatter.py b/compose/cli/formatter.py index d0ed0f87..5f580645 100644 --- a/compose/cli/formatter.py +++ b/compose/cli/formatter.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import logging import os +import six import texttable from compose.cli import colors @@ -44,5 +45,7 @@ class ConsoleWarningFormatter(logging.Formatter): return '' def format(self, record): + if isinstance(record.msg, six.binary_type): + record.msg = record.msg.decode('utf-8') message = super(ConsoleWarningFormatter, self).format(record) - return self.get_level_message(record) + message + return '{0}{1}'.format(self.get_level_message(record), message) diff --git a/tests/unit/cli/formatter_test.py b/tests/unit/cli/formatter_test.py index 1c3b6a68..4aa025e6 100644 --- a/tests/unit/cli/formatter_test.py +++ b/tests/unit/cli/formatter_test.py @@ -11,8 +11,8 @@ from tests import unittest MESSAGE = 'this is the message' -def makeLogRecord(level): - return logging.LogRecord('name', level, 'pathame', 0, MESSAGE, (), None) +def make_log_record(level, message=None): + return logging.LogRecord('name', level, 'pathame', 0, message or MESSAGE, (), None) class ConsoleWarningFormatterTestCase(unittest.TestCase): @@ -21,15 +21,33 @@ class ConsoleWarningFormatterTestCase(unittest.TestCase): self.formatter = ConsoleWarningFormatter() def test_format_warn(self): - output = self.formatter.format(makeLogRecord(logging.WARN)) + output = self.formatter.format(make_log_record(logging.WARN)) expected = colors.yellow('WARNING') + ': ' assert output == expected + MESSAGE def test_format_error(self): - output = self.formatter.format(makeLogRecord(logging.ERROR)) + output = self.formatter.format(make_log_record(logging.ERROR)) expected = colors.red('ERROR') + ': ' assert output == expected + MESSAGE def test_format_info(self): - output = self.formatter.format(makeLogRecord(logging.INFO)) + output = self.formatter.format(make_log_record(logging.INFO)) assert output == MESSAGE + + def test_format_unicode_info(self): + message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' + output = self.formatter.format(make_log_record(logging.INFO, message)) + print(output) + assert output == message.decode('utf-8') + + def test_format_unicode_warn(self): + message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' + output = self.formatter.format(make_log_record(logging.WARN, message)) + expected = colors.yellow('WARNING') + ': ' + assert output == '{0}{1}'.format(expected, message.decode('utf-8')) + + def test_format_unicode_error(self): + message = b'\xec\xa0\x95\xec\x88\x98\xec\xa0\x95' + output = self.formatter.format(make_log_record(logging.ERROR, message)) + expected = colors.red('ERROR') + ': ' + assert output == '{0}{1}'.format(expected, message.decode('utf-8')) From e5ded6ff9b56835d9b40b3b6768658f02f347b07 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Thu, 29 Sep 2016 12:51:01 -0700 Subject: [PATCH 108/118] Add support for enable_ipv6 in network definition. Signed-off-by: Joffrey F --- compose/config/config_schema_v2.1.json | 3 +- compose/network.py | 5 ++- tests/integration/project_test.py | 45 ++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index de4ddf25..617f8ebe 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -246,7 +246,8 @@ "name": {"type": "string"} }, "additionalProperties": false - } + }, + "enable_ipv6": {"type": "boolean"} }, "additionalProperties": false }, diff --git a/compose/network.py b/compose/network.py index 8962a892..c3af9aa1 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, internal=False): + ipam=None, external_name=None, internal=False, enable_ipv6=False): self.client = client self.project = project self.name = name @@ -24,6 +24,7 @@ class Network(object): self.ipam = create_ipam_config_from_dict(ipam) self.external_name = external_name self.internal = internal + self.enable_ipv6 = enable_ipv6 def ensure(self): if self.external_name: @@ -70,6 +71,7 @@ class Network(object): options=self.driver_opts, ipam=self.ipam, internal=self.internal, + enable_ipv6=self.enable_ipv6 ) def remove(self): @@ -118,6 +120,7 @@ def build_networks(name, config_data, client): ipam=data.get('ipam'), external_name=data.get('external_name'), internal=data.get('internal'), + enable_ipv6=data.get('enable_ipv6'), ) for network_name, data in network_config.items() } diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 4427fe6b..94eac530 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -721,6 +721,51 @@ class ProjectTest(DockerClientTestCase): assert IPAMConfig.get('IPv4Address') == '172.16.100.100' assert IPAMConfig.get('IPv6Address') == 'fe80::1001:102' + @v2_1_only() + def test_up_with_enable_ipv6(self): + self.require_api_version('1.23') + config_data = config.Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'command': 'top', + 'networks': { + 'static_test': { + 'ipv6_address': 'fe80::1001:102' + } + }, + }], + volumes={}, + networks={ + 'static_test': { + 'driver': 'bridge', + 'enable_ipv6': True, + 'ipam': { + 'driver': 'default', + 'config': [ + {"subnet": "fe80::/64", + "gateway": "fe80::1001:1"} + ] + } + } + } + ) + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + project.up(detached=True) + network = self.client.networks(names=['static_test'])[0] + service_container = project.get_service('web').containers()[0] + + assert network['EnableIPv6'] is True + ipam_config = (service_container.inspect().get('NetworkSettings', {}). + get('Networks', {}).get('composetest_static_test', {}). + get('IPAMConfig', {})) + assert ipam_config.get('IPv6Address') == 'fe80::1001:102' + @v2_only() def test_up_with_network_static_addresses_missing_subnet(self): config_data = config.Config( From 0603b445e28502fc27850ce16dbdb158b3089f7e Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 4 Oct 2016 17:35:20 -0700 Subject: [PATCH 109/118] Properly merge logging dictionaries in overriding configs Signed-off-by: Joffrey F --- compose/config/config.py | 12 +++ tests/unit/config/config_test.py | 158 ++++++++++++++++++++++++++++++- 2 files changed, 169 insertions(+), 1 deletion(-) diff --git a/compose/config/config.py b/compose/config/config.py index aea1e094..8582d83c 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -760,6 +760,8 @@ def merge_service_dicts(base, override, version): for field in ['dns', 'dns_search', 'env_file', 'tmpfs']: md.merge_field(field, merge_list_or_string) + md.merge_field('logging', merge_logging) + for field in set(ALLOWED_KEYS) - set(md): md.merge_scalar(field) @@ -789,6 +791,16 @@ def merge_build(output, base, override): return dict(md) +def merge_logging(base, override): + md = MergeDict(base, override) + md.merge_scalar('driver') + if md.get('driver') == base.get('driver') or base.get('driver') is None: + md.merge_mapping('options', lambda m: m or {}) + else: + md['options'] = override.get('options') + return dict(md) + + def legacy_v1_merge_image_or_build(output, base, override): output.pop('image', None) output.pop('build', None) diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 88b990e5..d9269ab4 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1330,7 +1330,7 @@ class ConfigTest(unittest.TestCase): 'image': 'alpine', 'group_add': ["docker", 777] } - } + } })) assert actual.services == [ @@ -1429,6 +1429,162 @@ class ConfigTest(unittest.TestCase): 'command': 'true', } + def test_merge_logging_v2(self): + base = { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'json-file', + 'options': { + 'frequency': '2000', + 'timeout': '23' + } + } + } + override = { + 'logging': { + 'options': { + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + actual = config.merge_service_dicts(base, override, V2_0) + assert actual == { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'json-file', + 'options': { + 'frequency': '2000', + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + def test_merge_logging_v2_override_driver(self): + base = { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'json-file', + 'options': { + 'frequency': '2000', + 'timeout': '23' + } + } + } + override = { + 'logging': { + 'driver': 'syslog', + 'options': { + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + actual = config.merge_service_dicts(base, override, V2_0) + assert actual == { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'syslog', + 'options': { + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + def test_merge_logging_v2_no_base_driver(self): + base = { + 'image': 'alpine:edge', + 'logging': { + 'options': { + 'frequency': '2000', + 'timeout': '23' + } + } + } + override = { + 'logging': { + 'driver': 'json-file', + 'options': { + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + actual = config.merge_service_dicts(base, override, V2_0) + assert actual == { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'json-file', + 'options': { + 'frequency': '2000', + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + def test_merge_logging_v2_no_drivers(self): + base = { + 'image': 'alpine:edge', + 'logging': { + 'options': { + 'frequency': '2000', + 'timeout': '23' + } + } + } + override = { + 'logging': { + 'options': { + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + actual = config.merge_service_dicts(base, override, V2_0) + assert actual == { + 'image': 'alpine:edge', + 'logging': { + 'options': { + 'frequency': '2000', + 'timeout': '360', + 'pretty-print': 'on' + } + } + } + + def test_merge_logging_v2_no_override_options(self): + base = { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'json-file', + 'options': { + 'frequency': '2000', + 'timeout': '23' + } + } + } + override = { + 'logging': { + 'driver': 'syslog' + } + } + + actual = config.merge_service_dicts(base, override, V2_0) + assert actual == { + 'image': 'alpine:edge', + 'logging': { + 'driver': 'syslog', + 'options': None + } + } + def test_external_volume_config(self): config_details = build_config_details({ 'version': '2', From bdcce13f4a50126ba42b76f70d1526dbe0ce3068 Mon Sep 17 00:00:00 2001 From: dbdd Date: Tue, 27 Sep 2016 11:02:56 +0800 Subject: [PATCH 110/118] add support for creating volume and network with label definition Signed-off-by: dbdd --- compose/config/config.py | 3 + compose/config/config_schema_v2.1.json | 4 +- compose/network.py | 5 +- compose/volume.py | 8 ++- tests/acceptance/cli_test.py | 41 ++++++++++++ tests/fixtures/networks/network-label.yml | 13 ++++ tests/fixtures/volumes/volume-label.yml | 13 ++++ tests/integration/project_test.py | 76 +++++++++++++++++++++++ tests/unit/config/config_test.py | 53 ++++++++++++++++ 9 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/networks/network-label.yml create mode 100644 tests/fixtures/volumes/volume-label.yml diff --git a/compose/config/config.py b/compose/config/config.py index 4d32b50c..b3e01778 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -361,6 +361,9 @@ def load_mapping(config_files, get_func, entity_type): config['driver_opts'] ) + if 'labels' in config: + config['labels'] = parse_labels(config['labels']) + return mapping diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index de4ddf25..980e4ba1 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -246,7 +246,8 @@ "name": {"type": "string"} }, "additionalProperties": false - } + }, + "labels": {"$ref": "#/definitions/list_or_dict"} }, "additionalProperties": false }, @@ -268,6 +269,7 @@ "name": {"type": "string"} } }, + "labels": {"$ref": "#/definitions/list_or_dict"}, "additionalProperties": false }, "additionalProperties": false diff --git a/compose/network.py b/compose/network.py index 8962a892..796836fe 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, internal=False): + ipam=None, external_name=None, internal=False, labels=None): self.client = client self.project = project self.name = name @@ -24,6 +24,7 @@ class Network(object): self.ipam = create_ipam_config_from_dict(ipam) self.external_name = external_name self.internal = internal + self.labels = labels def ensure(self): if self.external_name: @@ -70,6 +71,7 @@ class Network(object): options=self.driver_opts, ipam=self.ipam, internal=self.internal, + labels=self.labels, ) def remove(self): @@ -118,6 +120,7 @@ def build_networks(name, config_data, client): ipam=data.get('ipam'), external_name=data.get('external_name'), internal=data.get('internal'), + labels=data.get('labels'), ) for network_name, data in network_config.items() } diff --git a/compose/volume.py b/compose/volume.py index f440ba40..1fd1d51c 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -12,17 +12,18 @@ log = logging.getLogger(__name__) class Volume(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - external_name=None): + external_name=None, labels=None): self.client = client self.project = project self.name = name self.driver = driver self.driver_opts = driver_opts self.external_name = external_name + self.labels = labels def create(self): return self.client.create_volume( - self.full_name, self.driver, self.driver_opts + self.full_name, self.driver, self.driver_opts, labels=self.labels ) def remove(self): @@ -68,7 +69,8 @@ class ProjectVolumes(object): name=vol_name, driver=data.get('driver'), driver_opts=data.get('driver_opts'), - external_name=data.get('external_name') + external_name=data.get('external_name'), + labels=data.get('labels') ) for vol_name, data in config_volumes.items() } diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 2247ffff..54737c7a 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -22,6 +22,7 @@ from compose.project import OneOffFilter from tests.integration.testcases import DockerClientTestCase from tests.integration.testcases import get_links from tests.integration.testcases import pull_busybox +from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only @@ -767,6 +768,46 @@ class CLITestCase(DockerClientTestCase): container = self.project.containers()[0] assert list(container.get('NetworkSettings.Networks')) == [network_name] + @v2_1_only() + def test_up_with_network_labels(self): + filename = 'network-label.yml' + + self.base_dir = 'tests/fixtures/networks' + self._project = get_project(self.base_dir, [filename]) + + self.dispatch(['-f', filename, 'up', '-d'], returncode=0) + + network_with_label = '{}_network_with_label'.format(self.project.name) + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('{}_'.format(self.project.name)) + ] + + assert [n['Name'] for n in networks] == [network_with_label] + + assert networks[0]['Labels'] == {'label_key': 'label_val'} + + @v2_1_only() + def test_up_with_volume_labels(self): + filename = 'volume-label.yml' + + self.base_dir = 'tests/fixtures/volumes' + self._project = get_project(self.base_dir, [filename]) + + self.dispatch(['-f', filename, 'up', '-d'], returncode=0) + + volume_with_label = '{}_volume_with_label'.format(self.project.name) + + volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].startswith('{}_'.format(self.project.name)) + ] + + assert [v['Name'] for v in volumes] == [volume_with_label] + + assert volumes[0]['Labels'] == {'label_key': 'label_val'} + @v2_only() def test_up_no_services(self): self.base_dir = 'tests/fixtures/no-services' diff --git a/tests/fixtures/networks/network-label.yml b/tests/fixtures/networks/network-label.yml new file mode 100644 index 00000000..fdb24f65 --- /dev/null +++ b/tests/fixtures/networks/network-label.yml @@ -0,0 +1,13 @@ +version: "2.1" + +services: + web: + image: busybox + command: top + networks: + - network_with_label + +networks: + network_with_label: + labels: + - "label_key=label_val" diff --git a/tests/fixtures/volumes/volume-label.yml b/tests/fixtures/volumes/volume-label.yml new file mode 100644 index 00000000..a5f33a5a --- /dev/null +++ b/tests/fixtures/volumes/volume-label.yml @@ -0,0 +1,13 @@ +version: "2.1" + +services: + web: + image: busybox + command: top + volumes: + - volume_with_label:/data + +volumes: + volume_with_label: + labels: + - "label_key=label_val" diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 4427fe6b..149facfe 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -821,6 +821,42 @@ class ProjectTest(DockerClientTestCase): assert network['Internal'] is True + @v2_1_only() + def test_project_up_with_network_label(self): + self.require_api_version('1.23') + + network_name = 'network_with_label' + + config_data = config.Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'networks': {network_name: None} + }], + volumes={}, + networks={ + network_name: {'labels': {'label_key': 'label_val'}} + } + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data + ) + + project.up() + + networks = [ + n for n in self.client.networks() + if n['Name'].startswith('composetest_') + ] + + assert [n['Name'] for n in networks] == ['composetest_{}'.format(network_name)] + + assert networks[0]['Labels'] == {'label_key': 'label_val'} + @v2_only() def test_project_up_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) @@ -847,6 +883,46 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(volume_data['Name'], full_vol_name) self.assertEqual(volume_data['Driver'], 'local') + @v2_1_only() + def test_project_up_with_volume_labels(self): + self.require_api_version('1.23') + + volume_name = 'volume_with_label' + + config_data = config.Config( + version=V2_0, + services=[{ + 'name': 'web', + 'image': 'busybox:latest', + 'volumes': [VolumeSpec.parse('{}:/data'.format(volume_name))] + }], + volumes={ + volume_name: { + 'labels': { + 'label_key': 'label_val' + } + } + }, + networks={}, + ) + + project = Project.from_config( + client=self.client, + name='composetest', + config_data=config_data, + ) + + project.up() + + volumes = [ + v for v in self.client.volumes().get('Volumes', []) + if v['Name'].startswith('composetest_') + ] + + assert [v['Name'] for v in volumes] == ['composetest_{}'.format(volume_name)] + + assert volumes[0]['Labels'] == {'label_key': 'label_val'} + @v2_only() def test_project_up_logging_with_multiple_files(self): base_file = config.ConfigFile( diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 88b990e5..7a8832d2 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -376,6 +376,59 @@ class ConfigTest(unittest.TestCase): } } + def test_load_config_volume_and_network_labels(self): + base_file = config.ConfigFile( + 'base.yaml', + { + 'version': '2.1', + 'services': { + 'web': { + 'image': 'example/web', + }, + }, + 'networks': { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + }, + 'volumes': { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + } + } + ) + + details = config.ConfigDetails('.', [base_file]) + network_dict = config.load(details).networks + volume_dict = config.load(details).volumes + + self.assertEqual( + network_dict, + { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + } + ) + + self.assertEqual( + volume_dict, + { + 'with_label': { + 'labels': { + 'label_key': 'label_val' + } + } + } + ) + def test_load_config_invalid_service_names(self): for invalid_name in ['?not?allowed', ' ', '', '!', '/', '\xe2']: with pytest.raises(ConfigurationError) as exc: From 22d91f60bea78cbe232a1ac86ba747930ecf7d1a Mon Sep 17 00:00:00 2001 From: realityone Date: Fri, 14 Oct 2016 18:20:55 +0800 Subject: [PATCH 111/118] fix serialize restart spec with null string Signed-off-by: realityone --- compose/config/types.py | 2 ++ tests/acceptance/cli_test.py | 4 ++++ tests/fixtures/restart/docker-compose.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/compose/config/types.py b/compose/config/types.py index 9664b580..c450a0f9 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -93,6 +93,8 @@ def parse_restart_spec(restart_config): def serialize_restart_spec(restart_spec): + if not restart_spec: + return '' parts = [restart_spec['Name']] if restart_spec['MaximumRetryCount']: parts.append(six.text_type(restart_spec['MaximumRetryCount'])) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 0c7c17bd..e2c02798 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -236,6 +236,10 @@ class CLITestCase(DockerClientTestCase): 'image': 'busybox', 'restart': 'on-failure:5', }, + 'restart-null': { + 'image': 'busybox', + 'restart': '' + }, }, 'networks': {}, 'volumes': {}, diff --git a/tests/fixtures/restart/docker-compose.yml b/tests/fixtures/restart/docker-compose.yml index 2d10aa39..ecfdfbf5 100644 --- a/tests/fixtures/restart/docker-compose.yml +++ b/tests/fixtures/restart/docker-compose.yml @@ -12,3 +12,6 @@ services: on-failure-5: image: busybox restart: "on-failure:5" + restart-null: + image: busybox + restart: "" From 8b383ad79571fd15e47c88dff71e2a4836bd3ffd Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 14 Oct 2016 17:27:57 +0100 Subject: [PATCH 112/118] Refactor "docker not found" message generation, add Windows message Signed-off-by: Aanand Prasad --- compose/cli/errors.py | 38 ++++++++++++++++++-------------------- compose/cli/utils.py | 5 +++++ 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/compose/cli/errors.py b/compose/cli/errors.py index f9a20b9e..4fdec08a 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -17,6 +17,7 @@ from .utils import call_silently from .utils import is_docker_for_mac_installed from .utils import is_mac from .utils import is_ubuntu +from .utils import is_windows log = logging.getLogger(__name__) @@ -90,11 +91,7 @@ def exit_with_error(msg): def get_conn_error_message(url): if call_silently(['which', 'docker']) != 0: - if is_mac(): - return docker_not_found_mac - if is_ubuntu(): - return docker_not_found_ubuntu - return docker_not_found_generic + return docker_not_found_msg("Couldn't connect to Docker daemon.") if is_docker_for_mac_installed(): return conn_error_docker_for_mac if call_silently(['which', 'docker-machine']) == 0: @@ -102,25 +99,26 @@ def get_conn_error_message(url): return conn_error_generic.format(url=url) -docker_not_found_mac = """ - Couldn't connect to Docker daemon. You might need to install Docker: - - https://docs.docker.com/engine/installation/mac/ -""" +def docker_not_found_msg(problem): + return "{} You might need to install Docker:\n\n{}".format( + problem, docker_install_url()) -docker_not_found_ubuntu = """ - Couldn't connect to Docker daemon. You might need to install Docker: - - https://docs.docker.com/engine/installation/ubuntulinux/ -""" +def docker_install_url(): + if is_mac(): + return docker_install_url_mac + elif is_ubuntu(): + return docker_install_url_ubuntu + elif is_windows(): + return docker_install_url_windows + else: + return docker_install_url_generic -docker_not_found_generic = """ - Couldn't connect to Docker daemon. You might need to install Docker: - - https://docs.docker.com/engine/installation/ -""" +docker_install_url_mac = "https://docs.docker.com/engine/installation/mac/" +docker_install_url_ubuntu = "https://docs.docker.com/engine/installation/ubuntulinux/" +docker_install_url_windows = "https://docs.docker.com/engine/installation/windows/" +docker_install_url_generic = "https://docs.docker.com/engine/installation/" conn_error_docker_machine = """ diff --git a/compose/cli/utils.py b/compose/cli/utils.py index e10a3674..580bd1b0 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -11,6 +11,7 @@ import sys import docker import compose +from ..const import IS_WINDOWS_PLATFORM # WindowsError is not defined on non-win32 platforms. Avoid runtime errors by # defining it as OSError (its parent class) if missing. @@ -73,6 +74,10 @@ def is_ubuntu(): return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu' +def is_windows(): + return IS_WINDOWS_PLATFORM + + def get_version_info(scope): versioninfo = 'docker-compose version {}, build {}'.format( compose.__version__, From 8314a48a2e96a0e34e913fb6b3a2973f1bceec5a Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Mon, 26 Sep 2016 13:18:16 +0100 Subject: [PATCH 113/118] Attach interactively on Windows by shelling out Signed-off-by: Aanand Prasad --- compose/cli/main.py | 60 ++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index 438753a2..cbbb1325 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -7,6 +7,7 @@ import functools import json import logging import re +import subprocess import sys from inspect import getdoc from operator import attrgetter @@ -406,11 +407,6 @@ class TopLevelCommand(object): service = self.project.get_service(options['SERVICE']) detach = options['-d'] - if IS_WINDOWS_PLATFORM and not detach: - raise UserError( - "Interactive mode is not yet supported on Windows.\n" - "Please pass the -d flag when using `docker-compose exec`." - ) try: container = service.get_container(number=index) except ValueError as e: @@ -418,6 +414,28 @@ class TopLevelCommand(object): command = [options['COMMAND']] + options['ARGS'] tty = not options["-T"] + if IS_WINDOWS_PLATFORM and not detach: + args = ["docker", "exec"] + + if options["-d"]: + args += ["--detach"] + else: + args += ["--interactive"] + + if not options["-T"]: + args += ["--tty"] + + if options["--privileged"]: + args += ["--privileged"] + + if options["--user"]: + args += ["--user", options["--user"]] + + args += [container.id] + args += command + + sys.exit(subprocess.call(args)) + create_exec_options = { "privileged": options["--privileged"], "user": options["--user"], @@ -675,12 +693,6 @@ class TopLevelCommand(object): service = self.project.get_service(options['SERVICE']) detach = options['-d'] - if IS_WINDOWS_PLATFORM and not detach: - raise UserError( - "Interactive mode is not yet supported on Windows.\n" - "Please pass the -d flag when using `docker-compose run`." - ) - if options['--publish'] and options['--service-ports']: raise UserError( 'Service port mapping and manual port mapping ' @@ -969,17 +981,21 @@ def run_one_off_container(container_options, project, service, options): signals.set_signal_handler_to_shutdown() try: try: - operation = RunOperation( - project.client, - container.id, - interactive=not options['-T'], - logs=False, - ) - pty = PseudoTerminal(project.client, operation) - sockets = pty.sockets() - service.start_container(container) - pty.start(sockets) - exit_code = container.wait() + if IS_WINDOWS_PLATFORM: + args = ["docker", "start", "--attach", "--interactive", container.id] + exit_code = subprocess.call(args) + else: + operation = RunOperation( + project.client, + container.id, + interactive=not options['-T'], + logs=False, + ) + pty = PseudoTerminal(project.client, operation) + sockets = pty.sockets() + service.start_container(container) + pty.start(sockets) + exit_code = container.wait() except signals.ShutdownException: project.client.stop(container.id) exit_code = 1 From 925915eb2535a069d3eefe4c14d35e5964182ff9 Mon Sep 17 00:00:00 2001 From: Aanand Prasad Date: Fri, 14 Oct 2016 17:30:10 +0100 Subject: [PATCH 114/118] Show clear error when docker binary can't be found Signed-off-by: Aanand Prasad --- compose/cli/main.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/compose/cli/main.py b/compose/cli/main.py index cbbb1325..58b95c2f 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -6,6 +6,7 @@ import contextlib import functools import json import logging +import pipes import re import subprocess import sys @@ -415,7 +416,7 @@ class TopLevelCommand(object): tty = not options["-T"] if IS_WINDOWS_PLATFORM and not detach: - args = ["docker", "exec"] + args = ["exec"] if options["-d"]: args += ["--detach"] @@ -434,7 +435,7 @@ class TopLevelCommand(object): args += [container.id] args += command - sys.exit(subprocess.call(args)) + sys.exit(call_docker(args)) create_exec_options = { "privileged": options["--privileged"], @@ -982,8 +983,7 @@ def run_one_off_container(container_options, project, service, options): try: try: if IS_WINDOWS_PLATFORM: - args = ["docker", "start", "--attach", "--interactive", container.id] - exit_code = subprocess.call(args) + exit_code = call_docker(["start", "--attach", "--interactive", container.id]) else: operation = RunOperation( project.client, @@ -1060,3 +1060,15 @@ def exit_if(condition, message, exit_code): if condition: log.error(message) raise SystemExit(exit_code) + + +def call_docker(args): + try: + executable_path = subprocess.check_output(["which", "docker"]).strip() + except subprocess.CalledProcessError: + raise UserError(errors.docker_not_found_msg("Couldn't find `docker` binary.")) + + args = [executable_path] + args + log.debug(" ".join(map(pipes.quote, args))) + + return subprocess.call(args) From 882084932dd86ecf5e695c8d48f3621b134006d2 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 11 Oct 2016 17:56:02 -0700 Subject: [PATCH 115/118] Upgrade docker-py to latest version Adjust required requests version Signed-off-by: Joffrey F --- requirements.txt | 4 ++-- setup.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7acdd130..474efbdf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyYAML==3.11 backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 -docker-py==1.10.3 +docker-py==1.10.4 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4; python_version < '3.4' @@ -9,7 +9,7 @@ functools32==3.2.3.post2; python_version < '3.2' ipaddress==1.0.16 jsonschema==2.5.1 pypiwin32==219; sys_platform == 'win32' -requests==2.7.0 +requests==2.11.1 six==1.10.0 texttable==0.8.4 websocket-client==0.32.0 diff --git a/setup.py b/setup.py index 80258fbd..442bc80c 100644 --- a/setup.py +++ b/setup.py @@ -31,10 +31,10 @@ install_requires = [ 'cached-property >= 1.2.0, < 2', 'docopt >= 0.6.1, < 0.7', 'PyYAML >= 3.10, < 4', - 'requests >= 2.6.1, < 2.8', + 'requests >= 2.6.1, != 2.11.0, < 2.12', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker-py >= 1.10.3, < 2.0', + 'docker-py >= 1.10.4, < 2.0', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', From efb09af271a1522914431818409b1c16c4bd24a9 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Mon, 10 Oct 2016 17:44:49 -0700 Subject: [PATCH 116/118] Do not normalize volume paths on Windows by default Add environment variable to enable normalization if needed. Do not normalize internal paths Signed-off-by: Joffrey F --- compose/config/config.py | 5 ++- compose/config/types.py | 27 +++++++++++---- tests/unit/config/types_test.py | 58 +++++++++++++++++++++++++-------- tests/unit/service_test.py | 16 ++++----- 4 files changed, 77 insertions(+), 29 deletions(-) diff --git a/compose/config/config.py b/compose/config/config.py index 870bbad9..437ed389 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -651,7 +651,10 @@ def finalize_service(service_config, service_names, version, environment): if 'volumes' in service_dict: service_dict['volumes'] = [ - VolumeSpec.parse(v) for v in service_dict['volumes']] + VolumeSpec.parse( + v, environment.get('COMPOSE_CONVERT_WINDOWS_PATHS') + ) for v in service_dict['volumes'] + ] if 'net' in service_dict: network_mode = service_dict.pop('net') diff --git a/compose/config/types.py b/compose/config/types.py index c450a0f9..4c106747 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -5,6 +5,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import os +import re from collections import namedtuple import six @@ -14,6 +15,8 @@ from compose.config.errors import ConfigurationError from compose.const import IS_WINDOWS_PLATFORM from compose.utils import splitdrive +win32_root_path_pattern = re.compile(r'^[A-Za-z]\:\\.*') + class VolumeFromSpec(namedtuple('_VolumeFromSpec', 'source mode type')): @@ -154,7 +157,7 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): return cls(external, internal, mode) @classmethod - def _parse_win32(cls, volume_config): + def _parse_win32(cls, volume_config, normalize): # relative paths in windows expand to include the drive, eg C:\ # so we join the first 2 parts back together to count as one mode = 'rw' @@ -168,13 +171,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): parts = separate_next_section(volume_config) if len(parts) == 1: - internal = normalize_path_for_engine(os.path.normpath(parts[0])) + internal = parts[0] external = None else: external = parts[0] parts = separate_next_section(parts[1]) - external = normalize_path_for_engine(os.path.normpath(external)) - internal = normalize_path_for_engine(os.path.normpath(parts[0])) + external = os.path.normpath(external) + internal = parts[0] if len(parts) > 1: if ':' in parts[1]: raise ConfigurationError( @@ -183,15 +186,18 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): ) mode = parts[1] + if normalize: + external = normalize_path_for_engine(external) if external else None + return cls(external, internal, mode) @classmethod - def parse(cls, volume_config): + def parse(cls, volume_config, normalize=False): """Parse a volume_config path and split it into external:internal[:mode] parts to be returned as a valid VolumeSpec. """ if IS_WINDOWS_PLATFORM: - return cls._parse_win32(volume_config) + return cls._parse_win32(volume_config, normalize) else: return cls._parse_unix(volume_config) @@ -201,7 +207,14 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): @property def is_named_volume(self): - return self.external and not self.external.startswith(('.', '/', '~')) + res = self.external and not self.external.startswith(('.', '/', '~')) + if not IS_WINDOWS_PLATFORM: + return res + + return ( + res and not self.external.startswith('\\') and + not win32_root_path_pattern.match(self.external) + ) class ServiceLink(namedtuple('_ServiceLink', 'target alias')): diff --git a/tests/unit/config/types_test.py b/tests/unit/config/types_test.py index 8dfa65d5..11427352 100644 --- a/tests/unit/config/types_test.py +++ b/tests/unit/config/types_test.py @@ -63,35 +63,67 @@ class TestVolumeSpec(object): VolumeSpec.parse('one:two:three:four') assert 'has incorrect format' in exc.exconly() - def test_parse_volume_windows_absolute_path(self): - windows_path = "c:\\Users\\me\\Documents\\shiny\\config:\\opt\\shiny\\config:ro" - assert VolumeSpec._parse_win32(windows_path) == ( + def test_parse_volume_windows_absolute_path_normalized(self): + windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" + assert VolumeSpec._parse_win32(windows_path, True) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro" ) - def test_parse_volume_windows_internal_path(self): + def test_parse_volume_windows_absolute_path_native(self): + windows_path = "c:\\Users\\me\\Documents\\shiny\\config:/opt/shiny/config:ro" + assert VolumeSpec._parse_win32(windows_path, False) == ( + "c:\\Users\\me\\Documents\\shiny\\config", + "/opt/shiny/config", + "ro" + ) + + def test_parse_volume_windows_internal_path_normalized(self): windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' - assert VolumeSpec._parse_win32(windows_path) == ( + assert VolumeSpec._parse_win32(windows_path, True) == ( '/c/Users/reimu/scarlet', - '/c/scarlet/app', + 'C:\\scarlet\\app', 'ro' ) - def test_parse_volume_windows_just_drives(self): + def test_parse_volume_windows_internal_path_native(self): + windows_path = 'C:\\Users\\reimu\\scarlet:C:\\scarlet\\app:ro' + assert VolumeSpec._parse_win32(windows_path, False) == ( + 'C:\\Users\\reimu\\scarlet', + 'C:\\scarlet\\app', + 'ro' + ) + + def test_parse_volume_windows_just_drives_normalized(self): windows_path = 'E:\\:C:\\:ro' - assert VolumeSpec._parse_win32(windows_path) == ( + assert VolumeSpec._parse_win32(windows_path, True) == ( '/e/', - '/c/', + 'C:\\', 'ro' ) - def test_parse_volume_windows_mixed_notations(self): - windows_path = '/c/Foo:C:\\bar' - assert VolumeSpec._parse_win32(windows_path) == ( + def test_parse_volume_windows_just_drives_native(self): + windows_path = 'E:\\:C:\\:ro' + assert VolumeSpec._parse_win32(windows_path, False) == ( + 'E:\\', + 'C:\\', + 'ro' + ) + + def test_parse_volume_windows_mixed_notations_normalized(self): + windows_path = 'C:\\Foo:/root/foo' + assert VolumeSpec._parse_win32(windows_path, True) == ( '/c/Foo', - '/c/bar', + '/root/foo', + 'rw' + ) + + def test_parse_volume_windows_mixed_notations_native(self): + windows_path = 'C:\\Foo:/root/foo' + assert VolumeSpec._parse_win32(windows_path, False) == ( + 'C:\\Foo', + '/root/foo', 'rw' ) diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index a259c476..1d5aa10f 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -786,7 +786,7 @@ class ServiceVolumesTest(unittest.TestCase): self.mock_client = mock.create_autospec(docker.Client) def test_build_volume_binding(self): - binding = build_volume_binding(VolumeSpec.parse('/outside:/inside')) + binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True)) assert binding == ('/inside', '/outside:/inside:rw') def test_get_container_data_volumes(self): @@ -845,10 +845,10 @@ class ServiceVolumesTest(unittest.TestCase): def test_merge_volume_bindings(self): options = [ - VolumeSpec.parse('/host/volume:/host/volume:ro'), - VolumeSpec.parse('/host/rw/volume:/host/rw/volume'), - VolumeSpec.parse('/new/volume'), - VolumeSpec.parse('/existing/volume'), + VolumeSpec.parse('/host/volume:/host/volume:ro', True), + VolumeSpec.parse('/host/rw/volume:/host/rw/volume', True), + VolumeSpec.parse('/new/volume', True), + VolumeSpec.parse('/existing/volume', True), ] self.mock_client.inspect_image.return_value = { @@ -882,8 +882,8 @@ class ServiceVolumesTest(unittest.TestCase): 'web', image='busybox', volumes=[ - VolumeSpec.parse('/host/path:/data1'), - VolumeSpec.parse('/host/path:/data2'), + VolumeSpec.parse('/host/path:/data1', True), + VolumeSpec.parse('/host/path:/data2', True), ], client=self.mock_client, ) @@ -1007,7 +1007,7 @@ class ServiceVolumesTest(unittest.TestCase): 'web', client=self.mock_client, image='busybox', - volumes=[VolumeSpec.parse(volume)], + volumes=[VolumeSpec.parse(volume, True)], ).create_container() assert self.mock_client.create_container.call_count == 1 From cd94c37f5d3f1c901002d44b9a17b29f68147e24 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Tue, 18 Oct 2016 14:09:31 -0700 Subject: [PATCH 117/118] Fix merge error (missing Network.labels attribute) Signed-off-by: Joffrey F --- compose/network.py | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/network.py b/compose/network.py index 00d68aa4..e581a4fe 100644 --- a/compose/network.py +++ b/compose/network.py @@ -26,6 +26,7 @@ class Network(object): self.external_name = external_name self.internal = internal self.enable_ipv6 = enable_ipv6 + self.labels = labels def ensure(self): if self.external_name: From 28788bd9b5f708c4dc96ba3921a0b63d3bd829a2 Mon Sep 17 00:00:00 2001 From: Joffrey F Date: Wed, 19 Oct 2016 14:12:20 -0700 Subject: [PATCH 118/118] Bump 1.9.0-rc1 Signed-off-by: Joffrey F --- CHANGELOG.md | 97 ++++++++++++++++++++++++++++++++++++++ compose/__init__.py | 2 +- script/release/make-branch | 4 +- script/run/run.sh | 2 +- 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec7d5b5..85448fbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,103 @@ Change log ========== +1.9.0 (2016-10-20) +----------------- + +**Breaking changes** + +- When using Compose with Docker Toolbox/Machine on Windows, volume paths are + no longer converted from `C:\Users` to `/c/Users`-style by default. To + re-enable this conversion so that your volumes keep working, set the + environment variable `COMPOSE_CONVERT_WINDOWS_PATHS=1`. Users of + Docker for Windows are not affected and do not need to set the variable. + +New Features + +- Interactive mode for `docker-compose run` and `docker-compose exec` is + now supported on Windows platforms. Please note that the `docker` binary + is required to be present on the system for this feature to work. + +- Introduced version 2.1 of the `docker-compose.yml` specification. This + version requires to be used with Docker Engine 1.12 or above. + - Added support for setting volume labels and network labels in + `docker-compose.yml`. + - Added support for the `isolation` parameter in service definitions. + - Added support for link-local IPs in the service networks definitions. + - Added support for shell-style inline defaults in variable interpolation. + The supported forms are `${FOO-default}` (fall back if FOO is unset) and + `${FOO:-default}` (fall back if FOO is unset or empty). + +- Added support for the `group_add` and `oom_score_adj` parameters in + service definitions. + +- Added support for the `internal` and `enable_ipv6` parameters in network + definitions. + +- Compose now defaults to using the `npipe` protocol on Windows. + +- Overriding a `logging` configuration will now properly merge the `options` + mappings if the `driver` values do not conflict. + +Bug Fixes + +- Fixed several bugs related to `npipe` protocol support on Windows. + +- Fixed an issue with Windows paths being incorrectly converted when + using Docker on Windows Server. + +- Fixed a bug where an empty `restart` value would sometimes result in an + exception being raised. + +- Fixed an issue where service logs containing unicode characters would + sometimes cause an error to occur. + +- Fixed a bug where unicode values in environment variables would sometimes + raise a unicode exception when retrieved. + + +1.8.1 (2016-09-22) +----------------- + +Bug Fixes + +- Fixed a bug where users using a credentials store were not able + to access their private images. + +- Fixed a bug where users using identity tokens to authenticate + were not able to access their private images. + +- Fixed a bug where an `HttpHeaders` entry in the docker configuration + file would cause Compose to crash when trying to build an image. + +- Fixed a few bugs related to the handling of Windows paths in volume + binding declarations. + +- Fixed a bug where Compose would sometimes crash while trying to + read a streaming response from the engine. + +- Fixed an issue where Compose would crash when encountering an API error + while streaming container logs. + +- Fixed an issue where Compose would erroneously try to output logs from + drivers not handled by the Engine's API. + +- Fixed a bug where options from the `docker-machine config` command would + not be properly interpreted by Compose. + +- Fixed a bug where the connection to the Docker Engine would + sometimes fail when running a large number of services simultaneously. + +- Fixed an issue where Compose would sometimes print a misleading + suggestion message when running the `bundle` command. + +- Fixed a bug where connection errors would not be handled properly by + Compose during the project initialization phase. + +- Fixed a bug where a misleading error would appear when encountering + a connection timeout. + + 1.8.0 (2016-06-14) ----------------- diff --git a/compose/__init__.py b/compose/__init__.py index 6e610652..d74e1aa3 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.9.0dev' +__version__ = '1.9.0-rc1' diff --git a/script/release/make-branch b/script/release/make-branch index 7ccf3f05..bf81219d 100755 --- a/script/release/make-branch +++ b/script/release/make-branch @@ -65,8 +65,8 @@ git config "branch.${BRANCH}.release" $VERSION editor=${EDITOR:-vim} -echo "Update versions in docs/install.md, compose/__init__.py, script/run/run.sh" -$editor docs/install.md +echo "Update versions in compose/__init__.py, script/run/run.sh" +# $editor docs/install.md $editor compose/__init__.py $editor script/run/run.sh diff --git a/script/run/run.sh b/script/run/run.sh index 6205747a..dec83b54 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.8.0" +VERSION="1.9.0-rc1" IMAGE="docker/compose:$VERSION"