Merge pull request #1763 from aanand/bump-1.4.0

Bump 1.4.0
This commit is contained in:
Aanand Prasad 2015-08-11 18:38:49 +01:00
commit aa7c7cdf93
73 changed files with 2558 additions and 1147 deletions

View File

@ -1,6 +1,45 @@
Change log
==========
1.4.0 (2015-08-04)
------------------
- By default, `docker-compose up` now only recreates containers for services whose configuration has changed since they were created. This should result in a dramatic speed-up for many applications.
The experimental `--x-smart-recreate` flag which introduced this feature in Compose 1.3.0 has been removed, and a `--force-recreate` flag has been added for when you want to recreate everything.
- Several of Compose's commands - `scale`, `stop`, `kill` and `rm` - now perform actions on multiple containers in parallel, rather than in sequence, which will run much faster on larger applications.
- You can now specify a custom name for a service's container with `container_name`. Because Docker container names must be unique, this means you can't scale the service beyond one container.
- You no longer have to specify a `file` option when using `extends` - it will default to the current file.
- Service names can now contain dots, dashes and underscores.
- Compose can now read YAML configuration from standard input, rather than from a file, by specifying `-` as the filename. This makes it easier to generate configuration dynamically:
$ echo 'redis: {"image": "redis"}' | docker-compose --file - up
- There's a new `docker-compose version` command which prints extended information about Compose's bundled dependencies.
- `docker-compose.yml` now supports `log_opt` as well as `log_driver`, allowing you to pass extra configuration to a service's logging driver.
- `docker-compose.yml` now supports `memswap_limit`, similar to `docker run --memory-swap`.
- When mounting volumes with the `volumes` option, you can now pass in any mode supported by the daemon, not just `:ro` or `:rw`. For example, SELinux users can pass `:z` or `:Z`.
- You can now specify a custom volume driver with the `volume_driver` option in `docker-compose.yml`, much like `docker run --volume-driver`.
- A bug has been fixed where Compose would fail to pull images from private registries serving plain (unsecured) HTTP. The `--allow-insecure-ssl` flag, which was previously used to work around this issue, has been deprecated and now has no effect.
- A bug has been fixed where `docker-compose build` would fail if the build depended on a private Hub image or an image from a private registry.
- A bug has been fixed where Compose would crash if there were containers which the Docker daemon had not finished removing.
- Two bugs have been fixed where Compose would sometimes fail with a "Duplicate bind mount" error, or fail to attach volumes to a container, if there was a volume path specified in `docker-compose.yml` with a trailing slash.
Thanks @mnowster, @dnephin, @ekristen, @funkyfuture, @jeffk and @lukemarsden!
1.3.3 (2015-07-15)
------------------

View File

