diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c07e9ca..0064a5cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,263 +1,6 @@ Change log ========== -1.11.0 (2017-02-08) -------------------- - -### New Features - -#### Compose file version 3.1 - -- Introduced version 3.1 of the `docker-compose.yml` specification. This - version requires Docker Engine 1.13.0 or above. It introduces support - for secrets. See the documentation for more information - -#### Compose file version 2.0 and up - -- Introduced the `docker-compose top` command that displays processes running - for the different services managed by Compose. - -### Bugfixes - -- Fixed a bug where extending a service defining a healthcheck dictionary - would cause `docker-compose` to error out. - -- Fixed an issue where the `pid` entry in a service definition was being - ignored when using multiple Compose files. - -1.10.1 (2017-02-01) ------------------- - -### Bugfixes - -- Fixed an issue where presence of older versions of the docker-py - package would cause unexpected crashes while running Compose - -- Fixed an issue where healthcheck dependencies would be lost when - using multiple compose files for a project - -- Fixed a few issues that made the output of the `config` command - invalid - -- Fixed an issue where adding volume labels to v3 Compose files would - result in an error - -- Fixed an issue on Windows where build context paths containing unicode - characters were being improperly encoded - -- Fixed a bug where Compose would occasionally crash while streaming logs - when containers would stop or restart - -1.10.0 (2017-01-18) -------------------- - -### New Features - -#### Compose file version 3.0 - -- Introduced version 3.0 of the `docker-compose.yml` specification. This - version requires to be used with Docker Engine 1.13 or above and is - specifically designed to work with the `docker stack` commands. - -#### Compose file version 2.1 and up - -- Healthcheck configuration can now be done in the service definition using - the `healthcheck` parameter - -- Containers dependencies can now be set up to wait on positive healthchecks - when declared using `depends_on`. See the documentation for the updated - syntax. - **Note:** This feature will not be ported to version 3 Compose files. - -- Added support for the `sysctls` parameter in service definitions - -- Added support for the `userns_mode` parameter in service definitions - -- Compose now adds identifying labels to networks and volumes it creates - -#### Compose file version 2.0 and up - -- Added support for the `stop_grace_period` option in service definitions. - -### Bugfixes - -- Colored output now works properly on Windows. - -- Fixed a bug where docker-compose run would fail to set up link aliases - in interactive mode on Windows. - -- Networks created by Compose are now always made attachable - (Compose files v2.1 and up). - -- Fixed a bug where falsy values of `COMPOSE_CONVERT_WINDOWS_PATHS` - (`0`, `false`, empty value) were being interpreted as true. - -- Fixed a bug where forward slashes in some .dockerignore patterns weren't - being parsed correctly on Windows - - -1.9.0 (2016-11-16) ------------------ - -**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. - -- Fixed an issue where Compose would incorrectly detect a configuration - mismatch for overlay networks. - - -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) ------------------ - -**Breaking Changes** - -- 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. - -- Added `docker-compose push`, a command that pushes service images - to a registry. - -- 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. - -- `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. - -- 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 output 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. - -- `docker-compose create` and `docker-compose start` show more - descriptive error messages when something goes wrong. - - 1.7.1 (2016-05-04) ----------------- @@ -620,7 +363,7 @@ Bug Fixes: if at least one container is using the network. - When printings logs during `up` or `logs`, flush the output buffer after - each line to prevent buffering issues from hiding logs. + each line to prevent buffering issues from hideing logs. - Recreate a container if one of its dependencies is being created. Previously a container was only recreated if it's dependencies already @@ -909,7 +652,7 @@ Fig has been renamed to Docker Compose, or just Compose for short. This has seve - The command you type is now `docker-compose`, not `fig`. - You should rename your fig.yml to docker-compose.yml. -- If you’re installing via PyPI, the package is now `docker-compose`, so install it with `pip install docker-compose`. +- If you’re installing via PyPi, the package is now `docker-compose`, so install it with `pip install docker-compose`. Besides that, there’s a lot of new stuff in this release: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 16bccf98..50e58ddc 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.io) by +Install the git pre-commit hooks using [tox](https://tox.readthedocs.org) by running `tox -e pre-commit` or by following the [pre-commit install guide](http://pre-commit.com/#install). diff --git a/Dockerfile b/Dockerfile index a03e1510..63fac3eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ RUN set -ex; \ ca-certificates \ curl \ libsqlite3-dev \ - libbz2-dev \ ; \ rm -rf /var/lib/apt/lists/* @@ -21,32 +20,40 @@ RUN curl https://get.docker.com/builds/Linux/x86_64/docker-1.8.3 \ -o /usr/local/bin/docker && \ chmod +x /usr/local/bin/docker -# Build Python 2.7.13 from source +# Build Python 2.7.9 from source RUN set -ex; \ - curl -L https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz | tar -xz; \ - cd Python-2.7.13; \ + curl -L https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz | tar -xz; \ + cd Python-2.7.9; \ ./configure --enable-shared; \ make; \ make install; \ cd ..; \ - rm -rf /Python-2.7.13 + rm -rf /Python-2.7.9 # Build python 3.4 from source RUN set -ex; \ - curl -L https://www.python.org/ftp/python/3.4.6/Python-3.4.6.tgz | tar -xz; \ - cd Python-3.4.6; \ + curl -L https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz | tar -xz; \ + cd Python-3.4.3; \ ./configure --enable-shared; \ make; \ make install; \ cd ..; \ - rm -rf /Python-3.4.6 + rm -rf /Python-3.4.3 # Make libpython findable ENV LD_LIBRARY_PATH /usr/local/lib +# Install setuptools +RUN set -ex; \ + curl -L https://bootstrap.pypa.io/ez_setup.py | python + # Install pip RUN set -ex; \ - curl -L https://bootstrap.pypa.io/get-pip.py | python + curl -L https://pypi.python.org/packages/source/p/pip/pip-8.1.1.tar.gz | tar -xz; \ + cd pip-8.1.1; \ + python setup.py install; \ + cd ..; \ + rm -rf pip-8.1.1 # Python3 requires a valid locale RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen diff --git a/Dockerfile.run b/Dockerfile.run index de46e35e..792077ad 100644 --- a/Dockerfile.run +++ b/Dockerfile.run @@ -1,6 +1,5 @@ -FROM alpine:3.4 -ARG version +FROM alpine:edge RUN apk -U add \ python \ py-pip @@ -8,7 +7,7 @@ RUN apk -U add \ COPY requirements.txt /code/requirements.txt RUN pip install -r /code/requirements.txt -COPY dist/docker_compose-${version}-py2.py3-none-any.whl /code/ -RUN pip install --no-deps /code/docker_compose-${version}-py2.py3-none-any.whl +ADD dist/docker-compose-release.tar.gz /code/docker-compose +RUN pip install --no-deps /code/docker-compose/docker-compose-* ENTRYPOINT ["/usr/bin/docker-compose"] diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 51136b1f..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,64 +0,0 @@ -#!groovy - -def image - -def buildImage = { -> - wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) { - stage("build image") { - checkout(scm) - def imageName = "dockerbuildbot/compose:${gitCommit()}" - image = docker.image(imageName) - try { - image.pull() - } catch (Exception exc) { - image = docker.build(imageName, ".") - image.push() - } - } - } -} - -def runTests = { Map settings -> - def dockerVersions = settings.get("dockerVersions", null) - def pythonVersions = settings.get("pythonVersions", null) - - if (!pythonVersions) { - throw new Exception("Need Python versions to test. e.g.: `runTests(pythonVersions: 'py27,py34')`") - } - if (!dockerVersions) { - throw new Exception("Need Docker versions to test. e.g.: `runTests(dockerVersions: 'all')`") - } - - { -> - wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) { - stage("test python=${pythonVersions} / docker=${dockerVersions}") { - checkout(scm) - def storageDriver = sh(script: 'docker info | awk -F \': \' \'$1 == "Storage Driver" { print $2; exit }\'', returnStdout: true).trim() - echo "Using local system's storage driver: ${storageDriver}" - sh """docker run \\ - -t \\ - --rm \\ - --privileged \\ - --volume="\$(pwd)/.git:/code/.git" \\ - --volume="/var/run/docker.sock:/var/run/docker.sock" \\ - -e "TAG=${image.id}" \\ - -e "STORAGE_DRIVER=${storageDriver}" \\ - -e "DOCKER_VERSIONS=${dockerVersions}" \\ - -e "BUILD_NUMBER=\$BUILD_TAG" \\ - -e "PY_TEST_VERSIONS=${pythonVersions}" \\ - --entrypoint="script/ci" \\ - ${image.id} \\ - --verbose - """ - } - } - } -} - -buildImage() -// TODO: break this out into meaningful "DOCKER_VERSIONS" values instead of all -parallel( - failFast: true, - all_py27: runTests(pythonVersions: "py27", dockerVersions: "all"), - all_py34: runTests(pythonVersions: "py34", dockerVersions: "all"), -) diff --git a/README.md b/README.md index 35a10b90..f8822151 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a Compose file to configure your application's services. Then, using a single command, you create and start all the services from your configuration. To learn more about all the features of Compose -see [the list of features](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#features). +see [the list of features](https://github.com/docker/compose/blob/release/docs/overview.md#features). Compose is great for development, testing, and staging environments, as well as CI workflows. You can learn more about each case in -[Common Use Cases](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#common-use-cases). +[Common Use Cases](https://github.com/docker/compose/blob/release/docs/overview.md#common-use-cases). Using Compose is basically a three-step process. @@ -22,20 +22,19 @@ they can be run together in an isolated environment: A `docker-compose.yml` looks like this: - version: '2' - - services: - web: - build: . - ports: - - "5000:5000" - volumes: - - .:/code - redis: - image: redis + web: + build: . + ports: + - "5000:5000" + volumes: + - .:/code + links: + - redis + redis: + image: redis For more information about the Compose file, see the -[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file/compose-versioning.md) +[Compose file reference](https://github.com/docker/compose/blob/release/docs/compose-file.md) Compose has commands for managing the whole lifecycle of your application: diff --git a/ROADMAP.md b/ROADMAP.md index 287e5468..67903492 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,21 +1,13 @@ # Roadmap -## An even better tool for development environments - -Compose is a great tool for development environments, but it could be even better. For example: - -- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp) - ## More than just development environments -Compose currently works really well in development, but we want to make the Compose file format better for test, staging, and production environments. To support these use cases, there will need to be improvements to the file format, improvements to the command-line tool, integrations with other tools, and perhaps new tools altogether. - -Some specific things we are considering: +Over time we will extend Compose's remit to cover test, staging and production environments. This is not a simple task, and will take many incremental improvements such as: - Compose currently will attempt to get your application into the correct state when running `up`, but it has a number of shortcomings: - It should roll back to a known good state if it fails. - It should allow a user to check the actions it is about to perform before running them. -- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports, volume mount paths, or volume drivers. ([#1377](https://github.com/docker/compose/issues/1377)) +- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#1377](https://github.com/docker/compose/issues/1377)) - Compose should recommend a technique for zero-downtime deploys. - It should be possible to continuously attempt to keep an application in the correct state, instead of just performing `up` a single time. @@ -30,3 +22,10 @@ The current state of integration is documented in [SWARM.md](SWARM.md). Compose works well for applications that are in a single repository and depend on services that are hosted on Docker Hub. If your application depends on another application within your organisation, Compose doesn't work as well. There are several ideas about how this could work, such as [including external files](https://github.com/docker/fig/issues/318). + +## An even better tool for development environments + +Compose is a great tool for development environments, but it could be even better. For example: + +- [Compose could watch your code and automatically kick off builds when something changes.](https://github.com/docker/fig/issues/184) +- It should be possible to define hostnames for containers which work from the host machine, e.g. “mywebcontainer.local”. This is needed by apps comprising multiple web services which generate links to one another (e.g. a frontend website and a separate admin webapp) diff --git a/compose/__init__.py b/compose/__init__.py index b2ca86f8..6c5bb8e7 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.12.0dev' +__version__ = '1.7.1' diff --git a/compose/bundle.py b/compose/bundle.py deleted file mode 100644 index 505ce91f..00000000 --- a/compose/bundle.py +++ /dev/null @@ -1,258 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import json -import logging - -import six -from docker.utils import split_command -from docker.utils.ports import split_port - -from .cli.errors import UserError -from .config.serialize import denormalize_config -from .network import get_network_defs_for_service -from .service import format_environment -from .service import NoSuchImageError -from .service import parse_repository_tag - - -log = logging.getLogger(__name__) - - -SERVICE_KEYS = { - 'working_dir': 'WorkingDir', - 'user': 'User', - 'labels': 'Labels', -} - -IGNORED_KEYS = {'build'} - -SUPPORTED_KEYS = { - 'image', - 'ports', - 'expose', - 'networks', - 'command', - 'environment', - 'entrypoint', -} | set(SERVICE_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, service_name): - self.image_name = image_name - self.service_name = service_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): - return json.dumps(to_bundle(config, image_digests), indent=2, sort_keys=True) - - -def get_image_digests(project, allow_push=False): - digests = {} - needs_push = set() - needs_pull = set() - - for service in project.services: - try: - digests[service.name] = get_image_digest( - service, - allow_push=allow_push, - ) - except NeedsPush as e: - needs_push.add(e.image_name) - except NeedsPull as e: - needs_pull.add(e.service_name) - - if needs_push or needs_pull: - raise MissingDigests(needs_push, needs_pull) - - return digests - - -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 " - "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']) - # Compose file already uses a digest, no lookup required - if separator == '@': - return service.options['image'] - - try: - image = service.image() - except NoSuchImageError: - action = 'build' if 'build' in service.options else 'pull' - raise UserError( - "Image not found for service '{service}'. " - "You might need to run `docker-compose {action} {service}`." - .format(service=service.name, action=action)) - - if image['RepoDigests']: - # TODO: pick a digest based on the image tag if there are multiple - # digests - return image['RepoDigests'][0] - - if 'build' not in service.options: - raise NeedsPull(service.image_name, service.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) - - repo, _, _ = parse_repository_tag(service.options['image']) - identifier = '{repo}@{digest}'.format(repo=repo, digest=digest) - - # 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 - # 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': { - name: convert_service_to_bundle( - name, - service_dict, - image_digests[name], - ) - for name, service_dict in config['services'].items() - }, - } - - -def convert_service_to_bundle(name, service_dict, image_digest): - container_config = {'Image': image_digest} - - for key, value in service_dict.items(): - if key in IGNORED_KEYS: - continue - - if key not in SUPPORTED_KEYS: - log.warn("Unsupported key '{}' in services.{} - ignoring".format(key, name)) - continue - - if key == 'environment': - container_config['Env'] = format_environment({ - envkey: envvalue for envkey, envvalue in value.items() - if envvalue - }) - continue - - if key in SERVICE_KEYS: - container_config[SERVICE_KEYS[key]] = value - continue - - set_command_and_args( - container_config, - service_dict.get('entrypoint', []), - service_dict.get('command', [])) - container_config['Networks'] = make_service_networks(name, service_dict) - - ports = make_port_specs(service_dict) - if ports: - container_config['Ports'] = ports - - return container_config - - -# See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95 -def set_command_and_args(config, entrypoint, command): - if isinstance(entrypoint, six.string_types): - entrypoint = split_command(entrypoint) - if isinstance(command, six.string_types): - command = split_command(command) - - if entrypoint: - config['Command'] = entrypoint + command - return - - if command: - config['Args'] = command - - -def make_service_networks(name, service_dict): - networks = [] - - for network_name, network_def in get_network_defs_for_service(service_dict).items(): - for key in network_def.keys(): - log.warn( - "Unsupported key '{}' in services.{}.networks.{} - ignoring" - .format(key, name, network_name)) - - networks.append(network_name) - - return networks - - -def make_port_specs(service_dict): - ports = [] - - internal_ports = [ - internal_port - for port_def in service_dict.get('ports', []) - for internal_port in split_port(port_def)[0] - ] - - internal_ports += service_dict.get('expose', []) - - for internal_port in internal_ports: - spec = make_port_spec(internal_port) - if spec not in ports: - ports.append(spec) - - return ports - - -def make_port_spec(value): - components = six.text_type(value).partition('/') - return { - 'Protocol': components[2] or 'tcp', - 'Port': int(components[0]), - } diff --git a/compose/cli/__init__.py b/compose/cli/__init__.py index c5db4455..e69de29b 100644 --- a/compose/cli/__init__.py +++ b/compose/cli/__init__.py @@ -1,37 +0,0 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -import subprocess -import sys - -# Attempt to detect https://github.com/docker/compose/issues/4344 -try: - # We don't try importing pip because it messes with package imports - # on some Linux distros (Ubuntu, Fedora) - # https://github.com/docker/compose/issues/4425 - # https://github.com/docker/compose/issues/4481 - # https://github.com/pypa/pip/blob/master/pip/_vendor/__init__.py - s_cmd = subprocess.Popen( - ['pip', 'freeze'], stderr=subprocess.PIPE, stdout=subprocess.PIPE - ) - packages = s_cmd.communicate()[0].splitlines() - dockerpy_installed = len( - list(filter(lambda p: p.startswith(b'docker-py=='), packages)) - ) > 0 - if dockerpy_installed: - from .colors import red - print( - red('ERROR:'), - "Dependency conflict: an older version of the 'docker-py' package " - "is polluting the namespace. " - "Run the following command to remedy the issue:\n" - "pip uninstall docker docker-py; pip install docker", - file=sys.stderr - ) - sys.exit(1) - -except OSError: - # pip command is not available, which indicates it's probably the binary - # distribution of Compose which is not affected - pass diff --git a/compose/cli/colors.py b/compose/cli/colors.py index f1251e43..3c18886f 100644 --- a/compose/cli/colors.py +++ b/compose/cli/colors.py @@ -1,8 +1,5 @@ from __future__ import absolute_import from __future__ import unicode_literals - -import colorama - NAMES = [ 'grey', 'red', @@ -33,7 +30,6 @@ def make_color_fn(code): return lambda s: ansi_color(code, s) -colorama.init(strip=False) for (name, code) in get_pairs(): globals()[name] = make_color_fn(code) diff --git a/compose/cli/command.py b/compose/cli/command.py index 02035428..8ac3aff4 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -4,11 +4,9 @@ from __future__ import unicode_literals import logging import os import re -import ssl import six -from . import errors from . import verbose_proxy from .. import config from ..config.environment import Environment @@ -37,16 +35,6 @@ def project_from_options(project_dir, options): ) -def get_config_from_options(base_dir, options): - environment = Environment.from_env_file(base_dir) - config_path = get_config_path_from_options( - base_dir, options, environment - ) - return config.load( - config.find(base_dir, config_path, environment) - ) - - def get_config_path_from_options(base_dir, options, environment): file_option = options.get('--file') if file_option: @@ -58,29 +46,10 @@ def get_config_path_from_options(base_dir, options, environment): return None -def get_tls_version(environment): - compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None) - if not compose_tls_version: - return None - - 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 ' - 'version of Python or OpenSSL. Falling back to TLSv1 (default).' - .format(compose_tls_version) - ) - return None - - return getattr(ssl, tls_attr_name) - - -def get_client(environment, verbose=False, version=None, tls_config=None, host=None, - tls_version=None): - +def get_client(environment, verbose=False, version=None, tls_config=None, host=None): client = docker_client( version=version, tls_config=tls_config, host=host, - environment=environment, tls_version=get_tls_version(environment) + environment=environment ) if verbose: version_info = six.iteritems(client.version()) @@ -105,14 +74,12 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, api_version = environment.get( 'COMPOSE_API_VERSION', API_VERSIONS[config_data.version]) - client = get_client( verbose=verbose, version=api_version, tls_config=tls_config, host=host, environment=environment ) - with errors.handle_connection_errors(client): - return Project.from_config(project_name, config_data, client) + return Project.from_config(project_name, config_data, client) def get_project_name(working_dir, project_name=None, environment=None): diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index 018d2451..0c0113bb 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -3,24 +3,22 @@ from __future__ import unicode_literals import logging -from docker import APIClient +from docker import Client from docker.errors import TLSParameterError from docker.tls import TLSConfig 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 = unquote_path(options.get('--tlscacert')) - cert = unquote_path(options.get('--tlscert')) - key = unquote_path(options.get('--tlskey')) + ca_cert = options.get('--tlscacert') + cert = options.get('--tlscert') + key = options.get('--tlskey') verify = options.get('--tlsverify') skip_hostname_check = options.get('--skip-hostname-check', False) @@ -41,14 +39,17 @@ def tls_config_from_options(options): return None -def docker_client(environment, version=None, tls_config=None, host=None, - tls_version=None): +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) + kwargs = kwargs_from_env(environment=environment) except TLSParameterError: raise UserError( "TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY " @@ -69,6 +70,4 @@ def docker_client(environment, version=None, tls_config=None, host=None, else: kwargs['timeout'] = HTTP_TIMEOUT - kwargs['user_agent'] = generate_user_agent() - - return APIClient(**kwargs) + return Client(**kwargs) diff --git a/compose/cli/errors.py b/compose/cli/errors.py index 5b977095..2c68d36d 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import contextlib import logging import socket -from distutils.spawn import find_executable from textwrap import dedent from docker.errors import APIError @@ -14,10 +13,10 @@ from requests.exceptions import SSLError from requests.packages.urllib3.exceptions import ReadTimeoutError from ..const import API_VERSION_TO_ENGINE_VERSION -from .utils import is_docker_for_mac_installed +from ..const import HTTP_TIMEOUT +from .utils import call_silently from .utils import is_mac from .utils import is_ubuntu -from .utils import is_windows log = logging.getLogger(__name__) @@ -47,24 +46,33 @@ def handle_connection_errors(client): raise ConnectionError() except RequestsConnectionError as e: if e.args and isinstance(e.args[0], ReadTimeoutError): - log_timeout_error(client.timeout) + log_timeout_error() raise ConnectionError() - exit_with_error(get_conn_error_message(client.base_url)) + + 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)) except APIError as e: log_api_error(e, client.api_version) raise ConnectionError() except (ReadTimeout, socket.timeout) as e: - log_timeout_error(client.timeout) + log_timeout_error() raise ConnectionError() -def log_timeout_error(timeout): +def log_timeout_error(): 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)." % timeout) + "value (current value: %s)." % HTTP_TIMEOUT) def log_api_error(e, client_version): @@ -89,46 +97,31 @@ def exit_with_error(msg): raise ConnectionError() -def get_conn_error_message(url): - if find_executable('docker') is None: - return docker_not_found_msg("Couldn't connect to Docker daemon.") - if is_docker_for_mac_installed(): - return conn_error_docker_for_mac - if find_executable('docker-machine') is not None: - 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: + + 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: - -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/" + https://docs.docker.com/engine/installation/ +""" 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/formatter.py b/compose/cli/formatter.py index 5f580645..d0ed0f87 100644 --- a/compose/cli/formatter.py +++ b/compose/cli/formatter.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals import logging import os -import six import texttable from compose.cli import colors @@ -45,7 +44,5 @@ 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 '{0}{1}'.format(self.get_level_message(record), message) + return self.get_level_message(record) + message diff --git a/compose/cli/log_printer.py b/compose/cli/log_printer.py index 299ddea4..b48462ff 100644 --- a/compose/cli/log_printer.py +++ b/compose/cli/log_printer.py @@ -6,7 +6,6 @@ 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 @@ -177,14 +176,8 @@ def build_log_generator(container, log_args): def wait_on_exit(container): - 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]' - ) + exit_code = container.wait() + return "%s exited with code %s\n" % (container.name, exit_code) def start_producer_thread(thread_args): diff --git a/compose/cli/main.py b/compose/cli/main.py index 51ba36a0..c0de1782 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -6,26 +6,21 @@ import contextlib import functools import json import logging -import pipes import re -import subprocess import sys -from distutils.spawn import find_executable from inspect import getdoc from operator import attrgetter 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 config 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 ..errors import StreamParseError from ..progress_stream import StreamOutputError from ..project import NoSuchService from ..project import OneOffFilter @@ -35,8 +30,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 get_config_path_from_options from .command import project_from_options from .docopt_command import DocoptDispatcher from .docopt_command import get_handler @@ -65,8 +59,7 @@ def main(): except (KeyboardInterrupt, signals.ShutdownException): log.error("Aborting.") sys.exit(1) - except (UserError, NoSuchService, ConfigurationError, - ProjectError, OperationFailedError) as e: + except (UserError, NoSuchService, ConfigurationError, ProjectError) as e: log.error(e.msg) sys.exit(1) except BuildError as e: @@ -78,7 +71,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, StreamParseError): + except errors.ConnectionError: sys.exit(1) @@ -105,7 +98,7 @@ def perform_command(options, handler, command_options): handler(command_options) return - if options['COMMAND'] in ('config', 'bundle'): + if options['COMMAND'] == 'config': command = TopLevelCommand(None) handler(command, options, command_options) return @@ -171,7 +164,6 @@ class TopLevelCommand(object): Commands: build Build or rebuild services - bundle Generate a Docker bundle from the Compose file config Validate and view the compose file create Create services down Stop and remove containers, networks, images, and volumes @@ -183,15 +175,13 @@ class TopLevelCommand(object): pause Pause services port Print the public port for a port binding ps List containers - pull Pull service images - push Push service images + 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 - top Display the running processes unpause Unpause services up Create and start containers version Show the Docker-Compose version information @@ -222,75 +212,6 @@ class TopLevelCommand(object): pull=bool(options.get('--pull', False)), force_rm=bool(options.get('--force-rm', False))) - def bundle(self, config_options, options): - """ - 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. - - 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". - """ - 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 = "{}.dab".format(self.project.name) - - with errors.handle_connection_errors(self.project.client): - try: - image_digests = get_image_digests( - self.project, - allow_push=options['--push-images'], - ) - 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: - 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: - command_hint = ( - "Use `docker-compose pull {}` to pull them. " - .format(" ".join(sorted(e.needs_pull))) - ) - - paras += [ - "The following images need to be pulled:", - list_images(e.needs_pull), - command_hint, - ] - - raise UserError("\n\n".join(paras)) - - with open(output, 'w') as f: - f.write(serialize_bundle(compose_config, image_digests)) - - log.info("Wrote bundle to {}".format(output)) - def config(self, config_options, options): """ Validate and view the compose file. @@ -303,7 +224,13 @@ class TopLevelCommand(object): --services Print the service names, one per line. """ - compose_config = get_config_from_options(self.project_dir, config_options) + environment = Environment.from_env_file(self.project_dir) + config_path = get_config_path_from_options( + self.project_dir, config_options, environment + ) + compose_config = config.load( + config.find(self.project_dir, config_path, environment) + ) if options['--quiet']: return @@ -338,29 +265,18 @@ class TopLevelCommand(object): def down(self, options): """ - 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. + Stop containers and remove containers, networks, volumes, and images + created by `up`. Only containers and networks are removed by default. 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 + --rmi type 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 + -v, --volumes Remove data volumes + --remove-orphans Remove containers for services not defined in + the Compose file """ image_type = image_type_from_opt('--rmi', options['--rmi']) self.project.down(image_type, options['--volumes'], options['--remove-orphans']) @@ -407,8 +323,6 @@ class TopLevelCommand(object): """ index = int(options.get('--index')) service = self.project.get_service(options['SERVICE']) - detach = options['-d'] - try: container = service.get_container(number=index) except ValueError as e: @@ -416,28 +330,6 @@ class TopLevelCommand(object): command = [options['COMMAND']] + options['ARGS'] tty = not options["-T"] - if IS_WINDOWS_PLATFORM and not detach: - args = ["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(call_docker(args)) - create_exec_options = { "privileged": options["--privileged"], "user": options["--user"], @@ -447,7 +339,7 @@ class TopLevelCommand(object): exec_id = container.create_exec(command, **create_exec_options) - if detach: + if options['-d']: container.start_exec(exec_id, tty=tty) return @@ -608,26 +500,12 @@ class TopLevelCommand(object): ignore_pull_failures=options.get('--ignore-pull-failures') ) - def push(self, options): - """ - Pushes images for services. - - Usage: push [options] [SERVICE...] - - Options: - --ignore-push-failures Push what it can and ignores images with push failures. - """ - self.project.push( - service_names=options['SERVICE'], - ignore_push_failures=options.get('--ignore-push-failures') - ) - def rm(self, options): """ - Removes stopped service containers. + Remove 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`. + By default, volumes attached to containers will not be removed. You can see all + volumes with `docker volume ls`. Any data which is not in a volume will be lost. @@ -635,15 +513,18 @@ class TopLevelCommand(object): Options: -f, --force Don't ask to confirm removal - -v Remove any anonymous volumes attached to containers - -a, --all Deprecated - no effect. + -v Remove volumes associated with containers + -a, --all Also remove one-off containers created by + docker-compose run """ if options.get('--all'): + one_off = OneOffFilter.include + else: log.warn( - '--all flag is obsolete. This is now the default behavior ' - 'of `docker-compose rm`' - ) - one_off = OneOffFilter.include + 'Not including one-off containers created by `docker-compose run`.\n' + 'To include them, use `docker-compose rm --all`.\n' + 'This will be the default behavior in the next version of Compose.\n') + one_off = OneOffFilter.exclude all_containers = self.project.containers( service_names=options['SERVICE'], stopped=True, one_off=one_off @@ -695,16 +576,20 @@ 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 ' - 'can not be used together' + 'can not be used togather' ) - if options['COMMAND'] is not None: + if options['COMMAND']: command = [options['COMMAND']] + options['ARGS'] - elif options['--entrypoint'] is not None: - command = [] else: command = service.options.get('command') @@ -726,7 +611,7 @@ class TopLevelCommand(object): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) for s in options['SERVICE=NUM']: if '=' not in s: @@ -760,7 +645,7 @@ class TopLevelCommand(object): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) self.project.stop(service_names=options['SERVICE'], timeout=timeout) def restart(self, options): @@ -773,37 +658,10 @@ class TopLevelCommand(object): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout) exit_if(not containers, 'No containers to restart', 1) - def top(self, options): - """ - Display the running processes - - Usage: top [SERVICE...] - - """ - containers = sorted( - self.project.containers(service_names=options['SERVICE'], stopped=False) + - self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only), - key=attrgetter('name') - ) - - for idx, container in enumerate(containers): - if idx > 0: - print() - - top_data = self.project.client.top(container.name) - headers = top_data.get("Titles") - rows = [] - - for process in top_data.get("Processes", []): - rows.append(process) - - print(container.name) - print(Formatter().table(headers, rows)) - def unpause(self, options): """ Unpause services. @@ -858,7 +716,7 @@ class TopLevelCommand(object): start_deps = not options['--no-deps'] cascade_stop = options['--abort-on-container-exit'] service_names = options['SERVICE'] - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) remove_orphans = options['--remove-orphans'] detached = options.get('-d') @@ -923,11 +781,6 @@ def convergence_strategy_from_opts(options): return ConvergenceStrategy.changed -def timeout_from_opts(options): - timeout = options.get('--timeout') - return None if timeout is None else int(timeout) - - def image_type_from_opt(flag, value): if not value: return ImageType.none @@ -959,9 +812,7 @@ def build_container_options(options, detach, command): } if options['-e']: - container_options['environment'] = Environment.from_command_line( - parse_environment(options['-e']) - ) + container_options['environment'] = parse_environment(options['-e']) if options['--entrypoint']: container_options['entrypoint'] = options.get('--entrypoint') @@ -1015,21 +866,17 @@ def run_one_off_container(container_options, project, service, options): signals.set_signal_handler_to_shutdown() try: try: - if IS_WINDOWS_PLATFORM: - service.connect_container_to_networks(container) - exit_code = call_docker(["start", "--attach", "--interactive", container.id]) - 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() + 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 @@ -1094,14 +941,3 @@ def exit_if(condition, message, exit_code): if condition: log.error(message) raise SystemExit(exit_code) - - -def call_docker(args): - executable_path = find_executable('docker') - if not executable_path: - 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) diff --git a/compose/cli/utils.py b/compose/cli/utils.py index 580bd1b0..fff4a543 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -6,12 +6,11 @@ import os import platform import ssl import subprocess -import sys import docker +from six.moves import input 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. @@ -43,16 +42,6 @@ def yesno(prompt, default=None): return None -def input(prompt): - """ - Version of input (raw_input in Python 2) which forces a flush of sys.stdout - to avoid problems where the prompt fails to appear due to line buffering - """ - sys.stdout.write(prompt) - sys.stdout.flush() - return sys.stdin.readline().rstrip('\n') - - def call_silently(*args, **kwargs): """ Like subprocess.call(), but redirects stdout and stderr to /dev/null. @@ -74,10 +63,6 @@ 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__, @@ -108,30 +93,3 @@ 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') - - -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) - - -def unquote_path(s): - if not s: - return s - if s[0] == '"' and s[-1] == '"': - return s[1:-1] - return s diff --git a/compose/config/config.py b/compose/config/config.py index 4c9cf423..e52de4bf 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import functools import logging +import operator import os import string import sys @@ -12,15 +13,9 @@ import six import yaml from cached_property import cached_property -from . import types 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 ..const import COMPOSEFILE_V3_0 as V3_0 -from ..const import COMPOSEFILE_V3_1 as V3_1 from ..utils import build_string_dict -from ..utils import parse_nanoseconds_int -from ..utils import splitdrive from .environment import env_vars_from_file from .environment import Environment from .environment import split_env @@ -66,9 +61,7 @@ DOCKER_CONFIG_KEYS = [ 'env_file', 'environment', 'extra_hosts', - 'group_add', 'hostname', - 'healthcheck', 'image', 'ipc', 'labels', @@ -76,23 +69,18 @@ DOCKER_CONFIG_KEYS = [ 'mac_address', 'mem_limit', 'memswap_limit', - 'mem_swappiness', 'net', - 'oom_score_adj', 'pid', 'ports', 'privileged', 'read_only', 'restart', - 'secrets', 'security_opt', 'shm_size', 'stdin_open', 'stop_signal', - 'sysctls', 'tty', 'user', - 'userns_mode', 'volume_driver', 'volumes', 'volumes_from', @@ -183,8 +171,10 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')): if version == '2': version = V2_0 - if version == '3': - version = V3_0 + if version != V2_0: + raise ConfigurationError( + 'Version in "{}" is unsupported. {}' + .format(self.filename, VERSION_EXPLANATION)) return version @@ -200,11 +190,8 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')): def get_networks(self): return {} if self.version == V1 else self.config.get('networks', {}) - def get_secrets(self): - return {} if self.version < V3_1 else self.config.get('secrets', {}) - -class Config(namedtuple('_Config', 'version services volumes networks secrets')): +class Config(namedtuple('_Config', 'version services volumes networks')): """ :param version: configuration version :type version: int @@ -329,22 +316,13 @@ def load(config_details): networks = load_mapping( config_details.config_files, 'get_networks', 'Network' ) - secrets = load_secrets(config_details.config_files, config_details.working_dir) service_dicts = load_services(config_details, main_file) if main_file.version != V1: for service_dict in service_dicts: match_named_volumes(service_dict, volumes) - services_using_deploy = [s for s in service_dicts if s.get('deploy')] - if services_using_deploy: - log.warn( - "Some services ({}) use the 'deploy' key, which will be ignored. " - "Compose does not support deploy configuration - use " - "`docker stack deploy` to deploy to a swarm." - .format(", ".join(sorted(s['name'] for s in services_using_deploy)))) - - return Config(main_file.version, service_dicts, volumes, networks, secrets) + return Config(main_file.version, service_dicts, volumes, networks) def load_mapping(config_files, get_func, entity_type): @@ -358,53 +336,27 @@ def load_mapping(config_files, get_func, entity_type): external = config.get('external') if external: - validate_external(entity_type, name, config) + if len(config.keys()) > 1: + raise ConfigurationError( + '{} {} declared as external but specifies' + ' additional attributes ({}). '.format( + entity_type, + name, + ', '.join([k for k in config.keys() if k != 'external']) + ) + ) if isinstance(external, dict): config['external_name'] = external.get('name') else: config['external_name'] = name + mapping[name] = config + if 'driver_opts' in config: config['driver_opts'] = build_string_dict( config['driver_opts'] ) - if 'labels' in config: - config['labels'] = parse_labels(config['labels']) - - return mapping - - -def validate_external(entity_type, name, config): - if len(config.keys()) <= 1: - return - - raise ConfigurationError( - "{} {} declared as external but specifies additional attributes " - "({}).".format( - entity_type, name, ', '.join(k for k in config if k != 'external'))) - - -def load_secrets(config_files, working_dir): - mapping = {} - - for config_file in config_files: - for name, config in config_file.get_secrets().items(): - mapping[name] = config or {} - if not config: - continue - - external = config.get('external') - if external: - validate_external('Secret', name, config) - if isinstance(external, dict): - config['external_name'] = external.get('name') - else: - config['external_name'] = name - - if 'file' in config: - config['file'] = expand_path(working_dir, config['file']) - return mapping @@ -457,42 +409,34 @@ def load_services(config_details, config_file): return build_services(service_config) -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 interpolate_config_section(filename, config, section, environment): + validate_config_section(filename, config, section) + return interpolate_environment_variables(config, section, environment) def process_config_file(config_file, environment, service_name=None): services = interpolate_config_section( - config_file, + config_file.filename, config_file.get_service_dicts(), 'service', - environment) + environment,) - if config_file.version in (V2_0, V2_1, V3_0, V3_1): + if config_file.version == V2_0: processed_config = dict(config_file.config) processed_config['services'] = services processed_config['volumes'] = interpolate_config_section( - config_file, + config_file.filename, config_file.get_volumes(), 'volume', - environment) + environment,) processed_config['networks'] = interpolate_config_section( - config_file, + config_file.filename, config_file.get_networks(), 'network', - environment) - elif config_file.version == V1: + environment,) + + if config_file.version == V1: processed_config = services - else: - raise ConfigurationError( - 'Version in "{}" is unsupported. {}' - .format(config_file.filename, VERSION_EXPLANATION)) config_file = config_file._replace(config=processed_config) validate_against_config_schema(config_file) @@ -673,59 +617,10 @@ def process_service(service_config): if 'extra_hosts' in service_dict: service_dict['extra_hosts'] = parse_extra_hosts(service_dict['extra_hosts']) - if 'sysctls' in service_dict: - service_dict['sysctls'] = build_string_dict(parse_sysctls(service_dict['sysctls'])) - - service_dict = process_depends_on(service_dict) - for field in ['dns', 'dns_search', 'tmpfs']: if field in service_dict: service_dict[field] = to_list(service_dict[field]) - service_dict = process_healthcheck(service_dict, service_config.name) - - return service_dict - - -def process_depends_on(service_dict): - if 'depends_on' in service_dict and not isinstance(service_dict['depends_on'], dict): - service_dict['depends_on'] = dict([ - (svc, {'condition': 'service_started'}) for svc in service_dict['depends_on'] - ]) - return service_dict - - -def process_healthcheck(service_dict, service_name): - if 'healthcheck' not in service_dict: - return service_dict - - hc = {} - raw = service_dict['healthcheck'] - - if raw.get('disable'): - if len(raw) > 1: - raise ConfigurationError( - 'Service "{}" defines an invalid healthcheck: ' - '"disable: true" cannot be combined with other options' - .format(service_name)) - hc['test'] = ['NONE'] - elif 'test' in raw: - hc['test'] = raw['test'] - - if 'interval' in raw: - if not isinstance(raw['interval'], six.integer_types): - hc['interval'] = parse_nanoseconds_int(raw['interval']) - else: # Conversion has been done previously - hc['interval'] = raw['interval'] - if 'timeout' in raw: - if not isinstance(raw['timeout'], six.integer_types): - hc['timeout'] = parse_nanoseconds_int(raw['timeout']) - else: # Conversion has been done previously - hc['timeout'] = raw['timeout'] - if 'retries' in raw: - hc['retries'] = raw['retries'] - - service_dict['healthcheck'] = hc return service_dict @@ -744,10 +639,7 @@ def finalize_service(service_config, service_names, version, environment): if 'volumes' in service_dict: service_dict['volumes'] = [ - VolumeSpec.parse( - v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS') - ) for v in service_dict['volumes'] - ] + VolumeSpec.parse(v) for v in service_dict['volumes']] if 'net' in service_dict: network_mode = service_dict.pop('net') @@ -763,11 +655,6 @@ def finalize_service(service_config, service_names, version, environment): if 'restart' in service_dict: service_dict['restart'] = parse_restart_spec(service_dict['restart']) - if 'secrets' in service_dict: - service_dict['secrets'] = [ - types.ServiceSecret.parse(s) for s in service_dict['secrets'] - ] - normalize_build(service_dict, service_config.working_dir, environment) service_dict['name'] = service_config.name @@ -855,25 +742,23 @@ def merge_service_dicts(base, override, version): md.merge_mapping('labels', parse_labels) md.merge_mapping('ulimits', parse_ulimits) md.merge_mapping('networks', parse_networks) - md.merge_mapping('sysctls', parse_sysctls) - md.merge_mapping('depends_on', parse_depends_on) md.merge_sequence('links', ServiceLink.parse) - md.merge_sequence('secrets', types.ServiceSecret.parse) for field in ['volumes', 'devices']: md.merge_field(field, merge_path_mappings) for field in [ - 'ports', 'cap_add', 'cap_drop', 'expose', 'external_links', - 'security_opt', 'volumes_from', + 'depends_on', + 'expose', + 'external_links', + 'ports', + 'volumes_from', ]: - md.merge_field(field, merge_unique_items_lists, default=[]) + md.merge_field(field, operator.add, default=[]) for field in ['dns', 'dns_search', 'env_file', 'tmpfs']: md.merge_field(field, merge_list_or_string) - md.merge_field('logging', merge_logging, default={}) - for field in set(ALLOWED_KEYS) - set(md): md.merge_scalar(field) @@ -885,10 +770,6 @@ def merge_service_dicts(base, override, version): return dict(md) -def merge_unique_items_lists(base, override): - return sorted(set().union(base, override)) - - def merge_build(output, base, override): def to_dict(service): build_config = service.get('build', {}) @@ -903,16 +784,6 @@ 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) @@ -932,11 +803,11 @@ def merge_environment(base, override): return env -def split_kv(kvpair): - if '=' in kvpair: - return kvpair.split('=', 1) +def split_label(label): + if '=' in label: + return label.split('=', 1) else: - return kvpair, '' + return label, '' def parse_dict_or_list(split_func, type_name, arguments): @@ -957,12 +828,8 @@ def parse_dict_or_list(split_func, type_name, arguments): parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments') parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment') -parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels') +parse_labels = functools.partial(parse_dict_or_list, split_label, 'labels') parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks') -parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls') -parse_depends_on = functools.partial( - parse_dict_or_list, lambda k: (k, {'condition': 'service_started'}), 'depends_on' -) def parse_ulimits(ulimits): @@ -1072,7 +939,12 @@ def split_path_mapping(volume_path): path. Using splitdrive so windows absolute paths won't cause issues with splitting on ':'. """ - drive, volume_config = splitdrive(volume_path) + # splitdrive has limitations when it comes to relative paths, so when it's + # relative, handle special case to set the drive to '' + if volume_path.startswith('.') or volume_path.startswith('~'): + drive, volume_config = '', volume_path + else: + drive, volume_config = os.path.splitdrive(volume_path) if ':' in volume_config: (host, container) = volume_config.split(':', 1) diff --git a/compose/config/config_schema_v1.json b/compose/config/config_schema_v1.json index 94354cda..36a93793 100644 --- a/compose/config/config_schema_v1.json +++ b/compose/config/config_schema_v1.json @@ -85,7 +85,6 @@ "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 59c7b30c..e84d1317 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -139,7 +139,6 @@ "mac_address": {"type": "string"}, "mem_limit": {"type": ["number", "string"]}, "memswap_limit": {"type": ["number", "string"]}, - "mem_swappiness": {"type": "integer"}, "network_mode": {"type": "string"}, "networks": { @@ -167,14 +166,6 @@ } ] }, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, "pid": {"type": ["string", "null"]}, "ports": { @@ -192,7 +183,6 @@ "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "shm_size": {"type": ["number", "string"]}, "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, "stop_signal": {"type": "string"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, @@ -255,8 +245,7 @@ "name": {"type": "string"} }, "additionalProperties": false - }, - "internal": {"type": "boolean"} + } }, "additionalProperties": false }, @@ -276,9 +265,9 @@ "type": ["boolean", "object"], "properties": { "name": {"type": "string"} - }, - "additionalProperties": false - } + } + }, + "additionalProperties": false }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json deleted file mode 100644 index d1ffff89..00000000 --- a/compose/config/config_schema_v2.1.json +++ /dev/null @@ -1,376 +0,0 @@ -{ - "$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": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy"] - } - }, - "required": ["condition"] - } - } - } - ] - }, - "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"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "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}, - - "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"]}, - "mem_swappiness": {"type": "integer"}, - "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 - } - ] - }, - "oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000}, - "group_add": { - "type": "array", - "items": { - "type": ["string", "number"] - }, - "uniqueItems": true - }, - "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"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "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"}, - "userns_mode": {"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 - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - - "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 - }, - "internal": {"type": "boolean"}, - "enable_ipv6": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "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 - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "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/config_schema_v3.0.json b/compose/config/config_schema_v3.0.json deleted file mode 100644 index fbcd8bb8..00000000 --- a/compose/config/config_schema_v3.0.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.0.json", - "type": "object", - "required": ["version"], - - "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": { - "deploy": {"$ref": "#/definitions/deployment"}, - "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"}, - "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 - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "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", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "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"} - }, - "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"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "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"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - } - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "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 - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "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/config_schema_v3.1.json b/compose/config/config_schema_v3.1.json deleted file mode 100644 index b7037485..00000000 --- a/compose/config/config_schema_v3.1.json +++ /dev/null @@ -1,428 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.1.json", - "type": "object", - "required": ["version"], - - "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 - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "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"}, - "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 - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "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", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "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"} - }, - "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"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "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"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - } - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "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 - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "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/environment.py b/compose/config/environment.py index 4ba228c8..ad5c0b3d 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import codecs -import contextlib import logging import os @@ -29,15 +28,12 @@ def env_vars_from_file(filename): """ if not os.path.exists(filename): raise ConfigurationError("Couldn't find env file: %s" % filename) - elif not os.path.isfile(filename): - raise ConfigurationError("%s is not a file." % (filename)) env = {} - with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj: - for line in fileobj: - line = line.strip() - if line and not line.startswith('#'): - k, v = split_env(line) - env[k] = v + for line in codecs.open(filename, 'r', 'utf-8'): + line = line.strip() + if line and not line.startswith('#'): + k, v = split_env(line) + env[k] = v return env @@ -62,18 +58,6 @@ 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) @@ -107,14 +91,3 @@ class Environment(dict): super(Environment, self).get(key.upper(), *args, **kwargs) ) return super(Environment, self).get(key, *args, **kwargs) - - def get_boolean(self, key): - # Convert a value to a boolean using "common sense" rules. - # Unset, empty, "0" and "false" (i-case) yield False. - # All other values yield True. - value = self.get(key) - if not value: - return False - if value.lower() in ['0', 'false']: - return False - return True diff --git a/compose/config/errors.py b/compose/config/errors.py index 16ed01b8..d14cbbdd 100644 --- a/compose/config/errors.py +++ b/compose/config/errors.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals VERSION_EXPLANATION = ( - 'You might be seeing this error because you\'re using the wrong Compose file version. ' - 'Either specify a supported version ("2.0", "2.1", "3.0") and place your ' + 'You might be seeing this error because you\'re using the wrong Compose ' + 'file version. Either specify a version of "2" (or "2.0") and place your ' 'service definitions under the `services` key, or omit the `version` key ' 'and place your service definitions at the root of the file to use ' 'version 1.\nFor more on the Compose file format versions, see ' diff --git a/compose/config/interpolation.py b/compose/config/interpolation.py index 1b270b9e..63020d91 100644 --- a/compose/config/interpolation.py +++ b/compose/config/interpolation.py @@ -7,35 +7,14 @@ 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__) -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 interpolate_environment_variables(config, section, environment): def process_item(name, config_dict): return dict( - (key, interpolate_value(name, key, val, section, interpolator)) + (key, interpolate_value(name, key, val, section, environment)) for key, val in (config_dict or {}).items() ) @@ -45,9 +24,9 @@ def interpolate_environment_variables(version, config, section, environment): ) -def interpolate_value(name, config_key, value, section, interpolator): +def interpolate_value(name, config_key, value, section, mapping): try: - return recursive_interpolate(value, interpolator) + return recursive_interpolate(value, mapping) except InvalidInterpolation as e: raise ConfigurationError( 'Invalid interpolation format for "{config_key}" option ' @@ -58,44 +37,25 @@ def interpolate_value(name, config_key, value, section, interpolator): string=e.string)) -def recursive_interpolate(obj, interpolator): +def recursive_interpolate(obj, mapping): if isinstance(obj, six.string_types): - return interpolator.interpolate(obj) - if isinstance(obj, dict): + return interpolate(obj, mapping) + elif isinstance(obj, dict): return dict( - (key, recursive_interpolate(val, interpolator)) + (key, recursive_interpolate(val, mapping)) for (key, val) in obj.items() ) - if isinstance(obj, list): - return [recursive_interpolate(val, interpolator) for val in obj] - return obj + elif isinstance(obj, list): + return [recursive_interpolate(val, mapping) for val in obj] + else: + return obj -class TemplateWithDefaults(Template): - idpattern = r'[_a-z][_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) +def interpolate(string, mapping): + try: + return Template(string).substitute(mapping) + except ValueError: + raise InvalidInterpolation(string) class InvalidInterpolation(Exception): diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 46d283f0..1b498c01 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -6,7 +6,7 @@ import yaml from compose.config import types from compose.config.config import V1 -from compose.config.config import V2_1 +from compose.config.config import V2_0 def serialize_config_type(dumper, data): @@ -18,7 +18,7 @@ yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type) yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type) -def denormalize_config(config): +def serialize_config(config): denormalized_services = [ denormalize_service_dict(service_dict, config.version) for service_dict in config.services @@ -27,82 +27,28 @@ def denormalize_config(config): service_dict.pop('name'): service_dict for service_dict in denormalized_services } - networks = config.networks.copy() - for net_name, net_conf in networks.items(): - if 'external_name' in net_conf: - del net_conf['external_name'] - volumes = config.volumes.copy() - for vol_name, vol_conf in volumes.items(): - if 'external_name' in vol_conf: - del vol_conf['external_name'] - - version = config.version - if version == V1: - version = V2_1 - - return { - 'version': version, + output = { + 'version': V2_0, 'services': services, - 'networks': networks, - 'volumes': volumes, + 'networks': config.networks, + 'volumes': config.volumes, } - -def serialize_config(config): return yaml.safe_dump( - denormalize_config(config), + output, default_flow_style=False, indent=2, width=80) -def serialize_ns_time_value(value): - result = (value, 'ns') - table = [ - (1000., 'us'), - (1000., 'ms'), - (1000., 's'), - (60., 'm'), - (60., 'h') - ] - for stage in table: - tmp = value / stage[0] - if tmp == int(value / stage[0]): - value = tmp - result = (int(value), stage[1]) - else: - break - return '{0}{1}'.format(*result) - - def denormalize_service_dict(service_dict, version): service_dict = service_dict.copy() if 'restart' in service_dict: - service_dict['restart'] = types.serialize_restart_spec( - service_dict['restart'] - ) + service_dict['restart'] = types.serialize_restart_spec(service_dict['restart']) if version == V1 and 'network_mode' not in service_dict: service_dict['network_mode'] = 'bridge' - if 'depends_on' in service_dict and version != V2_1: - service_dict['depends_on'] = sorted([ - svc for svc in service_dict['depends_on'].keys() - ]) - - if 'healthcheck' in service_dict: - if 'interval' in service_dict['healthcheck']: - service_dict['healthcheck']['interval'] = serialize_ns_time_value( - service_dict['healthcheck']['interval'] - ) - if 'timeout' in service_dict['healthcheck']: - service_dict['healthcheck']['timeout'] = serialize_ns_time_value( - service_dict['healthcheck']['timeout'] - ) - - if 'secrets' in service_dict: - service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets']) - return service_dict diff --git a/compose/config/types.py b/compose/config/types.py index 811e6c1f..e6a3dea0 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -5,17 +5,13 @@ from __future__ import absolute_import from __future__ import unicode_literals import os -import re from collections import namedtuple import six -from ..const import COMPOSEFILE_V1 as V1 -from .errors import ConfigurationError +from compose.config.config import V1 +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')): @@ -96,8 +92,6 @@ 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'])) @@ -120,23 +114,41 @@ def parse_extra_hosts(extra_hosts_config): return extra_hosts_dict -def normalize_path_for_engine(path): +def normalize_paths_for_engine(external_path, internal_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/ """ - drive, tail = splitdrive(path) + if not IS_WINDOWS_PLATFORM: + return external_path, internal_path - if drive: - path = '/' + drive.lower().rstrip(':') + tail + if external_path: + drive, tail = os.path.splitdrive(external_path) - return path.replace('\\', '/') + if drive: + external_path = '/' + drive.lower().rstrip(':') + tail + + external_path = external_path.replace('\\', '/') + + return external_path, internal_path.replace('\\', '/') class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): @classmethod - def _parse_unix(cls, volume_config): - parts = volume_config.split(':') + 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(':') if len(parts) > 3: raise ConfigurationError( @@ -144,11 +156,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): "external:internal[:mode]" % volume_config) if len(parts) == 1: - external = None - internal = os.path.normpath(parts[0]) + external, internal = normalize_paths_for_engine( + None, + os.path.normpath(parts[0])) else: - external = os.path.normpath(parts[0]) - internal = os.path.normpath(parts[1]) + external, internal = normalize_paths_for_engine( + os.path.normpath(parts[0]), + os.path.normpath(parts[1])) mode = 'rw' if len(parts) == 3: @@ -156,65 +170,13 @@ class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')): return cls(external, internal, mode) - @classmethod - 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' - - 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 = parts[0] - external = None - else: - external = parts[0] - parts = separate_next_section(parts[1]) - external = os.path.normpath(external) - internal = 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] - - if normalize: - external = normalize_path_for_engine(external) if external else None - - return cls(external, internal, mode) - - @classmethod - 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, normalize) - 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) @property def is_named_volume(self): - 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) - ) + return self.external and not self.external.startswith(('.', '/', '~')) class ServiceLink(namedtuple('_ServiceLink', 'target alias')): @@ -234,27 +196,3 @@ class ServiceLink(namedtuple('_ServiceLink', 'target alias')): @property def merge_field(self): return self.alias - - -class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')): - - @classmethod - def parse(cls, spec): - if isinstance(spec, six.string_types): - return cls(spec, None, None, None, None) - return cls( - spec.get('source'), - spec.get('target'), - spec.get('uid'), - spec.get('gid'), - spec.get('mode'), - ) - - @property - def merge_field(self): - return self.source - - def repr(self): - return dict( - [(k, v) for k, v in self._asdict().items() if v is not None] - ) diff --git a/compose/config/validation.py b/compose/config/validation.py index 3f23f0a7..7452e984 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -180,13 +180,11 @@ def validate_links(service_config, service_names): def validate_depends_on(service_config, service_names): - deps = service_config.config.get('depends_on', {}) - for dependency in deps.keys(): + for dependency in service_config.config.get('depends_on', []): if dependency not in service_names: raise ConfigurationError( "Service '{s.name}' depends on service '{dep}' which is " - "undefined.".format(s=service_config, dep=dependency) - ) + "undefined.".format(s=service_config, dep=dependency)) def get_unsupported_config_msg(path, error_key): @@ -203,7 +201,7 @@ def anglicize_json_type(json_type): def is_service_dict_schema(schema_id): - return schema_id in ('config_schema_v1.json', '#/properties/services') + return schema_id in ('config_schema_v1.json', '#/properties/services') def handle_error_for_schema_with_id(error, path): diff --git a/compose/const.py b/compose/const.py index e694dbda..9e00d96e 100644 --- a/compose/const.py +++ b/compose/const.py @@ -1,41 +1,29 @@ from __future__ import absolute_import from __future__ import unicode_literals +import os import sys DEFAULT_TIMEOUT = 10 -HTTP_TIMEOUT = 60 -IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag'] +HTTP_TIMEOUT = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60)) +IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag'] IS_WINDOWS_PLATFORM = (sys.platform == "win32") LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_ONE_OFF = 'com.docker.compose.oneoff' LABEL_PROJECT = 'com.docker.compose.project' LABEL_SERVICE = 'com.docker.compose.service' -LABEL_NETWORK = 'com.docker.compose.network' LABEL_VERSION = 'com.docker.compose.version' -LABEL_VOLUME = 'com.docker.compose.volume' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' -SECRETS_PATH = '/run/secrets' - COMPOSEFILE_V1 = '1' COMPOSEFILE_V2_0 = '2.0' -COMPOSEFILE_V2_1 = '2.1' -COMPOSEFILE_V3_0 = '3.0' -COMPOSEFILE_V3_1 = '3.1' API_VERSIONS = { COMPOSEFILE_V1: '1.21', COMPOSEFILE_V2_0: '1.22', - COMPOSEFILE_V2_1: '1.24', - COMPOSEFILE_V3_0: '1.25', - COMPOSEFILE_V3_1: '1.25', } API_VERSION_TO_ENGINE_VERSION = { API_VERSIONS[COMPOSEFILE_V1]: '1.9.0', - API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0', - API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', - API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', - API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0', + API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0' } diff --git a/compose/container.py b/compose/container.py index bda4e659..2c16863d 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 in ('json-file', 'journald') + return not log_type or log_type != 'none' def attach_log_stream(self): """A log stream can only be attached if the container uses a json-file diff --git a/compose/errors.py b/compose/errors.py deleted file mode 100644 index 415b41e7..00000000 --- a/compose/errors.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import absolute_import -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 - - -class HealthCheckException(Exception): - def __init__(self, reason): - self.msg = reason - - -class HealthCheckFailed(HealthCheckException): - def __init__(self, container_id): - super(HealthCheckFailed, self).__init__( - 'Container "{}" is unhealthy.'.format(container_id) - ) - - -class NoHealthCheckConfigured(HealthCheckException): - def __init__(self, service_name): - super(NoHealthCheckConfigured, self).__init__( - 'Service "{}" is missing a healthcheck configuration'.format( - service_name - ) - ) diff --git a/compose/network.py b/compose/network.py index d98f68d2..affba7c2 100644 --- a/compose/network.py +++ b/compose/network.py @@ -4,27 +4,18 @@ from __future__ import unicode_literals import logging from docker.errors import NotFound -from docker.types import IPAMConfig -from docker.types import IPAMPool -from docker.utils import version_gte -from docker.utils import version_lt +from docker.utils import create_ipam_config +from docker.utils import create_ipam_pool from .config import ConfigurationError -from .const import LABEL_NETWORK -from .const import LABEL_PROJECT log = logging.getLogger(__name__) -OPTS_EXCEPTIONS = [ - 'com.docker.network.driver.overlay.vxlanid_list', -] - class Network(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - ipam=None, external_name=None, internal=False, enable_ipv6=False, - labels=None): + ipam=None, external_name=None): self.client = client self.project = project self.name = name @@ -32,9 +23,6 @@ class Network(object): self.driver_opts = driver_opts self.ipam = create_ipam_config_from_dict(ipam) self.external_name = external_name - self.internal = internal - self.enable_ipv6 = enable_ipv6 - self.labels = labels def ensure(self): if self.external_name: @@ -57,7 +45,14 @@ class Network(object): try: data = self.inspect() - check_remote_network_config(data, self) + if self.driver and data['Driver'] != self.driver: + raise ConfigurationError( + 'Network "{}" needs to be recreated - driver has changed' + .format(self.full_name)) + if data['Options'] != (self.driver_opts or {}): + raise ConfigurationError( + 'Network "{}" needs to be recreated - options have changed' + .format(self.full_name)) except NotFound: driver_name = 'the default driver' if self.driver: @@ -73,10 +68,6 @@ class Network(object): driver=self.driver, options=self.driver_opts, ipam=self.ipam, - internal=self.internal, - enable_ipv6=self.enable_ipv6, - labels=self._labels, - attachable=version_gte(self.client._version, '1.24') or None, ) def remove(self): @@ -96,26 +87,15 @@ class Network(object): return self.external_name return '{0}_{1}'.format(self.project, self.name) - @property - def _labels(self): - if version_lt(self.client._version, '1.23'): - return None - labels = self.labels.copy() if self.labels else {} - labels.update({ - LABEL_PROJECT: self.project, - LABEL_NETWORK: self.name, - }) - return labels - def create_ipam_config_from_dict(ipam_dict): if not ipam_dict: return None - return IPAMConfig( + return create_ipam_config( driver=ipam_dict.get('driver'), pool_configs=[ - IPAMPool( + create_ipam_pool( subnet=config.get('subnet'), iprange=config.get('ip_range'), gateway=config.get('gateway'), @@ -126,24 +106,6 @@ def create_ipam_config_from_dict(ipam_dict): ) -def check_remote_network_config(remote, local): - if local.driver and remote.get('Driver') != local.driver: - raise ConfigurationError( - 'Network "{}" needs to be recreated - driver has changed' - .format(local.full_name) - ) - local_opts = local.driver_opts or {} - remote_opts = remote.get('Options') or {} - for k in set.union(set(remote_opts.keys()), set(local_opts.keys())): - if k in OPTS_EXCEPTIONS: - continue - if remote_opts.get(k) != local_opts.get(k): - raise ConfigurationError( - 'Network "{}" needs to be recreated - options have changed' - .format(local.full_name) - ) - - def build_networks(name, config_data, client): network_config = config_data.networks or {} networks = { @@ -153,9 +115,6 @@ 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'), - enable_ipv6=data.get('enable_ipv6'), - labels=data.get('labels'), ) for network_name, data in network_config.items() } diff --git a/compose/parallel.py b/compose/parallel.py index e495410c..50b2dbea 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -12,9 +12,6 @@ from six.moves.queue import Empty from six.moves.queue import Queue from compose.cli.signals import ShutdownException -from compose.errors import HealthCheckFailed -from compose.errors import NoHealthCheckConfigured -from compose.errors import OperationFailedError from compose.utils import get_output_stream @@ -50,9 +47,6 @@ 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, HealthCheckFailed, NoHealthCheckConfigured)): - errors[get_name(obj)] = exception.msg - writer.write(get_name(obj), 'error') elif isinstance(exception, UpstreamError): writer.write(get_name(obj), 'error') else: @@ -166,27 +160,20 @@ def feed_queue(objects, func, get_deps, results, state): for obj in pending: deps = get_deps(obj) - try: - if any(dep[0] in state.failed for dep in deps): - log.debug('{} has upstream errors - not processing'.format(obj)) - results.put((obj, None, UpstreamError())) - state.failed.add(obj) - elif all( - dep not in objects or ( - dep in state.finished and (not ready_check or ready_check(dep)) - ) for dep, ready_check in deps - ): - log.debug('Starting producer thread for {}'.format(obj)) - t = Thread(target=producer, args=(obj, func, results)) - t.daemon = True - t.start() - state.started.add(obj) - except (HealthCheckFailed, NoHealthCheckConfigured) as e: - log.debug( - 'Healthcheck for service(s) upstream of {} failed - ' - 'not processing'.format(obj) - ) - results.put((obj, None, e)) + + if any(dep in state.failed for dep in deps): + log.debug('{} has upstream errors - not processing'.format(obj)) + results.put((obj, None, UpstreamError())) + state.failed.add(obj) + elif all( + dep not in objects or dep in state.finished + for dep in deps + ): + log.debug('Starting producer thread for {}'.format(obj)) + t = Thread(target=producer, args=(obj, func, results)) + t.daemon = True + t.start() + state.started.add(obj) if state.is_done(): results.put(STOP) @@ -257,3 +244,7 @@ def parallel_unpause(containers, options): def parallel_kill(containers, options): parallel_operation(containers, 'kill', options, 'Killing') + + +def parallel_restart(containers, options): + parallel_operation(containers, 'restart', options, 'Restarting') diff --git a/compose/progress_stream.py b/compose/progress_stream.py index 5314f89f..1f873d1d 100644 --- a/compose/progress_stream.py +++ b/compose/progress_stream.py @@ -32,11 +32,12 @@ def stream_output(output, stream): if not image_id: continue - if image_id not in lines: + if image_id in lines: + diff = len(lines) - lines[image_id] + else: lines[image_id] = len(lines) stream.write("\n") - - diff = len(lines) - lines[image_id] + diff = 0 # move cursor up `diff` rows stream.write("%c[%dA" % (27, diff)) @@ -90,22 +91,3 @@ def print_output_event(event, stream, is_terminal): stream.write("%s%s" % (event['stream'], terminator)) else: stream.write("%s%s\n" % (status, terminator)) - - -def get_digest_from_pull(events): - for event in events: - status = event.get('status') - if not status or 'Digest' not in status: - continue - - _, digest = status.split(':', 1) - return digest.strip() - return None - - -def get_digest_from_push(events): - for event in events: - digest = event.get('aux', {}).get('Digest') - if digest: - return digest - return None diff --git a/compose/project.py b/compose/project.py index 133071e7..d965c4a3 100644 --- a/compose/project.py +++ b/compose/project.py @@ -14,6 +14,7 @@ from .config import ConfigurationError from .config.config import V1 from .config.sort_services import get_container_name_from_network_mode from .config.sort_services import get_service_name_from_network_mode +from .const import DEFAULT_TIMEOUT from .const import IMAGE_EVENTS from .const import LABEL_ONE_OFF from .const import LABEL_PROJECT @@ -104,11 +105,6 @@ class Project(object): for volume_spec in service_dict.get('volumes', []) ] - secrets = get_secrets( - service_dict['name'], - service_dict.pop('secrets', None) or [], - config_data.secrets) - project.services.append( Service( service_dict.pop('name'), @@ -119,7 +115,6 @@ class Project(object): links=links, network_mode=network_mode, volumes_from=volumes_from, - secrets=secrets, **service_dict) ) @@ -233,10 +228,7 @@ class Project(object): services = self.get_services(service_names) def get_deps(service): - return { - (self.get_service(dep), config) - for dep, config in service.get_dependency_configs().items() - } + return {self.get_service(dep) for dep in service.get_dependency_names()} parallel.parallel_execute( services, @@ -252,13 +244,13 @@ class Project(object): def get_deps(container): # actually returning inversed dependencies - return {(other, None) for other in containers + return {other for other in containers if container.service in self.get_service(other.service).get_dependency_names()} parallel.parallel_execute( containers, - self.build_container_operation_with_timeout_func('stop', options), + operator.methodcaller('stop', **options), operator.attrgetter('name'), 'Stopping', get_deps) @@ -299,12 +291,7 @@ class Project(object): def restart(self, service_names=None, **options): containers = self.containers(service_names, stopped=True) - - parallel.parallel_execute( - containers, - self.build_container_operation_with_timeout_func('restart', options), - operator.attrgetter('name'), - 'Restarting') + parallel.parallel_restart(containers, options) return containers def build(self, service_names=None, no_cache=False, pull=False, force_rm=False): @@ -365,7 +352,7 @@ class Project(object): # TODO: get labels from the API v1.22 , see github issue 2618 try: - # this can fail if the container has been removed + # this can fail if the conatiner has been removed container = Container.from_id(self.client, event['id']) except APIError: continue @@ -378,12 +365,10 @@ class Project(object): start_deps=True, strategy=ConvergenceStrategy.changed, do_build=BuildAction.none, - timeout=None, + timeout=DEFAULT_TIMEOUT, detached=False, remove_orphans=False): - warn_for_swarm_mode(self.client) - self.initialize() self.find_orphan_containers(remove_orphans) @@ -403,10 +388,7 @@ class Project(object): ) def get_deps(service): - return { - (self.get_service(dep), config) - for dep, config in service.get_dependency_configs().items() - } + return {self.get_service(dep) for dep in service.get_dependency_names()} results, errors = parallel.parallel_execute( services, @@ -458,10 +440,6 @@ class Project(object): for service in self.get_services(service_names, include_deps=False): service.pull(ignore_pull_failures) - def push(self, service_names=None, ignore_push_failures=False): - for service in self.get_services(service_names, include_deps=False): - service.push(ignore_push_failures) - def _labeled_containers(self, stopped=False, one_off=OneOffFilter.exclude): return list(filter(None, [ Container.from_ps(self.client, container) @@ -522,14 +500,6 @@ class Project(object): dep_services.append(service) return acc + dep_services - def build_container_operation_with_timeout_func(self, operation, options): - def container_operation_with_timeout(container): - if options.get('timeout') is None: - service = self.get_service(container.service) - options['timeout'] = service.stop_timeout(None) - return getattr(container, operation)(**options) - return container_operation_with_timeout - def get_volumes_from(project, service_dict): volumes_from = service_dict.pop('volumes_from', None) @@ -559,49 +529,6 @@ def get_volumes_from(project, service_dict): return [build_volume_from(vf) for vf in volumes_from] -def get_secrets(service, service_secrets, secret_defs): - secrets = [] - - for secret in service_secrets: - secret_def = secret_defs.get(secret.source) - if not secret_def: - raise ConfigurationError( - "Service \"{service}\" uses an undefined secret \"{secret}\" " - .format(service=service, secret=secret.source)) - - if secret_def.get('external_name'): - log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. " - "External secrets are not available to containers created by " - "docker-compose.".format(service=service, secret=secret.source)) - continue - - if secret.uid or secret.gid or secret.mode: - log.warn("Service \"{service}\" uses secret \"{secret}\" with uid, " - "gid, or mode. These fields are not supported by this " - "implementation of the Compose file".format( - service=service, secret=secret.source)) - - secrets.append({'secret': secret, 'file': secret_def.get('file')}) - - return secrets - - -def warn_for_swarm_mode(client): - info = client.info() - if info.get('Swarm', {}).get('LocalNodeState') == 'active': - if info.get('ServerVersion', '').startswith('ucp'): - # UCP does multi-node scheduling with traditional Compose files. - return - - 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 `docker stack deploy`.\n" - ) - - class NoSuchService(Exception): def __init__(self, name): self.name = name @@ -612,5 +539,4 @@ class NoSuchService(Exception): class ProjectError(Exception): - def __init__(self, msg): - self.msg = msg + pass diff --git a/compose/service.py b/compose/service.py index 9f2fc68b..e8624fa6 100644 --- a/compose/service.py +++ b/compose/service.py @@ -10,20 +10,15 @@ from operator import attrgetter import enum import six from docker.errors import APIError -from docker.errors import ImageNotFound -from docker.errors import NotFound -from docker.types import LogConfig +from docker.utils import LogConfig from docker.utils.ports import build_port_bindings from docker.utils.ports import split_port from . import __version__ -from . import const -from . import progress_stream from .config import DOCKER_CONFIG_KEYS from .config import merge_environment from .config.types import VolumeSpec from .const import DEFAULT_TIMEOUT -from .const import IS_WINDOWS_PLATFORM from .const import LABEL_CONFIG_HASH from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_ONE_OFF @@ -31,15 +26,11 @@ from .const import LABEL_PROJECT from .const import LABEL_SERVICE from .const import LABEL_VERSION from .container import Container -from .errors import HealthCheckFailed -from .errors import NoHealthCheckConfigured -from .errors import OperationFailedError from .parallel import parallel_execute from .parallel import parallel_start from .progress_stream import stream_output from .progress_stream import StreamOutputError from .utils import json_hash -from .utils import parse_seconds_float log = logging.getLogger(__name__) @@ -55,28 +46,20 @@ DOCKER_START_KEYS = [ 'dns_search', 'env_file', 'extra_hosts', - 'group_add', 'ipc', 'read_only', 'log_driver', 'log_opt', 'mem_limit', 'memswap_limit', - 'oom_score_adj', - 'mem_swappiness', 'pid', 'privileged', 'restart', 'security_opt', 'shm_size', - 'sysctls', - 'userns_mode', 'volumes_from', ] -CONDITION_STARTED = 'service_started' -CONDITION_HEALTHY = 'service_healthy' - class BuildError(Exception): def __init__(self, service, reason): @@ -140,7 +123,6 @@ class Service(object): volumes_from=None, network_mode=None, networks=None, - secrets=None, **options ): self.name = name @@ -151,7 +133,6 @@ class Service(object): self.volumes_from = volumes_from or [] self.network_mode = network_mode or NetworkMode(None) self.networks = networks or {} - self.secrets = secrets or [] self.options = options def __repr__(self): @@ -182,7 +163,7 @@ class Service(object): self.start_container_if_stopped(c, **options) return containers - def scale(self, desired_num, timeout=None): + def scale(self, desired_num, timeout=DEFAULT_TIMEOUT): """ Adjusts the number of containers to the specified number and ensures they are running. @@ -198,7 +179,7 @@ class Service(object): 'Remove the custom name to scale the service.' % (self.name, self.custom_container_name)) - if self.specifies_host_port() and desired_num > 1: + if self.specifies_host_port(): log.warn('The "%s" service specifies a port on the host. If multiple containers ' 'for this service are created on a single host, the port will clash.' % self.name) @@ -209,7 +190,7 @@ class Service(object): return container def stop_and_remove(container): - container.stop(timeout=self.stop_timeout(timeout)) + container.stop(timeout=timeout) container.remove() running_containers = self.containers(stopped=False) @@ -295,11 +276,7 @@ class Service(object): if 'name' in container_options and not quiet: log.info("Creating %s" % container_options['name']) - 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)) + return Container.create(self.client, **container_options) def ensure_image_exists(self, do_build=BuildAction.none): if self.can_be_built() and do_build == BuildAction.force: @@ -328,8 +305,11 @@ class Service(object): def image(self): try: return self.client.inspect_image(self.image_name) - except ImageNotFound: - raise NoSuchImageError("Image '{}' not found".format(self.image_name)) + except APIError as e: + if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation): + raise NoSuchImageError("Image '{}' not found".format(self.image_name)) + else: + raise @property def image_name(self): @@ -384,7 +364,7 @@ class Service(object): def execute_convergence_plan(self, plan, - timeout=None, + timeout=DEFAULT_TIMEOUT, detached=False, start=True): (action, containers) = plan @@ -431,7 +411,7 @@ class Service(object): def recreate_container( self, container, - timeout=None, + timeout=DEFAULT_TIMEOUT, attach_logs=False, start_new_container=True): """Recreate a container. @@ -442,7 +422,7 @@ class Service(object): """ log.info("Recreating %s" % container.name) - container.stop(timeout=self.stop_timeout(timeout)) + container.stop(timeout=timeout) container.rename_to_tmp_name() new_container = self.create_container( previous_container=container, @@ -456,14 +436,6 @@ class Service(object): container.remove() return new_container - def stop_timeout(self, timeout): - if timeout is not None: - return timeout - timeout = parse_seconds_float(self.options.get('stop_grace_period')) - if timeout is not None: - return timeout - return DEFAULT_TIMEOUT - def start_container_if_stopped(self, container, attach_logs=False, quiet=False): if not container.is_running: if not quiet: @@ -474,10 +446,7 @@ class Service(object): def start_container(self, container): self.connect_container_to_networks(container) - try: - container.start() - except APIError as ex: - raise OperationFailedError("Cannot start service %s: %s" % (self.name, ex.explanation)) + container.start() return container def connect_container_to_networks(self, container): @@ -497,14 +466,12 @@ 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), - link_local_ips=netdefs.get('link_local_ips', None), - ) + links=self._get_links(False)) - def remove_duplicate_containers(self, timeout=None): + def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT): for c in self.duplicate_containers(): log.info('Removing %s' % c.name) - c.stop(timeout=self.stop_timeout(timeout)) + c.stop(timeout=timeout) c.remove() def duplicate_containers(self): @@ -540,38 +507,10 @@ class Service(object): def get_dependency_names(self): net_name = self.network_mode.service_name - return ( - self.get_linked_service_names() + - self.get_volumes_from_names() + - ([net_name] if net_name else []) + - list(self.options.get('depends_on', {}).keys()) - ) - - def get_dependency_configs(self): - net_name = self.network_mode.service_name - configs = dict( - [(name, None) for name in self.get_linked_service_names()] - ) - configs.update(dict( - [(name, None) for name in self.get_volumes_from_names()] - )) - configs.update({net_name: None} if net_name else {}) - configs.update(self.options.get('depends_on', {})) - for svc, config in self.options.get('depends_on', {}).items(): - if config['condition'] == CONDITION_STARTED: - configs[svc] = lambda s: True - elif config['condition'] == CONDITION_HEALTHY: - configs[svc] = lambda s: s.is_healthy() - else: - # The config schema already prevents this, but it might be - # bypassed if Compose is called programmatically. - raise ValueError( - 'depends_on condition "{}" is invalid.'.format( - config['condition'] - ) - ) - - return configs + return (self.get_linked_service_names() + + self.get_volumes_from_names() + + ([net_name] if net_name else []) + + self.options.get('depends_on', [])) def get_linked_service_names(self): return [service.name for (service, _) in self.links] @@ -695,14 +634,9 @@ class Service(object): override_options['binds'] = binds container_options['environment'].update(affinity) - container_options['volumes'] = dict( - (v.internal, {}) for v in container_options.get('volumes') or {}) - - secret_volumes = self.get_secret_volumes() - if secret_volumes: - override_options['binds'].extend(v.repr() for v in secret_volumes) - container_options['volumes'].update( - (v.internal, {}) for v in secret_volumes) + if 'volumes' in container_options: + container_options['volumes'] = dict( + (v.internal, {}) for v in container_options['volumes']) container_options['image'] = self.image_name @@ -734,7 +668,7 @@ class Service(object): logging_dict = options.get('logging', None) log_config = get_log_config(logging_dict) - host_config = self.client.create_host_config( + return 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'), @@ -759,37 +693,17 @@ class Service(object): cgroup_parent=options.get('cgroup_parent'), cpu_quota=options.get('cpu_quota'), shm_size=options.get('shm_size'), - sysctls=options.get('sysctls'), tmpfs=options.get('tmpfs'), - oom_score_adj=options.get('oom_score_adj'), - mem_swappiness=options.get('mem_swappiness'), - group_add=options.get('group_add'), - userns_mode=options.get('userns_mode') ) - # 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 get_secret_volumes(self): - def build_spec(secret): - target = '{}/{}'.format( - const.SECRETS_PATH, - secret['secret'].target or secret['secret'].source) - return VolumeSpec(secret['file'], target, 'ro') - - return [build_spec(secret) for secret in self.secrets] - def build(self, no_cache=False, pull=False, force_rm=False): log.info('Building %s' % self.name) build_opts = self.options.get('build', {}) path = build_opts.get('context') - # python2 os.stat() doesn't support unicode on some UNIX, so we - # encode it to a bytestring to be safe - if not six.PY3 and not IS_WINDOWS_PLATFORM: + # python2 os.path() doesn't support unicode, so we need to encode it to + # a byte string + if not six.PY3: path = path.encode('utf8') build_output = self.client.build( @@ -892,52 +806,20 @@ class Service(object): repo, tag, separator = parse_repository_tag(self.options['image']) tag = tag or 'latest' log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag)) + output = self.client.pull( + repo, + tag=tag, + stream=True, + ) + try: - output = self.client.pull(repo, tag=tag, stream=True) - return progress_stream.get_digest_from_pull( - stream_output(output, sys.stdout)) - except (StreamOutputError, NotFound) as e: + stream_output(output, sys.stdout) + except StreamOutputError as e: if not ignore_pull_failures: raise else: log.error(six.text_type(e)) - def push(self, ignore_push_failures=False): - if 'image' not in self.options or 'build' not in self.options: - return - - repo, tag, separator = parse_repository_tag(self.options['image']) - tag = tag or 'latest' - log.info('Pushing %s (%s%s%s)...' % (self.name, repo, separator, tag)) - output = self.client.push(repo, tag=tag, stream=True) - - try: - return progress_stream.get_digest_from_push( - stream_output(output, sys.stdout)) - except StreamOutputError as e: - if not ignore_push_failures: - raise - else: - log.error(six.text_type(e)) - - def is_healthy(self): - """ Check that all containers for this service report healthy. - Returns false if at least one healthcheck is pending. - If an unhealthy container is detected, raise a HealthCheckFailed - exception. - """ - result = True - for ctnr in self.containers(): - ctnr.inspect() - status = ctnr.get('State.Health.Status') - if status is None: - raise NoHealthCheckConfigured(self.name) - elif status == 'starting': - result = False - elif status == 'unhealthy': - raise HealthCheckFailed(ctnr.short_id) - return result - def short_id_alias_exists(container, network): aliases = container.get( @@ -1197,8 +1079,6 @@ 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/compose/timeparse.py b/compose/timeparse.py deleted file mode 100644 index 16ef8a6d..00000000 --- a/compose/timeparse.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -''' -timeparse.py -(c) Will Roberts 1 February, 2014 - -This is a vendored and modified copy of: -github.com/wroberts/pytimeparse @ cc0550d - -It has been modified to mimic the behaviour of -https://golang.org/pkg/time/#ParseDuration -''' -# MIT LICENSE -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -from __future__ import absolute_import -from __future__ import unicode_literals - -import re - -HOURS = r'(?P[\d.]+)h' -MINS = r'(?P[\d.]+)m' -SECS = r'(?P[\d.]+)s' -MILLI = r'(?P[\d.]+)ms' -MICRO = r'(?P[\d.]+)(?:us|µs)' -NANO = r'(?P[\d.]+)ns' - - -def opt(x): - return r'(?:{x})?'.format(x=x) - - -TIMEFORMAT = r'{HOURS}{MINS}{SECS}{MILLI}{MICRO}{NANO}'.format( - HOURS=opt(HOURS), - MINS=opt(MINS), - SECS=opt(SECS), - MILLI=opt(MILLI), - MICRO=opt(MICRO), - NANO=opt(NANO), -) - -MULTIPLIERS = dict([ - ('hours', 60 * 60), - ('mins', 60), - ('secs', 1), - ('milli', 1.0 / 1000), - ('micro', 1.0 / 1000.0 / 1000), - ('nano', 1.0 / 1000.0 / 1000.0 / 1000.0), -]) - - -def timeparse(sval): - """Parse a time expression, returning it as a number of seconds. If - possible, the return value will be an `int`; if this is not - possible, the return will be a `float`. Returns `None` if a time - expression cannot be parsed from the given string. - - Arguments: - - `sval`: the string value to parse - - >>> timeparse('1m24s') - 84 - >>> timeparse('1.2 minutes') - 72 - >>> timeparse('1.2 seconds') - 1.2 - """ - match = re.match(r'\s*' + TIMEFORMAT + r'\s*$', sval, re.I) - if not match or not match.group(0).strip(): - return - - mdict = match.groupdict() - return sum( - MULTIPLIERS[k] * cast(v) for (k, v) in mdict.items() if v is not None) - - -def cast(value): - return int(value, 10) if value.isdigit() else float(value) diff --git a/compose/utils.py b/compose/utils.py index b8bdf732..494beea3 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -5,17 +5,11 @@ import codecs import hashlib import json import json.decoder -import logging -import ntpath import six -from .errors import StreamParseError -from .timeparse import timeparse - json_decoder = json.JSONDecoder() -log = logging.getLogger(__name__) def get_output_stream(stream): @@ -66,21 +60,13 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a): yield item if buffered: - try: - yield decoder(buffered) - except Exception as e: - log.error( - 'Compose tried decoding the following data chunk, but failed:' - '\n%s' % repr(buffered) - ) - raise StreamParseError(e) + yield decoder(buffered) 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():] @@ -108,28 +94,5 @@ def microseconds_from_time_nano(time_nano): return int(time_nano % 1000000000 / 1000) -def nanoseconds_from_time_seconds(time_seconds): - return time_seconds * 1000000000 - - -def parse_seconds_float(value): - return timeparse(value or '') - - -def parse_nanoseconds_int(value): - parsed = timeparse(value or '') - if parsed is None: - return None - return int(parsed * 1000000000) - - 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) + return dict((k, str(v)) for k, v in source_dict.items()) diff --git a/compose/volume.py b/compose/volume.py index ab6a88fa..f440ba40 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -4,29 +4,25 @@ from __future__ import unicode_literals import logging from docker.errors import NotFound -from docker.utils import version_lt from .config import ConfigurationError -from .const import LABEL_PROJECT -from .const import LABEL_VOLUME log = logging.getLogger(__name__) class Volume(object): def __init__(self, client, project, name, driver=None, driver_opts=None, - external_name=None, labels=None): + external_name=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, labels=self._labels + self.full_name, self.driver, self.driver_opts ) def remove(self): @@ -56,17 +52,6 @@ class Volume(object): return self.external_name return '{0}_{1}'.format(self.project, self.name) - @property - def _labels(self): - if version_lt(self.client._version, '1.23'): - return None - labels = self.labels.copy() if self.labels else {} - labels.update({ - LABEL_PROJECT: self.project, - LABEL_VOLUME: self.name, - }) - return labels - class ProjectVolumes(object): @@ -83,8 +68,7 @@ class ProjectVolumes(object): name=vol_name, driver=data.get('driver'), driver_opts=data.get('driver_opts'), - external_name=data.get('external_name'), - labels=data.get('labels') + external_name=data.get('external_name') ) for vol_name, data in config_volumes.items() } diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 77d02b42..66747fbd 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -109,18 +109,6 @@ _docker_compose_build() { } -_docker_compose_bundle() { - case "$prev" in - --output|-o) - _filedir - return - ;; - esac - - COMPREPLY=( $( compgen -W "--push-images --help --output -o" -- "$cur" ) ) -} - - _docker_compose_config() { COMPREPLY=( $( compgen -W "--help --quiet -q --services" -- "$cur" ) ) } @@ -316,18 +304,6 @@ _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) @@ -349,7 +325,7 @@ _docker_compose_restart() { _docker_compose_rm() { case "$cur" in -*) - COMPREPLY=( $( compgen -W "--force -f --help -v" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--all -a --force -f --help -v" -- "$cur" ) ) ;; *) __docker_compose_services_stopped @@ -434,18 +410,6 @@ _docker_compose_stop() { } -_docker_compose_top() { - case "$cur" in - -*) - COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) - ;; - *) - __docker_compose_services_running - ;; - esac -} - - _docker_compose_unpause() { case "$cur" in -*) @@ -491,7 +455,6 @@ _docker_compose() { local commands=( build - bundle config create down @@ -504,14 +467,12 @@ _docker_compose() { port ps pull - push restart rm run scale start stop - top unpause up version diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 66d924f7..ec9cb682 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -19,49 +19,52 @@ # * @felixr docker zsh completion script : https://github.com/felixr/docker-zsh-completion # ------------------------------------------------------------------------- -__docker-compose_q() { - docker-compose 2>/dev/null $compose_options "$@" +# 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 } -# All services defined in docker-compose.yml -__docker-compose_all_services_in_compose_file() { +# 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_q config --services \ - | grep -Ev "^(${already_selected})$" + awk -F: '/^[a-zA-Z0-9]/{print $1}' "${compose_file:-$(__docker-compose_compose_file)}" 2>/dev/null | grep -Ev "$already_selected" } # All services, even those without an existing container __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 " " "|") # flatten sections to one line, then filter lines containing the key and return section name. - __docker-compose_q config \ - | sed -n -e '/^services:/,/^[^ ]/p' \ - | sed -n 's/^ //p' \ - | awk '/^[a-zA-Z0-9]/{printf "\n"};{printf $0;next;}' \ - | grep " \+$1:" \ - | cut -d: -f1 \ - | grep -Ev "^(${already_selected})$" + 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" } # All services that are defined by a Dockerfile reference __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 @@ -71,7 +74,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 @@ -93,7 +96,7 @@ __docker-compose_get_services() { shift [[ $kind =~ (stopped|all) ]] && args=($args -a) - lines=(${(f)"$(_call_program commands docker $docker_options ps $args)"}) + lines=(${(f)"$(_call_program commands docker ps $args)"}) services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"}) # Parse header line to find columns @@ -182,17 +185,7 @@ __docker-compose_commands() { } __docker-compose_subcommand() { - 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.]" - + local opts_help='(: -)--help[Print usage]' integer ret=1 case "$words[1]" in @@ -200,16 +193,10 @@ __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 ;; - (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) _arguments \ $opts_help \ @@ -219,23 +206,21 @@ __docker-compose_subcommand() { (create) _arguments \ $opts_help \ - $opts_force_recreate \ - $opts_no_recreate \ - $opts_no_build \ - "(--no-build)--build[Build images before creating containers.]" \ + "(--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]" \ '*:services:__docker-compose_services_all' && ret=0 ;; (down) _arguments \ $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.]" \ - $opts_remove_orphans && ret=0 + "--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 ;; (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) @@ -245,7 +230,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 @@ -263,7 +248,7 @@ __docker-compose_subcommand() { _arguments \ $opts_help \ '(-f --follow)'{-f,--follow}'[Follow log output]' \ - $opts_no_color \ + '--no-color[Produce monochrome output.]' \ '--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 @@ -276,8 +261,8 @@ __docker-compose_subcommand() { (port) _arguments \ $opts_help \ - '--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: ' \ + '--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: ' \ '1:running services:__docker-compose_runningservices' \ '2:port:_ports' && ret=0 ;; @@ -293,17 +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 \ + '(-a --all)'{-a,--all}"[Also remove one-off containers]" \ '(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \ - '-v[Remove any anonymous volumes attached to containers]' \ + '-v[Remove volumes associated with containers]' \ '*:stopped services:__docker-compose_stoppedservices' && ret=0 ;; (run) @@ -312,14 +292,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: ' \ - $opts_no_deps \ - '(-p --publish)'{-p,--publish=}"[Publish a container's port(s) to the host]" \ + '--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.]" \ '--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 @@ -327,7 +307,7 @@ __docker-compose_subcommand() { (scale) _arguments \ $opts_help \ - $opts_timeout \ + '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ '*:running services:__docker-compose_runningservices' && ret=0 ;; (start) @@ -338,12 +318,7 @@ __docker-compose_subcommand() { (stop|restart) _arguments \ $opts_help \ - $opts_timeout \ - '*:running services:__docker-compose_runningservices' && ret=0 - ;; - (top) - _arguments \ - $opts_help \ + '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ '*:running services:__docker-compose_runningservices' && ret=0 ;; (unpause) @@ -354,16 +329,15 @@ __docker-compose_subcommand() { (up) _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.]' \ - $opts_no_color \ - $opts_no_deps \ - $opts_force_recreate \ - $opts_no_recreate \ - $opts_no_build \ - "(--no-build)--build[Build images before starting containers.]" \ + '(--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.]" \ + "--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]" \ "(-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: " \ - $opts_remove_orphans \ + '(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \ '*:services:__docker-compose_services_all' && ret=0 ;; (version) @@ -391,67 +365,18 @@ _docker-compose() { integer ret=1 typeset -A opt_args - local file_description - - if [[ -n ${words[(r)-f]} || -n ${words[(r)--file]} ]] ; then - file_description="Specify an override docker-compose file (default: docker-compose.override.yml)" - else - file_description="Specify an alternate docker-compose file (default: docker-compose.yml)" - fi - _arguments -C \ '(- :)'{-h,--help}'[Get help]' \ - '*'{-f,--file}"[${file_description}]: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)]" \ + '(-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:' \ '(-): :->command' \ '(-)*:: :->option-or-argument' && ret=0 - 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 + 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}" case $state in (command) diff --git a/docker-compose.spec b/docker-compose.spec index ef0e2593..3a165dd6 100644 --- a/docker-compose.spec +++ b/docker-compose.spec @@ -27,21 +27,6 @@ exe = EXE(pyz, 'compose/config/config_schema_v2.0.json', 'DATA' ), - ( - 'compose/config/config_schema_v2.1.json', - 'compose/config/config_schema_v2.1.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.0.json', - 'compose/config/config_schema_v3.0.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.1.json', - 'compose/config/config_schema_v3.1.json', - 'DATA' - ), ( 'compose/GITSHA', 'compose/GITSHA', diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..b16d0d2c --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,18 @@ +FROM docs/base:latest +MAINTAINER Mary Anthony (@moxiegirl) + +RUN svn checkout https://github.com/docker/docker/trunk/docs /docs/content/engine +RUN svn checkout https://github.com/docker/swarm/trunk/docs /docs/content/swarm +RUN svn checkout https://github.com/docker/machine/trunk/docs /docs/content/machine +RUN svn checkout https://github.com/docker/distribution/trunk/docs /docs/content/registry +RUN svn checkout https://github.com/docker/notary/trunk/docs /docs/content/notary +RUN svn checkout https://github.com/docker/kitematic/trunk/docs /docs/content/kitematic +RUN svn checkout https://github.com/docker/toolbox/trunk/docs /docs/content/toolbox +RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content/project + + +ENV PROJECT=compose +# To get the git info for this repo +COPY . /src + +COPY . /docs/content/$PROJECT/ diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..b9ef0548 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,55 @@ +.PHONY: all binary build cross default docs docs-build docs-shell shell test test-unit test-integration test-integration-cli test-docker-py validate + +# env vars passed through directly to Docker's build scripts +# to allow things like `make DOCKER_CLIENTONLY=1 binary` easily +# `docs/sources/contributing/devenvironment.md ` and `project/PACKAGERS.md` have some limited documentation of some of these +DOCKER_ENVS := \ + -e BUILDFLAGS \ + -e DOCKER_CLIENTONLY \ + -e DOCKER_EXECDRIVER \ + -e DOCKER_GRAPHDRIVER \ + -e TESTDIRS \ + -e TESTFLAGS \ + -e TIMEOUT +# note: we _cannot_ add "-e DOCKER_BUILDTAGS" here because even if it's unset in the shell, that would shadow the "ENV DOCKER_BUILDTAGS" set in our Dockerfile, which is very important for our official builds + +# 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) +DOCKER_IMAGE := docker$(if $(GIT_BRANCH),:$(GIT_BRANCH)) +DOCKER_DOCS_IMAGE := docs-base$(if $(GIT_BRANCH),:$(GIT_BRANCH)) + + +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 + + +docs-build: +# ( git remote | grep -v upstream ) || git diff --name-status upstream/release..upstream/docs ./ > ./changed-files +# echo "$(GIT_BRANCH)" > GIT_BRANCH +# echo "$(AWS_S3_BUCKET)" > AWS_S3_BUCKET +# echo "$(GITCOMMIT)" > GITCOMMIT + docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/README.md b/docs/README.md index 50c91d20..e60fa48c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,16 +1,86 @@ -# The docs have been moved! + -The documentation for Compose has been merged into -[the general documentation repo](https://github.com/docker/docker.github.io). +# Contributing to the Docker Compose documentation -The docs for Compose are now here: -https://github.com/docker/docker.github.io/tree/master/compose +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. -Please submit pull requests for unpublished features on the `vnext-compose` branch (https://github.com/docker/docker.github.io/tree/vnext-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 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). +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. -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` +## Documentation contributing workflow -As always, the docs remain open-source and we appreciate your feedback and -pull requests! +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. diff --git a/docs/completion.md b/docs/completion.md new file mode 100644 index 00000000..2076d512 --- /dev/null +++ b/docs/completion.md @@ -0,0 +1,68 @@ + + +# 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 new file mode 100644 index 00000000..e9ec0a2d --- /dev/null +++ b/docs/compose-file.md @@ -0,0 +1,1132 @@ + + + +# 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 tags the built image +with the tag specified in `image`: + + build: ./dir + image: webapp + +This will result in an image tagged `webapp`, 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. 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. + +Build arguments with only a key are resolved to their environment value on the +machine Compose is running on. + + build: + args: + buildno: 1 + user: someuser + + build: + args: + - buildno=1 + - user=someuser + +### 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 + +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 + +### 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 + +### 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). + + 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. + + 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 + +### 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, 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 + 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/postgres/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 + +### 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`). + +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 + + +### 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`, declared 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 `POSTGRES_VERSION=9.3` and you supply this +configuration: + + db: + image: "postgres:${POSTGRES_VERSION}" + +When you run `docker-compose up` with this configuration, Compose looks for the +`POSTGRES_VERSION` environment variable in the shell and substitutes its value +in. For this example, Compose resolves the `image` to `postgres:9.3` before +running the configuration. + +If an environment variable is not set, Compose substitutes with an empty +string. In the example above, if `POSTGRES_VERSION` is not set, the value for +the `image` option is `postgres:`. + +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 new file mode 100644 index 00000000..fb1fa214 --- /dev/null +++ b/docs/django.md @@ -0,0 +1,194 @@ + + + +# 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](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) + and the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/). + +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 new file mode 100644 index 00000000..be2625f8 --- /dev/null +++ b/docs/env-file.md @@ -0,0 +1,43 @@ + + + +# 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/extends.md b/docs/extends.md new file mode 100644 index 00000000..6f457391 --- /dev/null +++ b/docs/extends.md @@ -0,0 +1,354 @@ + + + +# 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 new file mode 100644 index 00000000..45885255 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,128 @@ + + +# 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 new file mode 100644 index 00000000..60482bce --- /dev/null +++ b/docs/gettingstarted.md @@ -0,0 +1,191 @@ + + + +# 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](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](http://docs.docker.com/reference/builder/). + +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 new file mode 100644 index 00000000..75769754 Binary files /dev/null and b/docs/images/django-it-worked.png differ diff --git a/docs/images/rails-welcome.png b/docs/images/rails-welcome.png new file mode 100644 index 00000000..51512dbd Binary files /dev/null and b/docs/images/rails-welcome.png differ diff --git a/docs/images/wordpress-files.png b/docs/images/wordpress-files.png new file mode 100644 index 00000000..4762935b Binary files /dev/null and b/docs/images/wordpress-files.png differ diff --git a/docs/images/wordpress-lang.png b/docs/images/wordpress-lang.png new file mode 100644 index 00000000..f0bd864e Binary files /dev/null and b/docs/images/wordpress-lang.png differ diff --git a/docs/images/wordpress-welcome.png b/docs/images/wordpress-welcome.png new file mode 100644 index 00000000..c9ba2036 Binary files /dev/null and b/docs/images/wordpress-welcome.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..f1b71079 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ + + + +# 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 new file mode 100644 index 00000000..76e4a868 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,136 @@ + + + +# 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.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). + +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.7.1 + + +## 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.7.1/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 new file mode 100644 index 00000000..55ba5f2d --- /dev/null +++ b/docs/link-env-deprecated.md @@ -0,0 +1,48 @@ + + +# 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] 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` + +[Docker links]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/ + +## 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 new file mode 100644 index 00000000..9739a088 --- /dev/null +++ b/docs/networking.md @@ -0,0 +1,154 @@ + + + +# 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 new file mode 100644 index 00000000..03ade356 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,188 @@ + + + +# 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](https://docs.docker.com/machine/) or an entire +[Docker Swarm](https://docs.docker.com/swarm/) 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 new file mode 100644 index 00000000..9acf64e5 --- /dev/null +++ b/docs/production.md @@ -0,0 +1,88 @@ + + + +## 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) 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), 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 new file mode 100644 index 00000000..a8fc383e --- /dev/null +++ b/docs/rails.md @@ -0,0 +1,174 @@ + + +## 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 + 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](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/). + +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](https://docs.docker.com/machine/), 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 new file mode 100644 index 00000000..84aefc25 --- /dev/null +++ b/docs/reference/build.md @@ -0,0 +1,25 @@ + + +# 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/config.md b/docs/reference/config.md new file mode 100644 index 00000000..1a9706f4 --- /dev/null +++ b/docs/reference/config.md @@ -0,0 +1,23 @@ + + +# 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 new file mode 100644 index 00000000..5065e8be --- /dev/null +++ b/docs/reference/create.md @@ -0,0 +1,26 @@ + + +# 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 new file mode 100644 index 00000000..e8b1db59 --- /dev/null +++ b/docs/reference/down.md @@ -0,0 +1,28 @@ + + +# down + +``` +Stop containers and remove containers, networks, volumes, and images +created by `up`. Only containers and networks are removed by default. + +Usage: down [options] + +Options: + --rmi type 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 + -v, --volumes Remove data volumes + + --remove-orphans Remove containers for services not defined in the + Compose file +``` diff --git a/docs/reference/envvars.md b/docs/reference/envvars.md new file mode 100644 index 00000000..6f7fb791 --- /dev/null +++ b/docs/reference/envvars.md @@ -0,0 +1,87 @@ + + + +# 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. + + +## 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 new file mode 100644 index 00000000..827258f2 --- /dev/null +++ b/docs/reference/events.md @@ -0,0 +1,34 @@ + + +# 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 new file mode 100644 index 00000000..6c0eeb04 --- /dev/null +++ b/docs/reference/exec.md @@ -0,0 +1,29 @@ + + +# 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 new file mode 100644 index 00000000..613708ed --- /dev/null +++ b/docs/reference/help.md @@ -0,0 +1,18 @@ + + +# help + +``` +Usage: help COMMAND +``` + +Displays help and usage instructions for a command. diff --git a/docs/reference/index.md b/docs/reference/index.md new file mode 100644 index 00000000..2ac3676a --- /dev/null +++ b/docs/reference/index.md @@ -0,0 +1,42 @@ + + +## 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 new file mode 100644 index 00000000..dc4bf23a --- /dev/null +++ b/docs/reference/kill.md @@ -0,0 +1,24 @@ + + +# 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 new file mode 100644 index 00000000..745d24f7 --- /dev/null +++ b/docs/reference/logs.md @@ -0,0 +1,25 @@ + + +# 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 new file mode 100644 index 00000000..d59fa565 --- /dev/null +++ b/docs/reference/overview.md @@ -0,0 +1,127 @@ + + + +# 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 new file mode 100644 index 00000000..a0ffab03 --- /dev/null +++ b/docs/reference/pause.md @@ -0,0 +1,18 @@ + + +# 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 new file mode 100644 index 00000000..c946a97d --- /dev/null +++ b/docs/reference/port.md @@ -0,0 +1,23 @@ + + +# 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 new file mode 100644 index 00000000..546d68e7 --- /dev/null +++ b/docs/reference/ps.md @@ -0,0 +1,21 @@ + + +# ps + +``` +Usage: ps [options] [SERVICE...] + +Options: +-q Only display IDs +``` + +Lists containers. diff --git a/docs/reference/pull.md b/docs/reference/pull.md new file mode 100644 index 00000000..5ec184b7 --- /dev/null +++ b/docs/reference/pull.md @@ -0,0 +1,21 @@ + + +# 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/restart.md b/docs/reference/restart.md new file mode 100644 index 00000000..bbd4a68b --- /dev/null +++ b/docs/reference/restart.md @@ -0,0 +1,21 @@ + + +# 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 new file mode 100644 index 00000000..97698b58 --- /dev/null +++ b/docs/reference/rm.md @@ -0,0 +1,28 @@ + + +# rm + +``` +Usage: rm [options] [SERVICE...] + +Options: +-f, --force Don't ask to confirm removal +-v Remove volumes associated with containers +-a, --all Also remove one-off containers +``` + +Removes stopped service containers. + +By default, volumes attached to containers will not be removed. You can see all +volumes with `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 new file mode 100644 index 00000000..86354424 --- /dev/null +++ b/docs/reference/run.md @@ -0,0 +1,56 @@ + + +# 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 new file mode 100644 index 00000000..75140ee9 --- /dev/null +++ b/docs/reference/scale.md @@ -0,0 +1,21 @@ + + +# 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 new file mode 100644 index 00000000..f0bdd5a9 --- /dev/null +++ b/docs/reference/start.md @@ -0,0 +1,18 @@ + + +# start + +``` +Usage: start [SERVICE...] +``` + +Starts existing containers for a service. diff --git a/docs/reference/stop.md b/docs/reference/stop.md new file mode 100644 index 00000000..ec7e6688 --- /dev/null +++ b/docs/reference/stop.md @@ -0,0 +1,22 @@ + + +# 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 new file mode 100644 index 00000000..846b229e --- /dev/null +++ b/docs/reference/unpause.md @@ -0,0 +1,18 @@ + + +# unpause + +``` +Usage: unpause [SERVICE...] +``` + +Unpauses paused containers of a service. diff --git a/docs/reference/up.md b/docs/reference/up.md new file mode 100644 index 00000000..3951f879 --- /dev/null +++ b/docs/reference/up.md @@ -0,0 +1,55 @@ + + +# 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 new file mode 100644 index 00000000..c67e1829 --- /dev/null +++ b/docs/startup-order.md @@ -0,0 +1,88 @@ + + +# 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 new file mode 100644 index 00000000..ece72193 --- /dev/null +++ b/docs/swarm.md @@ -0,0 +1,181 @@ + + + +# Using Compose with Swarm + +Docker Compose and [Docker Swarm](/swarm/overview) aim to have full integration, meaning +you can point a Compose app at a Swarm cluster and have it all just work as if +you were using a single Docker host. + +The actual extent of integration depends on which version of the [Compose file +format](compose-file.md#versioning) you are using: + +1. If you're using version 1 along with `links`, your app will work, but Swarm + will schedule all containers on one host, because links between containers + do not work across hosts with the old networking system. + +2. If you're using version 2, your app should work with no changes: + + - subject to the [limitations](#limitations) described below, + + - as long as the Swarm cluster is configured to use the [overlay driver](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) 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 new file mode 100644 index 00000000..62f50c24 --- /dev/null +++ b/docs/wordpress.md @@ -0,0 +1,149 @@ + + + +# 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 `Dockerfile`, a `docker-compose.yaml` file, along with a downloaded `wordpress` directory and a custom `wp-config.php`, all of which you will create in the following steps. + +2. Change directories into your project directory. + + For example, if you named your directory `my_wordpress`: + + $ cd my-wordpress/ + +3. Create a `Dockerfile`, a file that defines the environment in which your application will run. + + For more information on how to write Dockerfiles, see the [Docker Engine user guide](https://docs.docker.com/engine/userguide/dockerimages/#building-an-image-from-a-dockerfile) and the [Dockerfile reference](https://docs.docker.com/engine/reference/builder/). + + In this case, your Dockerfile should include these two lines: + + FROM orchardup/php5 + ADD . /code + + This tells the Docker Engine daemon how to build an image defining a container that contains PHP and WordPress. + +4. Create a `docker-compose.yml` file that will start your web service and a separate MySQL instance: + + version: '2' + services: + web: + build: . + command: php -S 0.0.0.0:8000 -t /code/wordpress/ + ports: + - "8000:8000" + depends_on: + - db + volumes: + - .:/code + db: + image: orchardup/mysql + environment: + MYSQL_DATABASE: wordpress + +5. Download WordPress into the current directory: + + $ curl https://wordpress.org/latest.tar.gz | tar -xvzf - + + This creates a directory called `wordpress` in your project directory. + +6. Create a `wp-config.php` file within the `wordpress` directory. + + A supporting file is needed to get this working. At the top level of the wordpress directory, add a new file called `wp-config.php` as shown. This is the standard WordPress config file with a single change to point the database configuration at the `db` container: + + + +7. Verify the contents and structure of your project directory. + + + ![WordPress files](images/wordpress-files.png) + +### Build the project + +With those four new files in place, run `docker-compose up` from your project directory. This will pull and build the needed images, and then start the web and database containers. + +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. + +![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) diff --git a/experimental/compose_swarm_networking.md b/experimental/compose_swarm_networking.md index 905f52f8..b1fb25dc 100644 --- a/experimental/compose_swarm_networking.md +++ b/experimental/compose_swarm_networking.md @@ -1,5 +1,183 @@ # Experimental: Compose, Swarm and Multi-Host Networking -Compose now supports multi-host networking as standard. Read more here: +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. -https://docs.docker.com/compose/networking +> 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. diff --git a/project/RELEASE-PROCESS.md b/project/RELEASE-PROCESS.md index c1834f2f..930af15a 100644 --- a/project/RELEASE-PROCESS.md +++ b/project/RELEASE-PROCESS.md @@ -20,30 +20,18 @@ release. As part of this script you'll be asked to: -1. Update the version in `compose/__init__.py` and `script/run/run.sh`. +1. Update the version in `docs/install.md` and `compose/__init__.py`. - If the next release will be an RC, append `-rcN`, e.g. `1.4.0-rc1`. + If the next release will be an RC, append `rcN`, e.g. `1.4.0rc1`. 2. Write release notes in `CHANGES.md`. - Almost every feature enhancement should be mentioned, with the most - visible/exciting ones first. Use descriptive sentences and give context - where appropriate. + Almost every feature enhancement should be mentioned, with the most visible/exciting ones first. Use descriptive sentences and give context where appropriate. - Bug fixes are worth mentioning if it's likely that they've affected lots - of people, or if they were regressions in the previous version. + Bug fixes are worth mentioning if it's likely that they've affected lots of people, or if they were regressions in the previous version. Improvements to the code are not worth mentioning. -3. Create a new repository on [bintray](https://bintray.com/docker-compose). - The name has to match the name of the branch (e.g. `bump-1.9.0`) and the - type should be "Generic". Other fields can be left blank. - -4. Check that the `vnext-compose` branch on - [the docs repo](https://github.com/docker/docker.github.io/) has - documentation for all the new additions in the upcoming release, and create - a PR there for what needs to be amended. - ## When a PR is merged into master that we want in the release @@ -67,8 +55,8 @@ Check out the bump branch and run the `build-binaries` script When prompted build the non-linux binaries and test them. -1. Download the osx binary from Bintray. Make sure that the latest Travis - build has finished, otherwise you'll be downloading an old binary. +1. Download the osx binary from Bintray. Make sure that the latest build has + finished, otherwise you'll be downloading an old binary. https://dl.bintray.com/docker-compose/$BRANCH_NAME/ @@ -79,24 +67,22 @@ When prompted build the non-linux binaries and test them. 3. Draft a release from the tag on GitHub (the script will open the window for you) - The tag will only be present on Github when you run the `push-release` - script in step 7, but you can pre-fill it at that point. + In the "Tag version" dropdown, select the tag you just pushed. -4. Paste in installation instructions and release notes. Here's an example - - change the Compose version and Docker version as appropriate: +4. Paste in installation instructions and release notes. Here's an example - change the Compose version and Docker version as appropriate: - If you're a Mac or Windows user, the best way to install Compose and keep it up-to-date is **[Docker for Mac and Windows](https://www.docker.com/products/docker)**. + Firstly, note that Compose 1.5.0 requires Docker 1.8.0 or later. - Note that Compose 1.9.0 requires Docker Engine 1.10.0 or later for version 2 of the Compose File format, and Docker Engine 1.9.1 or later for version 1. Docker for Mac and Windows will automatically install the latest version of Docker Engine for you. + Secondly, if you're a Mac user, the **[Docker Toolbox](https://www.docker.com/toolbox)** will install Compose 1.5.0 for you, alongside the latest versions of the Docker Engine, Machine and Kitematic. - Alternatively, you can use the usual commands to install or upgrade Compose: + Otherwise, you can use the usual commands to install/upgrade. Either download the binary: - ``` - curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose - ``` + curl -L https://github.com/docker/compose/releases/download/1.5.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose - See the [install docs](https://docs.docker.com/compose/install/) for more install options and instructions. + Or install the PyPi package: + + pip install -U docker-compose==1.5.0 Here's what's new: @@ -113,8 +99,6 @@ When prompted build the non-linux binaries and test them. ./script/release/push-release -8. Merge the bump PR. - 8. Publish the release on GitHub. 9. Check that all the binaries download (following the install instructions) and run. @@ -123,7 +107,19 @@ When prompted build the non-linux binaries and test them. ## If it’s a stable release (not an RC) -1. Close the release’s milestone. +1. Merge the bump PR. + +2. Make sure `origin/release` is updated locally: + + git fetch origin + +3. Update the `docs` branch on the upstream repo: + + git push git@github.com:docker/compose.git origin/release:docs + +4. Let the docs team know that it’s been updated so they can publish it. + +5. Close the release’s milestone. ## If it’s a minor release (1.x.0), rather than a patch release (1.x.y) diff --git a/requirements-build.txt b/requirements-build.txt index 27f610ca..3f1dbd75 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1 +1 @@ -pyinstaller==3.2.1 +pyinstaller==3.1.1 diff --git a/requirements.txt b/requirements.txt index 3b06bff4..eb5275f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,11 @@ PyYAML==3.11 -backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 -colorama==0.3.7 -docker==2.0.2 +docker-py==1.8.1 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 +enum34==1.0.4 jsonschema==2.5.1 -pypiwin32==219; sys_platform == 'win32' -requests==2.11.1 -six==1.10.0 +requests==2.7.0 +six==1.7.3 texttable==0.8.4 websocket-client==0.32.0 diff --git a/script/build/image b/script/build/image index 3590ce14..bdd98f03 100755 --- a/script/build/image +++ b/script/build/image @@ -11,5 +11,6 @@ TAG=$1 VERSION="$(python setup.py --version)" ./script/build/write-git-sha -python setup.py sdist bdist_wheel -docker build --build-arg version=$VERSION -t docker/compose:$TAG -f Dockerfile.run . +python setup.py sdist +cp dist/docker-compose-$VERSION.tar.gz dist/docker-compose-release.tar.gz +docker build -t docker/compose:$TAG -f Dockerfile.run . diff --git a/script/release/contributors b/script/release/contributors index 4657dd80..1e69b143 100755 --- a/script/release/contributors +++ b/script/release/contributors @@ -15,10 +15,10 @@ EOM [[ -n "$1" ]] || usage PREV_RELEASE=$1 -BRANCH="$(git rev-parse --abbrev-ref HEAD)" +VERSION=HEAD URL="https://api.github.com/repos/docker/compose/compare" -contribs=$(curl -sf "$URL/$PREV_RELEASE...$BRANCH" | \ +contribs=$(curl -sf "$URL/$PREV_RELEASE...$VERSION" | \ jq -r '.commits[].author.login' | \ sort | \ uniq -c | \ diff --git a/script/release/push-release b/script/release/push-release index 9db6f689..33d0d777 100755 --- a/script/release/push-release +++ b/script/release/push-release @@ -54,19 +54,18 @@ git push $GITHUB_REPO $VERSION echo "Uploading the docker image" docker push docker/compose:$VERSION -echo "Uploading package to PyPI" +echo "Uploading sdist to pypi" pandoc -f markdown -t rst README.md -o README.rst sed -i -e 's/logo.png?raw=true/https:\/\/github.com\/docker\/compose\/raw\/master\/logo.png?raw=true/' README.rst ./script/build/write-git-sha -python setup.py sdist bdist_wheel +python setup.py sdist if [ "$(command -v twine 2> /dev/null)" ]; then - twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz ./dist/docker_compose-${VERSION/-/}-py2.py3-none-any.whl + twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz else python setup.py upload fi echo "Testing pip package" -deactivate || true virtualenv venv-test source venv-test/bin/activate pip install docker-compose==$VERSION diff --git a/script/release/utils.sh b/script/release/utils.sh index 321c1fb7..b4e5a2e6 100644 --- a/script/release/utils.sh +++ b/script/release/utils.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Util functions for release scripts +# Util functions for release scritps # set -e diff --git a/script/run/run.sh b/script/run/run.sh index 4e173894..c0ecc3dd 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.12.0dev" +VERSION="1.7.1" IMAGE="docker/compose:$VERSION" @@ -35,7 +35,7 @@ if [ "$(pwd)" != '/' ]; then VOLUMES="-v $(pwd):$(pwd)" fi if [ -n "$COMPOSE_FILE" ]; then - compose_dir=$(realpath $(dirname $COMPOSE_FILE)) + compose_dir=$(dirname $COMPOSE_FILE) fi # TODO: also check --file argument if [ -n "$compose_dir" ]; then diff --git a/script/setup/osx b/script/setup/osx index e6ab62a8..10bbbecc 100755 --- a/script/setup/osx +++ b/script/setup/osx @@ -10,13 +10,13 @@ openssl_version() { python -c "import ssl; print ssl.OPENSSL_VERSION" } -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_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_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" +desired_openssl_version="1.0.1j" +desired_openssl_brew_version="1.0.1j_1" +openssl_formula="https://raw.githubusercontent.com/Homebrew/homebrew/62fc2a1a65e83ba9dbb30b2e0a2b7355831c714b/Library/Formula/openssl.rb" PATH="/usr/local/bin:$PATH" diff --git a/script/test/all b/script/test/all index 7151a75e..08bf1618 100755 --- a/script/test/all +++ b/script/test/all @@ -24,7 +24,6 @@ fi BUILD_NUMBER=${BUILD_NUMBER-$USER} -PY_TEST_VERSIONS=${PY_TEST_VERSIONS:-py27,py34} for version in $DOCKER_VERSIONS; do >&2 echo "Running tests against Docker $version" @@ -59,6 +58,6 @@ for version in $DOCKER_VERSIONS; do --env="DOCKER_VERSION=$version" \ --entrypoint="tox" \ "$TAG" \ - -e "$PY_TEST_VERSIONS" -- "$@" + -e py27,py34 -- "$@" done diff --git a/script/test/versions.py b/script/test/versions.py index 0c3b8162..98f97ef3 100755 --- a/script/test/versions.py +++ b/script/test/versions.py @@ -5,7 +5,7 @@ version tags for recent releases, or the default release. The default release is the most recent non-RC version. -Recent is a list of unique major.minor versions, where each is the most +Recent is a list of unqiue major.minor versions, where each is the most recent version in the series. For example, if the list of versions is: @@ -28,7 +28,6 @@ from __future__ import unicode_literals import argparse import itertools import operator -import sys from collections import namedtuple import requests @@ -104,14 +103,6 @@ def get_default(versions): return version -def get_versions(tags): - for tag in tags: - try: - yield Version.parse(tag['name']) - except ValueError: - print("Skipping invalid tag: {name}".format(**tag), file=sys.stderr) - - def get_github_releases(project): """Query the Github API for a list of version tags and return them in sorted order. @@ -121,7 +112,7 @@ def get_github_releases(project): url = '{}/{}/tags'.format(GITHUB_API, project) response = requests.get(url) response.raise_for_status() - versions = get_versions(response.json()) + versions = [Version.parse(tag['name']) for tag in response.json()] return sorted(versions, reverse=True, key=operator.attrgetter('order')) diff --git a/script/travis/ci b/script/travis/ci index cd4fcc6d..4cce1bc8 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 - tox -e py27 -- tests/unit + python -m tox -e py27 -- tests/unit fi diff --git a/script/travis/install b/script/travis/install index d4b34786..a23667bf 100755 --- a/script/travis/install +++ b/script/travis/install @@ -5,6 +5,5 @@ set -ex if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install tox==2.1.1 else - sudo pip install --upgrade pip tox==2.1.1 virtualenv - pip --version + pip install --user tox==2.1.1 fi diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py index eafbc356..0b37c1dd 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import -from __future__ import print_function from __future__ import unicode_literals import codecs @@ -9,7 +8,6 @@ import os import re import sys -import pkg_resources from setuptools import find_packages from setuptools import setup @@ -31,13 +29,12 @@ def find_version(*file_paths): install_requires = [ 'cached-property >= 1.2.0, < 2', - 'colorama >= 0.3.7, < 0.4', 'docopt >= 0.6.1, < 0.7', 'PyYAML >= 3.10, < 4', - 'requests >= 2.6.1, != 2.11.0, < 2.12', + 'requests >= 2.6.1, < 2.8', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker >= 2.0.2, < 3.0', + 'docker-py >= 1.8.1, < 2', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', @@ -51,25 +48,7 @@ tests_require = [ if sys.version_info[:2] < (3, 4): tests_require.append('mock >= 1.0.1') - -extras_require = { - ':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'], - ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5'], - ':python_version < "3.3"': ['ipaddress >= 1.0.16'], -} - - -try: - if 'bdist_wheel' not in sys.argv: - for key, value in extras_require.items(): - if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]): - install_requires.extend(value) -except Exception as e: - print("Failed to compute platform dependencies: {}. ".format(e) + - "All dependencies will be installed as a result.", file=sys.stderr) - for key, value in extras_require.items(): - if key.startswith(':'): - install_requires.extend(value) + install_requires.append('enum34 >= 1.0.4, < 2') setup( @@ -83,7 +62,6 @@ setup( include_package_data=True, test_suite='nose.collector', install_requires=install_requires, - extras_require=extras_require, tests_require=tests_require, entry_points=""" [console_scripts] diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 8366ca75..1a4f9f53 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- from __future__ import absolute_import from __future__ import unicode_literals import datetime import json import os +import shlex import signal import subprocess import time @@ -12,8 +12,6 @@ from collections import Counter from collections import namedtuple from operator import attrgetter -import py -import six import yaml from docker import errors @@ -21,13 +19,10 @@ from .. import mock from compose.cli.command import get_project from compose.container import Container from compose.project import OneOffFilter -from compose.utils import nanoseconds_from_time_seconds 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 -from tests.integration.testcases import v3_only ProcessResult = namedtuple('ProcessResult', 'stdout stderr') @@ -118,8 +113,6 @@ 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() @@ -159,19 +152,6 @@ 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']) @@ -239,48 +219,16 @@ class CLITestCase(DockerClientTestCase): 'image': 'busybox', 'restart': 'on-failure:5', }, - 'restart-null': { - 'image': 'busybox', - 'restart': '' - }, }, 'networks': {}, 'volumes': {}, } - def test_config_external_network(self): - self.base_dir = 'tests/fixtures/networks' - result = self.dispatch(['-f', 'external-networks.yml', 'config']) - json_result = yaml.load(result.stdout) - assert 'networks' in json_result - assert json_result['networks'] == { - 'networks_foo': { - 'external': True # {'name': 'networks_foo'} - }, - 'bar': { - 'external': {'name': 'networks_bar'} - } - } - - def test_config_external_volume(self): - self.base_dir = 'tests/fixtures/volumes' - result = self.dispatch(['-f', 'external-volumes.yml', 'config']) - json_result = yaml.load(result.stdout) - assert 'volumes' in json_result - assert json_result['volumes'] == { - 'foo': { - 'external': True - }, - 'bar': { - 'external': {'name': 'some_bar'} - } - } - def test_config_v1(self): self.base_dir = 'tests/fixtures/v1-config' result = self.dispatch(['config']) assert yaml.load(result.stdout) == { - 'version': '2.1', + 'version': '2.0', 'services': { 'net': { 'image': 'busybox', @@ -301,68 +249,6 @@ class CLITestCase(DockerClientTestCase): 'volumes': {}, } - @v3_only() - def test_config_v3(self): - self.base_dir = 'tests/fixtures/v3-full' - result = self.dispatch(['config']) - - assert yaml.load(result.stdout) == { - 'version': '3.0', - 'networks': {}, - 'volumes': { - 'foobar': { - 'labels': { - 'com.docker.compose.test': 'true', - }, - }, - }, - 'services': { - 'web': { - 'image': 'busybox', - 'deploy': { - 'mode': 'replicated', - 'replicas': 6, - 'labels': ['FOO=BAR'], - 'update_config': { - 'parallelism': 3, - 'delay': '10s', - 'failure_action': 'continue', - 'monitor': '60s', - 'max_failure_ratio': 0.3, - }, - 'resources': { - 'limits': { - 'cpus': '0.001', - 'memory': '50M', - }, - 'reservations': { - 'cpus': '0.0001', - 'memory': '20M', - }, - }, - 'restart_policy': { - 'condition': 'on_failure', - 'delay': '5s', - 'max_attempts': 3, - 'window': '120s', - }, - 'placement': { - 'constraints': ['node=foo'], - }, - }, - - 'healthcheck': { - 'test': 'cat /etc/passwd', - 'interval': '10s', - 'timeout': '1s', - 'retries': 5, - }, - - 'stop_grace_period': '20s', - }, - }, - } - def test_ps(self): self.project.get_service('simple').create_container() result = self.dispatch(['ps']) @@ -408,13 +294,12 @@ class CLITestCase(DockerClientTestCase): def test_pull_with_ignore_pull_failures(self): result = self.dispatch([ '-f', 'ignore-pull-failures.yml', - 'pull', '--ignore-pull-failures'] - ) + 'pull', '--ignore-pull-failures']) assert 'Pulling simple (busybox:latest)...' in result.stderr assert 'Pulling another (nonexisting-image:latest)...' in result.stderr - assert ('repository nonexisting-image not found' in result.stderr or - 'image library/nonexisting-image:latest not found' in result.stderr) + assert 'Error: image library/nonexisting-image' in result.stderr + assert 'not found' in result.stderr def test_build_plain(self): self.base_dir = 'tests/fixtures/simple-dockerfile' @@ -479,32 +364,6 @@ 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') @@ -676,24 +535,6 @@ 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' @@ -853,46 +694,6 @@ 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 'label_key' in networks[0]['Labels'] - 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 'label_key' in volumes[0]['Labels'] - assert volumes[0]['Labels']['label_key'] == 'label_val' - @v2_only() def test_up_no_services(self): self.base_dir = 'tests/fixtures/no-services' @@ -948,50 +749,6 @@ class CLITestCase(DockerClientTestCase): assert foo_container.get('HostConfig.NetworkMode') == \ 'container:{}'.format(bar_container.id) - @v3_only() - def test_up_with_healthcheck(self): - def wait_on_health_status(container, status): - def condition(): - container.inspect() - return container.get('State.Health.Status') == status - - return wait_on_condition(condition, delay=0.5) - - self.base_dir = 'tests/fixtures/healthcheck' - self.dispatch(['up', '-d'], None) - - passes = self.project.get_service('passes') - passes_container = passes.containers()[0] - - assert passes_container.get('Config.Healthcheck') == { - "Test": ["CMD-SHELL", "/bin/true"], - "Interval": nanoseconds_from_time_seconds(1), - "Timeout": nanoseconds_from_time_seconds(30 * 60), - "Retries": 1, - } - - wait_on_health_status(passes_container, 'healthy') - - fails = self.project.get_service('fails') - fails_container = fails.containers()[0] - - assert fails_container.get('Config.Healthcheck') == { - "Test": ["CMD", "/bin/false"], - "Interval": nanoseconds_from_time_seconds(2.5), - "Retries": 2, - } - - wait_on_health_status(fails_container, 'unhealthy') - - disabled = self.project.get_service('disabled') - disabled_container = disabled.containers()[0] - - assert disabled_container.get('Config.Healthcheck') == { - "Test": ["NONE"], - } - - assert 'Health' not in disabled_container.get('State') - def test_up_with_no_deps(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['up', '-d', '--no-deps', 'web'], None) @@ -1167,54 +924,16 @@ class CLITestCase(DockerClientTestCase): [u'/bin/true'], ) - 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_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_user_overridden(self): self.base_dir = 'tests/fixtures/user-composefile' @@ -1234,7 +953,7 @@ class CLITestCase(DockerClientTestCase): container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] self.assertEqual(user, container.get('Config.User')) - def test_run_service_with_environment_overridden(self): + def test_run_service_with_environement_overridden(self): name = 'service' self.base_dir = 'tests/fixtures/environment-composefile' self.dispatch([ @@ -1246,9 +965,9 @@ class CLITestCase(DockerClientTestCase): ]) service = self.project.get_service(name) container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] - # env overridden + # env overriden self.assertEqual('notbar', container.environment['foo']) - # keep environment from yaml + # keep environement from yaml self.assertEqual('world', container.environment['hello']) # added option from command line self.assertEqual('beta', container.environment['alpha']) @@ -1293,7 +1012,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(port_range[0], "0.0.0.0:49153") self.assertEqual(port_range[1], "0.0.0.0:49154") - def test_run_service_with_explicitly_mapped_ports(self): + def test_run_service_with_explicitly_maped_ports(self): # create one off container self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple']) @@ -1310,7 +1029,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(port_short, "0.0.0.0:30000") self.assertEqual(port_full, "0.0.0.0:30001") - def test_run_service_with_explicitly_mapped_ip_ports(self): + def test_run_service_with_explicitly_maped_ip_ports(self): # create one off container self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch([ @@ -1402,10 +1121,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'] or []) - set([container.short_id]) - assert not aliases + assert not config['Aliases'] @v2_only() def test_run_detached_connects_to_network(self): @@ -1422,10 +1138,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'] or []) - set([container.short_id]) - assert not aliases + assert not config['Aliases'] assert self.lookup(container, 'app') assert self.lookup(container, 'db') @@ -1456,35 +1169,6 @@ 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' - os.environ['BAR'] = 'baz' - - 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') service.create_container() @@ -1508,6 +1192,8 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 1) self.dispatch(['rm', '-f'], None) self.assertEqual(len(service.containers(stopped=True)), 0) + self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 1) + self.dispatch(['rm', '-f', '-a'], None) self.assertEqual(len(service.containers(stopped=True, one_off=OneOffFilter.only)), 0) service.create_container(one_off=False) @@ -1620,14 +1306,13 @@ class CLITestCase(DockerClientTestCase): 'logscomposefile_another_1', 'exited')) - self.dispatch(['kill', 'simple']) - - result = wait_on_process(proc) - - assert 'hello' in result.stdout + # sleep for a short period to allow the tailing thread to receive the + # event. This is not great, but there isn't an easy way to do this + # without being able to stream stdout from the process. + time.sleep(0.5) + os.kill(proc.pid, signal.SIGINT) + result = wait_on_process(proc, returncode=1) assert 'test' in result.stdout - assert 'logscomposefile_another_1 exited with code 0' in result.stdout - assert 'logscomposefile_simple_1 exited with code 137' in result.stdout def test_logs_default(self): self.base_dir = 'tests/fixtures/logs-composefile' @@ -1789,17 +1474,6 @@ class CLITestCase(DockerClientTestCase): assert Counter(e['action'] for e in lines) == {'create': 2, 'start': 2} def test_events_human_readable(self): - - def has_timestamp(string): - str_iso_date, str_iso_time, container_info = string.split(' ', 2) - try: - return isinstance(datetime.datetime.strptime( - '%s %s' % (str_iso_date, str_iso_time), - '%Y-%m-%d %H:%M:%S.%f'), - datetime.datetime) - except ValueError: - return False - events_proc = start_process(self.base_dir, ['events']) self.dispatch(['up', '-d', 'simple']) wait_on_condition(ContainerCountCondition(self.project, 1)) @@ -1816,8 +1490,7 @@ class CLITestCase(DockerClientTestCase): assert expected_template.format('create', container.id) in lines[0] assert expected_template.format('start', container.id) in lines[1] - - assert has_timestamp(lines[0]) + assert lines[0].startswith(datetime.date.today().isoformat()) def test_env_file_relative_to_compose_file(self): config_path = os.path.abspath('tests/fixtures/env-file/docker-compose.yml') @@ -1907,23 +1580,3 @@ class CLITestCase(DockerClientTestCase): "BAZ=2", ]) self.assertTrue(expected_env <= set(web.get('Config.Env'))) - - def test_top_services_not_running(self): - self.base_dir = 'tests/fixtures/top' - result = self.dispatch(['top']) - assert len(result.stdout) == 0 - - def test_top_services_running(self): - self.base_dir = 'tests/fixtures/top' - self.dispatch(['up', '-d']) - result = self.dispatch(['top']) - - self.assertIn('top_service_a', result.stdout) - self.assertIn('top_service_b', result.stdout) - self.assertNotIn('top_not_a_service', result.stdout) - - def test_top_processes_running(self): - self.base_dir = 'tests/fixtures/top' - self.dispatch(['up', '-d']) - result = self.dispatch(['top']) - assert result.stdout.count("top") == 4 diff --git a/tests/fixtures/bundle-with-digests/docker-compose.yml b/tests/fixtures/bundle-with-digests/docker-compose.yml deleted file mode 100644 index b7013512..00000000 --- a/tests/fixtures/bundle-with-digests/docker-compose.yml +++ /dev/null @@ -1,9 +0,0 @@ - -version: '2.0' - -services: - web: - image: dockercloud/hello-world@sha256:fe79a2cfbd17eefc344fb8419420808df95a1e22d93b7f621a7399fd1e9dca1d - - redis: - image: redis@sha256:a84cb8f53a70e19f61ff2e1d5e73fb7ae62d374b2b7392de1e7d77be26ef8f7b diff --git a/tests/fixtures/entrypoint-dockerfile/Dockerfile b/tests/fixtures/dockerfile_with_entrypoint/Dockerfile similarity index 57% rename from tests/fixtures/entrypoint-dockerfile/Dockerfile rename to tests/fixtures/dockerfile_with_entrypoint/Dockerfile index 49f4416c..e7454e59 100644 --- a/tests/fixtures/entrypoint-dockerfile/Dockerfile +++ b/tests/fixtures/dockerfile_with_entrypoint/Dockerfile @@ -1,4 +1,3 @@ FROM busybox:latest LABEL com.docker.compose.test_image=true -ENTRYPOINT ["printf"] -CMD ["default", "args"] +ENTRYPOINT echo "From prebuilt entrypoint" diff --git a/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml b/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml new file mode 100644 index 00000000..78631502 --- /dev/null +++ b/tests/fixtures/dockerfile_with_entrypoint/docker-compose.yml @@ -0,0 +1,2 @@ +service: + build: . diff --git a/tests/fixtures/entrypoint-composefile/docker-compose.yml b/tests/fixtures/entrypoint-composefile/docker-compose.yml deleted file mode 100644 index e9880973..00000000 --- a/tests/fixtures/entrypoint-composefile/docker-compose.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: "2" -services: - test: - image: busybox - entrypoint: printf - command: default args diff --git a/tests/fixtures/entrypoint-dockerfile/docker-compose.yml b/tests/fixtures/entrypoint-dockerfile/docker-compose.yml deleted file mode 100644 index 8318e61f..00000000 --- a/tests/fixtures/entrypoint-dockerfile/docker-compose.yml +++ /dev/null @@ -1,4 +0,0 @@ -version: "2" -services: - test: - build: . diff --git a/tests/fixtures/extends/healthcheck-1.yml b/tests/fixtures/extends/healthcheck-1.yml deleted file mode 100644 index 4c311e62..00000000 --- a/tests/fixtures/extends/healthcheck-1.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '2.1' -services: - demo: - image: foobar:latest - healthcheck: - test: ["CMD", "/health.sh"] - interval: 10s - timeout: 5s - retries: 36 diff --git a/tests/fixtures/extends/healthcheck-2.yml b/tests/fixtures/extends/healthcheck-2.yml deleted file mode 100644 index 11bc9f09..00000000 --- a/tests/fixtures/extends/healthcheck-2.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: '2.1' -services: - demo: - extends: - file: healthcheck-1.yml - service: demo diff --git a/tests/fixtures/healthcheck/docker-compose.yml b/tests/fixtures/healthcheck/docker-compose.yml deleted file mode 100644 index 2c45b8d8..00000000 --- a/tests/fixtures/healthcheck/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: "3" -services: - passes: - image: busybox - command: top - healthcheck: - test: "/bin/true" - interval: 1s - timeout: 30m - retries: 1 - - fails: - image: busybox - command: top - healthcheck: - test: ["CMD", "/bin/false"] - interval: 2.5s - retries: 2 - - disabled: - image: busybox - command: top - healthcheck: - disable: true diff --git a/tests/fixtures/logs-composefile/docker-compose.yml b/tests/fixtures/logs-composefile/docker-compose.yml index b719c91e..0af9d805 100644 --- a/tests/fixtures/logs-composefile/docker-compose.yml +++ b/tests/fixtures/logs-composefile/docker-compose.yml @@ -1,6 +1,6 @@ simple: image: busybox:latest - command: sh -c "echo hello && tail -f /dev/null" + command: sh -c "echo hello && sleep 200" another: image: busybox:latest command: sh -c "echo test" diff --git a/tests/fixtures/networks/network-internal.yml b/tests/fixtures/networks/network-internal.yml deleted file mode 100755 index 1fa339b1..00000000 --- a/tests/fixtures/networks/network-internal.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: "2" - -services: - web: - image: busybox - command: top - networks: - - internal - -networks: - internal: - driver: bridge - internal: True diff --git a/tests/fixtures/networks/network-label.yml b/tests/fixtures/networks/network-label.yml deleted file mode 100644 index fdb24f65..00000000 --- a/tests/fixtures/networks/network-label.yml +++ /dev/null @@ -1,13 +0,0 @@ -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/restart/docker-compose.yml b/tests/fixtures/restart/docker-compose.yml index ecfdfbf5..2d10aa39 100644 --- a/tests/fixtures/restart/docker-compose.yml +++ b/tests/fixtures/restart/docker-compose.yml @@ -12,6 +12,3 @@ services: on-failure-5: image: busybox restart: "on-failure:5" - restart-null: - image: busybox - restart: "" diff --git a/tests/fixtures/secrets/default b/tests/fixtures/secrets/default deleted file mode 100644 index f9dc2014..00000000 --- a/tests/fixtures/secrets/default +++ /dev/null @@ -1 +0,0 @@ -This is the secret diff --git a/tests/fixtures/top/docker-compose.yml b/tests/fixtures/top/docker-compose.yml deleted file mode 100644 index d632a836..00000000 --- a/tests/fixtures/top/docker-compose.yml +++ /dev/null @@ -1,6 +0,0 @@ -service_a: - image: busybox:latest - command: top -service_b: - image: busybox:latest - command: top diff --git a/tests/fixtures/unicode-environment/docker-compose.yml b/tests/fixtures/unicode-environment/docker-compose.yml deleted file mode 100644 index a41af4f0..00000000 --- a/tests/fixtures/unicode-environment/docker-compose.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: '2' -services: - simple: - image: busybox:latest - command: sh -c 'echo $$FOO' - environment: - FOO: ${BAR} diff --git a/tests/fixtures/v3-full/docker-compose.yml b/tests/fixtures/v3-full/docker-compose.yml deleted file mode 100644 index a1661ab9..00000000 --- a/tests/fixtures/v3-full/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: "3" -services: - web: - image: busybox - - deploy: - mode: replicated - replicas: 6 - labels: [FOO=BAR] - update_config: - parallelism: 3 - delay: 10s - failure_action: continue - monitor: 60s - max_failure_ratio: 0.3 - resources: - limits: - cpus: '0.001' - memory: 50M - reservations: - cpus: '0.0001' - memory: 20M - restart_policy: - condition: on_failure - delay: 5s - max_attempts: 3 - window: 120s - placement: - constraints: [node=foo] - - healthcheck: - test: cat /etc/passwd - interval: 10s - timeout: 1s - retries: 5 - - stop_grace_period: 20s -volumes: - foobar: - labels: - com.docker.compose.test: 'true' diff --git a/tests/fixtures/volumes-from-container/docker-compose.yml b/tests/fixtures/volumes-from-container/docker-compose.yml deleted file mode 100644 index 495fcaae..00000000 --- a/tests/fixtures/volumes-from-container/docker-compose.yml +++ /dev/null @@ -1,5 +0,0 @@ -version: "2" -services: - test: - image: busybox - volumes_from: ["container:composetest_data_container"] diff --git a/tests/fixtures/volumes/docker-compose.yml b/tests/fixtures/volumes/docker-compose.yml deleted file mode 100644 index da711ac4..00000000 --- a/tests/fixtures/volumes/docker-compose.yml +++ /dev/null @@ -1,2 +0,0 @@ -version: '2.1' -services: {} diff --git a/tests/fixtures/volumes/external-volumes.yml b/tests/fixtures/volumes/external-volumes.yml deleted file mode 100644 index 05c6c484..00000000 --- a/tests/fixtures/volumes/external-volumes.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "2.1" - -services: - web: - image: busybox - command: top - volumes: - - foo:/var/lib/ - - bar:/etc/ - -volumes: - foo: - external: true - bar: - external: - name: some_bar diff --git a/tests/fixtures/volumes/volume-label.yml b/tests/fixtures/volumes/volume-label.yml deleted file mode 100644 index a5f33a5a..00000000 --- a/tests/fixtures/volumes/volume-label.yml +++ /dev/null @@ -1,13 +0,0 @@ -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/network_test.py b/tests/integration/network_test.py deleted file mode 100644 index 2ff610fb..00000000 --- a/tests/integration/network_test.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -from .testcases import DockerClientTestCase -from compose.const import LABEL_NETWORK -from compose.const import LABEL_PROJECT -from compose.network import Network - - -class NetworkTest(DockerClientTestCase): - def test_network_default_labels(self): - net = Network(self.client, 'composetest', 'foonet') - net.ensure() - net_data = net.inspect() - labels = net_data['Labels'] - assert labels[LABEL_NETWORK] == net.name - assert labels[LABEL_PROJECT] == net.project diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 28762cd2..7ef492a5 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os.path import random import py @@ -9,36 +8,20 @@ import pytest from docker.errors import NotFound from .. import mock -from ..helpers import build_config as load_config +from ..helpers import build_config from .testcases import DockerClientTestCase from compose.config import config from compose.config import ConfigurationError -from compose.config import types from compose.config.config import V2_0 -from compose.config.config import V2_1 -from compose.config.config import V3_1 from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.container import Container -from compose.errors import HealthCheckFailed -from compose.errors import NoHealthCheckConfigured 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 -from tests.integration.testcases import v3_only - - -def build_config(**kwargs): - return config.Config( - version=kwargs.get('version'), - services=kwargs.get('services'), - volumes=kwargs.get('volumes'), - networks=kwargs.get('networks'), - secrets=kwargs.get('secrets')) class ProjectTest(DockerClientTestCase): @@ -83,7 +66,7 @@ class ProjectTest(DockerClientTestCase): def test_volumes_from_service(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'data': { 'image': 'busybox:latest', 'volumes': ['/var/data'], @@ -109,7 +92,7 @@ class ProjectTest(DockerClientTestCase): ) project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'db': { 'image': 'busybox:latest', 'volumes_from': ['composetest_data_container'], @@ -125,7 +108,7 @@ class ProjectTest(DockerClientTestCase): project = Project.from_config( name='composetest', client=self.client, - config_data=load_config({ + config_data=build_config({ 'version': V2_0, 'services': { 'net': { @@ -152,7 +135,7 @@ class ProjectTest(DockerClientTestCase): def get_project(): return Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'version': V2_0, 'services': { 'web': { @@ -187,7 +170,7 @@ class ProjectTest(DockerClientTestCase): def test_net_from_service_v1(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'net': { 'image': 'busybox:latest', 'command': ["top"] @@ -211,7 +194,7 @@ class ProjectTest(DockerClientTestCase): def get_project(): return Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'web': { 'image': 'busybox:latest', 'net': 'container:composetest_net_container' @@ -482,7 +465,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_starts_depends(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'console': { 'image': 'busybox:latest', 'command': ["top"], @@ -517,7 +500,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_with_no_deps(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'console': { 'image': 'busybox:latest', 'command': ["top"], @@ -577,7 +560,7 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_project_up_networks(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -589,6 +572,7 @@ class ProjectTest(DockerClientTestCase): 'baz': {'aliases': ['extra']}, }, }], + volumes={}, networks={ 'foo': {'driver': 'bridge'}, 'bar': {'driver': None}, @@ -622,13 +606,14 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_up_with_ipam_config(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', 'image': 'busybox:latest', 'networks': {'front': None}, }], + volumes={}, networks={ 'front': { 'driver': 'bridge', @@ -682,7 +667,7 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_up_with_network_static_addresses(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -695,6 +680,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -733,53 +719,9 @@ 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 = build_config( - version=V2_0, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'command': 'top', - 'networks': { - 'static_test': { - 'ipv6_address': 'fe80::1001:102' - } - }, - }], - 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 = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -791,6 +733,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -813,146 +756,11 @@ class ProjectTest(DockerClientTestCase): with self.assertRaises(ProjectError): project.up() - @v2_1_only() - def test_up_with_network_link_local_ips(self): - config_data = build_config( - version=V2_1, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'networks': { - 'linklocaltest': { - 'link_local_ips': ['169.254.8.8'] - } - } - }], - networks={ - 'linklocaltest': {'driver': 'bridge'} - } - ) - project = Project.from_config( - client=self.client, - name='composetest', - config_data=config_data - ) - project.up(detached=True) - - service_container = project.get_service('web').containers(stopped=True)[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_1_only() - def test_up_with_isolation(self): - self.require_api_version('1.24') - config_data = build_config( - version=V2_1, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'isolation': 'default' - }], - ) - project = Project.from_config( - client=self.client, - name='composetest', - config_data=config_data - ) - project.up(detached=True) - service_container = project.get_service('web').containers(stopped=True)[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 = build_config( - version=V2_1, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'isolation': 'foobar' - }], - ) - 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') - config_data = build_config( - version=V2_0, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'networks': {'internal': None}, - }], - 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_1_only() - def test_project_up_with_network_label(self): - self.require_api_version('1.23') - - network_name = 'network_with_label' - - config_data = build_config( - version=V2_0, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'networks': {network_name: None} - }], - 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 'label_key' in networks[0]['Labels'] - assert networks[0]['Labels']['label_key'] == 'label_val' - @v2_only() def test_project_up_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -960,6 +768,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( @@ -973,46 +782,6 @@ 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 = build_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' - } - } - }, - ) - - 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 'label_key' in volumes[0]['Labels'] - assert volumes[0]['Labels']['label_key'] == 'label_val' - @v2_only() def test_project_up_logging_with_multiple_files(self): base_file = config.ConfigFile( @@ -1065,47 +834,11 @@ class ProjectTest(DockerClientTestCase): self.assertTrue(log_config) self.assertEqual(log_config.get('Type'), 'none') - @v2_only() - def test_project_up_port_mappings_with_multiple_files(self): - base_file = config.ConfigFile( - 'base.yml', - { - 'version': V2_0, - 'services': { - 'simple': { - 'image': 'busybox:latest', - 'command': 'top', - 'ports': ['1234:1234'] - }, - }, - - }) - override_file = config.ConfigFile( - 'override.yml', - { - 'version': V2_0, - 'services': { - 'simple': { - 'ports': ['1234:1234'] - } - } - - }) - details = config.ConfigDetails('.', [base_file, override_file]) - - config_data = config.load(details) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - project.up() - containers = project.containers() - self.assertEqual(len(containers), 1) - @v2_only() def test_initialize_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1113,6 +846,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, + networks={}, ) project = Project.from_config( @@ -1122,14 +856,14 @@ class ProjectTest(DockerClientTestCase): project.volumes.initialize() volume_data = self.client.inspect_volume(full_vol_name) - assert volume_data['Name'] == full_vol_name - assert volume_data['Driver'] == 'local' + self.assertEqual(volume_data['Name'], full_vol_name) + self.assertEqual(volume_data['Driver'], 'local') @v2_only() def test_project_up_implicit_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1137,6 +871,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, + networks={}, ) project = Project.from_config( @@ -1149,47 +884,11 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(volume_data['Name'], full_vol_name) self.assertEqual(volume_data['Driver'], 'local') - @v3_only() - def test_project_up_with_secrets(self): - create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default')) - - config_data = build_config( - version=V3_1, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'command': 'cat /run/secrets/special', - 'secrets': [ - types.ServiceSecret.parse({'source': 'super', 'target': 'special'}), - ], - }], - secrets={ - 'super': { - 'file': os.path.abspath('tests/fixtures/secrets/default'), - }, - }, - ) - - project = Project.from_config( - client=self.client, - name='composetest', - config_data=config_data, - ) - project.up() - project.stop() - - containers = project.containers(stopped=True) - assert len(containers) == 1 - container, = containers - - output = container.logs() - assert output == b"This is the secret\n" - @v2_only() def test_initialize_volumes_invalid_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1197,6 +896,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'foobar'}}, + networks={}, ) project = Project.from_config( @@ -1211,7 +911,7 @@ class ProjectTest(DockerClientTestCase): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1219,6 +919,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( name='composetest', @@ -1249,7 +950,7 @@ class ProjectTest(DockerClientTestCase): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1257,6 +958,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( name='composetest', @@ -1287,7 +989,7 @@ class ProjectTest(DockerClientTestCase): vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) self.client.create_volume(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1297,6 +999,7 @@ class ProjectTest(DockerClientTestCase): volumes={ vol_name: {'external': True, 'external_name': vol_name} }, + networks=None, ) project = Project.from_config( name='composetest', @@ -1311,7 +1014,7 @@ class ProjectTest(DockerClientTestCase): def test_initialize_volumes_inexistent_external_volume(self): vol_name = '{0:x}'.format(random.getrandbits(32)) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1321,6 +1024,7 @@ class ProjectTest(DockerClientTestCase): volumes={ vol_name: {'external': True, 'external_name': vol_name} }, + networks=None, ) project = Project.from_config( name='composetest', @@ -1377,7 +1081,7 @@ class ProjectTest(DockerClientTestCase): } } - config_data = load_config(config_dict) + config_data = build_config(config_dict) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) @@ -1385,7 +1089,7 @@ class ProjectTest(DockerClientTestCase): config_dict['service2'] = config_dict['service1'] del config_dict['service1'] - config_data = load_config(config_dict) + config_data = build_config(config_dict) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) @@ -1405,142 +1109,3 @@ class ProjectTest(DockerClientTestCase): ctnr for ctnr in project._labeled_containers() if ctnr.labels.get(LABEL_SERVICE) == 'service1' ]) == 0 - - @v2_1_only() - def test_project_up_healthy_dependency(self): - config_dict = { - 'version': '2.1', - 'services': { - 'svc1': { - 'image': 'busybox:latest', - 'command': 'top', - 'healthcheck': { - 'test': 'exit 0', - 'retries': 1, - 'timeout': '10s', - 'interval': '0.1s' - }, - }, - 'svc2': { - 'image': 'busybox:latest', - 'command': 'top', - 'depends_on': { - 'svc1': {'condition': 'service_healthy'}, - } - } - } - } - config_data = load_config(config_dict) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - project.up() - containers = project.containers() - assert len(containers) == 2 - - svc1 = project.get_service('svc1') - svc2 = project.get_service('svc2') - assert 'svc1' in svc2.get_dependency_names() - assert svc1.is_healthy() - - @v2_1_only() - def test_project_up_unhealthy_dependency(self): - config_dict = { - 'version': '2.1', - 'services': { - 'svc1': { - 'image': 'busybox:latest', - 'command': 'top', - 'healthcheck': { - 'test': 'exit 1', - 'retries': 1, - 'timeout': '10s', - 'interval': '0.1s' - }, - }, - 'svc2': { - 'image': 'busybox:latest', - 'command': 'top', - 'depends_on': { - 'svc1': {'condition': 'service_healthy'}, - } - } - } - } - config_data = load_config(config_dict) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - with pytest.raises(ProjectError): - project.up() - containers = project.containers() - assert len(containers) == 1 - - svc1 = project.get_service('svc1') - svc2 = project.get_service('svc2') - assert 'svc1' in svc2.get_dependency_names() - with pytest.raises(HealthCheckFailed): - svc1.is_healthy() - - @v2_1_only() - def test_project_up_no_healthcheck_dependency(self): - config_dict = { - 'version': '2.1', - 'services': { - 'svc1': { - 'image': 'busybox:latest', - 'command': 'top', - 'healthcheck': { - 'disable': True - }, - }, - 'svc2': { - 'image': 'busybox:latest', - 'command': 'top', - 'depends_on': { - 'svc1': {'condition': 'service_healthy'}, - } - } - } - } - config_data = load_config(config_dict) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - with pytest.raises(ProjectError): - project.up() - containers = project.containers() - assert len(containers) == 1 - - svc1 = project.get_service('svc1') - svc2 = project.get_service('svc2') - assert 'svc1' in svc2.get_dependency_names() - with pytest.raises(NoHealthCheckConfigured): - svc1.is_healthy() - - -def create_host_file(client, filename): - dirname = os.path.dirname(filename) - - with open(filename, 'r') as fh: - content = fh.read() - - container = client.create_container( - 'busybox:latest', - ['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)], - volumes={dirname: {}}, - host_config=client.create_host_config( - binds={dirname: {'bind': dirname, 'ro': False}}, - network_mode='none', - ), - ) - try: - client.start(container) - exitcode = client.wait(container) - - if exitcode != 0: - output = client.logs(container) - raise Exception( - "Container exited with code {}:\n{}".format(exitcode, output)) - finally: - client.remove_container(container, force=True) diff --git a/tests/integration/resilience_test.py b/tests/integration/resilience_test.py index 2a2d1b56..b544783a 100644 --- a/tests/integration/resilience_test.py +++ b/tests/integration/resilience_test.py @@ -20,11 +20,6 @@ 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] diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 09758eee..df50d513 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -30,7 +30,6 @@ from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy from compose.service import NetworkMode from compose.service import Service -from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only @@ -398,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 @@ -594,12 +593,12 @@ class ServiceTest(DockerClientTestCase): service.build() assert service.image() - def test_start_container_stays_unprivileged(self): + def test_start_container_stays_unpriviliged(self): service = self.create_service('web') container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], False) - def test_start_container_becomes_privileged(self): + def test_start_container_becomes_priviliged(self): service = self.create_service('web', privileged=True) container = create_and_start_container(service).inspect() self.assertEqual(container['HostConfig']['Privileged'], True) @@ -739,10 +738,7 @@ class ServiceTest(DockerClientTestCase): self.assertEqual(len(service.containers()), 1) self.assertTrue(service.containers()[0].is_running) - self.assertIn( - "ERROR: for composetest_web_2 Cannot create container for service web: Boom", - mock_stderr.getvalue() - ) + self.assertIn("ERROR: for composetest_web_2 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 @@ -843,18 +839,6 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.PidMode'), 'host') - @v2_1_only() - def test_userns_mode_none_defined(self): - service = self.create_service('web', userns_mode=None) - container = create_and_start_container(service) - self.assertEqual(container.get('HostConfig.UsernsMode'), '') - - @v2_1_only() - def test_userns_mode_host(self): - service = self.create_service('web', userns_mode='host') - container = create_and_start_container(service) - self.assertEqual(container.get('HostConfig.UsernsMode'), 'host') - def test_dns_no_value(self): service = self.create_service('web') container = create_and_start_container(service) @@ -865,29 +849,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) 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_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/integration/testcases.py b/tests/integration/testcases.py index efc1551b..8d69d531 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -12,8 +12,6 @@ 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.config import V3_0 from compose.config.environment import Environment from compose.const import API_VERSIONS from compose.const import LABEL_PROJECT @@ -35,26 +33,19 @@ def get_links(container): return [format_link(link) for link in links] -def engine_max_version(): +def engine_version_too_low_for_v2(): if 'DOCKER_VERSION' not in os.environ: - return V3_0 + return False version = os.environ['DOCKER_VERSION'].partition('-')[0] - if version_lt(version, '1.10'): - return V1 - if version_lt(version, '1.12'): - return V2_0 - if version_lt(version, '1.13'): - return V2_1 - return V3_0 + return version_lt(version, '1.10') -def build_version_required_decorator(ignored_versions): +def v2_only(): def decorator(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): - max_version = engine_max_version() - if max_version in ignored_versions: - skip("Engine version %s is too low" % max_version) + if engine_version_too_low_for_v2(): + skip("Engine version is too low") return return f(self, *args, **kwargs) return wrapper @@ -62,27 +53,15 @@ def build_version_required_decorator(ignored_versions): return decorator -def v2_only(): - return build_version_required_decorator((V1,)) - - -def v2_1_only(): - return build_version_required_decorator((V1, V2_0)) - - -def v3_only(): - return build_version_required_decorator((V1, V2_0, V2_1)) - - class DockerClientTestCase(unittest.TestCase): @classmethod def setUpClass(cls): - version = API_VERSIONS[engine_max_version()] - cls.client = docker_client(Environment(), version) + if engine_version_too_low_for_v2(): + version = API_VERSIONS[V1] + else: + version = API_VERSIONS[V2_0] - @classmethod - def tearDownClass(cls): - del cls.client + cls.client = docker_client(Environment(), version) def tearDown(self): for c in self.client.containers( diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index add16962..706179ed 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals from docker.errors import DockerException from .testcases import DockerClientTestCase -from compose.const import LABEL_PROJECT -from compose.const import LABEL_VOLUME from compose.volume import Volume @@ -19,8 +17,6 @@ class VolumeTest(DockerClientTestCase): self.client.remove_volume(volume.full_name) 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): @@ -96,11 +92,3 @@ class VolumeTest(DockerClientTestCase): assert vol.exists() is False vol.create() assert vol.exists() is True - - def test_volume_default_labels(self): - vol = self.create_volume('volume01') - vol.create() - vol_data = vol.inspect() - labels = vol_data['Labels'] - assert labels[LABEL_VOLUME] == vol.name - assert labels[LABEL_PROJECT] == vol.project diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py deleted file mode 100644 index 21bdb31b..00000000 --- a/tests/unit/bundle_test.py +++ /dev/null @@ -1,219 +0,0 @@ -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.APIClient), - 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_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.push_image(mock_service) - assert digest == image_id + '@' + expected - - mock_service.push.assert_called_once_with() - 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(): - image_digests = {'a': 'aaaa', 'b': 'bbbb'} - services = [ - {'name': 'a', 'build': '.', }, - {'name': 'b', 'build': './b'}, - ] - config = Config( - version=2, - services=services, - volumes={'special': {}}, - networks={'extra': {}}, - secrets={}) - - 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/cli/command_test.py b/tests/unit/cli/command_test.py index 50fc84e1..3502d636 100644 --- a/tests/unit/cli/command_test.py +++ b/tests/unit/cli/command_test.py @@ -2,12 +2,10 @@ from __future__ import absolute_import from __future__ import unicode_literals import os -import ssl import pytest from compose.cli.command import get_config_path_from_options -from compose.cli.command import get_tls_version from compose.config.environment import Environment from compose.const import IS_WINDOWS_PLATFORM from tests import mock @@ -48,21 +46,3 @@ class TestGetConfigPathFromOptions(object): def test_no_path(self): environment = Environment.from_env_file('.') assert not get_config_path_from_options('.', {}, environment) - - -class TestGetTlsVersion(object): - def test_get_tls_version_default(self): - 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 - - def test_get_tls_version_unavailable(self): - environment = {'COMPOSE_TLS_VERSION': 'TLSv5_5'} - with mock.patch('compose.cli.command.log') as mock_log: - tls_version = get_tls_version(environment) - mock_log.warn.assert_called_once_with(mock.ANY) - assert tls_version is None diff --git a/tests/unit/cli/docker_client_test.py b/tests/unit/cli/docker_client_test.py index aaa935af..5334a944 100644 --- a/tests/unit/cli/docker_client_test.py +++ b/tests/unit/cli/docker_client_test.py @@ -2,13 +2,10 @@ 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 from tests import mock @@ -22,43 +19,11 @@ 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): - 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] - - 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( - compose.__version__, - docker.__version__, - platform.system(), - platform.release() - ) - self.assertEqual(client.headers['User-Agent'], expected) + timeout = 300 + with mock.patch('compose.cli.docker_client.HTTP_TIMEOUT', 300): + client = docker_client(os.environ) + self.assertEqual(client.timeout, int(timeout)) class TLSConfigTestCase(unittest.TestCase): @@ -144,16 +109,3 @@ 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/errors_test.py b/tests/unit/cli/errors_test.py index a7b57562..71fa9dee 100644 --- a/tests/unit/cli/errors_test.py +++ b/tests/unit/cli/errors_test.py @@ -16,9 +16,9 @@ def mock_logging(): yield mock_log -def patch_find_executable(side_effect): +def patch_call_silently(side_effect): return mock.patch( - 'compose.cli.errors.find_executable', + 'compose.cli.errors.call_silently', autospec=True, side_effect=side_effect) @@ -27,12 +27,12 @@ class TestHandleConnectionErrors(object): def test_generic_connection_error(self, mock_logging): with pytest.raises(errors.ConnectionError): - with patch_find_executable(['/bin/docker', None]): + with patch_call_silently([0, 1]): with handle_connection_errors(mock.Mock()): raise ConnectionError() _, args, _ = mock_logging.error.mock_calls[0] - assert "Couldn't connect to Docker daemon" in args[0] + assert "Couldn't connect to Docker daemon at" in args[0] def test_api_error_version_mismatch(self, mock_logging): with pytest.raises(errors.ConnectionError): diff --git a/tests/unit/cli/formatter_test.py b/tests/unit/cli/formatter_test.py index 4aa025e6..1c3b6a68 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 make_log_record(level, message=None): - return logging.LogRecord('name', level, 'pathame', 0, message or MESSAGE, (), None) +def makeLogRecord(level): + return logging.LogRecord('name', level, 'pathame', 0, MESSAGE, (), None) class ConsoleWarningFormatterTestCase(unittest.TestCase): @@ -21,33 +21,15 @@ class ConsoleWarningFormatterTestCase(unittest.TestCase): self.formatter = ConsoleWarningFormatter() def test_format_warn(self): - output = self.formatter.format(make_log_record(logging.WARN)) + output = self.formatter.format(makeLogRecord(logging.WARN)) expected = colors.yellow('WARNING') + ': ' assert output == expected + MESSAGE def test_format_error(self): - output = self.formatter.format(make_log_record(logging.ERROR)) + output = self.formatter.format(makeLogRecord(logging.ERROR)) expected = colors.red('ERROR') + ': ' assert output == expected + MESSAGE def test_format_info(self): - output = self.formatter.format(make_log_record(logging.INFO)) + output = self.formatter.format(makeLogRecord(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')) diff --git a/tests/unit/cli/log_printer_test.py b/tests/unit/cli/log_printer_test.py index b908eb68..ab48eefc 100644 --- a/tests/unit/cli/log_printer_test.py +++ b/tests/unit/cli/log_printer_test.py @@ -4,9 +4,7 @@ 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 @@ -58,26 +56,6 @@ 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' diff --git a/tests/unit/cli/utils_test.py b/tests/unit/cli/utils_test.py deleted file mode 100644 index 066fb359..00000000 --- a/tests/unit/cli/utils_test.py +++ /dev/null @@ -1,23 +0,0 @@ -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"' diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 317650cb..2c90b29b 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -97,7 +97,7 @@ class CLITestCase(unittest.TestCase): @mock.patch('compose.cli.main.RunOperation', autospec=True) @mock.patch('compose.cli.main.PseudoTerminal', autospec=True) def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) project = Project.from_config( name='composetest', client=mock_client, @@ -128,7 +128,7 @@ class CLITestCase(unittest.TestCase): assert call_kwargs['logs'] is False def test_run_service_with_restart_always(self): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) project = Project.from_config( name='composetest', @@ -184,7 +184,7 @@ class CLITestCase(unittest.TestCase): mock_client.create_host_config.call_args[1].get('restart_policy') ) - def test_command_manual_and_service_ports_together(self): + def test_command_manula_and_service_ports_together(self): project = Project.from_config( name='composetest', client=None, diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index c26272d9..48830558 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -13,22 +13,15 @@ import pytest from ...helpers import build_config_details from compose.config import config -from compose.config import types 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.config import V3_0 -from compose.config.config import V3_1 from compose.config.environment import Environment from compose.config.errors import ConfigurationError from compose.config.errors import VERSION_EXPLANATION -from compose.config.serialize import denormalize_service_dict -from compose.config.serialize import serialize_ns_time_value from compose.config.types import VolumeSpec from compose.const import IS_WINDOWS_PLATFORM -from compose.utils import nanoseconds_from_time_seconds from tests import mock from tests import unittest @@ -54,10 +47,6 @@ def service_sort(services): return sorted(services, key=itemgetter('name')) -def secret_sort(secrets): - return sorted(secrets, key=itemgetter('source')) - - class ConfigTest(unittest.TestCase): def test_load(self): service_dicts = config.load( @@ -112,10 +101,6 @@ class ConfigTest(unittest.TestCase): {'subnet': '172.28.0.0/16'} ] } - }, - 'internal': { - 'driver': 'bridge', - 'internal': True } } }, 'working_dir', 'filename.yml') @@ -155,10 +140,6 @@ class ConfigTest(unittest.TestCase): {'subnet': '172.28.0.0/16'} ] } - }, - 'internal': { - 'driver': 'bridge', - 'internal': True } }) @@ -167,16 +148,6 @@ class ConfigTest(unittest.TestCase): 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 - - for version in ['3', '3.0']: - cfg = config.load(build_config_details({'version': version})) - assert cfg.version == V3_0 - - cfg = config.load(build_config_details({'version': '3.1'})) - assert cfg.version == V3_1 - def test_v1_file_version(self): cfg = config.load(build_config_details({'web': {'image': 'busybox'}})) assert cfg.version == V1 @@ -203,7 +174,7 @@ class ConfigTest(unittest.TestCase): with pytest.raises(ConfigurationError) as excinfo: config.load( build_config_details( - {'version': '2.18'}, + {'version': '2.1'}, filename='filename.yml', ) ) @@ -365,88 +336,6 @@ class ConfigTest(unittest.TestCase): }, 'working_dir', 'filename.yml') ) - def test_load_config_link_local_ips_network(self): - base_file = config.ConfigFile( - 'base.yaml', - { - 'version': V2_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_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: @@ -826,35 +715,7 @@ class ConfigTest(unittest.TestCase): ).services[0] assert 'args' in service['build'] assert 'foo' in service['build']['args'] - assert service['build']['args']['foo'] == '' - - # If build argument is None then it will be converted to the empty - # string. Make sure that int zero kept as it is, i.e. not converted to - # the empty string - def test_build_args_check_zero_preserved(self): - service = config.load( - build_config_details( - { - 'version': '2', - 'services': { - 'web': { - 'build': { - 'context': '.', - 'dockerfile': 'Dockerfile-alt', - 'args': { - 'foo': 0 - } - } - } - } - }, - 'tests/fixtures/extends', - 'filename.yml' - ) - ).services[0] - assert 'args' in service['build'] - assert 'foo' in service['build']['args'] - assert service['build']['args']['foo'] == '0' + assert service['build']['args']['foo'] == 'None' def test_load_with_multiple_files_mismatched_networks_format(self): base_file = config.ConfigFile( @@ -931,10 +792,7 @@ class ConfigTest(unittest.TestCase): 'build': {'context': os.path.abspath('/')}, 'image': 'example/web', 'volumes': [VolumeSpec.parse('/home/user/project:/code')], - 'depends_on': { - 'db': {'condition': 'service_started'}, - 'other': {'condition': 'service_started'}, - }, + 'depends_on': ['db', 'other'], }, { 'name': 'db', @@ -1357,83 +1215,6 @@ 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_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_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_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'], @@ -1522,291 +1303,6 @@ 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_merge_logging_v2_no_base(self): - base = { - 'image': 'alpine:edge' - } - override = { - 'logging': { - 'driver': 'json-file', - 'options': { - 'frequency': '2000' - } - } - } - actual = config.merge_service_dicts(base, override, V2_0) - assert actual == { - 'image': 'alpine:edge', - 'logging': { - 'driver': 'json-file', - 'options': { - 'frequency': '2000' - } - } - } - - def test_merge_logging_v2_no_override(self): - base = { - 'image': 'alpine:edge', - 'logging': { - 'driver': 'syslog', - 'options': { - 'frequency': '2000' - } - } - } - override = {} - actual = config.merge_service_dicts(base, override, V2_0) - assert actual == { - 'image': 'alpine:edge', - 'logging': { - 'driver': 'syslog', - 'options': { - 'frequency': '2000' - } - } - } - - def test_merge_depends_on_no_override(self): - base = { - 'image': 'busybox', - 'depends_on': { - 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'} - } - } - override = {} - actual = config.merge_service_dicts(base, override, V2_1) - assert actual == base - - def test_merge_depends_on_mixed_syntax(self): - base = { - 'image': 'busybox', - 'depends_on': { - 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'} - } - } - override = { - 'depends_on': ['app3'] - } - - actual = config.merge_service_dicts(base, override, V2_1) - assert actual == { - 'image': 'busybox', - 'depends_on': { - 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'}, - 'app3': {'condition': 'service_started'} - } - } - - def test_merge_pid(self): - # Regression: https://github.com/docker/compose/issues/4184 - base = { - 'image': 'busybox', - 'pid': 'host' - } - - override = { - 'labels': {'com.docker.compose.test': 'yes'} - } - - actual = config.merge_service_dicts(base, override, V2_0) - assert actual == { - 'image': 'busybox', - 'pid': 'host', - 'labels': {'com.docker.compose.test': 'yes'} - } - - def test_merge_different_secrets(self): - base = { - 'image': 'busybox', - 'secrets': [ - {'source': 'src.txt'} - ] - } - override = {'secrets': ['other-src.txt']} - - actual = config.merge_service_dicts(base, override, V3_1) - assert secret_sort(actual['secrets']) == secret_sort([ - {'source': 'src.txt'}, - {'source': 'other-src.txt'} - ]) - - def test_merge_secrets_override(self): - base = { - 'image': 'busybox', - 'secrets': ['src.txt'], - } - override = { - 'secrets': [ - { - 'source': 'src.txt', - 'target': 'data.txt', - 'mode': 0o400 - } - ] - } - actual = config.merge_service_dicts(base, override, V3_1) - assert actual['secrets'] == override['secrets'] - def test_external_volume_config(self): config_details = build_config_details({ 'version': '2', @@ -1886,91 +1382,6 @@ class ConfigTest(unittest.TestCase): config.load(config_details) assert 'has neither an image nor a build context' in exc.exconly() - def test_load_secrets(self): - base_file = config.ConfigFile( - 'base.yaml', - { - 'version': '3.1', - 'services': { - 'web': { - 'image': 'example/web', - 'secrets': [ - 'one', - { - 'source': 'source', - 'target': 'target', - 'uid': '100', - 'gid': '200', - 'mode': 0o777, - }, - ], - }, - }, - 'secrets': { - 'one': {'file': 'secret.txt'}, - }, - }) - details = config.ConfigDetails('.', [base_file]) - service_dicts = config.load(details).services - expected = [ - { - 'name': 'web', - 'image': 'example/web', - 'secrets': [ - types.ServiceSecret('one', None, None, None, None), - types.ServiceSecret('source', 'target', '100', '200', 0o777), - ], - }, - ] - assert service_sort(service_dicts) == service_sort(expected) - - def test_load_secrets_multi_file(self): - base_file = config.ConfigFile( - 'base.yaml', - { - 'version': '3.1', - 'services': { - 'web': { - 'image': 'example/web', - 'secrets': ['one'], - }, - }, - 'secrets': { - 'one': {'file': 'secret.txt'}, - }, - }) - override_file = config.ConfigFile( - 'base.yaml', - { - 'version': '3.1', - 'services': { - 'web': { - 'secrets': [ - { - 'source': 'source', - 'target': 'target', - 'uid': '100', - 'gid': '200', - 'mode': 0o777, - }, - ], - }, - }, - }) - details = config.ConfigDetails('.', [base_file, override_file]) - service_dicts = config.load(details).services - expected = [ - { - 'name': 'web', - 'image': 'example/web', - 'secrets': [ - types.ServiceSecret('one', None, None, None, None), - types.ServiceSecret('source', 'target', '100', '200', 0o777), - ], - }, - ] - assert service_sort(service_dicts) == service_sort(expected) - class NetworkModeTest(unittest.TestCase): def test_network_mode_standard(self): @@ -2501,14 +1912,6 @@ class MergePortsTest(unittest.TestCase, MergeListsTest): base_config = ['10:8000', '9000'] override_config = ['20:8000'] - def test_duplicate_port_mappings(self): - service_dict = config.merge_service_dicts( - {self.config_name: self.base_config}, - {self.config_name: self.base_config}, - DEFAULT_VERSION - ) - assert set(service_dict[self.config_name]) == set(self.base_config) - class MergeNetworksTest(unittest.TestCase, MergeListsTest): config_name = 'networks' @@ -2683,15 +2086,6 @@ class EnvTest(unittest.TestCase): {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'bar'}, ) - def test_environment_overrides_env_file(self): - self.assertEqual( - resolve_environment({ - 'environment': {'FOO': 'baz'}, - 'env_file': ['tests/fixtures/env/one.env'], - }), - {'ONE': '2', 'TWO': '1', 'THREE': '3', 'FOO': 'baz'}, - ) - def test_resolve_environment_with_multiple_env_files(self): service_dict = { 'env_file': [ @@ -3238,22 +2632,7 @@ class ExtendsTest(unittest.TestCase): image: example """) services = load_from_filename(str(tmpdir.join('docker-compose.yml'))) - assert service_sort(services)[2]['depends_on'] == { - 'other': {'condition': 'service_started'} - } - - def test_extends_with_healthcheck(self): - service_dicts = load_from_filename('tests/fixtures/extends/healthcheck-2.yml') - assert service_sort(service_dicts) == [{ - 'name': 'demo', - 'image': 'foobar:latest', - 'healthcheck': { - 'test': ['CMD', '/health.sh'], - 'interval': 10000000000, - 'timeout': 5000000000, - 'retries': 36, - } - }] + assert service_sort(services)[2]['depends_on'] == ['other'] @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') @@ -3279,28 +2658,15 @@ class ExpandPathTest(unittest.TestCase): class VolumePathTest(unittest.TestCase): + + @pytest.mark.xfail((not IS_WINDOWS_PLATFORM), reason='does not have a drive') def test_split_path_mapping_with_windows_path(self): host_path = "c:\\Users\\msamblanet\\Documents\\anvil\\connect\\config" windows_volume_path = host_path + ":/opt/connect/config:ro" expected_mapping = ("/opt/connect/config:ro", host_path) mapping = config.split_path_mapping(windows_volume_path) - assert mapping == expected_mapping - - def test_split_path_mapping_with_windows_path_in_container(self): - host_path = 'c:\\Users\\remilia\\data' - container_path = 'c:\\scarletdevil\\data' - expected_mapping = (container_path, host_path) - - mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path)) - assert mapping == expected_mapping - - def test_split_path_mapping_with_root_mount(self): - host_path = '/' - container_path = '/var/hostroot' - expected_mapping = (container_path, host_path) - mapping = config.split_path_mapping('{0}:{1}'.format(host_path, container_path)) - assert mapping == expected_mapping + self.assertEqual(mapping, expected_mapping) @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') @@ -3370,54 +2736,6 @@ class BuildPathTest(unittest.TestCase): assert 'build path' in exc.exconly() -class HealthcheckTest(unittest.TestCase): - def test_healthcheck(self): - service_dict = make_service_dict( - 'test', - {'healthcheck': { - 'test': ['CMD', 'true'], - 'interval': '1s', - 'timeout': '1m', - 'retries': 3, - }}, - '.', - ) - - assert service_dict['healthcheck'] == { - 'test': ['CMD', 'true'], - 'interval': nanoseconds_from_time_seconds(1), - 'timeout': nanoseconds_from_time_seconds(60), - 'retries': 3, - } - - def test_disable(self): - service_dict = make_service_dict( - 'test', - {'healthcheck': { - 'disable': True, - }}, - '.', - ) - - assert service_dict['healthcheck'] == { - 'test': ['NONE'], - } - - def test_disable_with_other_config_is_invalid(self): - with pytest.raises(ConfigurationError) as excinfo: - make_service_dict( - 'invalid-healthcheck', - {'healthcheck': { - 'disable': True, - 'interval': '1s', - }}, - '.', - ) - - assert 'invalid-healthcheck' in excinfo.exconly() - assert 'disable' in excinfo.exconly() - - class GetDefaultConfigFilesTestCase(unittest.TestCase): files = [ @@ -3462,89 +2780,3 @@ def get_config_filename_for_files(filenames, subdir=None): return os.path.basename(filename) finally: shutil.rmtree(project_dir) - - -class SerializeTest(unittest.TestCase): - def test_denormalize_depends_on_v3(self): - service_dict = { - 'image': 'busybox', - 'command': 'true', - 'depends_on': { - 'service2': {'condition': 'service_started'}, - 'service3': {'condition': 'service_started'}, - } - } - - assert denormalize_service_dict(service_dict, V3_0) == { - 'image': 'busybox', - 'command': 'true', - 'depends_on': ['service2', 'service3'] - } - - def test_denormalize_depends_on_v2_1(self): - service_dict = { - 'image': 'busybox', - 'command': 'true', - 'depends_on': { - 'service2': {'condition': 'service_started'}, - 'service3': {'condition': 'service_started'}, - } - } - - assert denormalize_service_dict(service_dict, V2_1) == service_dict - - def test_serialize_time(self): - data = { - 9: '9ns', - 9000: '9us', - 9000000: '9ms', - 90000000: '90ms', - 900000000: '900ms', - 999999999: '999999999ns', - 1000000000: '1s', - 60000000000: '1m', - 60000000001: '60000000001ns', - 9000000000000: '150m', - 90000000000000: '25h', - } - - for k, v in data.items(): - assert serialize_ns_time_value(k) == v - - def test_denormalize_healthcheck(self): - service_dict = { - 'image': 'test', - 'healthcheck': { - 'test': 'exit 1', - 'interval': '1m40s', - 'timeout': '30s', - 'retries': 5 - } - } - processed_service = config.process_service(config.ServiceConfig( - '.', 'test', 'test', service_dict - )) - denormalized_service = denormalize_service_dict(processed_service, V2_1) - assert denormalized_service['healthcheck']['interval'] == '100s' - assert denormalized_service['healthcheck']['timeout'] == '30s' - - def test_denormalize_secrets(self): - service_dict = { - 'name': 'web', - 'image': 'example/web', - 'secrets': [ - types.ServiceSecret('one', None, None, None, None), - types.ServiceSecret('source', 'target', '100', '200', 0o777), - ], - } - denormalized_service = denormalize_service_dict(service_dict, V3_1) - assert secret_sort(denormalized_service['secrets']) == secret_sort([ - {'source': 'one'}, - { - 'source': 'source', - 'target': 'target', - 'uid': '100', - 'gid': '200', - 'mode': 0o777, - }, - ]) diff --git a/tests/unit/config/environment_test.py b/tests/unit/config/environment_test.py deleted file mode 100644 index 20446d2b..00000000 --- a/tests/unit/config/environment_test.py +++ /dev/null @@ -1,40 +0,0 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -from compose.config.environment import Environment -from tests import unittest - - -class EnvironmentTest(unittest.TestCase): - def test_get_simple(self): - env = Environment({ - 'FOO': 'bar', - 'BAR': '1', - 'BAZ': '' - }) - - assert env.get('FOO') == 'bar' - assert env.get('BAR') == '1' - assert env.get('BAZ') == '' - - def test_get_undefined(self): - env = Environment({ - 'FOO': 'bar' - }) - assert env.get('FOOBAR') is None - - def test_get_boolean(self): - env = Environment({ - 'FOO': '', - 'BAR': '0', - 'BAZ': 'FALSE', - 'FOOBAR': 'true', - }) - - assert env.get_boolean('FOO') is False - assert env.get_boolean('BAR') is False - assert env.get_boolean('BAZ') is False - assert env.get_boolean('FOOBAR') is True - assert env.get_boolean('UNDEFINED') is False diff --git a/tests/unit/config/interpolation_test.py b/tests/unit/config/interpolation_test.py index fd40153d..42b5db6e 100644 --- a/tests/unit/config/interpolation_test.py +++ b/tests/unit/config/interpolation_test.py @@ -1,28 +1,21 @@ 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.fixture +@pytest.yield_fixture def mock_env(): - 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 + with mock.patch.dict(os.environ): + os.environ['USER'] = 'jenny' + os.environ['FOO'] = 'bar' + yield def test_interpolate_environment_variables_in_services(mock_env): @@ -50,8 +43,9 @@ def test_interpolate_environment_variables_in_services(mock_env): } } } - value = interpolate_environment_variables("2.0", services, 'service', mock_env) - assert value == expected + assert interpolate_environment_variables( + services, 'service', Environment.from_env_file(None) + ) == expected def test_interpolate_environment_variables_in_volumes(mock_env): @@ -75,47 +69,6 @@ def test_interpolate_environment_variables_in_volumes(mock_env): }, 'other': {}, } - 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" - assert defaults_interpolator("ok ${BAR:-/non:-alphanumeric}") == "ok /non:-alphanumeric" - - -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 " + assert interpolate_environment_variables( + volumes, 'volume', Environment.from_env_file(None) + ) == expected diff --git a/tests/unit/config/types_test.py b/tests/unit/config/types_test.py index 11427352..c741a339 100644 --- a/tests/unit/config/types_test.py +++ b/tests/unit/config/types_test.py @@ -9,6 +9,7 @@ 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(): @@ -63,70 +64,15 @@ class TestVolumeSpec(object): VolumeSpec.parse('one:two:three:four') assert 'has incorrect format' in exc.exconly() - 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) == ( + @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) == ( "/c/Users/me/Documents/shiny/config", "/opt/shiny/config", "ro" ) - 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, True) == ( - '/c/Users/reimu/scarlet', - 'C:\\scarlet\\app', - 'ro' - ) - - 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, True) == ( - '/e/', - 'C:\\', - 'ro' - ) - - 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', - '/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' - ) - class TestVolumesFromSpec(object): diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 04f43016..47f60de8 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -98,7 +98,7 @@ class ContainerTest(unittest.TestCase): self.assertEqual(container.name_without_project, "custom_name_of_container") def test_inspect_if_not_inspected(self): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) container = Container(mock_client, dict(Id="the_id")) container.inspect_if_not_inspected() @@ -150,34 +150,6 @@ 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): diff --git a/tests/unit/interpolation_test.py b/tests/unit/interpolation_test.py new file mode 100644 index 00000000..c3050c2c --- /dev/null +++ b/tests/unit/interpolation_test.py @@ -0,0 +1,36 @@ +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())) diff --git a/tests/unit/network_test.py b/tests/unit/network_test.py deleted file mode 100644 index 12d06f41..00000000 --- a/tests/unit/network_test.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -import pytest - -from .. import unittest -from compose.config import ConfigurationError -from compose.network import check_remote_network_config -from compose.network import Network - - -class NetworkTest(unittest.TestCase): - def test_check_remote_network_config_success(self): - options = {'com.docker.network.driver.foo': 'bar'} - net = Network( - None, 'compose_test', 'net1', 'bridge', - options - ) - check_remote_network_config( - {'Driver': 'bridge', 'Options': options}, net - ) - - def test_check_remote_network_config_whitelist(self): - options = {'com.docker.network.driver.foo': 'bar'} - remote_options = { - 'com.docker.network.driver.overlay.vxlanid_list': '257', - 'com.docker.network.driver.foo': 'bar' - } - net = Network( - None, 'compose_test', 'net1', 'overlay', - options - ) - check_remote_network_config( - {'Driver': 'overlay', 'Options': remote_options}, net - ) - - def test_check_remote_network_config_driver_mismatch(self): - net = Network(None, 'compose_test', 'net1', 'overlay') - with pytest.raises(ConfigurationError): - check_remote_network_config( - {'Driver': 'bridge', 'Options': {}}, net - ) - - def test_check_remote_network_config_options_mismatch(self): - net = Network(None, 'compose_test', 'net1', 'overlay') - with pytest.raises(ConfigurationError): - check_remote_network_config({'Driver': 'overlay', 'Options': { - 'com.docker.network.driver.foo': 'baz' - }}, net) - - def test_check_remote_network_config_null_remote(self): - net = Network(None, 'compose_test', 'net1', 'overlay') - check_remote_network_config( - {'Driver': 'overlay', 'Options': None}, net - ) diff --git a/tests/unit/parallel_test.py b/tests/unit/parallel_test.py index 2a50b718..479c0f1d 100644 --- a/tests/unit/parallel_test.py +++ b/tests/unit/parallel_test.py @@ -25,7 +25,7 @@ deps = { def get_deps(obj): - return [(dep, None) for dep in deps[obj]] + return deps[obj] def test_parallel_execute(): diff --git a/tests/unit/progress_stream_test.py b/tests/unit/progress_stream_test.py index c0cb906d..b01be11a 100644 --- a/tests/unit/progress_stream_test.py +++ b/tests/unit/progress_stream_test.py @@ -65,23 +65,3 @@ 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 diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 32d0adfa..b6a52e08 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -19,7 +19,7 @@ from compose.service import Service class ProjectTest(unittest.TestCase): def setUp(self): - self.mock_client = mock.create_autospec(docker.APIClient) + self.mock_client = mock.create_autospec(docker.Client) def test_from_config(self): config = Config( @@ -36,7 +36,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ) project = Project.from_config( name='composetest', @@ -65,7 +64,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ) project = Project.from_config('composetest', config, None) self.assertEqual(len(project.services), 2) @@ -172,7 +170,6 @@ class ProjectTest(unittest.TestCase): }], networks=None, volumes=None, - secrets=None, ), ) assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"] @@ -205,7 +202,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"] @@ -231,7 +227,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) with mock.patch.object(Service, 'containers') as mock_return: @@ -365,7 +360,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) service = project.get_service('test') @@ -390,7 +384,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) service = project.get_service('test') @@ -424,7 +417,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) @@ -445,7 +437,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) @@ -466,7 +457,6 @@ class ProjectTest(unittest.TestCase): ], networks={'custom': {}}, volumes=None, - secrets=None, ), ) @@ -497,7 +487,6 @@ class ProjectTest(unittest.TestCase): }], networks=None, volumes=None, - secrets=None, ), ) self.assertEqual([c.id for c in project.containers()], ['1']) @@ -514,7 +503,6 @@ class ProjectTest(unittest.TestCase): }], networks={'default': {}}, volumes={'data': {}}, - secrets=None, ), ) self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops') @@ -522,35 +510,3 @@ 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 diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 2d5b1761..1994993c 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -34,7 +34,7 @@ from compose.service import warn_on_masked_volume class ServiceTest(unittest.TestCase): def setUp(self): - self.mock_client = mock.create_autospec(docker.APIClient) + self.mock_client = mock.create_autospec(docker.Client) def test_containers(self): service = Service('db', self.mock_client, 'myproject', image='foo') @@ -642,31 +642,11 @@ class ServiceTest(unittest.TestCase): service = Service('foo', project='testing') assert service.image_name == 'testing_foo' - @mock.patch('compose.service.log', autospec=True) - def test_only_log_warning_when_host_ports_clash(self, mock_log): - self.mock_client.inspect_image.return_value = {'Id': 'abcd'} - name = 'foo' - service = Service( - name, - client=self.mock_client, - ports=["8080:80"]) - - service.scale(0) - self.assertFalse(mock_log.warn.called) - - service.scale(1) - self.assertFalse(mock_log.warn.called) - - service.scale(2) - mock_log.warn.assert_called_once_with( - 'The "{}" service specifies a port on the host. If multiple containers ' - 'for this service are created on a single host, the port will clash.'.format(name)) - class TestServiceNetwork(object): def test_connect_container_to_networks_short_aliase_exists(self): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) service = Service( 'db', mock_client, @@ -751,7 +731,7 @@ class NetTestCase(unittest.TestCase): def test_network_mode_service(self): container_id = 'bbbb' service_name = 'web' - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) mock_client.containers.return_value = [ {'Id': container_id, 'Name': container_id, 'Image': 'abcd'}, ] @@ -765,7 +745,7 @@ class NetTestCase(unittest.TestCase): def test_network_mode_service_no_containers(self): service_name = 'web' - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) mock_client.containers.return_value = [] service = Service(name=service_name, client=mock_client) @@ -783,10 +763,10 @@ def build_mount(destination, source, mode='rw'): class ServiceVolumesTest(unittest.TestCase): def setUp(self): - self.mock_client = mock.create_autospec(docker.APIClient) + self.mock_client = mock.create_autospec(docker.Client) def test_build_volume_binding(self): - binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True)) + binding = build_volume_binding(VolumeSpec.parse('/outside:/inside')) assert binding == ('/inside', '/outside:/inside:rw') def test_get_container_data_volumes(self): @@ -845,10 +825,10 @@ class ServiceVolumesTest(unittest.TestCase): def test_merge_volume_bindings(self): options = [ - 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), + VolumeSpec.parse('/host/volume:/host/volume:ro'), + VolumeSpec.parse('/host/rw/volume:/host/rw/volume'), + VolumeSpec.parse('/new/volume'), + VolumeSpec.parse('/existing/volume'), ] self.mock_client.inspect_image.return_value = { @@ -882,8 +862,8 @@ class ServiceVolumesTest(unittest.TestCase): 'web', image='busybox', volumes=[ - VolumeSpec.parse('/host/path:/data1', True), - VolumeSpec.parse('/host/path:/data2', True), + VolumeSpec.parse('/host/path:/data1'), + VolumeSpec.parse('/host/path:/data2'), ], client=self.mock_client, ) @@ -1007,7 +987,7 @@ class ServiceVolumesTest(unittest.TestCase): 'web', client=self.mock_client, image='busybox', - volumes=[VolumeSpec.parse(volume, True)], + volumes=[VolumeSpec.parse(volume)], ).create_container() assert self.mock_client.create_container.call_count == 1 diff --git a/tests/unit/timeparse_test.py b/tests/unit/timeparse_test.py deleted file mode 100644 index 9915932c..00000000 --- a/tests/unit/timeparse_test.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -from compose import timeparse - - -def test_milli(): - assert timeparse.timeparse('5ms') == 0.005 - - -def test_milli_float(): - assert timeparse.timeparse('50.5ms') == 0.0505 - - -def test_second_milli(): - assert timeparse.timeparse('200s5ms') == 200.005 - - -def test_second_milli_micro(): - assert timeparse.timeparse('200s5ms10us') == 200.00501 - - -def test_second(): - assert timeparse.timeparse('200s') == 200 - - -def test_second_as_float(): - assert timeparse.timeparse('20.5s') == 20.5 - - -def test_minute(): - assert timeparse.timeparse('32m') == 1920 - - -def test_hour_minute(): - assert timeparse.timeparse('2h32m') == 9120 - - -def test_minute_as_float(): - assert timeparse.timeparse('1.5m') == 90 - - -def test_hour_minute_second(): - assert timeparse.timeparse('5h34m56s') == 20096 - - -def test_invalid_with_space(): - assert timeparse.timeparse('5h 34m 56s') is None - - -def test_invalid_with_comma(): - assert timeparse.timeparse('5h,34m,56s') is None - - -def test_invalid_with_empty_string(): - assert timeparse.timeparse('') is None diff --git a/tests/unit/utils_test.py b/tests/unit/utils_test.py index 85231957..8ee37b07 100644 --- a/tests/unit/utils_test.py +++ b/tests/unit/utils_test.py @@ -15,10 +15,6 @@ 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): @@ -47,16 +43,3 @@ 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} - ] diff --git a/tests/unit/volume_test.py b/tests/unit/volume_test.py index 24829192..d7ad0792 100644 --- a/tests/unit/volume_test.py +++ b/tests/unit/volume_test.py @@ -10,7 +10,7 @@ from tests import mock @pytest.fixture def mock_client(): - return mock.create_autospec(docker.APIClient) + return mock.create_autospec(docker.Client) class TestVolume(object):