@ -22,13 +22,18 @@ that should get you started.
1. Fork [https://github.com/docker/compose](https://github.com/docker/compose)
to your username.
2. Clone your forked repository locally `git clone git@github.com:yourusername/compose.git`.
3. Enter the local directory `cd compose`.
4. Set up a development environment by running `python setup.py develop`. This
3. You must [configure a remote](https://help.github.com/articles/configuring-a-remote-for-a-fork/) for your fork so that you can [sync changes you make](https://help.github.com/articles/syncing-a-fork/) with the original repository.
4. Enter the local directory `cd compose`.
5. Set up a development environment by running `python setup.py develop`. This
will install the dependencies and set up a symlink from your `docker-compose`
executable to the checkout of the repository. When you now run
`docker-compose` from anywhere on your machine, it will run your development
version of Compose.
## Submitting a pull request
See Docker's [basic contribution workflow](https://docs.docker.com/project/make-a-contribution/#the-basic-contribution-workflow) for a guide on how to submit a pull request for code or documentation.
## Running the test suite
Use the test script to run linting checks and then the full test suite against
@ -51,37 +56,8 @@ you can specify a test directory, file, module, class or method:
$ script/test tests.integration.service_test
$ script/test tests.integration.service_test:ServiceTest.test_containers
## Building binaries
## Finding things to work on
`script/build-linux` will build the Linux binary inside a Docker container:
We use a [Waffle.io board](https://waffle.io/docker/compose) to keep track of specific things we are working on and planning to work on. If you're looking for things to work on, stuff in the backlog is a great place to start.
$ script/build-linux
`script/build-osx` will build the Mac OS X binary inside a virtualenv:
$ script/build-osx
For official releases, you should build inside a Mountain Lion VM for proper
compatibility. Run the this script first to prepare the environment before
building - it will use Homebrew to make sure Python is installed and
up-to-date.
$ script/prepare-osx
## Release process
1. Open pull request that:
- Updates the version in `compose/__init__.py`
- Updates the binary URL in `docs/install.md`
- Adds release notes to `CHANGES.md`
2. Create unpublished GitHub release with release notes
3. Build Linux version on any Docker host with `script/build-linux` and attach
to release
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to
release as `docker-compose-Darwin-x86_64` and `docker-compose-Linux-x86_64`.
5. Publish GitHub release, creating tag
6. Update website with `script/deploy-docs`
7. Upload PyPi package
$ git checkout $VERSION
$ python setup.py sdist upload
For more information about our project planning, take a look at our [GitHub wiki](https://github.com/docker/compose/wiki).

View File

@ -48,16 +48,16 @@ RUN set -ex; \
rm -rf pip-7.0.1; \
rm pip-7.0.1.tar.gz
ENV ALL_DOCKER_VERSIONS 1.6.0 1.7.0
ENV ALL_DOCKER_VERSIONS 1.7.1 1.8.0-rc3
RUN set -ex; \
curl https://get.docker.com/builds/Linux/x86_64/docker-1.6.0 -o /usr/local/bin/docker-1.6.0; \
chmod +x /usr/local/bin/docker-1.6.0; \
curl https://test.docker.com/builds/Linux/x86_64/docker-1.7.0 -o /usr/local/bin/docker-1.7.0; \
chmod +x /usr/local/bin/docker-1.7.0
curl https://get.docker.com/builds/Linux/x86_64/docker-1.7.1 -o /usr/local/bin/docker-1.7.1; \
chmod +x /usr/local/bin/docker-1.7.1; \
curl https://test.docker.com/builds/Linux/x86_64/docker-1.8.0-rc3 -o /usr/local/bin/docker-1.8.0-rc3; \
chmod +x /usr/local/bin/docker-1.8.0-rc3
# Set the default Docker to be run
RUN ln -s /usr/local/bin/docker-1.6.0 /usr/local/bin/docker
RUN ln -s /usr/local/bin/docker-1.7.1 /usr/local/bin/docker
RUN useradd -d /home/user -m -s /bin/bash user
WORKDIR /code/

View File

@ -1,3 +1,4 @@
Aanand Prasad <aanand.prasad@gmail.com> (@aanand)
Ben Firshman <ben@firshman.co.uk> (@bfirsh)
Daniel Nephin <dnephin@gmail.com> (@dnephin)
Mazz Mosley <mazz@houseofmnowster.com> (@mnowster)

View File

@ -4,7 +4,7 @@ include requirements.txt
include requirements-dev.txt
include tox.ini
include *.md
include contrib/completion/bash/docker-compose
recursive-include contrib/completion *
recursive-include tests *
global-exclude *.pyc
global-exclude *.pyo

View File

@ -50,3 +50,8 @@ Contributing
[![Build Status](http://jenkins.dockerproject.org/buildStatus/icon?job=Compose%20Master)](http://jenkins.dockerproject.org/job/Compose%20Master/)
Want to help build Compose? Check out our [contributing documentation](https://github.com/docker/compose/blob/master/CONTRIBUTING.md).
Releasing
---------
Releases are built by maintainers, following an outline of the [release process](https://github.com/docker/compose/blob/master/RELEASE_PROCESS.md).

36
RELEASE_PROCESS.md Normal file
View File

@ -0,0 +1,36 @@
# Building a Compose release
## Building binaries
`script/build-linux` builds the Linux binary inside a Docker container:
$ script/build-linux
`script/build-osx` builds the Mac OS X binary inside a virtualenv:
$ script/build-osx
For official releases, you should build inside a Mountain Lion VM for proper
compatibility. Run the this script first to prepare the environment before
building - it will use Homebrew to make sure Python is installed and
up-to-date.
$ script/prepare-osx
## Release process
1. Open pull request that:
- Updates the version in `compose/__init__.py`
- Updates the binary URL in `docs/install.md`
- Adds release notes to `CHANGES.md`
2. Create unpublished GitHub release with release notes
3. Build Linux version on any Docker host with `script/build-linux` and attach
to release
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to
release as `docker-compose-Darwin-x86_64` and `docker-compose-Linux-x86_64`.
5. Publish GitHub release, creating tag
6. Update website with `script/deploy-docs`
7. Upload PyPi package
$ git checkout $VERSION
$ python setup.py sdist upload

View File

@ -4,9 +4,12 @@
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:
- Composes brute-force “delete and recreate everything” approach is great for dev and testing, but it not sufficient for production environments. You should be able to define a "desired" state that Compose will intelligently converge to.
- It should be possible to partially modify the config file for different environments (dev/test/staging/prod), passing in e.g. custom ports or volume mount paths. ([#426](https://github.com/docker/fig/issues/426))
- 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 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.
## Integration with Swarm

View File

@ -3,30 +3,37 @@ Docker Compose/Swarm integration
Eventually, Compose and Swarm aim to have full integration, meaning you can point a Compose app at a Swarm cluster and have it all just work as if you were using a single Docker host.
However, the current extent of integration is minimal: Compose can create containers on a Swarm cluster, but the majority of Compose apps wont work out of the box unless all containers are scheduled on one host, defeating much of the purpose of using Swarm in the first place.
However, integration is currently incomplete: Compose can create containers on a Swarm cluster, but the majority of Compose apps wont work out of the box unless all containers are scheduled on one host, because links between containers do not work across hosts.
Still, Compose and Swarm can be useful in a “batch processing” scenario (where a large number of containers need to be spun up and down to do independent computation) or a “shared cluster” scenario (where multiple teams want to deploy apps on a cluster without worrying about where to put them).
A number of things need to happen before full integration is achieved, which are documented below.
Links and networking
--------------------
The primary thing stopping multi-container apps from working seamlessly on Swarm is getting them to talk to one another: enabling private communication between containers on different hosts hasnt been solved in a non-hacky way.
Long-term, networking is [getting overhauled](https://github.com/docker/docker/issues/9983) in such a way that itll fit the multi-host model much better. For now, **linked containers are automatically scheduled on the same host**.
Docker networking is [getting overhauled](https://github.com/docker/libnetwork) in such a way that itll fit the multi-host model much better. For now, linked containers are automatically scheduled on the same host.
Building
--------
`docker build` against a Swarm cluster is not implemented, so for now the `build` option will not work - you will need to manually build your service's image, push it somewhere and use `image` to instruct Compose to pull it. Here's an example using the Docker Hub:
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
links: ["db"]
db:
image: postgres
$ docker-compose up -d
$ docker-compose scale web=3
Scheduling
----------
Swarm offers a rich set of scheduling and affinity hints, enabling you to control where containers are located. They are specified via container environment variables, so you can use Compose's `environment` option to set them.
environment:
# Schedule containers on a node that has the 'storage' label set to 'ssd'
- "constraint:storage==ssd"
# Schedule containers where the 'redis' image is already pulled
- "affinity:image==redis"
For the full set of available filters and expressions, see the [Swarm documentation](https://docs.docker.com/swarm/scheduler/filter/).

View File

@ -1,3 +1,3 @@
from __future__ import unicode_literals
__version__ = '1.3.3'
__version__ = '1.4.0'

View File

@ -10,7 +10,7 @@ from .. import config
from ..project import Project
from ..service import ConfigError
from .docopt_command import DocoptCommand
from .utils import call_silently, is_mac, is_ubuntu, find_candidates_in_parent_dirs
from .utils import call_silently, is_mac, is_ubuntu
from .docker_client import docker_client
from . import verbose_proxy
from . import errors
@ -18,13 +18,6 @@ from .. import __version__
log = logging.getLogger(__name__)
SUPPORTED_FILENAMES = [
'docker-compose.yml',
'docker-compose.yaml',
'fig.yml',
'fig.yaml',
]
class Command(DocoptCommand):
base_dir = '.'
@ -48,7 +41,7 @@ class Command(DocoptCommand):
raise errors.ConnectionErrorGeneric(self.get_client().base_url)
def perform_command(self, options, handler, command_options):
if options['COMMAND'] == 'help':
if options['COMMAND'] in ('help', 'version'):
# Skip looking up the compose file.
handler(None, command_options)
return
@ -59,7 +52,7 @@ class Command(DocoptCommand):
explicit_config_path = options.get('--file') or os.environ.get('COMPOSE_FILE') or os.environ.get('FIG_FILE')
project = self.get_project(
self.get_config_path(explicit_config_path),
explicit_config_path,
project_name=options.get('--project-name'),
verbose=options.get('--verbose'))
@ -76,16 +69,18 @@ class Command(DocoptCommand):
return verbose_proxy.VerboseProxy('docker', client)
return client
def get_project(self, config_path, project_name=None, verbose=False):
def get_project(self, config_path=None, project_name=None, verbose=False):
config_details = config.find(self.base_dir, config_path)
try:
return Project.from_dicts(
self.get_project_name(config_path, project_name),
config.load(config_path),
self.get_project_name(config_details.working_dir, project_name),
config.load(config_details),
self.get_client(verbose=verbose))
except ConfigError as e:
raise errors.UserError(six.text_type(e))
def get_project_name(self, config_path, project_name=None):
def get_project_name(self, working_dir, project_name=None):
def normalize_name(name):
return re.sub(r'[^a-z0-9]', '', name.lower())
@ -93,38 +88,15 @@ class Command(DocoptCommand):
log.warn('The FIG_PROJECT_NAME environment variable is deprecated.')
log.warn('Please use COMPOSE_PROJECT_NAME instead.')
project_name = project_name or os.environ.get('COMPOSE_PROJECT_NAME') or os.environ.get('FIG_PROJECT_NAME')
project_name = (
project_name or
os.environ.get('COMPOSE_PROJECT_NAME') or
os.environ.get('FIG_PROJECT_NAME'))
if project_name is not None:
return normalize_name(project_name)
project = os.path.basename(os.path.dirname(os.path.abspath(config_path)))
project = os.path.basename(os.path.abspath(working_dir))
if project:
return normalize_name(project)
return 'default'
def get_config_path(self, file_path=None):
if file_path:
return os.path.join(self.base_dir, file_path)
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, self.base_dir)
if len(candidates) == 0:
raise errors.ComposeFileNotFound(SUPPORTED_FILENAMES)
winner = candidates[0]
if len(candidates) > 1:
log.warning("Found multiple config files with supported names: %s", ", ".join(candidates))
log.warning("Using %s\n", winner)
if winner == 'docker-compose.yaml':
log.warning("Please be aware that .yml is the expected extension "
"in most cases, and using .yaml can cause compatibility "
"issues in future.\n")
if winner.startswith("fig."):
log.warning("%s is deprecated and will not be supported in future. "
"Please rename your config file to docker-compose.yml\n" % winner)
return os.path.join(path, winner)

View File

@ -14,6 +14,8 @@ def docker_client():
cert_path = os.path.join(os.environ.get('HOME', ''), '.docker')
base_url = os.environ.get('DOCKER_HOST')
api_version = os.environ.get('COMPOSE_API_VERSION', '1.19')
tls_config = None
if os.environ.get('DOCKER_TLS_VERIFY', '') != '':
@ -32,4 +34,4 @@ def docker_client():
)
timeout = int(os.environ.get('DOCKER_CLIENT_TIMEOUT', 60))
return Client(base_url=base_url, tls=tls_config, version='1.18', timeout=timeout)
return Client(base_url=base_url, tls=tls_config, version=api_version, timeout=timeout)

View File

@ -53,12 +53,3 @@ class ConnectionErrorGeneric(UserError):
If it's at a non-standard location, specify the URL with the DOCKER_HOST environment variable.
""" % url)
class ComposeFileNotFound(UserError):
def __init__(self, supported_filenames):
super(ComposeFileNotFound, self).__init__("""
Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?
Supported filenames: %s
""" % ", ".join(supported_filenames))

View File

@ -7,7 +7,7 @@ import texttable
def get_tty_width():
tty_size = os.popen('stty size', 'r').read().split()
if len(tty_size) != 2:
return 80
return 0
_, width = tty_size
return int(width)

View File

@ -10,20 +10,27 @@ import sys
from docker.errors import APIError
import dockerpty
from .. import __version__
from .. import legacy
from ..const import DEFAULT_TIMEOUT
from ..project import NoSuchService, ConfigurationError
from ..service import BuildError, NeedsBuildError
from ..config import parse_environment
from ..progress_stream import StreamOutputError
from .command import Command
from .docopt_command import NoSuchCommand
from .errors import UserError
from .formatter import Formatter
from .log_printer import LogPrinter
from .utils import get_version_info, yesno
from .utils import yesno, get_version_info
log = logging.getLogger(__name__)
INSECURE_SSL_WARNING = """
Warning: --allow-insecure-ssl is deprecated and has no effect.
It will be removed in a future version of Compose.
"""
def main():
setup_logging()
@ -47,6 +54,9 @@ def main():
except BuildError as e:
log.error("Service '%s' failed to build: %s" % (e.service.name, e.reason))
sys.exit(1)
except StreamOutputError as e:
log.error(e)
sys.exit(1)
except NeedsBuildError as e:
log.error("Service '%s' needs to be built, but --no-build was passed." % e.service.name)
sys.exit(1)
@ -100,11 +110,12 @@ class TopLevelCommand(Command):
stop Stop services
up Create and start containers
migrate-to-labels Recreate containers to add labels
version Show the Docker-Compose version information
"""
def docopt_options(self):
options = super(TopLevelCommand, self).docopt_options()
options['version'] = get_version_info()
options['version'] = get_version_info('compose')
return options
def build(self, project, options):
@ -226,13 +237,13 @@ class TopLevelCommand(Command):
Usage: pull [options] [SERVICE...]
Options:
--allow-insecure-ssl Allow insecure connections to the docker
registry
--allow-insecure-ssl Deprecated - no effect.
"""
insecure_registry = options['--allow-insecure-ssl']
if options['--allow-insecure-ssl']:
log.warn(INSECURE_SSL_WARNING)
project.pull(
service_names=options['SERVICE'],
insecure_registry=insecure_registry
)
def rm(self, project, options):
@ -274,8 +285,7 @@ class TopLevelCommand(Command):
Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
Options:
--allow-insecure-ssl Allow insecure connections to the docker
registry
--allow-insecure-ssl Deprecated - no effect.
-d Detached mode: Run container in the background, print
new container name.
--entrypoint CMD Override the entrypoint of the image.
@ -290,7 +300,8 @@ class TopLevelCommand(Command):
"""
service = project.get_service(options['SERVICE'])
insecure_registry = options['--allow-insecure-ssl']
if options['--allow-insecure-ssl']:
log.warn(INSECURE_SSL_WARNING)
if not options['--no-deps']:
deps = service.get_linked_names()
@ -300,7 +311,6 @@ class TopLevelCommand(Command):
service_names=deps,
start_deps=True,
allow_recreate=False,
insecure_registry=insecure_registry,
)
tty = True
@ -338,7 +348,6 @@ class TopLevelCommand(Command):
container = service.create_container(
quiet=True,
one_off=True,
insecure_registry=insecure_registry,
**container_options
)
except APIError as e:
@ -370,8 +379,14 @@ class TopLevelCommand(Command):
$ docker-compose scale web=2 worker=3
Usage: scale [SERVICE=NUM...]
Usage: scale [options] [SERVICE=NUM...]
Options:
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
(default: 10)
"""
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
for s in options['SERVICE=NUM']:
if '=' not in s:
raise UserError('Arguments to scale should be in the form service=num')
@ -381,7 +396,7 @@ class TopLevelCommand(Command):
except ValueError:
raise UserError('Number of containers for service "%s" is not a '
'number' % service_name)
project.get_service(service_name).scale(num)
project.get_service(service_name).scale(num, timeout=timeout)
def start(self, project, options):
"""
@ -421,58 +436,65 @@ class TopLevelCommand(Command):
def up(self, project, options):
"""
Build, (re)create, start and attach to containers for a service.
Builds, (re)creates, starts, and attaches to containers for a service.
By default, `docker-compose up` will aggregate the output of each container, and
when it exits, all containers will be stopped. If you run `docker-compose up -d`,
it'll start the containers in the background and leave them running.
Unless they are already running, this command also starts any linked services.
If there are existing containers for a service, `docker-compose up` will stop
and recreate them (preserving mounted volumes with volumes-from),
so that changes in `docker-compose.yml` are picked up. If you do not want existing
containers to be recreated, `docker-compose up --no-recreate` will re-use existing
containers.
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.
Usage: up [options] [SERVICE...]
Options:
--allow-insecure-ssl Allow insecure connections to the docker
registry
--allow-insecure-ssl Deprecated - no effect.
-d Detached mode: Run containers in the background,
print new container names.
--no-color Produce monochrome output.
--no-deps Don't start linked services.
--x-smart-recreate Only recreate containers whose configuration or
image needs to be updated. (EXPERIMENTAL)
--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
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
when attached or when containers are already
running. (default: 10)
"""
insecure_registry = options['--allow-insecure-ssl']
if options['--allow-insecure-ssl']:
log.warn(INSECURE_SSL_WARNING)
detached = options['-d']
monochrome = options['--no-color']
start_deps = not options['--no-deps']
allow_recreate = not options['--no-recreate']
smart_recreate = options['--x-smart-recreate']
force_recreate = options['--force-recreate']
service_names = options['SERVICE']
timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT)
project.up(
if force_recreate and not allow_recreate:
raise UserError("--force-recreate and --no-recreate cannot be combined.")
to_attach = project.up(
service_names=service_names,
start_deps=start_deps,
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
insecure_registry=insecure_registry,
force_recreate=force_recreate,
do_build=not options['--no-build'],
timeout=timeout
)
to_attach = [c for s in project.get_services(service_names) for c in s.containers()]
if not detached:
print("Attaching to", list_containers(to_attach))
log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome)
@ -514,6 +536,20 @@ class TopLevelCommand(Command):
"""
legacy.migrate_project_to_labels(project)
def version(self, project, options):
"""
Show version informations
Usage: version [--short]
Options:
--short Shows only Compose's version number.
"""
if options['--short']:
print(__version__)
else:
print(get_version_info('full'))
def list_containers(containers):
return ", ".join(c.name for c in containers)

View File

@ -1,13 +1,14 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from __future__ import division
import datetime
import os
import subprocess
import platform
import ssl
from .. import __version__
import datetime
from docker import version as docker_py_version
import os
import platform
import subprocess
import ssl
def yesno(prompt, default=None):
@ -125,9 +126,14 @@ def is_ubuntu():
return platform.system() == 'Linux' and platform.linux_distribution()[0] == 'Ubuntu'
def get_version_info():
return '\n'.join([
'docker-compose version: %s' % __version__,
"%s version: %s" % (platform.python_implementation(), platform.python_version()),
"OpenSSL version: %s" % ssl.OPENSSL_VERSION,
])
def get_version_info(scope):
versioninfo = 'docker-compose version: %s' % __version__
if scope == 'compose':
return versioninfo
elif scope == 'full':
return versioninfo + '\n' \
+ "docker-py version: %s\n" % docker_py_version \
+ "%s version: %s\n" % (platform.python_implementation(), platform.python_version()) \
+ "OpenSSL version: %s" % ssl.OPENSSL_VERSION
else:
raise RuntimeError('passed unallowed value to `cli.utils.get_version_info`')

View File

@ -1,7 +1,13 @@
import logging
import os
import sys
import yaml
from collections import namedtuple
import six
from compose.cli.utils import find_candidates_in_parent_dirs
DOCKER_CONFIG_KEYS = [
'cap_add',
@ -18,22 +24,26 @@ DOCKER_CONFIG_KEYS = [
'env_file',
'environment',
'extra_hosts',
'read_only',
'hostname',
'image',
'labels',
'links',
'mac_address',
'mem_limit',
'memswap_limit',
'net',
'log_driver',
'log_opt',
'pid',
'ports',
'privileged',
'read_only',
'restart',
'security_opt',
'stdin_open',
'tty',
'user',
'volume_driver',
'volumes',
'volumes_from',
'working_dir',
@ -41,6 +51,7 @@ DOCKER_CONFIG_KEYS = [
ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
'build',
'container_name',
'dockerfile',
'expose',
'external_links',
@ -54,6 +65,7 @@ DOCKER_CONFIG_HINTS = {
'extra_host': 'extra_hosts',
'device': 'devices',
'link': 'links',
'memory_swap': 'memswap_limit',
'port': 'ports',
'privilege': 'privileged',
'priviliged': 'privileged',
@ -63,12 +75,64 @@ DOCKER_CONFIG_HINTS = {
}
def load(filename):
working_dir = os.path.dirname(filename)
return from_dictionary(load_yaml(filename), working_dir=working_dir, filename=filename)
SUPPORTED_FILENAMES = [
'docker-compose.yml',
'docker-compose.yaml',
'fig.yml',
'fig.yaml',
]
def from_dictionary(dictionary, working_dir=None, filename=None):
PATH_START_CHARS = [
'/',
'.',
'~',
]
log = logging.getLogger(__name__)
ConfigDetails = namedtuple('ConfigDetails', 'config working_dir filename')
def find(base_dir, filename):
if filename == '-':
return ConfigDetails(yaml.safe_load(sys.stdin), os.getcwd(), None)
if filename:
filename = os.path.join(base_dir, filename)
else:
filename = get_config_path(base_dir)
return ConfigDetails(load_yaml(filename), os.path.dirname(filename), filename)
def get_config_path(base_dir):
(candidates, path) = find_candidates_in_parent_dirs(SUPPORTED_FILENAMES, base_dir)
if len(candidates) == 0:
raise ComposeFileNotFound(SUPPORTED_FILENAMES)
winner = candidates[0]
if len(candidates) > 1:
log.warn("Found multiple config files with supported names: %s", ", ".join(candidates))
log.warn("Using %s\n", winner)
if winner == 'docker-compose.yaml':
log.warn("Please be aware that .yml is the expected extension "
"in most cases, and using .yaml can cause compatibility "
"issues in future.\n")
if winner.startswith("fig."):
log.warn("%s is deprecated and will not be supported in future. "
"Please rename your config file to docker-compose.yml\n" % winner)
return os.path.join(path, winner)
def load(config_details):
dictionary, working_dir, filename = config_details
service_dicts = []
for service_name, service_dict in list(dictionary.items()):
@ -82,20 +146,20 @@ def from_dictionary(dictionary, working_dir=None, filename=None):
return service_dicts
def make_service_dict(name, service_dict, working_dir=None):
return ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict)
class ServiceLoader(object):
def __init__(self, working_dir, filename=None, already_seen=None):
self.working_dir = working_dir
self.filename = filename
self.working_dir = os.path.abspath(working_dir)
if filename:
self.filename = os.path.abspath(filename)
else:
self.filename = filename
self.already_seen = already_seen or []
def make_service_dict(self, name, service_dict):
def detect_cycle(self, name):
if self.signature(name) in self.already_seen:
raise CircularReference(self.already_seen)
raise CircularReference(self.already_seen + [self.signature(name)])
def make_service_dict(self, name, service_dict):
service_dict = service_dict.copy()
service_dict['name'] = name
service_dict = resolve_environment(service_dict, working_dir=self.working_dir)
@ -106,12 +170,17 @@ class ServiceLoader(object):
if 'extends' not in service_dict:
return service_dict
extends_options = process_extends_options(service_dict['name'], service_dict['extends'])
extends_options = self.validate_extends_options(service_dict['name'], service_dict['extends'])
if self.working_dir is None:
raise Exception("No working_dir passed to ServiceLoader()")
other_config_path = expand_path(self.working_dir, extends_options['file'])
if 'file' in extends_options:
extends_from_filename = extends_options['file']
other_config_path = expand_path(self.working_dir, extends_from_filename)
else:
other_config_path = self.filename
other_working_dir = os.path.dirname(other_config_path)
other_already_seen = self.already_seen + [self.signature(service_dict['name'])]
other_loader = ServiceLoader(
@ -122,6 +191,7 @@ class ServiceLoader(object):
other_config = load_yaml(other_config_path)
other_service_dict = other_config[extends_options['service']]
other_loader.detect_cycle(extends_options['service'])
other_service_dict = other_loader.make_service_dict(
service_dict['name'],
other_service_dict,
@ -137,25 +207,29 @@ class ServiceLoader(object):
def signature(self, name):
return (self.filename, name)
def validate_extends_options(self, service_name, extends_options):
error_prefix = "Invalid 'extends' configuration for %s:" % service_name
def process_extends_options(service_name, extends_options):
error_prefix = "Invalid 'extends' configuration for %s:" % service_name
if not isinstance(extends_options, dict):
raise ConfigurationError("%s must be a dictionary" % error_prefix)
if not isinstance(extends_options, dict):
raise ConfigurationError("%s must be a dictionary" % error_prefix)
if 'service' not in extends_options:
raise ConfigurationError(
"%s you need to specify a service, e.g. 'service: web'" % error_prefix
)
for k, _ in extends_options.items():
if k not in ['file', 'service']:
if 'service' not in extends_options:
raise ConfigurationError(
"%s unsupported configuration option '%s'" % (error_prefix, k)
"%s you need to specify a service, e.g. 'service: web'" % error_prefix
)
return extends_options
if 'file' not in extends_options and self.filename is None:
raise ConfigurationError(
"%s you need to specify a 'file', e.g. 'file: something.yml'" % error_prefix
)
for k, _ in extends_options.items():
if k not in ['file', 'service']:
raise ConfigurationError(
"%s unsupported configuration option '%s'" % (error_prefix, k)
)
return extends_options
def validate_extended_service_dict(service_dict, filename, service):
@ -182,8 +256,11 @@ def process_container_options(service_dict, working_dir=None):
service_dict = service_dict.copy()
if 'volumes' in service_dict:
service_dict['volumes'] = resolve_host_paths(service_dict['volumes'], working_dir=working_dir)
if 'memswap_limit' in service_dict and 'mem_limit' not in service_dict:
raise ConfigurationError("Invalid 'memswap_limit' configuration for %s service: when defining 'memswap_limit' you must set 'mem_limit' as well" % service_dict['name'])
if 'volumes' in service_dict and service_dict.get('volume_driver') is None:
service_dict['volumes'] = resolve_volume_paths(service_dict, working_dir=working_dir)
if 'build' in service_dict:
service_dict['build'] = resolve_build_path(service_dict['build'], working_dir=working_dir)
@ -344,18 +421,33 @@ def env_vars_from_file(filename):
return env
def resolve_host_paths(volumes, working_dir=None):
def resolve_volume_paths(service_dict, working_dir=None):
if working_dir is None:
raise Exception("No working_dir passed to resolve_host_paths()")
raise Exception("No working_dir passed to resolve_volume_paths()")
return [resolve_host_path(v, working_dir) for v in volumes]
return [
resolve_volume_path(v, working_dir, service_dict['name'])
for v in service_dict['volumes']
]
def resolve_host_path(volume, working_dir):
def resolve_volume_path(volume, working_dir, service_name):
container_path, host_path = split_path_mapping(volume)
container_path = os.path.expanduser(os.path.expandvars(container_path))
if host_path is not None:
host_path = os.path.expanduser(host_path)
host_path = os.path.expandvars(host_path)
host_path = os.path.expanduser(os.path.expandvars(host_path))
if not any(host_path.startswith(c) for c in PATH_START_CHARS):
log.warn(
'Warning: the mapping "{0}" in the volumes config for '
'service "{1}" is ambiguous. In a future version of Docker, '
'it will designate a "named" volume '
'(see https://github.com/docker/docker/pull/14242). '
'To prevent unexpected behaviour, change it to "./{0}"'
.format(volume, service_name)
)
return "%s:%s" % (expand_path(working_dir, host_path), container_path)
else:
return container_path
@ -487,3 +579,12 @@ class CircularReference(ConfigurationError):
for (filename, service_name) in self.trail
]
return "Circular reference:\n {}".format("\n extends ".join(lines))
class ComposeFileNotFound(ConfigurationError):
def __init__(self, supported_filenames):
super(ComposeFileNotFound, self).__init__("""
Can't find a suitable configuration file in this directory or any parent. Are you in the right directory?
Supported filenames: %s
""" % ", ".join(supported_filenames))

View File

@ -22,10 +22,14 @@ class Container(object):
"""
Construct a container object from the output of GET /containers/json.
"""
name = get_container_name(dictionary)
if name is None:
return None
new_dictionary = {
'Id': dictionary['Id'],
'Image': dictionary['Image'],
'Name': '/' + get_container_name(dictionary),
'Name': '/' + name,
}
return cls(client, new_dictionary, **kwargs)

View File

@ -1,15 +1,16 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import logging
from functools import reduce
import logging
from docker.errors import APIError
from .config import get_service_name_from_net, ConfigurationError
from .const import LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF, DEFAULT_TIMEOUT
from .service import Service
from .const import DEFAULT_TIMEOUT, LABEL_PROJECT, LABEL_SERVICE, LABEL_ONE_OFF
from .container import Container
from .legacy import check_for_legacy_containers
from .service import Service
from .utils import parallel_execute
log = logging.getLogger(__name__)
@ -197,12 +198,30 @@ class Project(object):
service.start(**options)
def stop(self, service_names=None, **options):
for service in reversed(self.get_services(service_names)):
service.stop(**options)
parallel_execute(
objects=self.containers(service_names),
obj_callable=lambda c: c.stop(**options),
msg_index=lambda c: c.name,
msg="Stopping"
)
def kill(self, service_names=None, **options):
for service in reversed(self.get_services(service_names)):
service.kill(**options)
parallel_execute(
objects=self.containers(service_names),
obj_callable=lambda c: c.kill(**options),
msg_index=lambda c: c.name,
msg="Killing"
)
def remove_stopped(self, service_names=None, **options):
all_containers = self.containers(service_names, stopped=True)
stopped_containers = [c for c in all_containers if not c.is_running]
parallel_execute(
objects=stopped_containers,
obj_callable=lambda c: c.remove(**options),
msg_index=lambda c: c.name,
msg="Removing"
)
def restart(self, service_names=None, **options):
for service in self.get_services(service_names):
@ -219,11 +238,13 @@ class Project(object):
service_names=None,
start_deps=True,
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
force_recreate=False,
do_build=True,
timeout=DEFAULT_TIMEOUT):
if force_recreate and not allow_recreate:
raise ValueError("force_recreate and allow_recreate are in conflict")
services = self.get_services(service_names, include_deps=start_deps)
for service in services:
@ -232,7 +253,7 @@ class Project(object):
plans = self._get_convergence_plans(
services,
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
force_recreate=force_recreate,
)
return [
@ -240,7 +261,6 @@ class Project(object):
for service in services
for container in service.execute_convergence_plan(
plans[service.name],
insecure_registry=insecure_registry,
do_build=do_build,
timeout=timeout
)
@ -249,7 +269,7 @@ class Project(object):
def _get_convergence_plans(self,
services,
allow_recreate=True,
smart_recreate=False):
force_recreate=False):
plans = {}
@ -261,32 +281,28 @@ class Project(object):
and plans[name].action == 'recreate'
]
if updated_dependencies:
if updated_dependencies and allow_recreate:
log.debug(
'%s has upstream changes (%s)',
service.name, ", ".join(updated_dependencies),
)
plan = service.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=False,
force_recreate=True,
)
else:
plan = service.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
force_recreate=force_recreate,
)
plans[service.name] = plan
return plans
def pull(self, service_names=None, insecure_registry=False):
def pull(self, service_names=None):
for service in self.get_services(service_names, include_deps=True):
service.pull(insecure_registry=insecure_registry)
def remove_stopped(self, service_names=None, **options):
for service in self.get_services(service_names):
service.remove_stopped(**options)
service.pull()
def containers(self, service_names=None, stopped=False, one_off=False):
if service_names:
@ -294,11 +310,11 @@ class Project(object):
else:
service_names = self.service_names
containers = [
containers = filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=stopped,
filters={'label': self.labels(one_off=one_off)})]
filters={'label': self.labels(one_off=one_off)})])
def matches_service_names(container):
return container.labels.get(LABEL_SERVICE) in service_names

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import
from collections import namedtuple
import logging
import re
import os
import sys
from operator import attrgetter
@ -24,7 +25,7 @@ from .const import (
from .container import Container
from .legacy import check_for_legacy_containers
from .progress_stream import stream_output, StreamOutputError
from .utils import json_hash
from .utils import json_hash, parallel_execute
log = logging.getLogger(__name__)
@ -40,6 +41,9 @@ DOCKER_START_KEYS = [
'read_only',
'net',
'log_driver',
'log_opt',
'mem_limit',
'memswap_limit',
'pid',
'privileged',
'restart',
@ -47,7 +51,7 @@ DOCKER_START_KEYS = [
'security_opt',
]
VALID_NAME_CHARS = '[a-zA-Z0-9]'
VALID_NAME_CHARS = '[a-zA-Z0-9\._\-]'
class BuildError(Exception):
@ -99,11 +103,11 @@ class Service(object):
self.options = options
def containers(self, stopped=False, one_off=False):
containers = [
containers = filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=stopped,
filters={'label': self.labels(one_off=one_off)})]
filters={'label': self.labels(one_off=one_off)})])
if not containers:
check_for_legacy_containers(
@ -128,6 +132,7 @@ class Service(object):
for c in self.containers(stopped=True):
self.start_container_if_stopped(c, **options)
# TODO: remove these functions, project takes care of starting/stopping,
def stop(self, **options):
for c in self.containers():
log.info("Stopping %s..." % c.name)
@ -143,7 +148,9 @@ class Service(object):
log.info("Restarting %s..." % c.name)
c.restart(**options)
def scale(self, desired_num):
# end TODO
def scale(self, desired_num, timeout=DEFAULT_TIMEOUT):
"""
Adjusts the number of containers to the specified number and ensures
they are running.
@ -153,51 +160,96 @@ class Service(object):
- starts containers until there are at least `desired_num` running
- removes all stopped containers
"""
if not self.can_be_scaled():
log.warn('Service %s specifies a port on the host. If multiple containers '
if self.custom_container_name() and desired_num > 1:
log.warn('The "%s" service is using the custom container name "%s". '
'Docker requires each container to have a unique name. '
'Remove the custom name to scale the service.'
% (self.name, self.custom_container_name()))
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)
# Create enough containers
containers = self.containers(stopped=True)
while len(containers) < desired_num:
containers.append(self.create_container())
def create_and_start(service, number):
container = service.create_container(number=number, quiet=True)
container.start()
return container
running_containers = []
stopped_containers = []
for c in containers:
if c.is_running:
running_containers.append(c)
else:
stopped_containers.append(c)
running_containers.sort(key=lambda c: c.number)
stopped_containers.sort(key=lambda c: c.number)
running_containers = self.containers(stopped=False)
num_running = len(running_containers)
# Stop containers
while len(running_containers) > desired_num:
c = running_containers.pop()
log.info("Stopping %s..." % c.name)
c.stop(timeout=1)
stopped_containers.append(c)
if desired_num == num_running:
# do nothing as we already have the desired number
log.info('Desired container number already achieved')
return
# Start containers
while len(running_containers) < desired_num:
c = stopped_containers.pop(0)
log.info("Starting %s..." % c.name)
self.start_container(c)
running_containers.append(c)
if desired_num > num_running:
# we need to start/create until we have desired_num
all_containers = self.containers(stopped=True)
if num_running != len(all_containers):
# we have some stopped containers, let's start them up again
stopped_containers = sorted([c for c in all_containers if not c.is_running], key=attrgetter('number'))
num_stopped = len(stopped_containers)
if num_stopped + num_running > desired_num:
num_to_start = desired_num - num_running
containers_to_start = stopped_containers[:num_to_start]
else:
containers_to_start = stopped_containers
parallel_execute(
objects=containers_to_start,
obj_callable=lambda c: c.start(),
msg_index=lambda c: c.name,
msg="Starting"
)
num_running += len(containers_to_start)
num_to_create = desired_num - num_running
next_number = self._next_container_number()
container_numbers = [
number for number in range(
next_number, next_number + num_to_create
)
]
parallel_execute(
objects=container_numbers,
obj_callable=lambda n: create_and_start(service=self, number=n),
msg_index=lambda n: n,
msg="Creating and starting"
)
if desired_num < num_running:
num_to_stop = num_running - desired_num
sorted_running_containers = sorted(running_containers, key=attrgetter('number'))
containers_to_stop = sorted_running_containers[-num_to_stop:]
parallel_execute(
objects=containers_to_stop,
obj_callable=lambda c: c.stop(timeout=timeout),
msg_index=lambda c: c.name,
msg="Stopping"
)
self.remove_stopped()
def remove_stopped(self, **options):
for c in self.containers(stopped=True):
if not c.is_running:
log.info("Removing %s..." % c.name)
c.remove(**options)
containers = [c for c in self.containers(stopped=True) if not c.is_running]
parallel_execute(
objects=containers,
obj_callable=lambda c: c.remove(**options),
msg_index=lambda c: c.name,
msg="Removing"
)
def create_container(self,
one_off=False,
insecure_registry=False,
do_build=True,
previous_container=None,
number=None,
@ -209,7 +261,6 @@ class Service(object):
"""
self.ensure_image_exists(
do_build=do_build,
insecure_registry=insecure_registry,
)
container_options = self._get_container_create_options(
@ -225,8 +276,7 @@ class Service(object):
return Container.create(self.client, **container_options)
def ensure_image_exists(self,
do_build=True,
insecure_registry=False):
do_build=True):
try:
self.image()
@ -240,7 +290,7 @@ class Service(object):
else:
raise NeedsBuildError(self)
else:
self.pull(insecure_registry=insecure_registry)
self.pull()
def image(self):
try:
@ -260,25 +310,28 @@ class Service(object):
def convergence_plan(self,
allow_recreate=True,
smart_recreate=False):
force_recreate=False):
if force_recreate and not allow_recreate:
raise ValueError("force_recreate and allow_recreate are in conflict")
containers = self.containers(stopped=True)
if not containers:
return ConvergencePlan('create', [])
if smart_recreate and not self._containers_have_diverged(containers):
stopped = [c for c in containers if not c.is_running]
if stopped:
return ConvergencePlan('start', stopped)
return ConvergencePlan('noop', containers)
if not allow_recreate:
return ConvergencePlan('start', containers)
return ConvergencePlan('recreate', containers)
if force_recreate or self._containers_have_diverged(containers):
return ConvergencePlan('recreate', containers)
stopped = [c for c in containers if not c.is_running]
if stopped:
return ConvergencePlan('start', stopped)
return ConvergencePlan('noop', containers)
def _containers_have_diverged(self, containers):
config_hash = None
@ -307,14 +360,12 @@ class Service(object):
def execute_convergence_plan(self,
plan,
insecure_registry=False,
do_build=True,
timeout=DEFAULT_TIMEOUT):
(action, containers) = plan
if action == 'create':
container = self.create_container(
insecure_registry=insecure_registry,
do_build=do_build,
)
self.start_container(container)
@ -325,7 +376,6 @@ class Service(object):
return [
self.recreate_container(
c,
insecure_registry=insecure_registry,
timeout=timeout
)
for c in containers
@ -348,7 +398,6 @@ class Service(object):
def recreate_container(self,
container,
insecure_registry=False,
timeout=DEFAULT_TIMEOUT):
"""Recreate a container.
@ -373,7 +422,6 @@ class Service(object):
'%s_%s' % (container.short_id, container.name))
new_container = self.create_container(
insecure_registry=insecure_registry,
do_build=False,
previous_container=container,
number=container.labels.get(LABEL_CONTAINER_NUMBER),
@ -448,12 +496,13 @@ class Service(object):
# TODO: this would benefit from github.com/docker/docker/pull/11943
# to remove the need to inspect every container
def _next_container_number(self, one_off=False):
numbers = [
Container.from_ps(self.client, container).number
containers = filter(None, [
Container.from_ps(self.client, container)
for container in self.client.containers(
all=True,
filters={'label': self.labels(one_off=one_off)})
]
])
numbers = [c.number for c in containers]
return 1 if not numbers else max(numbers) + 1
def _get_links(self, link_to_self):
@ -524,7 +573,10 @@ class Service(object):
for k in DOCKER_CONFIG_KEYS if k in self.options)
container_options.update(override_options)
container_options['name'] = self.get_container_name(number, one_off)
if self.custom_container_name() and not one_off:
container_options['name'] = self.custom_container_name()
else:
container_options['name'] = self.get_container_name(number, one_off)
if add_config_hash:
config_hash = self.config_hash()
@ -599,7 +651,10 @@ class Service(object):
privileged = options.get('privileged', False)
cap_add = options.get('cap_add', None)
cap_drop = options.get('cap_drop', None)
log_config = LogConfig(type=options.get('log_driver', 'json-file'))
log_config = LogConfig(
type=options.get('log_driver', 'json-file'),
config=options.get('log_opt', None)
)
pid = options.get('pid', None)
security_opt = options.get('security_opt', None)
@ -631,6 +686,8 @@ class Service(object):
restart_policy=restart,
cap_add=cap_add,
cap_drop=cap_drop,
mem_limit=options.get('mem_limit'),
memswap_limit=options.get('memswap_limit'),
log_config=log_config,
extra_hosts=extra_hosts,
read_only=read_only,
@ -693,13 +750,16 @@ class Service(object):
'{0}={1}'.format(LABEL_ONE_OFF, "True" if one_off else "False")
]
def can_be_scaled(self):
def custom_container_name(self):
return self.options.get('container_name')
def specifies_host_port(self):
for port in self.options.get('ports', []):
if ':' in str(port):
return False
return True
return True
return False
def pull(self, insecure_registry=False):
def pull(self):
if 'image' not in self.options:
return
@ -710,7 +770,7 @@ class Service(object):
repo,
tag=tag,
stream=True,
insecure_registry=insecure_registry)
)
stream_output(output, sys.stdout)
@ -794,15 +854,13 @@ def parse_volume_spec(volume_config):
"external:internal[:mode]" % volume_config)
if len(parts) == 1:
return VolumeSpec(None, parts[0], 'rw')
external = None
internal = os.path.normpath(parts[0])
else:
external = os.path.normpath(parts[0])
internal = os.path.normpath(parts[1])
if len(parts) == 2:
parts.append('rw')
external, internal, mode = parts
if mode not in ('rw', 'ro'):
raise ConfigError("Volume %s has invalid mode (%s), should be "
"one of: rw, ro." % (volume_config, mode))
mode = parts[2] if len(parts) == 3 else 'rw'
return VolumeSpec(external, internal, mode)

View File

@ -1,5 +1,89 @@
import json
import codecs
import hashlib
import json
import logging
import sys
from docker.errors import APIError
from Queue import Queue, Empty
from threading import Thread
log = logging.getLogger(__name__)
def parallel_execute(objects, obj_callable, msg_index, msg):
"""
For a given list of objects, call the callable passing in the first
object we give it.
"""
stream = codecs.getwriter('utf-8')(sys.stdout)
lines = []
errors = {}
for obj in objects:
write_out_msg(stream, lines, msg_index(obj), msg)
q = Queue()
def inner_execute_function(an_callable, parameter, msg_index):
try:
result = an_callable(parameter)
except APIError as e:
errors[msg_index] = e.explanation
result = "error"
q.put((msg_index, result))
for an_object in objects:
t = Thread(
target=inner_execute_function,
args=(obj_callable, an_object, msg_index(an_object)),
)
t.daemon = True
t.start()
done = 0
total_to_execute = len(objects)
while done < total_to_execute:
try:
msg_index, result = q.get(timeout=1)
if result == 'error':
write_out_msg(stream, lines, msg_index, msg, status='error')
else:
write_out_msg(stream, lines, msg_index, msg)
done += 1
except Empty:
pass
if errors:
stream.write("\n")
for error in errors:
stream.write("ERROR: for {} {} \n".format(error, errors[error]))
def write_out_msg(stream, lines, msg_index, msg, status="done"):
"""
Using special ANSI code characters we can write out the msg over the top of
a previous status message, if it exists.
"""
obj_index = msg_index
if msg_index in lines:
position = lines.index(obj_index)
diff = len(lines) - position
# move up
stream.write("%c[%dA" % (27, diff))
# erase
stream.write("%c[2K\r" % 27)
stream.write("{} {}... {}\n".format(msg, obj_index, status))
# move back down
stream.write("%c[%dB" % (27, diff))
else:
diff = 0
lines.append(obj_index)
stream.write("{} {}... \r\n".format(msg, obj_index))
stream.flush()
def json_hash(obj):

View File

@ -82,7 +82,7 @@ __docker-compose_services_stopped() {
_docker-compose_build() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--no-cache" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help --no-cache" -- "$cur" ) )
;;
*)
__docker-compose_services_from_build
@ -128,7 +128,7 @@ _docker-compose_kill() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-s" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help -s" -- "$cur" ) )
;;
*)
__docker-compose_services_running
@ -140,7 +140,7 @@ _docker-compose_kill() {
_docker-compose_logs() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--no-color" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help --no-color" -- "$cur" ) )
;;
*)
__docker-compose_services_all
@ -149,6 +149,15 @@ _docker-compose_logs() {
}
_docker-compose_migrate-to-labels() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
esac
}
_docker-compose_port() {
case "$prev" in
--protocol)
@ -162,7 +171,7 @@ _docker-compose_port() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--protocol --index" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help --index --protocol" -- "$cur" ) )
;;
*)
__docker-compose_services_all
@ -174,7 +183,7 @@ _docker-compose_port() {
_docker-compose_ps() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-q" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help -q" -- "$cur" ) )
;;
*)
__docker-compose_services_all
@ -186,7 +195,7 @@ _docker-compose_ps() {
_docker-compose_pull() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--allow-insecure-ssl" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
__docker-compose_services_from_image
@ -204,7 +213,7 @@ _docker-compose_restart() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-t --timeout" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help --timeout -t" -- "$cur" ) )
;;
*)
__docker-compose_services_running
@ -216,7 +225,7 @@ _docker-compose_restart() {
_docker-compose_rm() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--force -f -v" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--force -f --help -v" -- "$cur" ) )
;;
*)
__docker-compose_services_stopped
@ -239,7 +248,7 @@ _docker-compose_run() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --entrypoint -e --no-deps --rm --service-ports -T --user -u" -- "$cur" ) )
COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --no-deps --rm --service-ports -T --user -u" -- "$cur" ) )
;;
*)
__docker-compose_services_all
@ -258,11 +267,24 @@ _docker-compose_scale() {
compopt -o nospace
;;
esac
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
esac
}
_docker-compose_start() {
__docker-compose_services_stopped
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
__docker-compose_services_stopped
;;
esac
}
@ -275,7 +297,7 @@ _docker-compose_stop() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "-t --timeout" -- "$cur" ) )
COMPREPLY=( $( compgen -W "--help --timeout -t" -- "$cur" ) )
;;
*)
__docker-compose_services_running
@ -293,7 +315,7 @@ _docker-compose_up() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--allow-insecure-ssl -d --no-build --no-color --no-deps --no-recreate -t --timeout --x-smart-recreate" -- "$cur" ) )
COMPREPLY=( $( compgen -W "-d --help --no-build --no-color --no-deps --no-recreate --force-recreate --timeout -t" -- "$cur" ) )
;;
*)
__docker-compose_services_all
@ -302,6 +324,15 @@ _docker-compose_up() {
}
_docker-compose_version() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--short" -- "$cur" ) )
;;
esac
}
_docker-compose() {
local previous_extglob_setting=$(shopt -p extglob)
shopt -s extglob
@ -322,6 +353,7 @@ _docker-compose() {
start
stop
up
version
)
COMPREPLY=()

View File

@ -162,6 +162,7 @@ __docker-compose_subcommand () {
case "$words[1]" in
(build)
_arguments \
'--help[Print usage]' \
'--no-cache[Do not use cache when building the image]' \
'*:services:__docker-compose_services_from_build' && ret=0
;;
@ -170,20 +171,24 @@ __docker-compose_subcommand () {
;;
(kill)
_arguments \
'--help[Print usage]' \
'-s[SIGNAL to send to the container. Default signal is SIGKILL.]:signal:_signals' \
'*:running services:__docker-compose_runningservices' && ret=0
;;
(logs)
_arguments \
'--help[Print usage]' \
'--no-color[Produce monochrome output.]' \
'*:services:__docker-compose_services_all' && ret=0
;;
(migrate-to-labels)
_arguments \
_arguments -A '-*' \
'--help[Print usage]' \
'(-):Recreate containers to add labels' && ret=0
;;
(port)
_arguments \
'--help[Print usage]' \
'--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' \
@ -191,26 +196,28 @@ __docker-compose_subcommand () {
;;
(ps)
_arguments \
'--help[Print usage]' \
'-q[Only display IDs]' \
'*:services:__docker-compose_services_all' && ret=0
;;
(pull)
_arguments \
'--allow-insecure-ssl[Allow insecure connections to the docker registry]' \
'--help[Print usage]' \
'*:services:__docker-compose_services_from_image' && ret=0
;;
(rm)
_arguments \
'(-f --force)'{-f,--force}"[Don't ask to confirm removal]" \
'--help[Print usage]' \
'-v[Remove volumes associated with containers]' \
'*:stopped services:__docker-compose_stoppedservices' && ret=0
;;
(run)
_arguments \
'--allow-insecure-ssl[Allow insecure connections to the docker registry]' \
'-d[Detached mode: Run container in the background, print new container name.]' \
'--entrypoint[Overwrite the entrypoint of the image.]:entry point: ' \
'*-e[KEY=VAL Set an environment variable (can be used multiple times)]:environment variable KEY=VAL: ' \
'--help[Print usage]' \
'(-u --user)'{-u,--user=-}'[Run as specified username or uid]:username or uid:_users' \
"--no-deps[Don't start linked services.]" \
'--rm[Remove container after run. Ignored in detached mode.]' \
@ -221,28 +228,38 @@ __docker-compose_subcommand () {
'*::arguments: _normal' && ret=0
;;
(scale)
_arguments '*:running services:__docker-compose_runningservices' && ret=0
_arguments \
'--help[Print usage]' \
'*:running services:__docker-compose_runningservices' && ret=0
;;
(start)
_arguments '*:stopped services:__docker-compose_stoppedservices' && ret=0
_arguments \
'--help[Print usage]' \
'*:stopped services:__docker-compose_stoppedservices' && ret=0
;;
(stop|restart)
_arguments \
'--help[Print usage]' \
'(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
'*:running services:__docker-compose_runningservices' && ret=0
;;
(up)
_arguments \
'--allow-insecure-ssl[Allow insecure connections to the docker registry]' \
'-d[Detached mode: Run containers in the background, print new container names.]' \
'--help[Print usage]' \
'--no-color[Produce monochrome output.]' \
"--no-deps[Don't start linked services.]" \
"--no-recreate[If containers already exist, don't recreate them.]" \
"--force-recreate[Recreate containers even if their configuration and image haven't changed]" \
"--no-build[Don't build an image, even if it's missing]" \
'(-t --timeout)'{-t,--timeout}"[Specify a shutdown timeout in seconds. (default: 10)]:seconds: " \
"--x-smart-recreate[Only recreate containers whose configuration or image needs to be updated. (EXPERIMENTAL)]" \
'*:services:__docker-compose_services_all' && ret=0
;;
(version)
_arguments \
'--help[Print usage]' \
"--short[Shows only Compose's version number.]" && ret=0
;;
(*)
_message 'Unknown sub command'
esac

BIN
docker Executable file

Binary file not shown.

View File

@ -1,4 +1,4 @@
FROM docs/base:hugo
FROM docs/base:latest
MAINTAINER Mary Anthony <mary@docker.com> (@moxiegirl)
# To get the git info for this repo
@ -6,6 +6,14 @@ COPY . /src
COPY . /docs/content/compose/
RUN svn checkout https://github.com/docker/docker/trunk/docs /docs/content/docker
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/tutorials/trunk/docs /docs/content/tutorials
RUN svn checkout https://github.com/docker/opensource/trunk/docs /docs/content
# Sed to process GitHub Markdown
# 1-2 Remove comment code from metadata block
# 3 Change ](/word to ](/project/ in links
@ -15,10 +23,4 @@ COPY . /docs/content/compose/
# 7 Change ](../../ to ](/project/ --> not implemented
#
#
RUN find /docs/content/compose -type f -name "*.md" -exec sed -i.old \
-e '/^<!.*metadata]>/g' \
-e '/^<!.*end-metadata.*>/g' \
-e 's/\(\]\)\([(]\)\(\/\)/\1\2\/compose\//g' \
-e 's/\(\][(]\)\([A-z].*\)\(\.md\)/\1\/compose\/\2/g' \
-e 's/\([(]\)\(.*\)\(\.md\)/\1\2/g' \
-e 's/\(\][(]\)\(\.\.\/\)/\1\/compose\//g' {} \;
RUN /src/pre-process.sh /docs

View File

@ -12,7 +12,7 @@ If you want to add a new file or change the location of the document in the menu
2. Save your changes.
3. Make sure you in your `docs` subdirectory.
3. Make sure you are in the `docs` subdirectory.
4. Build the documentation.
@ -41,7 +41,7 @@ If you want to add a new file or change the location of the document in the menu
## 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 appears in GitHub.
The top of each Docker Compose documentation file contains TOML metadata. The metadata is commented out to prevent it from appearing in GitHub.
<!--[metadata]>
+++

View File

@ -1,202 +0,0 @@
<!--[metadata]>
+++
title = "Compose CLI reference"
description = "Compose CLI reference"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
[menu.main]
identifier = "smn_install_compose"
parent = "smn_compose_ref"
+++
<![end-metadata]-->
# Compose CLI reference
Most Docker Compose commands are run against one or more services. If
the service is not specified, the command will apply to all services.
For full usage information, run `docker-compose [COMMAND] --help`.
## Commands
### build
Builds or rebuilds services.
Services are built once and then tagged as `project_service`, e.g.,
`composetest_db`. If you change a service's Dockerfile or the contents of its
build directory, run `docker-compose build` to rebuild it.
### help
Displays help and usage instructions for a command.
### kill
Forces running containers to stop by sending a `SIGKILL` signal. Optionally the
signal can be passed, for example:
$ docker-compose kill -s SIGINT
### logs
Displays log output from services.
### port
Prints the public port for a port binding
### ps
Lists containers.
### pull
Pulls service images.
### restart
Restarts services.
### rm
Removes stopped service containers.
### run
Runs a one-off command on a service.
For example,
$ docker-compose run web python manage.py shell
will start the `web` service and then run `manage.py shell` in python.
Note that by default, linked services will also be started, unless they are
already running.
One-off commands are started in new containers with the same configuration as a
normal container for that service, so volumes, links, etc will all be created as
expected. When using `run`, there are two differences from bringing up a
container normally:
1. the command will be overridden with the one specified. So, if you run
`docker-compose run web bash`, the container's web command (which could default
to, e.g., `python app.py`) will be overridden to `bash`
2. by default no ports will be created in case they collide with already opened
ports.
Links are also created between one-off commands and the other containers which
are part of that service. So, for example, you could run:
$ docker-compose run db psql -h db -U docker
This would open up an interactive PostgreSQL shell for the linked `db` container
(which would get created or started as needed).
If you do not want linked containers to start when running the one-off command,
specify the `--no-deps` flag:
$ docker-compose run --no-deps web python manage.py shell
Similarly, if you do want the service's ports to be created and mapped to the
host, specify the `--service-ports` flag:
$ docker-compose run --service-ports web python manage.py shell
### scale
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
### start
Starts existing containers for a service.
### stop
Stops running containers without removing them. They can be started again with
`docker-compose start`.
### up
Builds, (re)creates, starts, and attaches to containers for a service.
Linked services will be started, unless they are already running.
By default, `docker-compose up` will aggregate the output of each container and,
when it exits, all containers will be stopped. Running `docker-compose up -d`,
will start the containers in the background and leave them running.
By default, if there are existing containers for a service, `docker-compose up` will stop and recreate them (preserving mounted volumes with [volumes-from]), so that changes in `docker-compose.yml` are picked up. If you do not want containers stopped and recreated, use `docker-compose up --no-recreate`. This will still start any stopped containers, if needed.
[volumes-from]: http://docs.docker.io/en/latest/use/working_with_volumes/
## Options
### --verbose
Shows more output
### -v, --version
Prints version and exits
### -f, --file FILE
Specify what file to read configuration from. If not provided, Compose will look
for `docker-compose.yml` in the current working directory, and then each parent
directory successively, until found.
### -p, --project-name NAME
Specifies an alternate project name (default: current directory name)
## Environment Variables
Several environment variables are available for you to configure Compose's behaviour.
Variables starting with `DOCKER_` are the same as those used to configure the
Docker command-line client. If you're using boot2docker, `eval "$(boot2docker shellinit)"`
will set them to their correct values.
### COMPOSE\_PROJECT\_NAME
Sets the project name, which is prepended to the name of every container started by Compose. Defaults to the `basename` of the current working directory.
### COMPOSE\_FILE
Specify what file to read configuration from. If not provided, Compose will look
for `docker-compose.yml` in the current working directory, and then each parent
directory successively, until found.
### 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 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 documentation
- [User guide](/)
- [Installing Compose](install.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with Wordpress](wordpress.md)
- [Yaml file reference](yml.md)
- [Compose environment variables](env.md)
- [Compose command line completion](completion.md)

View File

@ -23,7 +23,7 @@ On a Mac, install with `brew install bash-completion`
Place the completion script in `/etc/bash_completion.d/` (`/usr/local/etc/bash_completion.d/` on a Mac), using e.g.
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk '{print $2}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk 'NR==1{print $NF}')/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
Completion will be available upon next login.
@ -32,7 +32,7 @@ Completion will be available upon next login.
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 | awk '{print $2}')/contrib/completion/zsh/_docker-compose > ~/.zsh/completion/_docker-compose
curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose --version | awk 'NR==1{print $NF}')/contrib/completion/zsh/_docker-compose > ~/.zsh/completion/_docker-compose
Include the directory in your `$fpath`, e.g. by adding in `~/.zshrc`
@ -66,4 +66,4 @@ Enjoy working with Compose faster and with less typos!
- [Get started with Wordpress](wordpress.md)
- [Command line reference](cli.md)
- [Yaml file reference](yml.md)
- [Compose environment variables](env.md)
- [Compose environment variables](env.md)

View File

@ -61,7 +61,7 @@ mounted inside the containers, and what ports they expose.
links:
- db
See the [`docker-compose.yml` reference](yml.html) for more information on how
See the [`docker-compose.yml` reference](yml.md) for more information on how
this file works.
### Build the project
@ -115,8 +115,7 @@ Then, run `docker-compose up`:
myapp_web_1 | Starting development server at http://0.0.0.0:8000/
myapp_web_1 | Quit the server with CONTROL-C.
Your Django app should nw be running at port 8000 on your Docker daemon (if
you're using Boot2docker, `boot2docker ip` will tell you its address).
Your Django app should nw be running at port 8000 on your Docker daemon. If you are using a Docker Machine VM, you can use the `docker-machine ip MACHINE_NAME` to get the IP address.
You can also run management commands with Docker. To set up your database, for
example, run `docker-compose up` and in another terminal run:

View File

@ -10,7 +10,6 @@ weight=3
<![end-metadata]-->
# Compose environment variables reference
===============================
**Note:** Environment variables are no longer the recommended method for connecting to linked services. Instead, you should use the link name (by default, the name of the linked service) as the hostname to connect to. See the [docker-compose.yml documentation](yml.md#links) for details.

View File

@ -28,25 +28,21 @@ the configuration around.
When defining any service in `docker-compose.yml`, you can declare that you are
extending another service like this:
```yaml
web:
extends:
file: common-services.yml
service: webapp
```
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:
```yaml
webapp:
build: .
ports:
- "8000:8000"
volumes:
- "/data"
```
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 that `build`, `ports` and `volumes` configuration
@ -55,31 +51,27 @@ defined directly under `web`.
You can go further and define (or re-define) configuration locally in
`docker-compose.yml`:
```yaml
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
```
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
You can also write other services and link your `web` service to them:
```yaml
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
links:
- db
db:
image: postgres
```
web:
extends:
file: common-services.yml
service: webapp
environment:
- DEBUG=1
cpu_shares: 5
links:
- db
db:
image: postgres
For full details on how to use `extends`, refer to the [reference](#reference).
@ -241,11 +233,13 @@ manually keep both environments in sync.
### Reference
You can use `extends` on any service together with other configuration keys. It
always expects a dictionary that should always contain two keys: `file` and
`service`.
always expects a dictionary that should always contain the key: `service` and optionally the `file` key.
The `file` key specifies which file to look in. It can be an absolute path or a
relative one&mdash;if relative, it's treated as relative to the current file.
The `file` key specifies the location of a Compose configuration file defining
the extension. The `file` value can be an absolute or relative path. If you
specify a relative path, Docker Compose treats it as relative to the location
of the current file. If you don't specify a `file`, Compose looks in the
current configuration file.
The `service` key specifies the name of the service to extend, for example `web`
or `database`.
@ -271,103 +265,91 @@ For single-value options like `image`, `command` or `mem_limit`, the new value
replaces the old value. **This is the default behaviour - all exceptions are
listed below.**
```yaml
# original service
command: python app.py
# original service
command: python app.py
# local service
command: python otherapp.py
# local service
command: python otherapp.py
# result
command: python otherapp.py
```
# result
command: python otherapp.py
In the case of `build` and `image`, using one in the local service causes
Compose to discard the other, if it was defined in the original service.
```yaml
# original service
build: .
# original service
build: .
# local service
image: redis
# local service
image: redis
# result
image: redis
```
# result
image: redis
```yaml
# original service
image: redis
# original service
image: redis
# local service
build: .
# local service
build: .
# result
build: .
```
# result
build: .
For the **multi-value options** `ports`, `expose`, `external_links`, `dns` and
`dns_search`, Compose concatenates both sets of values:
```yaml
# original service
expose:
- "3000"
# original service
expose:
- "3000"
# local service
expose:
- "4000"
- "5000"
# local service
expose:
- "4000"
- "5000"
# result
expose:
- "3000"
- "4000"
- "5000"
```
# result
expose:
- "3000"
- "4000"
- "5000"
In the case of `environment` and `labels`, Compose "merges" entries together
with locally-defined values taking precedence:
```yaml
# original service
environment:
- FOO=original
- BAR=original
# original service
environment:
- FOO=original
- BAR=original
# local service
environment:
- BAR=local
- BAZ=local
# local service
environment:
- BAR=local
- BAZ=local
# result
environment:
- FOO=original
- BAR=local
- BAZ=local
```
# result
environment:
- FOO=original
- BAR=local
- BAZ=local
Finally, for `volumes` and `devices`, Compose "merges" entries together with
locally-defined bindings taking precedence:
```yaml
# original service
volumes:
- /original-dir/foo:/foo
- /original-dir/bar:/bar
# original service
volumes:
- /original-dir/foo:/foo
- /original-dir/bar:/bar
# local service
volumes:
- /local-dir/bar:/bar
- /local-dir/baz/:baz
# local service
volumes:
- /local-dir/bar:/bar
- /local-dir/baz/:baz
# result
volumes:
- /original-dir/foo:/foo
- /local-dir/bar:/bar
- /local-dir/baz/:baz
```
# result
volumes:
- /original-dir/foo:/foo
- /local-dir/bar:/bar
- /local-dir/baz/:baz
## Compose documentation

View File

@ -29,18 +29,16 @@ they can be run together in an isolated environment:
A `docker-compose.yml` looks like this:
```yaml
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
links:
- redis
redis:
image: redis
```
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
links:
- redis
redis:
image: redis
Compose has commands for managing the whole lifecycle of your application:
@ -77,23 +75,21 @@ Next, you'll want to make a directory for the project:
$ cd composetest
Inside this directory, create `app.py`, a simple web app that uses the Flask
framework and increments a value in Redis:
framework and increments a value in Redis. Don't worry if you don't have Redis installed, docker is going to take care of that for you when we [define services](#define-services):
```python
from flask import Flask
from redis import Redis
import os
app = Flask(__name__)
redis = Redis(host='redis', port=6379)
from flask import Flask
from redis import Redis
@app.route('/')
def hello():
redis.incr('hits')
return 'Hello World! I have been seen %s times.' % redis.get('hits')
app = Flask(__name__)
redis = Redis(host='redis', port=6379)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
```
@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)
Next, define the Python dependencies in a file called `requirements.txt`:
@ -163,18 +159,21 @@ Now, when you run `docker-compose up`, Compose will pull a Redis image, build an
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
The web app should now be listening on port 5000 on your Docker daemon host (if
you're using Boot2docker, `boot2docker ip` will tell you its address). In a browser,
open `http://ip-from-boot2docker:5000` and you should get a message in your browser saying:
If you're using [Docker Machine](https://docs.docker.com/machine), then `docker-machine ip MACHINE_VM` will tell you its address and you can open `http://MACHINE_VM_IP:5000` in a browser.
If you're not using Boot2docker and are on linux, 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 localhost:5000.
You should get a message in your browser saying:
`Hello World! I have been seen 1 times.`
Refreshing the page will increment the number.
If you want to run your services in the background, you can pass the `-d` flag
(for daemon mode) to `docker-compose up` and use `docker-compose ps` to see what
is currently running:
(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...
@ -191,7 +190,7 @@ services. For example, to see what environment variables are available to the
$ docker-compose run web env
See `docker-compose --help` to see other available commands.
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:

View File

@ -12,50 +12,87 @@ weight=4
# Install Docker Compose
To install Compose, you'll need to install Docker first. You'll then install
Compose with a `curl` command.
You can run Compose on OS X and 64-bit Linux. It is currently not supported on
the Windows operating system. To install Compose, you'll need to install Docker
first.
## Install Docker
Depending on how your system is configured, you may require `sudo` access to
install Compose. If your system requires `sudo`, you will receive "Permission
denied" errors when installing Compose. If this is the case for you, preface the
install commands with `sudo` to install.
First, install Docker version 1.6 or greater:
To install Compose, do the following:
- [Instructions for Mac OS X](http://docs.docker.com/installation/mac/)
- [Instructions for Ubuntu](http://docs.docker.com/installation/ubuntulinux/)
- [Instructions for other systems](http://docs.docker.com/installation/)
1. Install Docker Engine version 1.7.1 or greater:
## Install Compose
* <a href="https://docs.docker.com/installation/mac/" target="_blank">Mac OS X installation</a> (installs both Engine and Compose)
* <a href="https://docs.docker.com/installation/ubuntulinux/" target="_blank">Ubuntu installation</a>
* <a href="https://docs.docker.com/installation/" target="_blank">other system installations</a>
2. Mac OS X users are done installing. Others should continue to the next step.
3. Go to the <a href="https://github.com/docker/compose/releases" target="_blank">repository release page</a>.
To install Compose, run the following commands:
4. Enter the `curl` command in your termial.
curl -L https://github.com/docker/compose/releases/download/1.3.3/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
The command has the following format:
> 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 above, then `exit`.
curl -L https://github.com/docker/compose/releases/download/VERSION_NUM/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
If you have problems installing with `curl`, you can use `pip` instead: `pip install -U docker-compose`
4. Apply executable permissions to the binary:
Optionally, you can also install [command completion](completion.md) for the
bash shell.
$ chmod +x /usr/local/bin/docker-compose
Compose is available for OS X and 64-bit Linux. If you're on another platform,
Compose can also be installed as a Python package:
5. Optionally, install [command completion](completion.md) for the
`bash` and `zsh` shell.
$ sudo pip install -U docker-compose
6. Test the installation.
No further steps are required; Compose should now be successfully installed.
You can test the installation by running `docker-compose --version`.
$ docker-compose --version
docker-compose version: 1.4.0
### Upgrading
## Upgrading
If you're coming 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 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 migrate them with the following command:
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 migrate them with the following command:
docker-compose migrate-to-labels
$ docker-compose migrate-to-labels
Alternatively, if you're not worried about keeping them, you can remove them - Compose will just create new ones.
Alternatively, if you're not worried about keeping them, you can remove them &endash;
Compose will just create new ones.
docker rm -f myapp_web_1 myapp_db_1 ...
$ docker rm -f -v myapp_web_1 myapp_db_1 ...
## Compose documentation
## 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](/)
- [Get started with Django](django.md)

61
docs/pre-process.sh Executable file
View File

@ -0,0 +1,61 @@
#!/bin/bash -e
# Populate an array with just docker dirs and one with content dirs
docker_dir=(`ls -d /docs/content/docker/*`)
content_dir=(`ls -d /docs/content/*`)
# Loop content not of docker/
#
# Sed to process GitHub Markdown
# 1-2 Remove comment code from metadata block
# 3 Remove .md extension from link text
# 4 Change ](/ to ](/project/ in links
# 5 Change ](word) to ](/project/word)
# 6 Change ](../../ to ](/project/
# 7 Change ](../ to ](/project/word)
#
for i in "${content_dir[@]}"
do
:
case $i in
"/docs/content/windows")
;;
"/docs/content/mac")
;;
"/docs/content/linux")
;;
"/docs/content/docker")
y=${i##*/}
find $i -type f -name "*.md" -exec sed -i.old \
-e '/^<!.*metadata]>/g' \
-e '/^<!.*end-metadata.*>/g' {} \;
;;
*)
y=${i##*/}
find $i -type f -name "*.md" -exec sed -i.old \
-e '/^<!.*metadata]>/g' \
-e '/^<!.*end-metadata.*>/g' \
-e 's/\(\]\)\([(]\)\(\/\)/\1\2\/'$y'\//g' \
-e 's/\(\][(]\)\([A-z].*\)\(\.md\)/\1\/'$y'\/\2/g' \
-e 's/\([(]\)\(.*\)\(\.md\)/\1\2/g' \
-e 's/\(\][(]\)\(\.\/\)/\1\/'$y'\//g' \
-e 's/\(\][(]\)\(\.\.\/\.\.\/\)/\1\/'$y'\//g' \
-e 's/\(\][(]\)\(\.\.\/\)/\1\/'$y'\//g' {} \;
;;
esac
done
#
# Move docker directories to content
#
for i in "${docker_dir[@]}"
do
:
if [ -d $i ]
then
mv $i /docs/content/
fi
done
rm -rf /docs/content/docker

View File

@ -40,8 +40,6 @@ Finally, `docker-compose.yml` is where the magic happens. This file describes th
db:
image: postgres
ports:
- "5432"
web:
build: .
command: bundle exec rails s -p 3000 -b '0.0.0.0'
@ -119,8 +117,8 @@ 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 Boot2docker, `boot2docker ip` will tell you its address).
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.
## More Compose documentation

23
docs/reference/build.md Normal file
View File

@ -0,0 +1,23 @@
<!--[metadata]>
+++
title = "build"
description = "build"
keywords = ["fig, composition, compose, docker, orchestration, cli, build"]
[menu.main]
identifier="build.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# build
```
Usage: build [options] [SERVICE...]
Options:
--no-cache Do not use cache when building 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.

View File

@ -0,0 +1,55 @@
<!--[metadata]>
+++
title = "docker-compose"
description = "docker-compose Command Binary"
keywords = ["fig, composition, compose, docker, orchestration, cli, docker-compose"]
[menu.main]
parent = "smn_compose_cli"
weight=-2
+++
<![end-metadata]-->
# docker-compose Command
```
Usage:
docker-compose [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
Commands:
build Build or rebuild services
help Get help on a command
kill Kill containers
logs View output from containers
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
up Create and start containers
migrate-to-labels Recreate containers to add labels
```
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. This
flag is optional. If you don't provide this flag. Compose looks for a file named
`docker-compose.yml` in the working directory. If the file is not found,
Compose looks in each parent directory successively, until it finds the file.
Use a `-` as the filename to read configuration file from stdin. When stdin is
used all paths in the configuration are relative to the current working
directory.
Each configuration can 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.

18
docs/reference/help.md Normal file
View File

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "help"
description = "help"
keywords = ["fig, composition, compose, docker, orchestration, cli, help"]
[menu.main]
identifier="help.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# help
```
Usage: help COMMAND
```
Displays help and usage instructions for a command.

29
docs/reference/index.md Normal file
View File

@ -0,0 +1,29 @@
<!--[metadata]>
+++
title = "Compose CLI reference"
description = "Compose CLI reference"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
[menu.main]
identifier = "smn_compose_cli"
parent = "smn_compose_ref"
+++
<![end-metadata]-->
## Compose CLI reference
The following pages describe the usage information for the [docker-compose](/reference/docker-compose.md) subcommands. You can also see this information by running `docker-compose [SUBCOMMAND] --help` from the command line.
* [build](/reference/reference/build.md)
* [help](/reference/help.md)
* [kill](/reference/kill.md)
* [ps](/reference/ps.md)
* [restart](/reference/restart.md)
* [run](/reference/run.md)
* [start](/reference/start.md)
* [up](/reference/up.md)
* [logs](/reference/logs.md)
* [port](/reference/port.md)
* [pull](/reference/pull.md)
* [rm](/reference/rm.md)
* [scale](/reference/scale.md)
* [stop](/reference/stop.md)

24
docs/reference/kill.md Normal file
View File

@ -0,0 +1,24 @@
<!--[metadata]>
+++
title = "kill"
description = "Forces running containers to stop."
keywords = ["fig, composition, compose, docker, orchestration, cli, kill"]
[menu.main]
identifier="kill.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# 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

21
docs/reference/logs.md Normal file
View File

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "logs"
description = "Displays log output from services."
keywords = ["fig, composition, compose, docker, orchestration, cli, logs"]
[menu.main]
identifier="logs.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# logs
```
Usage: logs [options] [SERVICE...]
Options:
--no-color Produce monochrome output.
```
Displays log output from services.

View File

@ -0,0 +1,62 @@
<!--[metadata]>
+++
title = "Introduction to the CLI"
description = "Introduction to the CLI"
keywords = ["fig, composition, compose, docker, orchestration, cli, reference"]
[menu.main]
parent = "smn_compose_cli"
weight=-2
+++
<![end-metadata]-->
# Introduction to the CLI
This section describes the subcommands you can use with the `docker-compose` command. You can run subcommand against one or more services. To run against a specific service, you supply the service name from your compose configuration. If you do not specify the service name, the command runs against all the services in your configuration.
## 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.)
### 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 current working directory.
### COMPOSE\_FILE
Specify the file containing the compose configuration. 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.
### 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 documentation
- [User guide](/)
- [Installing Compose](install.md)
- [Get started with Django](django.md)
- [Get started with Rails](rails.md)
- [Get started with Wordpress](wordpress.md)
- [Yaml file reference](yml.md)
- [Compose environment variables](env.md)
- [Compose command line completion](completion.md)

23
docs/reference/port.md Normal file
View File

@ -0,0 +1,23 @@
<!--[metadata]>
+++
title = "port"
description = "Prints the public port for a port binding.s"
keywords = ["fig, composition, compose, docker, orchestration, cli, port"]
[menu.main]
identifier="port.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# 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.

21
docs/reference/ps.md Normal file
View File

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "ps"
description = "Lists containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, ps"]
[menu.main]
identifier="ps.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# ps
```
Usage: ps [options] [SERVICE...]
Options:
-q Only display IDs
```
Lists containers.

18
docs/reference/pull.md Normal file
View File

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "pull"
description = "Pulls service images."
keywords = ["fig, composition, compose, docker, orchestration, cli, pull"]
[menu.main]
identifier="pull.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# pull
```
Usage: pull [options] [SERVICE...]
```
Pulls service images.

21
docs/reference/restart.md Normal file
View File

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "restart"
description = "Restarts Docker Compose services."
keywords = ["fig, composition, compose, docker, orchestration, cli, restart"]
[menu.main]
identifier="restart.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# restart
```
Usage: restart [options] [SERVICE...]
Options:
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10)
```
Restarts services.

22
docs/reference/rm.md Normal file
View File

@ -0,0 +1,22 @@
<!--[metadata]>
+++
title = "rm"
description = "Removes stopped service containers."
keywords = ["fig, composition, compose, docker, orchestration, cli, rm"]
[menu.main]
identifier="rm.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# rm
```
Usage: rm [options] [SERVICE...]
Options:
-f, --force Don't ask to confirm removal
-v Remove volumes associated with containers
```
Removes stopped service containers.

53
docs/reference/run.md Normal file
View File

@ -0,0 +1,53 @@
<!--[metadata]>
+++
title = "run"
description = "Runs a one-off command on a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, run"]
[menu.main]
identifier="run.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# run
```
Usage: run [options] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
Options:
-d Detached mode: Run container in the background, print
new container name.
--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.
--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.
```
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
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

21
docs/reference/scale.md Normal file
View File

@ -0,0 +1,21 @@
<!--[metadata]>
+++
title = "scale"
description = "Sets the number of containers to run for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, scale"]
[menu.main]
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# 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

18
docs/reference/start.md Normal file
View File

@ -0,0 +1,18 @@
<!--[metadata]>
+++
title = "start"
description = "Starts existing containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, start"]
[menu.main]
identifier="start.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# start
```
Usage: start [SERVICE...]
```
Starts existing containers for a service.

22
docs/reference/stop.md Normal file
View File

@ -0,0 +1,22 @@
<!--[metadata]>
+++
title = "stop"
description = "Stops running containers without removing them. "
keywords = ["fig, composition, compose, docker, orchestration, cli, stop"]
[menu.main]
identifier="stop.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# 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`.

47
docs/reference/up.md Normal file
View File

@ -0,0 +1,47 @@
<!--[metadata]>
+++
title = "up"
description = "Builds, (re)creates, starts, and attaches to containers for a service."
keywords = ["fig, composition, compose, docker, orchestration, cli, up"]
[menu.main]
identifier="up.compose"
parent = "smn_compose_cli"
+++
<![end-metadata]-->
# up
```
Usage: up [options] [SERVICE...]
Options:
-d Detached mode: Run containers in the background,
print new container names.
--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
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
when attached or when containers are already
running. (default: 10)
```
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.

View File

@ -32,10 +32,8 @@ Dockerfiles, see the
[Dockerfile reference](http://docs.docker.com/reference/builder/). In this case,
your Dockerfile should be:
```
FROM orchardup/php5
ADD . /code
```
FROM orchardup/php5
ADD . /code
This tells Docker how to build an image defining a container that contains PHP
and Wordpress.
@ -43,81 +41,74 @@ and Wordpress.
Next you'll create a `docker-compose.yml` file that will start your web service
and a separate MySQL instance:
```
web:
build: .
command: php -S 0.0.0.0:8000 -t /code
ports:
- "8000:8000"
links:
- db
volumes:
- .:/code
db:
image: orchardup/mysql
environment:
MYSQL_DATABASE: wordpress
```
web:
build: .
command: php -S 0.0.0.0:8000 -t /code
ports:
- "8000:8000"
links:
- db
volumes:
- .:/code
db:
image: orchardup/mysql
environment:
MYSQL_DATABASE: wordpress
Two supporting files are needed to get this working - first, `wp-config.php` is
the standard Wordpress config file with a single change to point the database
configuration at the `db` container:
```
<?php
define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', "db:3306");
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
<?php
define('DB_NAME', 'wordpress');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', "db:3306");
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
define('AUTH_KEY', 'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');
$table_prefix = 'wp_';
define('WPLANG', '');
define('WP_DEBUG', false);
$table_prefix = 'wp_';
define('WPLANG', '');
define('WP_DEBUG', false);
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
if ( !defined('ABSPATH') )
define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');
```
require_once(ABSPATH . 'wp-settings.php');
Second, `router.php` tells PHP's built-in web server how to run Wordpress:
```
<?php
<?php
$root = $_SERVER['DOCUMENT_ROOT'];
chdir($root);
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
set_include_path(get_include_path().':'.__DIR__);
if(file_exists($root.$path))
{
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
$path = rtrim($path,'/').'/index.php';
if(strpos($path,'.php') === false) return false;
else {
chdir(dirname($root.$path));
require_once $root.$path;
}
}else include_once 'index.php';
$root = $_SERVER['DOCUMENT_ROOT'];
chdir($root);
$path = '/'.ltrim(parse_url($_SERVER['REQUEST_URI'])['path'],'/');
set_include_path(get_include_path().':'.__DIR__);
if(file_exists($root.$path))
{
if(is_dir($root.$path) && substr($path,strlen($path) - 1, 1) !== '/')
$path = rtrim($path,'/').'/index.php';
if(strpos($path,'.php') === false) return false;
else {
chdir(dirname($root.$path));
require_once $root.$path;
}
}else include_once 'index.php';
```
### Build the project
With those four files in place, run `docker-compose up` inside your Wordpress
directory and it'll pull and build the needed images, and then start the web and
database containers. You'll then be able to visit Wordpress at port 8000 on your
Docker daemon (if you're using Boot2docker, `boot2docker ip` will tell you its
address).
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.
## More Compose documentation

View File

@ -24,11 +24,9 @@ specify them again in `docker-compose.yml`.
Tag or partial image ID. Can be local or remote - Compose will attempt to
pull if it doesn't exist locally.
```
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
```
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
### build
@ -38,9 +36,7 @@ itself. This directory is also the build context that is sent to the Docker daem
Compose will build and tag it with a generated name, and use that image thereafter.
```
build: /path/to/build/dir
```
build: /path/to/build/dir
### dockerfile
@ -48,17 +44,13 @@ Alternate Dockerfile.
Compose will use an alternate file to build with.
```
dockerfile: Dockerfile-alternate
```
dockerfile: Dockerfile-alternate
### command
Override the default command.
```
command: bundle exec thin -p 3000
```
command: bundle exec thin -p 3000
<a name="links"></a>
### links
@ -67,21 +59,17 @@ Link to containers in another service. Either specify both the service name and
the link alias (`SERVICE:ALIAS`), or just the service name (which will also be
used for the alias).
```
links:
- db
- db:database
- redis
```
links:
- db
- db:database
- redis
An entry with the alias' name will be created in `/etc/hosts` inside containers
for this service, e.g:
```
172.17.2.186 db
172.17.2.186 database
172.17.2.187 redis
```
172.17.2.186 db
172.17.2.186 database
172.17.2.187 redis
Environment variables will also be created - see the [environment variable
reference](env.md) for details.
@ -93,29 +81,23 @@ 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
```
external_links:
- redis_1
- project_db_1:mysql
- project_db_1:postgresql
### 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"
```
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
```
162.242.195.82 somehost
50.31.209.229 otherhost
### ports
@ -127,46 +109,45 @@ port (a random host port will be chosen).
> parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason,
> we recommend always explicitly specifying your port mappings as strings.
```
ports:
- "3000"
- "8000:8000"
- "49100:22"
- "127.0.0.1:8001:8001"
```
ports:
- "3000"
- "8000:8000"
- "49100:22"
- "127.0.0.1:8001:8001"
### 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"
```
expose:
- "3000"
- "8000"
### volumes
Mount paths as volumes, optionally specifying a path on the host machine
(`HOST:CONTAINER`), or an access mode (`HOST:CONTAINER:ro`).
```
volumes:
- /var/lib/mysql
- cache/:/tmp/cache
- ~/configs:/etc/configs/:ro
```
volumes:
- /var/lib/mysql
- ./cache:/tmp/cache
- ~/configs:/etc/configs/:ro
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 `..`.
> Note: No path expansion will be done if you have also specified a
> `volume_driver`.
### volumes_from
Mount all of the volumes from another service or container.
```
volumes_from:
- service_name
- container_name
```
volumes_from:
- service_name
- container_name
### environment
@ -175,15 +156,13 @@ Add environment variables. You can use either an array or a dictionary.
Environment variables with only a key are resolved to their values on the
machine Compose is running on, which can be helpful for secret or host-specific values.
```
environment:
RACK_ENV: development
SESSION_SECRET:
environment:
RACK_ENV: development
SESSION_SECRET:
environment:
- RACK_ENV=development
- SESSION_SECRET
```
environment:
- RACK_ENV=development
- SESSION_SECRET
### env_file
@ -194,22 +173,18 @@ If you have specified a Compose file with `docker-compose -f FILE`, paths in
Environment variables specified in `environment` override these values.
```
env_file: .env
env_file: .env
env_file:
- ./common.env
- ./apps/web.env
- /opt/secrets.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
```
# Set Rails/Rack environment
RACK_ENV=development
### extends
@ -222,30 +197,26 @@ Here's a simple example. Suppose we have 2 files - **common.yml** and
**common.yml**
```
webapp:
build: ./webapp
environment:
- DEBUG=false
- SEND_EMAILS=false
```
webapp:
build: ./webapp
environment:
- DEBUG=false
- SEND_EMAILS=false
**development.yml**
```
web:
extends:
file: common.yml
service: webapp
ports:
- "8000:8000"
links:
- db
environment:
- DEBUG=true
db:
image: postgres
```
web:
extends:
file: common.yml
service: webapp
ports:
- "8000:8000"
links:
- db
environment:
- DEBUG=true
db:
image: postgres
Here, the `web` service in **development.yml** inherits the configuration of
the `webapp` service in **common.yml** - the `build` and `environment` keys -
@ -253,6 +224,9 @@ and adds `ports` and `links` configuration. It overrides one of the defined
environment variables (DEBUG) with a new value, and the other one
(SEND_EMAILS) is left untouched.
The `file` key is optional, if it is not set then Compose will look for the
service within the current file.
For more on `extends`, see the [tutorial](extends.md#example) and
[reference](extends.md#reference).
@ -262,17 +236,25 @@ Add metadata to containers using [Docker labels](http://docs.docker.com/userguid
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: ""
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"
### 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.
### log driver
@ -282,27 +264,30 @@ Allowed values are currently ``json-file``, ``syslog`` and ``none``. The list wi
The default value is json-file.
```
log_driver: "json-file"
log_driver: "syslog"
log_driver: "none"
```
log_driver: "json-file"
log_driver: "syslog"
log_driver: "none"
Specify logging options with `log_opt` for the logging driver, as with the ``--log-opt`` option for `docker run`.
Logging options are key value pairs. An example of `syslog` options:
log_driver: "syslog"
log_opt:
address: "tcp://192.168.0.42:123"
### net
Networking mode. Use the same values as the docker client `--net` parameter.
```
net: "bridge"
net: "none"
net: "container:[name or id]"
net: "host"
```
net: "bridge"
net: "none"
net: "container:[name or id]"
net: "host"
### pid
```
pid: "host"
```
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
@ -313,82 +298,76 @@ containers in the bare-metal machine's namespace and vise-versa.
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: 8.8.8.8
dns:
- 8.8.8.8
- 9.9.9.9
### cap_add, cap_drop
Add or drop container capabilities.
See `man 7 capabilities` for a full list.
```
cap_add:
- ALL
cap_add:
- ALL
cap_drop:
- NET_ADMIN
- SYS_ADMIN
```
cap_drop:
- NET_ADMIN
- SYS_ADMIN
### 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
```
dns_search: example.com
dns_search:
- dc1.example.com
- dc2.example.com
### devices
List of device mappings. Uses the same format as the `--device` docker
client create option.
```
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
```
devices:
- "/dev/ttyUSB0:/dev/ttyUSB0"
### security_opt
Override the default labeling scheme for each container.
```
security_opt:
- label:user:USER
- label:role:ROLE
```
security_opt:
- label:user:USER
- label:role:ROLE
### working\_dir, entrypoint, user, hostname, domainname, mem\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only
### working\_dir, entrypoint, user, hostname, domainname, mac\_address, mem\_limit, memswap\_limit, privileged, restart, stdin\_open, tty, cpu\_shares, cpuset, read\_only, volume\_driver
Each of these is a single value, analogous to its
[docker run](https://docs.docker.com/reference/run/) counterpart.
```
cpu_shares: 73
cpuset: 0,1
cpu_shares: 73
cpuset: 0,1
working_dir: /code
entrypoint: /code/entrypoint.sh
user: postgresql
working_dir: /code
entrypoint: /code/entrypoint.sh
user: postgresql
hostname: foo
domainname: foo.com
hostname: foo
domainname: foo.com
mem_limit: 1000000000
privileged: true
mac_address: 02:42:ac:11:65:43
restart: always
mem_limit: 1000000000
memswap_limit: 2000000000
privileged: true
stdin_open: true
tty: true
read_only: true
restart: always
stdin_open: true
tty: true
read_only: true
volume_driver: mydriver
```
## Compose documentation

View File

@ -0,0 +1,183 @@
# Experimental: Compose, Swarm and Multi-Host Networking
The [experimental build of Docker](https://github.com/docker/docker/tree/master/experimental) has an entirely new networking system, which enables secure communication between containers on multiple hosts. In combination with Docker Swarm and Docker Compose, you can now run multi-container apps on multi-host clusters with the same tooling and configuration format you use to develop them locally.
> 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, youll 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](http://docs.docker.com/machine/).
- To install Compose, follow the instructions [here](http://docs.docker.com/compose/install/).
Youll 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 youd set up a distributed consul, but thats 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 - its 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 youve 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 its 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, theyll be placed on different nodes across the cluster, thanks to Swarms 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 containers IP address and name. That means that if you have a running container named foo, other containers can access it at the hostname foo.
Lets verify that multi-host networking is functioning. Start a long-running container:
$ docker run -d --name long-running busybox top
<container id>
If you start a new container and inspect its /etc/hosts file, youll 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
Heres 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 were 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 (youll need a Hub account). Replace `<username>` with your Docker Hub username:
$ docker build -t <username>/counter .
$ docker push <username>/counter
Next, create a `docker-compose.yml`, which defines the configuration for the web and redis containers. Once again, replace `<username>` with your Hub username:
web:
image: <username>/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, youll 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.

View File

@ -1,5 +1,5 @@
PyYAML==3.10
docker-py==1.3.0
docker-py==1.3.1
dockerpty==0.3.4
docopt==0.6.1
requests==2.6.1

View File

@ -7,4 +7,4 @@ chmod 777 `pwd`/dist
pyinstaller -F bin/docker-compose
mv dist/docker-compose dist/docker-compose-Linux-x86_64
dist/docker-compose-Linux-x86_64 --version
dist/docker-compose-Linux-x86_64 version

View File

@ -10,4 +10,4 @@ venv/bin/pip install -r requirements-dev.txt
venv/bin/pip install .
venv/bin/pyinstaller -F bin/docker-compose
mv dist/docker-compose dist/docker-compose-Darwin-x86_64
dist/docker-compose-Darwin-x86_64 --version
dist/docker-compose-Darwin-x86_64 version

View File

@ -7,10 +7,17 @@ fi
# If a pidfile is still around (for example after a container restart),
# delete it so that docker can start.
rm -rf /var/run/docker.pid
docker -d $DOCKER_DAEMON_ARGS &>/var/log/docker.log &
docker -d --storage-driver="overlay" &>/var/log/docker.log &
docker_pid=$!
>&2 echo "Waiting for Docker to start..."
while ! docker ps &>/dev/null; do
if ! kill -0 "$docker_pid" &>/dev/null; then
>&2 echo "Docker failed to start"
cat /var/log/docker.log
exit 1
fi
sleep 1
done

View File

@ -29,8 +29,8 @@ install_requires = [
'PyYAML >= 3.10, < 4',
'requests >= 2.6.1, < 2.7',
'texttable >= 0.8.1, < 0.9',
'websocket-client >= 0.11.0, < 1.0',
'docker-py >= 1.3.0, < 1.4',
'websocket-client >= 0.32.0, < 1.0',
'docker-py >= 1.3.1, < 1.4',
'dockerpty >= 0.3.4, < 0.4',
'six >= 1.3.0, < 2',
]

View File

@ -0,0 +1,9 @@
myweb:
extends:
service: web
environment:
- "BAR=1"
web:
image: busybox
environment:
- "BAZ=3"

View File

@ -0,0 +1,16 @@
myweb:
extends:
file: specify-file-as-self.yml
service: web
environment:
- "BAR=1"
web:
extends:
file: specify-file-as-self.yml
service: otherweb
image: busybox
environment:
- "BAZ=3"
otherweb:
environment:
- "YEP=1"

View File

@ -9,6 +9,7 @@ from mock import patch
from .testcases import DockerClientTestCase
from compose.cli.main import TopLevelCommand
from compose.cli.errors import UserError
from compose.project import NoSuchService
@ -36,7 +37,7 @@ class CLITestCase(DockerClientTestCase):
if hasattr(self, '_project'):
return self._project
return self.command.get_project(self.command.get_config_path())
return self.command.get_project()
def test_help(self):
old_base_dir = self.command.base_dir
@ -136,21 +137,21 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(db.containers()), 0)
self.assertEqual(len(console.containers()), 0)
def test_up_with_recreate(self):
def test_up_with_force_recreate(self):
self.command.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')
self.assertEqual(len(service.containers()), 1)
old_ids = [c.id for c in service.containers()]
self.command.dispatch(['up', '-d'], None)
self.command.dispatch(['up', '-d', '--force-recreate'], None)
self.assertEqual(len(service.containers()), 1)
new_ids = [c.id for c in service.containers()]
self.assertNotEqual(old_ids, new_ids)
def test_up_with_keep_old(self):
def test_up_with_no_recreate(self):
self.command.dispatch(['up', '-d'], None)
service = self.project.get_service('simple')
self.assertEqual(len(service.containers()), 1)
@ -164,6 +165,10 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(old_ids, new_ids)
def test_up_with_force_recreate_and_no_recreate(self):
with self.assertRaises(UserError):
self.command.dispatch(['up', '-d', '--force-recreate', '--no-recreate'], None)
def test_up_with_timeout(self):
self.command.dispatch(['up', '-d', '-t', '1'], None)
service = self.project.get_service('simple')

View File

@ -65,7 +65,7 @@ class UtilitiesTestCase(unittest.TestCase):
legacy.is_valid_name("composetest_web_lol_1", one_off=True),
)
def test_get_legacy_containers_no_labels(self):
def test_get_legacy_containers(self):
client = Mock()
client.containers.return_value = [
{
@ -74,12 +74,23 @@ class UtilitiesTestCase(unittest.TestCase):
"Name": "composetest_web_1",
"Labels": None,
},
{
"Id": "ghi789",
"Image": "def456",
"Name": None,
"Labels": None,
},
{
"Id": "jkl012",
"Image": "def456",
"Labels": None,
},
]
containers = list(legacy.get_legacy_containers(
client, "composetest", ["web"]))
containers = legacy.get_legacy_containers(client, "composetest", ["web"])
self.assertEqual(len(containers), 1)
self.assertEqual(containers[0].id, 'abc123')
class LegacyTestCase(DockerClientTestCase):

View File

@ -7,6 +7,10 @@ from compose.container import Container
from .testcases import DockerClientTestCase
def build_service_dicts(service_config):
return config.load(config.ConfigDetails(service_config, 'working_dir', None))
class ProjectTest(DockerClientTestCase):
def test_containers(self):
@ -47,7 +51,7 @@ class ProjectTest(DockerClientTestCase):
)
def test_volumes_from_service(self):
service_dicts = config.from_dictionary({
service_dicts = build_service_dicts({
'data': {
'image': 'busybox:latest',
'volumes': ['/var/data'],
@ -56,7 +60,7 @@ class ProjectTest(DockerClientTestCase):
'image': 'busybox:latest',
'volumes_from': ['data'],
},
}, working_dir='.')
})
project = Project.from_dicts(
name='composetest',
service_dicts=service_dicts,
@ -76,7 +80,7 @@ class ProjectTest(DockerClientTestCase):
)
project = Project.from_dicts(
name='composetest',
service_dicts=config.from_dictionary({
service_dicts=build_service_dicts({
'db': {
'image': 'busybox:latest',
'volumes_from': ['composetest_data_container'],
@ -90,7 +94,7 @@ class ProjectTest(DockerClientTestCase):
def test_net_from_service(self):
project = Project.from_dicts(
name='composetest',
service_dicts=config.from_dictionary({
service_dicts=build_service_dicts({
'net': {
'image': 'busybox:latest',
'command': ["top"]
@ -122,7 +126,7 @@ class ProjectTest(DockerClientTestCase):
project = Project.from_dicts(
name='composetest',
service_dicts=config.from_dictionary({
service_dicts=build_service_dicts({
'web': {
'image': 'busybox:latest',
'net': 'container:composetest_net_container'
@ -193,7 +197,7 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(len(db.containers()), 1)
self.assertEqual(len(web.containers()), 1)
def test_project_up_recreates_containers(self):
def test_recreate_preserves_volumes(self):
web = self.create_service('web')
db = self.create_service('db', volumes=['/etc'])
project = Project('composetest', [web, db], self.client)
@ -205,7 +209,7 @@ class ProjectTest(DockerClientTestCase):
old_db_id = project.containers()[0].id
db_volume_path = project.containers()[0].get('Volumes./etc')
project.up()
project.up(force_recreate=True)
self.assertEqual(len(project.containers()), 2)
db_container = [c for c in project.containers() if 'db' in c.name][0]
@ -289,7 +293,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_starts_depends(self):
project = Project.from_dicts(
name='composetest',
service_dicts=config.from_dictionary({
service_dicts=build_service_dicts({
'console': {
'image': 'busybox:latest',
'command': ["top"],
@ -324,7 +328,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_with_no_deps(self):
project = Project.from_dicts(
name='composetest',
service_dicts=config.from_dictionary({
service_dicts=build_service_dicts({
'console': {
'image': 'busybox:latest',
'command': ["top"],

View File

@ -17,14 +17,14 @@ class ResilienceTest(DockerClientTestCase):
self.host_path = container.get('Volumes')['/var/db']
def test_successful_recreate(self):
self.project.up()
self.project.up(force_recreate=True)
container = self.db.containers()[0]
self.assertEqual(container.get('Volumes')['/var/db'], self.host_path)
def test_create_failure(self):
with mock.patch('compose.service.Service.create_container', crash):
with self.assertRaises(Crash):
self.project.up()
self.project.up(force_recreate=True)
self.project.up()
container = self.db.containers()[0]
@ -33,7 +33,7 @@ class ResilienceTest(DockerClientTestCase):
def test_start_failure(self):
with mock.patch('compose.service.Service.start_container', crash):
with self.assertRaises(Crash):
self.project.up()
self.project.up(force_recreate=True)
self.project.up()
container = self.db.containers()[0]

View File

@ -4,10 +4,10 @@ import os
from os import path
from docker.errors import APIError
import mock
from mock import patch
import tempfile
import shutil
import six
from six import StringIO, text_type
from compose import __version__
from compose.const import (
@ -117,11 +117,17 @@ class ServiceTest(DockerClientTestCase):
service.start_container(container)
self.assertIn('/var/db', container.get('Volumes'))
def test_create_container_with_volume_driver(self):
service = self.create_service('db', volume_driver='foodriver')
container = service.create_container()
service.start_container(container)
self.assertEqual('foodriver', container.get('Config.VolumeDriver'))
def test_create_container_with_cpu_shares(self):
service = self.create_service('db', cpu_shares=73)
container = service.create_container()
service.start_container(container)
self.assertEqual(container.inspect()['Config']['CpuShares'], 73)
self.assertEqual(container.get('HostConfig.CpuShares'), 73)
def test_build_extra_hosts(self):
# string
@ -183,7 +189,7 @@ class ServiceTest(DockerClientTestCase):
service = self.create_service('db', cpuset='0')
container = service.create_container()
service.start_container(container)
self.assertEqual(container.inspect()['Config']['Cpuset'], '0')
self.assertEqual(container.get('HostConfig.CpusetCpus'), '0')
def test_create_container_with_read_only_root_fs(self):
read_only = True
@ -199,6 +205,12 @@ class ServiceTest(DockerClientTestCase):
service.start_container(container)
self.assertEqual(set(container.get('HostConfig.SecurityOpt')), set(security_opt))
def test_create_container_with_mac_address(self):
service = self.create_service('db', mac_address='02:42:ac:11:65:43')
container = service.create_container()
service.start_container(container)
self.assertEqual(container.inspect()['Config']['MacAddress'], '02:42:ac:11:65:43')
def test_create_container_with_specified_volume(self):
host_path = '/tmp/host-path'
container_path = '/container-path'
@ -215,7 +227,53 @@ class ServiceTest(DockerClientTestCase):
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
@mock.patch.dict(os.environ)
def test_recreate_preserves_volume_with_trailing_slash(self):
"""
When the Compose file specifies a trailing slash in the container path, make
sure we copy the volume over when recreating.
"""
service = self.create_service('data', volumes=['/data/'])
old_container = create_and_start_container(service)
volume_path = old_container.get('Volumes')['/data']
new_container = service.recreate_container(old_container)
self.assertEqual(new_container.get('Volumes')['/data'], volume_path)
def test_duplicate_volume_trailing_slash(self):
"""
When an image specifies a volume, and the Compose file specifies a host path
but adds a trailing slash, make sure that we don't create duplicate binds.
"""
host_path = '/tmp/data'
container_path = '/data'
volumes = ['{}:{}/'.format(host_path, container_path)]
tmp_container = self.client.create_container(
'busybox', 'true',
volumes={container_path: {}},
labels={'com.docker.compose.test_image': 'true'},
)
image = self.client.commit(tmp_container)['Id']
service = self.create_service('db', image=image, volumes=volumes)
old_container = create_and_start_container(service)
self.assertEqual(
old_container.get('Config.Volumes'),
{container_path: {}},
)
service = self.create_service('db', image=image, volumes=volumes)
new_container = service.recreate_container(old_container)
self.assertEqual(
new_container.get('Config.Volumes'),
{container_path: {}},
)
self.assertEqual(service.containers(stopped=False), [new_container])
@patch.dict(os.environ)
def test_create_container_with_home_and_env_var_in_volume_path(self):
os.environ['VOLUME_NAME'] = 'my-volume'
os.environ['HOME'] = '/tmp/home-dir'
@ -463,7 +521,7 @@ class ServiceTest(DockerClientTestCase):
with open(os.path.join(base_dir, b'foo\xE2bar'), 'w') as f:
f.write("hello world\n")
self.create_service('web', build=six.text_type(base_dir)).build()
self.create_service('web', build=text_type(base_dir)).build()
self.assertEqual(len(self.client.images(name='composetest_web')), 1)
def test_start_container_stays_unpriviliged(self):
@ -543,6 +601,120 @@ class ServiceTest(DockerClientTestCase):
service.scale(0)
self.assertEqual(len(service.containers()), 0)
@patch('sys.stdout', new_callable=StringIO)
def test_scale_with_stopped_containers(self, mock_stdout):
"""
Given there are some stopped containers and scale is called with a
desired number that is the same as the number of stopped containers,
test that those containers are restarted and not removed/recreated.
"""
service = self.create_service('web')
next_number = service._next_container_number()
valid_numbers = [next_number, next_number + 1]
service.create_container(number=next_number, quiet=True)
service.create_container(number=next_number + 1, quiet=True)
for container in service.containers():
self.assertFalse(container.is_running)
service.scale(2)
self.assertEqual(len(service.containers()), 2)
for container in service.containers():
self.assertTrue(container.is_running)
self.assertTrue(container.number in valid_numbers)
captured_output = mock_stdout.getvalue()
self.assertNotIn('Creating', captured_output)
self.assertIn('Starting', captured_output)
@patch('sys.stdout', new_callable=StringIO)
def test_scale_with_stopped_containers_and_needing_creation(self, mock_stdout):
"""
Given there are some stopped containers and scale is called with a
desired number that is greater than the number of stopped containers,
test that those containers are restarted and required number are created.
"""
service = self.create_service('web')
next_number = service._next_container_number()
service.create_container(number=next_number, quiet=True)
for container in service.containers():
self.assertFalse(container.is_running)
service.scale(2)
self.assertEqual(len(service.containers()), 2)
for container in service.containers():
self.assertTrue(container.is_running)
captured_output = mock_stdout.getvalue()
self.assertIn('Creating', captured_output)
self.assertIn('Starting', captured_output)
@patch('sys.stdout', new_callable=StringIO)
def test_scale_with_api_returns_errors(self, mock_stdout):
"""
Test that when scaling if the API returns an error, that error is handled
and the remaining threads continue.
"""
service = self.create_service('web')
next_number = service._next_container_number()
service.create_container(number=next_number, quiet=True)
with patch(
'compose.container.Container.create',
side_effect=APIError(message="testing", response={}, explanation="Boom")):
service.scale(3)
self.assertEqual(len(service.containers()), 1)
self.assertTrue(service.containers()[0].is_running)
self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue())
@patch('compose.service.log')
def test_scale_with_desired_number_already_achieved(self, mock_log):
"""
Test that calling scale with a desired number that is equal to the
number of containers already running results in no change.
"""
service = self.create_service('web')
next_number = service._next_container_number()
container = service.create_container(number=next_number, quiet=True)
container.start()
self.assertTrue(container.is_running)
self.assertEqual(len(service.containers()), 1)
service.scale(1)
self.assertEqual(len(service.containers()), 1)
container.inspect()
self.assertTrue(container.is_running)
captured_output = mock_log.info.call_args[0]
self.assertIn('Desired container number already achieved', captured_output)
@patch('compose.service.log')
def test_scale_with_custom_container_name_outputs_warning(self, mock_log):
"""
Test that calling scale on a service that has a custom container name
results in warning output.
"""
service = self.create_service('web', container_name='custom-container')
self.assertEqual(service.custom_container_name(), 'custom-container')
service.scale(3)
captured_output = mock_log.warn.call_args[0][0]
self.assertEqual(len(service.containers()), 1)
self.assertIn(
"Remove the custom name to scale the service.",
captured_output
)
def test_scale_sets_ports(self):
service = self.create_service('web', ports=['8000'])
service.scale(2)
@ -644,7 +816,7 @@ class ServiceTest(DockerClientTestCase):
for k, v in {'ONE': '1', 'TWO': '2', 'THREE': '3', 'FOO': 'baz', 'DOO': 'dah'}.items():
self.assertEqual(env[k], v)
@mock.patch.dict(os.environ)
@patch.dict(os.environ)
def test_resolve_env(self):
os.environ['FILE_DEF'] = 'E1'
os.environ['FILE_DEF_EMPTY'] = 'E2'
@ -693,6 +865,16 @@ class ServiceTest(DockerClientTestCase):
for name in labels_list:
self.assertIn((name, ''), labels)
def test_custom_container_name(self):
service = self.create_service('web', container_name='my-web-container')
self.assertEqual(service.custom_container_name(), 'my-web-container')
container = create_and_start_container(service)
self.assertEqual(container.name, 'my-web-container')
one_off_container = service.create_container(one_off=True)
self.assertNotEqual(one_off_container.name, 'my-web-container')
def test_log_drive_invalid(self):
service = self.create_service('web', log_driver='xxx')
self.assertRaises(ValueError, lambda: create_and_start_container(service))

View File

@ -12,7 +12,6 @@ from .testcases import DockerClientTestCase
class ProjectTestCase(DockerClientTestCase):
def run_up(self, cfg, **kwargs):
kwargs.setdefault('smart_recreate', True)
kwargs.setdefault('timeout', 1)
project = self.make_project(cfg)
@ -23,7 +22,7 @@ class ProjectTestCase(DockerClientTestCase):
return Project.from_dicts(
name='composetest',
client=self.client,
service_dicts=config.from_dictionary(cfg),
service_dicts=config.load(config.ConfigDetails(cfg, 'working_dir', None))
)
@ -155,8 +154,7 @@ class ProjectWithDependenciesTest(ProjectTestCase):
def converge(service,
allow_recreate=True,
smart_recreate=False,
insecure_registry=False,
force_recreate=False,
do_build=True):
"""
If a container for this service doesn't exist, create and start one. If there are
@ -164,12 +162,11 @@ def converge(service,
"""
plan = service.convergence_plan(
allow_recreate=allow_recreate,
smart_recreate=smart_recreate,
force_recreate=force_recreate,
)
return service.execute_convergence_plan(
plan,
insecure_registry=insecure_registry,
do_build=do_build,
timeout=1,
)
@ -180,7 +177,7 @@ class ServiceStateTest(DockerClientTestCase):
def test_trigger_create(self):
web = self.create_service('web')
self.assertEqual(('create', []), web.convergence_plan(smart_recreate=True))
self.assertEqual(('create', []), web.convergence_plan())
def test_trigger_noop(self):
web = self.create_service('web')
@ -188,7 +185,7 @@ class ServiceStateTest(DockerClientTestCase):
web.start()
web = self.create_service('web')
self.assertEqual(('noop', [container]), web.convergence_plan(smart_recreate=True))
self.assertEqual(('noop', [container]), web.convergence_plan())
def test_trigger_start(self):
options = dict(command=["top"])
@ -205,7 +202,7 @@ class ServiceStateTest(DockerClientTestCase):
web = self.create_service('web', **options)
self.assertEqual(
('start', containers[0:1]),
web.convergence_plan(smart_recreate=True),
web.convergence_plan(),
)
def test_trigger_recreate_with_config_change(self):
@ -213,14 +210,14 @@ class ServiceStateTest(DockerClientTestCase):
container = web.create_container()
web = self.create_service('web', command=["top", "-d", "1"])
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
self.assertEqual(('recreate', [container]), web.convergence_plan())
def test_trigger_recreate_with_nonexistent_image_tag(self):
web = self.create_service('web', image="busybox:latest")
container = web.create_container()
web = self.create_service('web', image="nonexistent-image")
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
self.assertEqual(('recreate', [container]), web.convergence_plan())
def test_trigger_recreate_with_image_change(self):
repo = 'composetest_myimage'
@ -240,7 +237,7 @@ class ServiceStateTest(DockerClientTestCase):
self.client.remove_container(c)
web = self.create_service('web', image=image)
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
self.assertEqual(('recreate', [container]), web.convergence_plan())
finally:
self.client.remove_image(image)
@ -263,7 +260,7 @@ class ServiceStateTest(DockerClientTestCase):
web.build()
web = self.create_service('web', build=context)
self.assertEqual(('recreate', [container]), web.convergence_plan(smart_recreate=True))
self.assertEqual(('recreate', [container]), web.convergence_plan())
finally:
shutil.rmtree(context)

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from __future__ import absolute_import
from compose.service import Service
from compose.config import make_service_dict
from compose.config import ServiceLoader
from compose.const import LABEL_PROJECT
from compose.cli.docker_client import docker_client
from compose.progress_stream import stream_output
@ -30,10 +30,12 @@ class DockerClientTestCase(unittest.TestCase):
if 'command' not in kwargs:
kwargs['command'] = ["top"]
options = ServiceLoader(working_dir='.').make_service_dict(name, kwargs)
return Service(
project='composetest',
client=self.client,
**make_service_dict(name, kwargs, working_dir='.')
**options
)
def check_build(self, *args, **kwargs):

View File

@ -1,18 +1,13 @@
from __future__ import unicode_literals
from __future__ import absolute_import
import logging
import os
import tempfile
import shutil
from .. import unittest
import docker
import mock
from compose.cli import main
from compose.cli.main import TopLevelCommand
from compose.cli.docopt_command import NoSuchCommand
from compose.cli.errors import ComposeFileNotFound
from compose.cli.main import TopLevelCommand
from compose.service import Service
@ -23,7 +18,7 @@ class CLITestCase(unittest.TestCase):
try:
os.chdir('tests/fixtures/simple-composefile')
command = TopLevelCommand()
project_name = command.get_project_name(command.get_config_path())
project_name = command.get_project_name('.')
self.assertEquals('simplecomposefile', project_name)
finally:
os.chdir(cwd)
@ -31,13 +26,13 @@ class CLITestCase(unittest.TestCase):
def test_project_name_with_explicit_base_dir(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/simple-composefile'
project_name = command.get_project_name(command.get_config_path())
project_name = command.get_project_name(command.base_dir)
self.assertEquals('simplecomposefile', project_name)
def test_project_name_with_explicit_uppercase_base_dir(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/UpperCaseDir'
project_name = command.get_project_name(command.get_config_path())
project_name = command.get_project_name(command.base_dir)
self.assertEquals('uppercasedir', project_name)
def test_project_name_with_explicit_project_name(self):
@ -62,37 +57,10 @@ class CLITestCase(unittest.TestCase):
project_name = command.get_project_name(None)
self.assertEquals(project_name, name)
def test_filename_check(self):
files = [
'docker-compose.yml',
'docker-compose.yaml',
'fig.yml',
'fig.yaml',
]
"""Test with files placed in the basedir"""
self.assertEqual('docker-compose.yml', get_config_filename_for_files(files[0:]))
self.assertEqual('docker-compose.yaml', get_config_filename_for_files(files[1:]))
self.assertEqual('fig.yml', get_config_filename_for_files(files[2:]))
self.assertEqual('fig.yaml', get_config_filename_for_files(files[3:]))
self.assertRaises(ComposeFileNotFound, lambda: get_config_filename_for_files([]))
"""Test with files placed in the subdir"""
def get_config_filename_for_files_in_subdir(files):
return get_config_filename_for_files(files, subdir=True)
self.assertEqual('docker-compose.yml', get_config_filename_for_files_in_subdir(files[0:]))
self.assertEqual('docker-compose.yaml', get_config_filename_for_files_in_subdir(files[1:]))
self.assertEqual('fig.yml', get_config_filename_for_files_in_subdir(files[2:]))
self.assertEqual('fig.yaml', get_config_filename_for_files_in_subdir(files[3:]))
self.assertRaises(ComposeFileNotFound, lambda: get_config_filename_for_files_in_subdir([]))
def test_get_project(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/longer-filename-composefile'
project = command.get_project(command.get_config_path())
project = command.get_project()
self.assertEqual(project.name, 'longerfilenamecomposefile')
self.assertTrue(project.client)
self.assertTrue(project.services)
@ -118,11 +86,6 @@ class CLITestCase(unittest.TestCase):
with self.assertRaises(NoSuchCommand):
TopLevelCommand().dispatch(['help', 'nonexistent'], None)
def test_setup_logging(self):
main.setup_logging()
self.assertEqual(logging.getLogger().level, logging.DEBUG)
self.assertEqual(logging.getLogger('requests').propagate, False)
@mock.patch('compose.cli.main.dockerpty', autospec=True)
def test_run_with_environment_merged_with_options_list(self, mock_dockerpty):
command = TopLevelCommand()
@ -201,23 +164,3 @@ class CLITestCase(unittest.TestCase):
})
_, _, call_kwargs = mock_client.create_container.mock_calls[0]
self.assertFalse('RestartPolicy' in call_kwargs['host_config'])
def get_config_filename_for_files(filenames, subdir=None):
project_dir = tempfile.mkdtemp()
try:
make_files(project_dir, filenames)
command = TopLevelCommand()
if subdir:
command.base_dir = tempfile.mkdtemp(dir=project_dir)
else:
command.base_dir = project_dir
return os.path.basename(command.get_config_path())
finally:
shutil.rmtree(project_dir)
def make_files(dirname, filenames):
for fname in filenames:
with open(os.path.join(dirname, fname), 'w') as f:
f.write('')

View File

@ -1,16 +1,31 @@
import os
import mock
import os
import shutil
import tempfile
from .. import unittest
from compose import config
def make_service_dict(name, service_dict, working_dir):
"""
Test helper function to construct a ServiceLoader
"""
return config.ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict)
class ConfigTest(unittest.TestCase):
def test_from_dictionary(self):
service_dicts = config.from_dictionary({
'foo': {'image': 'busybox'},
'bar': {'environment': ['FOO=1']},
})
def test_load(self):
service_dicts = config.load(
config.ConfigDetails(
{
'foo': {'image': 'busybox'},
'bar': {'environment': ['FOO=1']},
},
'working_dir',
'filename.yml'
)
)
self.assertEqual(
sorted(service_dicts, key=lambda d: d['name']),
@ -26,33 +41,98 @@ class ConfigTest(unittest.TestCase):
])
)
def test_from_dictionary_throws_error_when_not_dict(self):
def test_load_throws_error_when_not_dict(self):
with self.assertRaises(config.ConfigurationError):
config.from_dictionary({
'web': 'busybox:latest',
})
config.load(
config.ConfigDetails(
{'web': 'busybox:latest'},
'working_dir',
'filename.yml'
)
)
def test_config_validation(self):
self.assertRaises(
config.ConfigurationError,
lambda: config.make_service_dict('foo', {'port': ['8000']})
lambda: make_service_dict('foo', {'port': ['8000']}, 'tests/')
)
config.make_service_dict('foo', {'ports': ['8000']})
make_service_dict('foo', {'ports': ['8000']}, 'tests/')
class VolumePathTest(unittest.TestCase):
@mock.patch.dict(os.environ)
def test_volume_binding_with_environ(self):
os.environ['VOLUME_PATH'] = '/host/path'
d = config.make_service_dict('foo', {'volumes': ['${VOLUME_PATH}:/container/path']}, working_dir='.')
d = make_service_dict('foo', {'volumes': ['${VOLUME_PATH}:/container/path']}, working_dir='.')
self.assertEqual(d['volumes'], ['/host/path:/container/path'])
@mock.patch.dict(os.environ)
def test_volume_binding_with_home(self):
os.environ['HOME'] = '/home/user'
d = config.make_service_dict('foo', {'volumes': ['~:/container/path']}, working_dir='.')
d = make_service_dict('foo', {'volumes': ['~:/container/path']}, working_dir='.')
self.assertEqual(d['volumes'], ['/home/user:/container/path'])
@mock.patch.dict(os.environ)
def test_volume_binding_with_local_dir_name_raises_warning(self):
def make_dict(**config):
make_service_dict('foo', config, working_dir='.')
with mock.patch('compose.config.log.warn') as warn:
make_dict(volumes=['/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['/data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['.:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['..:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['./data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['../data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['.profile:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['~:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['~/data:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['~tmp:/container/path'])
self.assertEqual(0, warn.call_count)
make_dict(volumes=['data:/container/path'], volume_driver='mydriver')
self.assertEqual(0, warn.call_count)
make_dict(volumes=['data:/container/path'])
self.assertEqual(1, warn.call_count)
warning = warn.call_args[0][0]
self.assertIn('"data:/container/path"', warning)
self.assertIn('"./data:/container/path"', warning)
def test_named_volume_with_driver_does_not_expand(self):
d = make_service_dict('foo', {
'volumes': ['namedvolume:/data'],
'volume_driver': 'foodriver',
}, working_dir='.')
self.assertEqual(d['volumes'], ['namedvolume:/data'])
@mock.patch.dict(os.environ)
def test_named_volume_with_special_chars(self):
os.environ['NAME'] = 'surprise!'
d = make_service_dict('foo', {
'volumes': ['~/${NAME}:/data'],
'volume_driver': 'foodriver',
}, working_dir='.')
self.assertEqual(d['volumes'], ['~/${NAME}:/data'])
class MergePathMappingTest(object):
def config_name(self):
@ -207,40 +287,65 @@ class MergeLabelsTest(unittest.TestCase):
def test_no_override(self):
service_dict = config.merge_service_dicts(
config.make_service_dict('foo', {'labels': ['foo=1', 'bar']}),
config.make_service_dict('foo', {}),
make_service_dict('foo', {'labels': ['foo=1', 'bar']}, 'tests/'),
make_service_dict('foo', {}, 'tests/'),
)
self.assertEqual(service_dict['labels'], {'foo': '1', 'bar': ''})
def test_no_base(self):
service_dict = config.merge_service_dicts(
config.make_service_dict('foo', {}),
config.make_service_dict('foo', {'labels': ['foo=2']}),
make_service_dict('foo', {}, 'tests/'),
make_service_dict('foo', {'labels': ['foo=2']}, 'tests/'),
)
self.assertEqual(service_dict['labels'], {'foo': '2'})
def test_override_explicit_value(self):
service_dict = config.merge_service_dicts(
config.make_service_dict('foo', {'labels': ['foo=1', 'bar']}),
config.make_service_dict('foo', {'labels': ['foo=2']}),
make_service_dict('foo', {'labels': ['foo=1', 'bar']}, 'tests/'),
make_service_dict('foo', {'labels': ['foo=2']}, 'tests/'),
)
self.assertEqual(service_dict['labels'], {'foo': '2', 'bar': ''})
def test_add_explicit_value(self):
service_dict = config.merge_service_dicts(
config.make_service_dict('foo', {'labels': ['foo=1', 'bar']}),
config.make_service_dict('foo', {'labels': ['bar=2']}),
make_service_dict('foo', {'labels': ['foo=1', 'bar']}, 'tests/'),
make_service_dict('foo', {'labels': ['bar=2']}, 'tests/'),
)
self.assertEqual(service_dict['labels'], {'foo': '1', 'bar': '2'})
def test_remove_explicit_value(self):
service_dict = config.merge_service_dicts(
config.make_service_dict('foo', {'labels': ['foo=1', 'bar=2']}),
config.make_service_dict('foo', {'labels': ['bar']}),
make_service_dict('foo', {'labels': ['foo=1', 'bar=2']}, 'tests/'),
make_service_dict('foo', {'labels': ['bar']}, 'tests/'),
)
self.assertEqual(service_dict['labels'], {'foo': '1', 'bar': ''})
class MemoryOptionsTest(unittest.TestCase):
def test_validation_fails_with_just_memswap_limit(self):
"""
When you set a 'memswap_limit' it is invalid config unless you also set
a mem_limit
"""
with self.assertRaises(config.ConfigurationError):
make_service_dict(
'foo', {
'memswap_limit': 2000000,
},
'tests/'
)
def test_validation_with_correct_memswap_values(self):
service_dict = make_service_dict(
'foo', {
'mem_limit': 1000000,
'memswap_limit': 2000000,
},
'tests/'
)
self.assertEqual(service_dict['memswap_limit'], 2000000)
class EnvTest(unittest.TestCase):
def test_parse_environment_as_list(self):
environment = [
@ -274,7 +379,7 @@ class EnvTest(unittest.TestCase):
os.environ['FILE_DEF_EMPTY'] = 'E2'
os.environ['ENV_DEF'] = 'E3'
service_dict = config.make_service_dict(
service_dict = make_service_dict(
'foo', {
'environment': {
'FILE_DEF': 'F1',
@ -283,6 +388,7 @@ class EnvTest(unittest.TestCase):
'NO_DEF': None
},
},
'tests/'
)
self.assertEqual(
@ -291,7 +397,7 @@ class EnvTest(unittest.TestCase):
)
def test_env_from_file(self):
service_dict = config.make_service_dict(
service_dict = make_service_dict(
'foo',
{'env_file': 'one.env'},
'tests/fixtures/env',
@ -302,7 +408,7 @@ class EnvTest(unittest.TestCase):
)
def test_env_from_multiple_files(self):
service_dict = config.make_service_dict(
service_dict = make_service_dict(
'foo',
{'env_file': ['one.env', 'two.env']},
'tests/fixtures/env',
@ -316,7 +422,7 @@ class EnvTest(unittest.TestCase):
options = {'env_file': 'nonexistent.env'}
self.assertRaises(
config.ConfigurationError,
lambda: config.make_service_dict('foo', options, 'tests/fixtures/env'),
lambda: make_service_dict('foo', options, 'tests/fixtures/env'),
)
@mock.patch.dict(os.environ)
@ -324,7 +430,7 @@ class EnvTest(unittest.TestCase):
os.environ['FILE_DEF'] = 'E1'
os.environ['FILE_DEF_EMPTY'] = 'E2'
os.environ['ENV_DEF'] = 'E3'
service_dict = config.make_service_dict(
service_dict = make_service_dict(
'foo',
{'env_file': 'resolve.env'},
'tests/fixtures/env',
@ -334,10 +440,33 @@ class EnvTest(unittest.TestCase):
{'FILE_DEF': 'F1', 'FILE_DEF_EMPTY': '', 'ENV_DEF': 'E3', 'NO_DEF': ''},
)
@mock.patch.dict(os.environ)
def test_resolve_path(self):
os.environ['HOSTENV'] = '/tmp'
os.environ['CONTAINERENV'] = '/host/tmp'
service_dict = make_service_dict(
'foo',
{'volumes': ['$HOSTENV:$CONTAINERENV']},
working_dir="tests/fixtures/env"
)
self.assertEqual(set(service_dict['volumes']), set(['/tmp:/host/tmp']))
service_dict = make_service_dict(
'foo',
{'volumes': ['/opt${HOSTENV}:/opt${CONTAINERENV}']},
working_dir="tests/fixtures/env"
)
self.assertEqual(set(service_dict['volumes']), set(['/opt/tmp:/opt/host/tmp']))
def load_from_filename(filename):
return config.load(config.find('.', filename))
class ExtendsTest(unittest.TestCase):
def test_extends(self):
service_dicts = config.load('tests/fixtures/extends/docker-compose.yml')
service_dicts = load_from_filename('tests/fixtures/extends/docker-compose.yml')
service_dicts = sorted(
service_dicts,
@ -364,7 +493,7 @@ class ExtendsTest(unittest.TestCase):
])
def test_nested(self):
service_dicts = config.load('tests/fixtures/extends/nested.yml')
service_dicts = load_from_filename('tests/fixtures/extends/nested.yml')
self.assertEqual(service_dicts, [
{
@ -378,9 +507,36 @@ class ExtendsTest(unittest.TestCase):
},
])
def test_self_referencing_file(self):
"""
We specify a 'file' key that is the filename we're already in.
"""
service_dicts = load_from_filename('tests/fixtures/extends/specify-file-as-self.yml')
self.assertEqual(service_dicts, [
{
'environment':
{
'YEP': '1', 'BAR': '1', 'BAZ': '3'
},
'image': 'busybox',
'name': 'myweb'
},
{
'environment':
{'YEP': '1'},
'name': 'otherweb'
},
{
'environment':
{'YEP': '1', 'BAZ': '3'},
'image': 'busybox',
'name': 'web'
}
])
def test_circular(self):
try:
config.load('tests/fixtures/extends/circle-1.yml')
load_from_filename('tests/fixtures/extends/circle-1.yml')
raise Exception("Expected config.CircularReference to be raised")
except config.CircularReference as e:
self.assertEqual(
@ -392,29 +548,81 @@ class ExtendsTest(unittest.TestCase):
],
)
def test_extends_validation(self):
def test_extends_validation_empty_dictionary(self):
dictionary = {'extends': None}
def load_config():
return config.make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
self.assertRaisesRegexp(config.ConfigurationError, 'dictionary', load_config)
dictionary['extends'] = {}
self.assertRaises(config.ConfigurationError, load_config)
dictionary['extends']['file'] = 'common.yml'
def test_extends_validation_missing_service_key(self):
dictionary = {'extends': {'file': 'common.yml'}}
def load_config():
return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
self.assertRaisesRegexp(config.ConfigurationError, 'service', load_config)
dictionary['extends']['service'] = 'web'
def test_extends_validation_invalid_key(self):
dictionary = {
'extends':
{
'service': 'web', 'file': 'common.yml', 'what': 'is this'
}
}
def load_config():
return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
self.assertRaisesRegexp(config.ConfigurationError, 'what', load_config)
def test_extends_validation_no_file_key_no_filename_set(self):
dictionary = {'extends': {'service': 'web'}}
def load_config():
return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
self.assertRaisesRegexp(config.ConfigurationError, 'file', load_config)
def test_extends_validation_valid_config(self):
dictionary = {'extends': {'service': 'web', 'file': 'common.yml'}}
def load_config():
return make_service_dict('myweb', dictionary, working_dir='tests/fixtures/extends')
self.assertIsInstance(load_config(), dict)
dictionary['extends']['what'] = 'is this'
self.assertRaisesRegexp(config.ConfigurationError, 'what', load_config)
def test_extends_file_defaults_to_self(self):
"""
Test not specifying a file in our extends options that the
config is valid and correctly extends from itself.
"""
service_dicts = load_from_filename('tests/fixtures/extends/no-file-specified.yml')
self.assertEqual(service_dicts, [
{
'name': 'myweb',
'image': 'busybox',
'environment': {
"BAR": "1",
"BAZ": "3",
}
},
{
'name': 'web',
'image': 'busybox',
'environment': {
"BAZ": "3",
}
}
])
def test_blacklisted_options(self):
def load_config():
return config.make_service_dict('myweb', {
return make_service_dict('myweb', {
'extends': {
'file': 'whatever',
'service': 'web',
@ -445,7 +653,7 @@ class ExtendsTest(unittest.TestCase):
print load_config()
def test_volume_path(self):
dicts = config.load('tests/fixtures/volume-path/docker-compose.yml')
dicts = load_from_filename('tests/fixtures/volume-path/docker-compose.yml')
paths = [
'%s:/foo' % os.path.abspath('tests/fixtures/volume-path/common/foo'),
@ -455,7 +663,7 @@ class ExtendsTest(unittest.TestCase):
self.assertEqual(set(dicts[0]['volumes']), set(paths))
def test_parent_build_path_dne(self):
child = config.load('tests/fixtures/extends/nonexistent-path-child.yml')
child = load_from_filename('tests/fixtures/extends/nonexistent-path-child.yml')
self.assertEqual(child, [
{
@ -475,18 +683,20 @@ class BuildPathTest(unittest.TestCase):
self.abs_context_path = os.path.join(os.getcwd(), 'tests/fixtures/build-ctx')
def test_nonexistent_path(self):
options = {'build': 'nonexistent.path'}
self.assertRaises(
config.ConfigurationError,
lambda: config.from_dictionary({
'foo': options,
'working_dir': 'tests/fixtures/build-path'
})
)
with self.assertRaises(config.ConfigurationError):
config.load(
config.ConfigDetails(
{
'foo': {'build': 'nonexistent.path'},
},
'working_dir',
'filename.yml'
)
)
def test_relative_path(self):
relative_build_path = '../build-ctx/'
service_dict = config.make_service_dict(
service_dict = make_service_dict(
'relpath',
{'build': relative_build_path},
working_dir='tests/fixtures/build-path'
@ -494,7 +704,7 @@ class BuildPathTest(unittest.TestCase):
self.assertEquals(service_dict['build'], self.abs_context_path)
def test_absolute_path(self):
service_dict = config.make_service_dict(
service_dict = make_service_dict(
'abspath',
{'build': self.abs_context_path},
working_dir='tests/fixtures/build-path'
@ -502,5 +712,56 @@ class BuildPathTest(unittest.TestCase):
self.assertEquals(service_dict['build'], self.abs_context_path)
def test_from_file(self):
service_dict = config.load('tests/fixtures/build-path/docker-compose.yml')
service_dict = load_from_filename('tests/fixtures/build-path/docker-compose.yml')
self.assertEquals(service_dict, [{'name': 'foo', 'build': self.abs_context_path}])
class GetConfigPathTestCase(unittest.TestCase):
files = [
'docker-compose.yml',
'docker-compose.yaml',
'fig.yml',
'fig.yaml',
]
def test_get_config_path_default_file_in_basedir(self):
files = self.files
self.assertEqual('docker-compose.yml', get_config_filename_for_files(files[0:]))
self.assertEqual('docker-compose.yaml', get_config_filename_for_files(files[1:]))
self.assertEqual('fig.yml', get_config_filename_for_files(files[2:]))
self.assertEqual('fig.yaml', get_config_filename_for_files(files[3:]))
with self.assertRaises(config.ComposeFileNotFound):
get_config_filename_for_files([])
def test_get_config_path_default_file_in_parent_dir(self):
"""Test with files placed in the subdir"""
files = self.files
def get_config_in_subdir(files):
return get_config_filename_for_files(files, subdir=True)
self.assertEqual('docker-compose.yml', get_config_in_subdir(files[0:]))
self.assertEqual('docker-compose.yaml', get_config_in_subdir(files[1:]))
self.assertEqual('fig.yml', get_config_in_subdir(files[2:]))
self.assertEqual('fig.yaml', get_config_in_subdir(files[3:]))
with self.assertRaises(config.ComposeFileNotFound):
get_config_in_subdir([])
def get_config_filename_for_files(filenames, subdir=None):
def make_files(dirname, filenames):
for fname in filenames:
with open(os.path.join(dirname, fname), 'w') as f:
f.write('')
project_dir = tempfile.mkdtemp()
try:
make_files(project_dir, filenames)
if subdir:
base_dir = tempfile.mkdtemp(dir=project_dir)
else:
base_dir = project_dir
return os.path.basename(config.get_config_path(base_dir))
finally:
shutil.rmtree(project_dir)

View File

@ -3,13 +3,16 @@ from .. import unittest
from compose.service import Service
from compose.project import Project
from compose.container import Container
from compose import config
from compose.const import LABEL_SERVICE
import mock
import docker
class ProjectTest(unittest.TestCase):
def setUp(self):
self.mock_client = mock.create_autospec(docker.Client)
def test_from_dict(self):
project = Project.from_dicts('composetest', [
{
@ -51,14 +54,16 @@ class ProjectTest(unittest.TestCase):
self.assertEqual(project.services[2].name, 'web')
def test_from_config(self):
dicts = config.from_dictionary({
'web': {
dicts = [
{
'name': 'web',
'image': 'busybox:latest',
},
'db': {
{
'name': 'db',
'image': 'busybox:latest',
},
})
]
project = Project.from_dicts('composetest', dicts, None)
self.assertEqual(len(project.services), 2)
self.assertEqual(project.get_service('web').name, 'web')
@ -154,21 +159,19 @@ class ProjectTest(unittest.TestCase):
def test_use_volumes_from_container(self):
container_id = 'aabbccddee'
container_dict = dict(Name='aaa', Id=container_id)
mock_client = mock.create_autospec(docker.Client)
mock_client.inspect_container.return_value = container_dict
self.mock_client.inspect_container.return_value = container_dict
project = Project.from_dicts('test', [
{
'name': 'test',
'image': 'busybox:latest',
'volumes_from': ['aaa']
}
], mock_client)
], self.mock_client)
self.assertEqual(project.get_service('test')._get_volumes_from(), [container_id])
def test_use_volumes_from_service_no_container(self):
container_name = 'test_vol_1'
mock_client = mock.create_autospec(docker.Client)
mock_client.containers.return_value = [
self.mock_client.containers.return_value = [
{
"Name": container_name,
"Names": [container_name],
@ -186,7 +189,7 @@ class ProjectTest(unittest.TestCase):
'image': 'busybox:latest',
'volumes_from': ['vol']
}
], mock_client)
], self.mock_client)
self.assertEqual(project.get_service('test')._get_volumes_from(), [container_name])
@mock.patch.object(Service, 'containers')
@ -210,13 +213,12 @@ class ProjectTest(unittest.TestCase):
self.assertEqual(project.get_service('test')._get_volumes_from(), container_ids)
def test_net_unset(self):
mock_client = mock.create_autospec(docker.Client)
project = Project.from_dicts('test', [
{
'name': 'test',
'image': 'busybox:latest',
}
], mock_client)
], self.mock_client)
service = project.get_service('test')
self.assertEqual(service._get_net(), None)
self.assertNotIn('NetworkMode', service._get_container_host_config({}))
@ -224,22 +226,20 @@ class ProjectTest(unittest.TestCase):
def test_use_net_from_container(self):
container_id = 'aabbccddee'
container_dict = dict(Name='aaa', Id=container_id)
mock_client = mock.create_autospec(docker.Client)
mock_client.inspect_container.return_value = container_dict
self.mock_client.inspect_container.return_value = container_dict
project = Project.from_dicts('test', [
{
'name': 'test',
'image': 'busybox:latest',
'net': 'container:aaa'
}
], mock_client)
], self.mock_client)
service = project.get_service('test')
self.assertEqual(service._get_net(), 'container:' + container_id)
def test_use_net_from_service(self):
container_name = 'test_aaa_1'
mock_client = mock.create_autospec(docker.Client)
mock_client.containers.return_value = [
self.mock_client.containers.return_value = [
{
"Name": container_name,
"Names": [container_name],
@ -257,7 +257,31 @@ class ProjectTest(unittest.TestCase):
'image': 'busybox:latest',
'net': 'container:aaa'
}
], mock_client)
], self.mock_client)
service = project.get_service('test')
self.assertEqual(service._get_net(), 'container:' + container_name)
def test_container_without_name(self):
self.mock_client.containers.return_value = [
{'Image': 'busybox:latest', 'Id': '1', 'Name': '1'},
{'Image': 'busybox:latest', 'Id': '2', 'Name': None},
{'Image': 'busybox:latest', 'Id': '3'},
]
self.mock_client.inspect_container.return_value = {
'Id': '1',
'Config': {
'Labels': {
LABEL_SERVICE: 'web',
},
},
}
project = Project.from_dicts(
'test',
[{
'name': 'web',
'image': 'busybox:latest',
}],
self.mock_client,
)
self.assertEqual([c.id for c in project.containers()], ['1'])

View File

@ -5,6 +5,7 @@ from .. import unittest
import mock
import docker
from docker.utils import LogConfig
from compose.service import Service
from compose.container import Container
@ -29,24 +30,29 @@ class ServiceTest(unittest.TestCase):
self.mock_client = mock.create_autospec(docker.Client)
def test_name_validations(self):
self.assertRaises(ConfigError, lambda: Service(name=''))
self.assertRaises(ConfigError, lambda: Service(name='', image='foo'))
self.assertRaises(ConfigError, lambda: Service(name=' '))
self.assertRaises(ConfigError, lambda: Service(name='/'))
self.assertRaises(ConfigError, lambda: Service(name='!'))
self.assertRaises(ConfigError, lambda: Service(name='\xe2'))
self.assertRaises(ConfigError, lambda: Service(name='_'))
self.assertRaises(ConfigError, lambda: Service(name='____'))
self.assertRaises(ConfigError, lambda: Service(name='foo_bar'))
self.assertRaises(ConfigError, lambda: Service(name='__foo_bar__'))
self.assertRaises(ConfigError, lambda: Service(name=' ', image='foo'))
self.assertRaises(ConfigError, lambda: Service(name='/', image='foo'))
self.assertRaises(ConfigError, lambda: Service(name='!', image='foo'))
self.assertRaises(ConfigError, lambda: Service(name='\xe2', image='foo'))
Service('a', image='foo')
Service('foo', image='foo')
Service('foo-bar', image='foo')
Service('foo.bar', image='foo')
Service('foo_bar', image='foo')
Service('_', image='foo')
Service('___', image='foo')
Service('-', image='foo')
Service('--', image='foo')
Service('.__.', image='foo')
def test_project_validation(self):
self.assertRaises(ConfigError, lambda: Service('bar'))
self.assertRaises(ConfigError, lambda: Service(name='foo', project='_', image='foo'))
Service(name='foo', project='bar', image='foo')
self.assertRaises(ConfigError, lambda: Service(name='foo', project='>', image='foo'))
Service(name='foo', project='bar.bar__', image='foo')
def test_containers(self):
service = Service('db', self.mock_client, 'myproject', image='foo')
@ -70,6 +76,18 @@ class ServiceTest(unittest.TestCase):
all=False,
filters={'label': expected_labels})
def test_container_without_name(self):
self.mock_client.containers.return_value = [
{'Image': 'foo', 'Id': '1', 'Name': '1'},
{'Image': 'foo', 'Id': '2', 'Name': None},
{'Image': 'foo', 'Id': '3'},
]
service = Service('db', self.mock_client, 'myproject', image='foo')
self.assertEqual([c.id for c in service.containers()], ['1'])
self.assertEqual(service._next_container_number(), 2)
self.assertEqual(service.get_container(1).id, '1')
def test_get_volumes_from_container(self):
container_id = 'aabbccddee'
service = Service(
@ -151,6 +169,23 @@ class ServiceTest(unittest.TestCase):
self.assertEqual(opts['hostname'], 'name', 'hostname')
self.assertFalse('domainname' in opts, 'domainname')
def test_memory_swap_limit(self):
service = Service(name='foo', image='foo', hostname='name', client=self.mock_client, mem_limit=1000000000, memswap_limit=2000000000)
self.mock_client.containers.return_value = []
opts = service._get_container_create_options({'some': 'overrides'}, 1)
self.assertEqual(opts['host_config']['MemorySwap'], 2000000000)
self.assertEqual(opts['host_config']['Memory'], 1000000000)
def test_log_opt(self):
log_opt = {'address': 'tcp://192.168.0.42:123'}
service = Service(name='foo', image='foo', hostname='name', client=self.mock_client, log_driver='syslog', log_opt=log_opt)
self.mock_client.containers.return_value = []
opts = service._get_container_create_options({'some': 'overrides'}, 1)
self.assertIsInstance(opts['host_config']['LogConfig'], LogConfig)
self.assertEqual(opts['host_config']['LogConfig'].type, 'syslog')
self.assertEqual(opts['host_config']['LogConfig'].config, log_opt)
def test_split_domainname_fqdn(self):
service = Service(
'foo',
@ -206,11 +241,10 @@ class ServiceTest(unittest.TestCase):
@mock.patch('compose.service.log', autospec=True)
def test_pull_image(self, mock_log):
service = Service('foo', client=self.mock_client, image='someimage:sometag')
service.pull(insecure_registry=True)
service.pull()
self.mock_client.pull.assert_called_once_with(
'someimage',
tag='sometag',
insecure_registry=True,
stream=True)
mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
@ -220,26 +254,8 @@ class ServiceTest(unittest.TestCase):
self.mock_client.pull.assert_called_once_with(
'ababab',
tag='latest',
insecure_registry=False,
stream=True)
def test_create_container_from_insecure_registry(self):
service = Service('foo', client=self.mock_client, image='someimage:sometag')
images = []
def pull(repo, tag=None, insecure_registry=False, **kwargs):
self.assertEqual('someimage', repo)
self.assertEqual('sometag', tag)
self.assertTrue(insecure_registry)
images.append({'Id': 'abc123'})
return []
service.image = lambda *args, **kwargs: mock_get_image(images)
self.mock_client.pull = pull
service.create_container(insecure_registry=True)
self.assertEqual(1, len(images))
@mock.patch('compose.service.Container', autospec=True)
def test_recreate_container(self, _):
mock_container = mock.create_autospec(Container)
@ -349,14 +365,13 @@ class ServiceVolumesTest(unittest.TestCase):
spec = parse_volume_spec('external:interval:ro')
self.assertEqual(spec, ('external', 'interval', 'ro'))
spec = parse_volume_spec('external:interval:z')
self.assertEqual(spec, ('external', 'interval', 'z'))
def test_parse_volume_spec_too_many_parts(self):
with self.assertRaises(ConfigError):
parse_volume_spec('one:two:three:four')
def test_parse_volume_bad_mode(self):
with self.assertRaises(ConfigError):
parse_volume_spec('one:two:notrw')
def test_build_volume_binding(self):
binding = build_volume_binding(parse_volume_spec('/outside:/inside'))
self.assertEqual(binding, ('/inside', '/outside:/inside:rw'))
@ -485,3 +500,26 @@ class ServiceVolumesTest(unittest.TestCase):
create_options['host_config']['Binds'],
['/mnt/sda1/host/path:/data:rw'],
)
def test_create_with_special_volume_mode(self):
self.mock_client.inspect_image.return_value = {'Id': 'imageid'}
create_calls = []
def create_container(*args, **kwargs):
create_calls.append((args, kwargs))
return {'Id': 'containerid'}
self.mock_client.create_container = create_container
volumes = ['/tmp:/foo:z']
Service(
'web',
client=self.mock_client,
image='busybox',
volumes=volumes,
).create_container()
self.assertEqual(len(create_calls), 1)
self.assertEqual(create_calls[0][1]['host_config']['Binds'], volumes)