Compare commits
93 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b5d5fcd58 | ||
|
|
d01f712376 | ||
|
|
ac75d35927 | ||
|
|
b24ca75914 | ||
|
|
2b75741e5a | ||
|
|
7ff8c2b224 | ||
|
|
db31adc208 | ||
|
|
805f6a7683 | ||
|
|
d92f323e6d | ||
|
|
cf2dbf55b8 | ||
|
|
8d4c724c2d | ||
|
|
9cb2770da4 | ||
|
|
6a23491fa9 | ||
|
|
294b9742be | ||
|
|
a9b1f15f92 | ||
|
|
28139ab90d | ||
|
|
d0792b49fa | ||
|
|
5548aa5c79 | ||
|
|
16440ff055 | ||
|
|
7850d6de45 | ||
|
|
74b4fb89bb | ||
|
|
22ccf35fa1 | ||
|
|
7ad1fe24bd | ||
|
|
450ba978c1 | ||
|
|
3d6946417d | ||
|
|
31cf63b374 | ||
|
|
5c853c4a2c | ||
|
|
ad922cd7a1 | ||
|
|
49bafdc4cd | ||
|
|
989b2491b9 | ||
|
|
ca2ce3a034 | ||
|
|
dfe9dccab8 | ||
|
|
d456c3909d | ||
|
|
29ceef6d93 | ||
|
|
8cff440800 | ||
|
|
e5f6ae767d | ||
|
|
cd44179305 | ||
|
|
c3c5b354b8 | ||
|
|
95cf195dbd | ||
|
|
a80afd67ab | ||
|
|
4bc4d273ac | ||
|
|
4911c77134 | ||
|
|
c1b9a76a54 | ||
|
|
c31e25af72 | ||
|
|
c8295d36cc | ||
|
|
b12c29479e | ||
|
|
cd47829f3d | ||
|
|
4d4ef4e0b3 | ||
|
|
882ef2ccd8 | ||
|
|
d6cd76c3c1 | ||
|
|
bd0be2cdc7 | ||
|
|
a8d7ebd987 | ||
|
|
00f61196a4 | ||
|
|
c21d6706b6 | ||
|
|
c3c5d91c47 | ||
|
|
7fa4cd1214 | ||
|
|
f353d9fbc0 | ||
|
|
09018855ce | ||
|
|
719954b02f | ||
|
|
67bc3fabe4 | ||
|
|
e724a346c7 | ||
|
|
87b4545b44 | ||
|
|
58a7844129 | ||
|
|
4353f7b9f9 | ||
|
|
8f8693e13e | ||
|
|
363a6563c7 | ||
|
|
59d6af73fa | ||
|
|
cd7f67018e | ||
|
|
b7e8770c4f | ||
|
|
ad4cc5d6df | ||
|
|
ca14ed68f7 | ||
|
|
71514cb380 | ||
|
|
8212f1bd45 | ||
|
|
dca3bbdea3 | ||
|
|
8ed7dfef6f | ||
|
|
631f5be02f | ||
|
|
4f4ea2a402 | ||
|
|
5a5bffebd1 | ||
|
|
8749bc0844 | ||
|
|
f3d0c63db2 | ||
|
|
93a846db31 | ||
|
|
686c25d50f | ||
|
|
ef6555f084 | ||
|
|
3c6652c101 | ||
|
|
43af1684c1 | ||
|
|
ed549155b3 | ||
|
|
39ae91c81c | ||
|
|
b6acb3cd8c | ||
|
|
a467a8a094 | ||
|
|
78227c3c06 | ||
|
|
e4e802d1f8 | ||
|
|
b24a60ba9f | ||
|
|
461b600068 |
41 changed files with 894 additions and 193 deletions
62
CHANGES.md
62
CHANGES.md
|
|
@ -1,6 +1,68 @@
|
|||
Change log
|
||||
==========
|
||||
|
||||
1.4.2 (2015-09-22)
|
||||
------------------
|
||||
|
||||
Fixes a regression in the 1.4.1 release that would cause `docker-compose up`
|
||||
without the `-d` option to exit immediately.
|
||||
|
||||
|
||||
1.4.1 (2015-09-10)
|
||||
------------------
|
||||
|
||||
The following bugs have been fixed:
|
||||
|
||||
- Some configuration changes (notably changes to `links`, `volumes_from`, and
|
||||
`net`) were not properly triggering a container recreate as part of
|
||||
`docker-compose up`.
|
||||
- `docker-compose up <service>` was showing logs for all services instead of
|
||||
just the specified services.
|
||||
- Containers with custom container names were showing up in logs as
|
||||
`service_number` instead of their custom container name.
|
||||
- When scaling a service sometimes containers would be recreated even when
|
||||
the configuration had not changed.
|
||||
|
||||
|
||||
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)
|
||||
------------------
|
||||
|
||||
|
|
|
|||
10
Dockerfile
10
Dockerfile
|
|
@ -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.2 1.7.1
|
||||
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.2 -o /usr/local/bin/docker-1.6.2; \
|
||||
chmod +x /usr/local/bin/docker-1.6.2; \
|
||||
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
|
||||
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.2 /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/
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
__version__ = '1.4.0dev'
|
||||
__version__ = '1.4.2'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ class TopLevelCommand(Command):
|
|||
log.warn(INSECURE_SSL_WARNING)
|
||||
|
||||
if not options['--no-deps']:
|
||||
deps = service.get_linked_names()
|
||||
deps = service.get_linked_service_names()
|
||||
|
||||
if len(deps) > 0:
|
||||
project.up(
|
||||
|
|
@ -496,19 +496,8 @@ class TopLevelCommand(Command):
|
|||
)
|
||||
|
||||
if not detached:
|
||||
print("Attaching to", list_containers(to_attach))
|
||||
log_printer = LogPrinter(to_attach, attach_params={"logs": True}, monochrome=monochrome)
|
||||
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
def handler(signal, frame):
|
||||
project.kill(service_names=service_names)
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||
project.stop(service_names=service_names, timeout=timeout)
|
||||
log_printer = build_log_printer(to_attach, service_names, monochrome)
|
||||
attach_to_logs(project, log_printer, service_names, timeout)
|
||||
|
||||
def migrate_to_labels(self, project, _options):
|
||||
"""
|
||||
|
|
@ -551,5 +540,29 @@ class TopLevelCommand(Command):
|
|||
print(get_version_info('full'))
|
||||
|
||||
|
||||
def build_log_printer(containers, service_names, monochrome):
|
||||
if service_names:
|
||||
containers = [c for c in containers if c.service in service_names]
|
||||
|
||||
return LogPrinter(
|
||||
containers,
|
||||
attach_params={"logs": True},
|
||||
monochrome=monochrome)
|
||||
|
||||
|
||||
def attach_to_logs(project, log_printer, service_names, timeout):
|
||||
print("Attaching to", list_containers(log_printer.containers))
|
||||
try:
|
||||
log_printer.run()
|
||||
finally:
|
||||
def handler(signal, frame):
|
||||
project.kill(service_names=service_names)
|
||||
sys.exit(0)
|
||||
signal.signal(signal.SIGINT, handler)
|
||||
|
||||
print("Gracefully stopping... (press Ctrl+C again to force)")
|
||||
project.stop(service_names=service_names, timeout=timeout)
|
||||
|
||||
|
||||
def list_containers(containers):
|
||||
return ", ".join(c.name for c in containers)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ from compose.cli.utils import find_candidates_in_parent_dirs
|
|||
DOCKER_CONFIG_KEYS = [
|
||||
'cap_add',
|
||||
'cap_drop',
|
||||
'command',
|
||||
'cpu_shares',
|
||||
'cpuset',
|
||||
'command',
|
||||
'detach',
|
||||
'devices',
|
||||
'dns',
|
||||
|
|
@ -28,12 +28,12 @@ DOCKER_CONFIG_KEYS = [
|
|||
'image',
|
||||
'labels',
|
||||
'links',
|
||||
'log_driver',
|
||||
'log_opt',
|
||||
'mac_address',
|
||||
'mem_limit',
|
||||
'memswap_limit',
|
||||
'net',
|
||||
'log_driver',
|
||||
'log_opt',
|
||||
'pid',
|
||||
'ports',
|
||||
'privileged',
|
||||
|
|
@ -43,6 +43,7 @@ DOCKER_CONFIG_KEYS = [
|
|||
'stdin_open',
|
||||
'tty',
|
||||
'user',
|
||||
'volume_driver',
|
||||
'volumes',
|
||||
'volumes_from',
|
||||
'working_dir',
|
||||
|
|
@ -82,6 +83,13 @@ SUPPORTED_FILENAMES = [
|
|||
]
|
||||
|
||||
|
||||
PATH_START_CHARS = [
|
||||
'/',
|
||||
'.',
|
||||
'~',
|
||||
]
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
|
@ -251,8 +259,8 @@ def process_container_options(service_dict, working_dir=None):
|
|||
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:
|
||||
service_dict['volumes'] = resolve_volume_paths(service_dict['volumes'], working_dir=working_dir)
|
||||
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)
|
||||
|
|
@ -374,7 +382,7 @@ def parse_environment(environment):
|
|||
return dict(split_env(e) for e in environment)
|
||||
|
||||
if isinstance(environment, dict):
|
||||
return environment
|
||||
return dict(environment)
|
||||
|
||||
raise ConfigurationError(
|
||||
"environment \"%s\" must be a list or mapping," %
|
||||
|
|
@ -413,18 +421,33 @@ def env_vars_from_file(filename):
|
|||
return env
|
||||
|
||||
|
||||
def resolve_volume_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_volume_paths()")
|
||||
|
||||
return [resolve_volume_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_volume_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(os.path.expandvars(host_path))
|
||||
|
||||
if not any(host_path.startswith(c) for c in PATH_START_CHARS):
|
||||
log.warn(
|
||||
'Warning: the mapping "{0}:{1}" in the volumes config for '
|
||||
'service "{2}" 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}:{1}"'
|
||||
.format(host_path, container_path, service_name)
|
||||
)
|
||||
|
||||
return "%s:%s" % (expand_path(working_dir, host_path), container_path)
|
||||
else:
|
||||
return container_path
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -58,9 +62,13 @@ class Container(object):
|
|||
def name(self):
|
||||
return self.dictionary['Name'][1:]
|
||||
|
||||
@property
|
||||
def service(self):
|
||||
return self.labels.get(LABEL_SERVICE)
|
||||
|
||||
@property
|
||||
def name_without_project(self):
|
||||
return '{0}_{1}'.format(self.labels.get(LABEL_SERVICE), self.number)
|
||||
return '{0}_{1}'.format(self.service, self.number)
|
||||
|
||||
@property
|
||||
def number(self):
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ from .config import get_service_name_from_net, ConfigurationError
|
|||
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 ContainerNet
|
||||
from .service import Net
|
||||
from .service import Service
|
||||
from .service import ServiceNet
|
||||
from .utils import parallel_execute
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -81,8 +84,14 @@ class Project(object):
|
|||
volumes_from = project.get_volumes_from(service_dict)
|
||||
net = project.get_net(service_dict)
|
||||
|
||||
project.services.append(Service(client=client, project=name, links=links, net=net,
|
||||
volumes_from=volumes_from, **service_dict))
|
||||
project.services.append(
|
||||
Service(
|
||||
client=client,
|
||||
project=name,
|
||||
links=links,
|
||||
net=net,
|
||||
volumes_from=volumes_from,
|
||||
**service_dict))
|
||||
return project
|
||||
|
||||
@property
|
||||
|
|
@ -172,26 +181,26 @@ class Project(object):
|
|||
return volumes_from
|
||||
|
||||
def get_net(self, service_dict):
|
||||
if 'net' in service_dict:
|
||||
net_name = get_service_name_from_net(service_dict.get('net'))
|
||||
net = service_dict.pop('net', None)
|
||||
if not net:
|
||||
return Net(None)
|
||||
|
||||
if net_name:
|
||||
try:
|
||||
net = self.get_service(net_name)
|
||||
except NoSuchService:
|
||||
try:
|
||||
net = Container.from_id(self.client, net_name)
|
||||
except APIError:
|
||||
raise ConfigurationError('Service "%s" is trying to use the network of "%s", which is not the name of a service or container.' % (service_dict['name'], net_name))
|
||||
else:
|
||||
net = service_dict['net']
|
||||
net_name = get_service_name_from_net(net)
|
||||
if not net_name:
|
||||
return Net(net)
|
||||
|
||||
del service_dict['net']
|
||||
|
||||
else:
|
||||
net = None
|
||||
|
||||
return net
|
||||
try:
|
||||
return ServiceNet(self.get_service(net_name))
|
||||
except NoSuchService:
|
||||
pass
|
||||
try:
|
||||
return ContainerNet(Container.from_id(self.client, net_name))
|
||||
except APIError:
|
||||
raise ConfigurationError(
|
||||
'Service "%s" is trying to use the network of "%s", '
|
||||
'which is not the name of a service or container.' % (
|
||||
service_dict['name'],
|
||||
net_name))
|
||||
|
||||
def start(self, service_names=None, **options):
|
||||
for service in self.get_services(service_names):
|
||||
|
|
@ -310,11 +319,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -41,6 +42,8 @@ DOCKER_START_KEYS = [
|
|||
'net',
|
||||
'log_driver',
|
||||
'log_opt',
|
||||
'mem_limit',
|
||||
'memswap_limit',
|
||||
'pid',
|
||||
'privileged',
|
||||
'restart',
|
||||
|
|
@ -80,7 +83,16 @@ ConvergencePlan = namedtuple('ConvergencePlan', 'action containers')
|
|||
|
||||
|
||||
class Service(object):
|
||||
def __init__(self, name, client=None, project='default', links=None, external_links=None, volumes_from=None, net=None, **options):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
client=None,
|
||||
project='default',
|
||||
links=None,
|
||||
volumes_from=None,
|
||||
net=None,
|
||||
**options
|
||||
):
|
||||
if not re.match('^%s+$' % VALID_NAME_CHARS, name):
|
||||
raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS))
|
||||
if not re.match('^%s+$' % VALID_NAME_CHARS, project):
|
||||
|
|
@ -94,17 +106,16 @@ class Service(object):
|
|||
self.client = client
|
||||
self.project = project
|
||||
self.links = links or []
|
||||
self.external_links = external_links or []
|
||||
self.volumes_from = volumes_from or []
|
||||
self.net = net or None
|
||||
self.net = net or Net(None)
|
||||
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(
|
||||
|
|
@ -466,26 +477,26 @@ class Service(object):
|
|||
return {
|
||||
'options': self.options,
|
||||
'image_id': self.image()['Id'],
|
||||
'links': self.get_link_names(),
|
||||
'net': self.net.id,
|
||||
'volumes_from': self.get_volumes_from_names(),
|
||||
}
|
||||
|
||||
def get_dependency_names(self):
|
||||
net_name = self.get_net_name()
|
||||
return (self.get_linked_names() +
|
||||
net_name = self.net.service_name
|
||||
return (self.get_linked_service_names() +
|
||||
self.get_volumes_from_names() +
|
||||
([net_name] if net_name else []))
|
||||
|
||||
def get_linked_names(self):
|
||||
return [s.name for (s, _) in self.links]
|
||||
def get_linked_service_names(self):
|
||||
return [service.name for (service, _) in self.links]
|
||||
|
||||
def get_link_names(self):
|
||||
return [(service.name, alias) for service, alias in self.links]
|
||||
|
||||
def get_volumes_from_names(self):
|
||||
return [s.name for s in self.volumes_from if isinstance(s, Service)]
|
||||
|
||||
def get_net_name(self):
|
||||
if isinstance(self.net, Service):
|
||||
return self.net.name
|
||||
else:
|
||||
return
|
||||
|
||||
def get_container_name(self, number, one_off=False):
|
||||
# TODO: Implement issue #652 here
|
||||
return build_container_name(self.project, self.name, number, one_off)
|
||||
|
|
@ -493,12 +504,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):
|
||||
|
|
@ -513,7 +525,7 @@ class Service(object):
|
|||
links.append((container.name, self.name))
|
||||
links.append((container.name, container.name))
|
||||
links.append((container.name, container.name_without_project))
|
||||
for external_link in self.external_links:
|
||||
for external_link in self.options.get('external_links') or []:
|
||||
if ':' not in external_link:
|
||||
link_name = external_link
|
||||
else:
|
||||
|
|
@ -536,32 +548,12 @@ class Service(object):
|
|||
|
||||
return volumes_from
|
||||
|
||||
def _get_net(self):
|
||||
if not self.net:
|
||||
return None
|
||||
|
||||
if isinstance(self.net, Service):
|
||||
containers = self.net.containers()
|
||||
if len(containers) > 0:
|
||||
net = 'container:' + containers[0].id
|
||||
else:
|
||||
log.warning("Warning: Service %s is trying to use reuse the network stack "
|
||||
"of another service that is not running." % (self.net.name))
|
||||
net = None
|
||||
elif isinstance(self.net, Container):
|
||||
net = 'container:' + self.net.id
|
||||
else:
|
||||
net = self.net
|
||||
|
||||
return net
|
||||
|
||||
def _get_container_create_options(
|
||||
self,
|
||||
override_options,
|
||||
number,
|
||||
one_off=False,
|
||||
previous_container=None):
|
||||
|
||||
add_config_hash = (not one_off and not override_options)
|
||||
|
||||
container_options = dict(
|
||||
|
|
@ -574,13 +566,6 @@ class Service(object):
|
|||
else:
|
||||
container_options['name'] = self.get_container_name(number, one_off)
|
||||
|
||||
if add_config_hash:
|
||||
config_hash = self.config_hash()
|
||||
if 'labels' not in container_options:
|
||||
container_options['labels'] = {}
|
||||
container_options['labels'][LABEL_CONFIG_HASH] = config_hash
|
||||
log.debug("Added config hash: %s" % config_hash)
|
||||
|
||||
if 'detach' not in container_options:
|
||||
container_options['detach'] = True
|
||||
|
||||
|
|
@ -628,7 +613,8 @@ class Service(object):
|
|||
container_options['labels'] = build_container_labels(
|
||||
container_options.get('labels', {}),
|
||||
self.labels(one_off=one_off),
|
||||
number)
|
||||
number,
|
||||
self.config_hash() if add_config_hash else None)
|
||||
|
||||
# Delete options which are only used when starting
|
||||
for key in DOCKER_START_KEYS:
|
||||
|
|
@ -675,13 +661,15 @@ class Service(object):
|
|||
binds=options.get('binds'),
|
||||
volumes_from=self._get_volumes_from(),
|
||||
privileged=privileged,
|
||||
network_mode=self._get_net(),
|
||||
network_mode=self.net.mode,
|
||||
devices=devices,
|
||||
dns=dns,
|
||||
dns_search=dns_search,
|
||||
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,
|
||||
|
|
@ -768,6 +756,61 @@ class Service(object):
|
|||
stream_output(output, sys.stdout)
|
||||
|
||||
|
||||
class Net(object):
|
||||
"""A `standard` network mode (ex: host, bridge)"""
|
||||
|
||||
service_name = None
|
||||
|
||||
def __init__(self, net):
|
||||
self.net = net
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.net
|
||||
|
||||
mode = id
|
||||
|
||||
|
||||
class ContainerNet(object):
|
||||
"""A network mode that uses a container's network stack."""
|
||||
|
||||
service_name = None
|
||||
|
||||
def __init__(self, container):
|
||||
self.container = container
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.container.id
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return 'container:' + self.container.id
|
||||
|
||||
|
||||
class ServiceNet(object):
|
||||
"""A network mode that uses a service's network stack."""
|
||||
|
||||
def __init__(self, service):
|
||||
self.service = service
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self.service.name
|
||||
|
||||
service_name = id
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
containers = self.service.containers()
|
||||
if containers:
|
||||
return 'container:' + containers[0].id
|
||||
|
||||
log.warn("Warning: Service %s is trying to use reuse the network stack "
|
||||
"of another service that is not running." % (self.id))
|
||||
return None
|
||||
|
||||
|
||||
# Names
|
||||
|
||||
|
||||
|
|
@ -848,12 +891,15 @@ 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')
|
||||
mode = parts[2] if len(parts) == 3 else 'rw'
|
||||
|
||||
return VolumeSpec(*parts)
|
||||
return VolumeSpec(external, internal, mode)
|
||||
|
||||
|
||||
# Ports
|
||||
|
|
@ -890,11 +936,16 @@ def split_port(port):
|
|||
# Labels
|
||||
|
||||
|
||||
def build_container_labels(label_options, service_labels, number, one_off=False):
|
||||
labels = label_options or {}
|
||||
def build_container_labels(label_options, service_labels, number, config_hash):
|
||||
labels = dict(label_options or {})
|
||||
labels.update(label.split('=', 1) for label in service_labels)
|
||||
labels[LABEL_CONTAINER_NUMBER] = str(number)
|
||||
labels[LABEL_VERSION] = __version__
|
||||
|
||||
if config_hash:
|
||||
log.debug("Added config hash: %s" % config_hash)
|
||||
labels[LABEL_CONFIG_HASH] = config_hash
|
||||
|
||||
return labels
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ def parallel_execute(objects, obj_callable, msg_index, msg):
|
|||
except APIError as e:
|
||||
errors[msg_index] = e.explanation
|
||||
result = "error"
|
||||
except Exception as e:
|
||||
errors[msg_index] = e
|
||||
result = 'unexpected_exception'
|
||||
|
||||
q.put((msg_index, result))
|
||||
|
||||
for an_object in objects:
|
||||
|
|
@ -48,6 +52,9 @@ def parallel_execute(objects, obj_callable, msg_index, msg):
|
|||
while done < total_to_execute:
|
||||
try:
|
||||
msg_index, result = q.get(timeout=1)
|
||||
|
||||
if result == 'unexpected_exception':
|
||||
raise errors[msg_index]
|
||||
if result == 'error':
|
||||
write_out_msg(stream, lines, msg_index, msg, status='error')
|
||||
else:
|
||||
|
|
|
|||
BIN
docker
Executable file
BIN
docker
Executable file
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,48 +12,67 @@ 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 and zsh 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.2
|
||||
|
||||
### 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 ...
|
||||
|
||||
|
||||
## Uninstallation
|
||||
|
|
@ -67,10 +86,13 @@ 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.
|
||||
>**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.
|
||||
|
||||
|
||||
## Compose documentation
|
||||
## Where to go next
|
||||
|
||||
- [User guide](/)
|
||||
- [Get started with Django](django.md)
|
||||
|
|
|
|||
61
docs/pre-process.sh
Executable file
61
docs/pre-process.sh
Executable 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
|
||||
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ title = "build"
|
|||
description = "build"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, cli, build"]
|
||||
[menu.main]
|
||||
identifier="build.compose"
|
||||
parent = "smn_compose_cli"
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ title = "help"
|
|||
description = "help"
|
||||
keywords = ["fig, composition, compose, docker, orchestration, cli, help"]
|
||||
[menu.main]
|
||||
identifier="help.compose"
|
||||
parent = "smn_compose_cli"
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ title = "ps"
|
|||
description = "Lists containers."
|
||||
keywords = ["fig, composition, compose, docker, orchestration, cli, ps"]
|
||||
[menu.main]
|
||||
identifier="ps.compose"
|
||||
parent = "smn_compose_cli"
|
||||
+++
|
||||
<![end-metadata]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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]-->
|
||||
|
|
|
|||
13
docs/yml.md
13
docs/yml.md
|
|
@ -131,9 +131,16 @@ Mount paths as volumes, optionally specifying a path on the host machine
|
|||
|
||||
volumes:
|
||||
- /var/lib/mysql
|
||||
- cache/:/tmp/cache
|
||||
- ./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.
|
||||
|
|
@ -333,7 +340,7 @@ Override the default labeling scheme for each container.
|
|||
- label:user:USER
|
||||
- label:role:ROLE
|
||||
|
||||
### working\_dir, entrypoint, user, hostname, domainname, mac\_address, mem\_limit, memswap\_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.
|
||||
|
|
@ -360,6 +367,8 @@ Each of these is a single value, analogous to its
|
|||
tty: true
|
||||
read_only: true
|
||||
|
||||
volume_driver: mydriver
|
||||
```
|
||||
|
||||
## Compose documentation
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import sys
|
||||
|
||||
import mock # noqa
|
||||
|
||||
if sys.version_info >= (2, 7):
|
||||
import unittest # NOQA
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import sys
|
|||
import os
|
||||
import shlex
|
||||
|
||||
import mock
|
||||
from six import StringIO
|
||||
from mock import patch
|
||||
|
||||
|
|
@ -104,7 +105,7 @@ class CLITestCase(DockerClientTestCase):
|
|||
output = mock_stdout.getvalue()
|
||||
self.assertNotIn(cache_indicator, output)
|
||||
|
||||
def test_up(self):
|
||||
def test_up_detached(self):
|
||||
self.command.dispatch(['up', '-d'], None)
|
||||
service = self.project.get_service('simple')
|
||||
another = self.project.get_service('another')
|
||||
|
|
@ -112,10 +113,28 @@ class CLITestCase(DockerClientTestCase):
|
|||
self.assertEqual(len(another.containers()), 1)
|
||||
|
||||
# Ensure containers don't have stdin and stdout connected in -d mode
|
||||
config = service.containers()[0].inspect()['Config']
|
||||
self.assertFalse(config['AttachStderr'])
|
||||
self.assertFalse(config['AttachStdout'])
|
||||
self.assertFalse(config['AttachStdin'])
|
||||
container, = service.containers()
|
||||
self.assertFalse(container.get('Config.AttachStderr'))
|
||||
self.assertFalse(container.get('Config.AttachStdout'))
|
||||
self.assertFalse(container.get('Config.AttachStdin'))
|
||||
|
||||
def test_up_attached(self):
|
||||
with mock.patch(
|
||||
'compose.cli.main.attach_to_logs',
|
||||
autospec=True
|
||||
) as mock_attach:
|
||||
self.command.dispatch(['up'], None)
|
||||
_, args, kwargs = mock_attach.mock_calls[0]
|
||||
_project, log_printer, _names, _timeout = args
|
||||
|
||||
service = self.project.get_service('simple')
|
||||
another = self.project.get_service('another')
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertEqual(len(another.containers()), 1)
|
||||
self.assertEqual(
|
||||
set(log_printer.containers),
|
||||
set(self.project.containers())
|
||||
)
|
||||
|
||||
def test_up_with_links(self):
|
||||
self.command.base_dir = 'tests/fixtures/links-composefile'
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class ProjectTest(DockerClientTestCase):
|
|||
|
||||
web = project.get_service('web')
|
||||
net = project.get_service('net')
|
||||
self.assertEqual(web._get_net(), 'container:' + net.containers()[0].id)
|
||||
self.assertEqual(web.net.mode, 'container:' + net.containers()[0].id)
|
||||
|
||||
def test_net_from_container(self):
|
||||
net_container = Container.create(
|
||||
|
|
@ -138,7 +138,7 @@ class ProjectTest(DockerClientTestCase):
|
|||
project.up()
|
||||
|
||||
web = project.get_service('web')
|
||||
self.assertEqual(web._get_net(), 'container:' + net_container.id)
|
||||
self.assertEqual(web.net.mode, 'container:' + net_container.id)
|
||||
|
||||
def test_start_stop_kill_remove(self):
|
||||
web = self.create_service('web')
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import tempfile
|
|||
import shutil
|
||||
from six import StringIO, text_type
|
||||
|
||||
from .testcases import DockerClientTestCase
|
||||
from .testcases import pull_busybox
|
||||
from compose import __version__
|
||||
from compose.const import (
|
||||
LABEL_CONTAINER_NUMBER,
|
||||
|
|
@ -17,14 +19,12 @@ from compose.const import (
|
|||
LABEL_SERVICE,
|
||||
LABEL_VERSION,
|
||||
)
|
||||
from compose.service import (
|
||||
ConfigError,
|
||||
ConvergencePlan,
|
||||
Service,
|
||||
build_extra_hosts,
|
||||
)
|
||||
from compose.container import Container
|
||||
from .testcases import DockerClientTestCase
|
||||
from compose.service import build_extra_hosts
|
||||
from compose.service import ConfigError
|
||||
from compose.service import ConvergencePlan
|
||||
from compose.service import Net
|
||||
from compose.service import Service
|
||||
|
||||
|
||||
def create_and_start_container(service, **override_options):
|
||||
|
|
@ -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
|
||||
|
|
@ -221,6 +227,52 @@ 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)))
|
||||
|
||||
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'
|
||||
|
|
@ -526,8 +578,10 @@ class ServiceTest(DockerClientTestCase):
|
|||
})
|
||||
|
||||
def test_create_with_image_id(self):
|
||||
# Image id for the current busybox:latest
|
||||
service = self.create_service('foo', image='8c2e06607696')
|
||||
# Get image id for the current busybox:latest
|
||||
pull_busybox(self.client)
|
||||
image_id = self.client.inspect_image('busybox:latest')['Id'][:12]
|
||||
service = self.create_service('foo', image=image_id)
|
||||
service.create_container()
|
||||
|
||||
def test_scale(self):
|
||||
|
|
@ -620,6 +674,25 @@ class ServiceTest(DockerClientTestCase):
|
|||
self.assertTrue(service.containers()[0].is_running)
|
||||
self.assertIn("ERROR: for 2 Boom", mock_stdout.getvalue())
|
||||
|
||||
@patch('sys.stdout', new_callable=StringIO)
|
||||
def test_scale_with_api_returns_unexpected_exception(self, mock_stdout):
|
||||
"""
|
||||
Test that when scaling if the API returns an error, that is not of type
|
||||
APIError, that error is re-raised.
|
||||
"""
|
||||
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=ValueError("BOOM")):
|
||||
with self.assertRaises(ValueError):
|
||||
service.scale(3)
|
||||
|
||||
self.assertEqual(len(service.containers()), 1)
|
||||
self.assertTrue(service.containers()[0].is_running)
|
||||
|
||||
@patch('compose.service.log')
|
||||
def test_scale_with_desired_number_already_achieved(self, mock_log):
|
||||
"""
|
||||
|
|
@ -672,17 +745,17 @@ class ServiceTest(DockerClientTestCase):
|
|||
self.assertEqual(list(container.inspect()['HostConfig']['PortBindings'].keys()), ['8000/tcp'])
|
||||
|
||||
def test_network_mode_none(self):
|
||||
service = self.create_service('web', net='none')
|
||||
service = self.create_service('web', net=Net('none'))
|
||||
container = create_and_start_container(service)
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'none')
|
||||
|
||||
def test_network_mode_bridged(self):
|
||||
service = self.create_service('web', net='bridge')
|
||||
service = self.create_service('web', net=Net('bridge'))
|
||||
container = create_and_start_container(service)
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'bridge')
|
||||
|
||||
def test_network_mode_host(self):
|
||||
service = self.create_service('web', net='host')
|
||||
service = self.create_service('web', net=Net('host'))
|
||||
container = create_and_start_container(service)
|
||||
self.assertEqual(container.get('HostConfig.NetworkMode'), 'host')
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
"""
|
||||
Integration tests which cover state convergence (aka smart recreate) performed
|
||||
by `docker-compose up`.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
import tempfile
|
||||
import shutil
|
||||
|
|
@ -151,6 +155,24 @@ class ProjectWithDependenciesTest(ProjectTestCase):
|
|||
|
||||
self.assertEqual(new_containers - old_containers, set())
|
||||
|
||||
def test_service_removed_while_down(self):
|
||||
next_cfg = {
|
||||
'web': {
|
||||
'image': 'busybox:latest',
|
||||
'command': 'tail -f /dev/null',
|
||||
},
|
||||
'nginx': self.cfg['nginx'],
|
||||
}
|
||||
|
||||
containers = self.run_up(self.cfg)
|
||||
self.assertEqual(len(containers), 3)
|
||||
|
||||
project = self.make_project(self.cfg)
|
||||
project.stop(timeout=1)
|
||||
|
||||
containers = self.run_up(next_cfg)
|
||||
self.assertEqual(len(containers), 2)
|
||||
|
||||
|
||||
def converge(service,
|
||||
allow_recreate=True,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from docker import errors
|
||||
|
||||
from compose.service import Service
|
||||
from compose.config import ServiceLoader
|
||||
from compose.const import LABEL_PROJECT
|
||||
|
|
@ -8,6 +11,13 @@ from compose.progress_stream import stream_output
|
|||
from .. import unittest
|
||||
|
||||
|
||||
def pull_busybox(client):
|
||||
try:
|
||||
client.inspect_image('busybox:latest')
|
||||
except errors.APIError:
|
||||
client.pull('busybox:latest', stream=False)
|
||||
|
||||
|
||||
class DockerClientTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
|
|
|||
57
tests/unit/cli/main_test.py
Normal file
57
tests/unit/cli/main_test.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from compose import container
|
||||
from compose.cli.log_printer import LogPrinter
|
||||
from compose.cli.main import attach_to_logs
|
||||
from compose.cli.main import build_log_printer
|
||||
from compose.project import Project
|
||||
from tests import mock
|
||||
from tests import unittest
|
||||
|
||||
|
||||
def mock_container(service, number):
|
||||
return mock.create_autospec(
|
||||
container.Container,
|
||||
service=service,
|
||||
number=number,
|
||||
name_without_project='{0}_{1}'.format(service, number))
|
||||
|
||||
|
||||
class CLIMainTestCase(unittest.TestCase):
|
||||
|
||||
def test_build_log_printer(self):
|
||||
containers = [
|
||||
mock_container('web', 1),
|
||||
mock_container('web', 2),
|
||||
mock_container('db', 1),
|
||||
mock_container('other', 1),
|
||||
mock_container('another', 1),
|
||||
]
|
||||
service_names = ['web', 'db']
|
||||
log_printer = build_log_printer(containers, service_names, True)
|
||||
self.assertEqual(log_printer.containers, containers[:3])
|
||||
|
||||
def test_build_log_printer_all_services(self):
|
||||
containers = [
|
||||
mock_container('web', 1),
|
||||
mock_container('db', 1),
|
||||
mock_container('other', 1),
|
||||
]
|
||||
service_names = []
|
||||
log_printer = build_log_printer(containers, service_names, True)
|
||||
self.assertEqual(log_printer.containers, containers)
|
||||
|
||||
def test_attach_to_logs(self):
|
||||
project = mock.create_autospec(Project)
|
||||
log_printer = mock.create_autospec(LogPrinter, containers=[])
|
||||
service_names = ['web', 'db']
|
||||
timeout = 12
|
||||
|
||||
with mock.patch('compose.cli.main.signal', autospec=True) as mock_signal:
|
||||
attach_to_logs(project, log_printer, service_names, timeout)
|
||||
|
||||
mock_signal.signal.assert_called_once_with(mock_signal.SIGINT, mock.ANY)
|
||||
log_printer.run.assert_called_once_with()
|
||||
project.stop.assert_called_once_with(
|
||||
service_names=service_names,
|
||||
timeout=timeout)
|
||||
|
|
@ -9,7 +9,7 @@ from compose import config
|
|||
|
||||
def make_service_dict(name, service_dict, working_dir):
|
||||
"""
|
||||
Test helper function to contruct a ServiceLoader
|
||||
Test helper function to construct a ServiceLoader
|
||||
"""
|
||||
return config.ServiceLoader(working_dir=working_dir).make_service_dict(name, service_dict)
|
||||
|
||||
|
|
@ -72,6 +72,67 @@ class VolumePathTest(unittest.TestCase):
|
|||
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):
|
||||
|
|
@ -451,7 +512,7 @@ class ExtendsTest(unittest.TestCase):
|
|||
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, [
|
||||
self.assertEqual(sorted(service_dicts), sorted([
|
||||
{
|
||||
'environment':
|
||||
{
|
||||
|
|
@ -471,7 +532,7 @@ class ExtendsTest(unittest.TestCase):
|
|||
'image': 'busybox',
|
||||
'name': 'web'
|
||||
}
|
||||
])
|
||||
]))
|
||||
|
||||
def test_circular(self):
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from .. import unittest
|
|||
from compose.service import Service
|
||||
from compose.project import Project
|
||||
from compose.container import Container
|
||||
from compose.const import LABEL_SERVICE
|
||||
|
||||
import mock
|
||||
import docker
|
||||
|
|
@ -219,7 +220,7 @@ class ProjectTest(unittest.TestCase):
|
|||
}
|
||||
], self.mock_client)
|
||||
service = project.get_service('test')
|
||||
self.assertEqual(service._get_net(), None)
|
||||
self.assertEqual(service.net.id, None)
|
||||
self.assertNotIn('NetworkMode', service._get_container_host_config({}))
|
||||
|
||||
def test_use_net_from_container(self):
|
||||
|
|
@ -234,7 +235,7 @@ class ProjectTest(unittest.TestCase):
|
|||
}
|
||||
], self.mock_client)
|
||||
service = project.get_service('test')
|
||||
self.assertEqual(service._get_net(), 'container:' + container_id)
|
||||
self.assertEqual(service.net.mode, 'container:' + container_id)
|
||||
|
||||
def test_use_net_from_service(self):
|
||||
container_name = 'test_aaa_1'
|
||||
|
|
@ -259,4 +260,28 @@ class ProjectTest(unittest.TestCase):
|
|||
], self.mock_client)
|
||||
|
||||
service = project.get_service('test')
|
||||
self.assertEqual(service._get_net(), 'container:' + container_name)
|
||||
self.assertEqual(service.net.mode, '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'])
|
||||
|
|
|
|||
|
|
@ -7,21 +7,25 @@ import mock
|
|||
import docker
|
||||
from docker.utils import LogConfig
|
||||
|
||||
from compose.service import Service
|
||||
from compose.const import LABEL_CONFIG_HASH
|
||||
from compose.const import LABEL_ONE_OFF
|
||||
from compose.const import LABEL_PROJECT
|
||||
from compose.const import LABEL_SERVICE
|
||||
from compose.container import Container
|
||||
from compose.const import LABEL_SERVICE, LABEL_PROJECT, LABEL_ONE_OFF
|
||||
from compose.service import (
|
||||
ConfigError,
|
||||
NeedsBuildError,
|
||||
NoSuchImageError,
|
||||
build_port_bindings,
|
||||
build_volume_binding,
|
||||
get_container_data_volumes,
|
||||
merge_volume_bindings,
|
||||
parse_repository_tag,
|
||||
parse_volume_spec,
|
||||
split_port,
|
||||
)
|
||||
from compose.service import ConfigError
|
||||
from compose.service import ContainerNet
|
||||
from compose.service import NeedsBuildError
|
||||
from compose.service import Net
|
||||
from compose.service import NoSuchImageError
|
||||
from compose.service import Service
|
||||
from compose.service import ServiceNet
|
||||
from compose.service import build_port_bindings
|
||||
from compose.service import build_volume_binding
|
||||
from compose.service import get_container_data_volumes
|
||||
from compose.service import merge_volume_bindings
|
||||
from compose.service import parse_repository_tag
|
||||
from compose.service import parse_volume_spec
|
||||
from compose.service import split_port
|
||||
|
||||
|
||||
class ServiceTest(unittest.TestCase):
|
||||
|
|
@ -76,6 +80,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(
|
||||
|
|
@ -161,8 +177,8 @@ class ServiceTest(unittest.TestCase):
|
|||
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['memswap_limit'], 2000000000)
|
||||
self.assertEqual(opts['mem_limit'], 1000000000)
|
||||
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'}
|
||||
|
|
@ -209,6 +225,40 @@ class ServiceTest(unittest.TestCase):
|
|||
self.assertEqual(opts['hostname'], 'name.sub', 'hostname')
|
||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||
|
||||
def test_get_container_create_options_does_not_mutate_options(self):
|
||||
labels = {'thing': 'real'}
|
||||
environment = {'also': 'real'}
|
||||
service = Service(
|
||||
'foo',
|
||||
image='foo',
|
||||
labels=dict(labels),
|
||||
client=self.mock_client,
|
||||
environment=dict(environment),
|
||||
)
|
||||
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
||||
prev_container = mock.Mock(
|
||||
id='ababab',
|
||||
image_config={'ContainerConfig': {}})
|
||||
|
||||
opts = service._get_container_create_options(
|
||||
{},
|
||||
1,
|
||||
previous_container=prev_container)
|
||||
|
||||
self.assertEqual(service.options['labels'], labels)
|
||||
self.assertEqual(service.options['environment'], environment)
|
||||
|
||||
self.assertEqual(
|
||||
opts['labels'][LABEL_CONFIG_HASH],
|
||||
'3c85881a8903b9d73a06c41860c8be08acce1494ab4cf8408375966dccd714de')
|
||||
self.assertEqual(
|
||||
opts['environment'],
|
||||
{
|
||||
'affinity:container': '=ababab',
|
||||
'also': 'real',
|
||||
}
|
||||
)
|
||||
|
||||
def test_get_container_not_found(self):
|
||||
self.mock_client.containers.return_value = []
|
||||
service = Service('foo', client=self.mock_client, image='foo')
|
||||
|
|
@ -328,6 +378,90 @@ class ServiceTest(unittest.TestCase):
|
|||
self.assertEqual(self.mock_client.build.call_count, 1)
|
||||
self.assertFalse(self.mock_client.build.call_args[1]['pull'])
|
||||
|
||||
def test_config_dict(self):
|
||||
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
||||
service = Service(
|
||||
'foo',
|
||||
image='example.com/foo',
|
||||
client=self.mock_client,
|
||||
net=ServiceNet(Service('other', image='foo')),
|
||||
links=[(Service('one', image='foo'), 'one')],
|
||||
volumes_from=[Service('two', image='foo')])
|
||||
|
||||
config_dict = service.config_dict()
|
||||
expected = {
|
||||
'image_id': 'abcd',
|
||||
'options': {'image': 'example.com/foo'},
|
||||
'links': [('one', 'one')],
|
||||
'net': 'other',
|
||||
'volumes_from': ['two'],
|
||||
}
|
||||
self.assertEqual(config_dict, expected)
|
||||
|
||||
def test_config_dict_with_net_from_container(self):
|
||||
self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
|
||||
container = Container(
|
||||
self.mock_client,
|
||||
{'Id': 'aaabbb', 'Name': '/foo_1'})
|
||||
service = Service(
|
||||
'foo',
|
||||
image='example.com/foo',
|
||||
client=self.mock_client,
|
||||
net=container)
|
||||
|
||||
config_dict = service.config_dict()
|
||||
expected = {
|
||||
'image_id': 'abcd',
|
||||
'options': {'image': 'example.com/foo'},
|
||||
'links': [],
|
||||
'net': 'aaabbb',
|
||||
'volumes_from': [],
|
||||
}
|
||||
self.assertEqual(config_dict, expected)
|
||||
|
||||
|
||||
class NetTestCase(unittest.TestCase):
|
||||
|
||||
def test_net(self):
|
||||
net = Net('host')
|
||||
self.assertEqual(net.id, 'host')
|
||||
self.assertEqual(net.mode, 'host')
|
||||
self.assertEqual(net.service_name, None)
|
||||
|
||||
def test_net_container(self):
|
||||
container_id = 'abcd'
|
||||
net = ContainerNet(Container(None, {'Id': container_id}))
|
||||
self.assertEqual(net.id, container_id)
|
||||
self.assertEqual(net.mode, 'container:' + container_id)
|
||||
self.assertEqual(net.service_name, None)
|
||||
|
||||
def test_net_service(self):
|
||||
container_id = 'bbbb'
|
||||
service_name = 'web'
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
mock_client.containers.return_value = [
|
||||
{'Id': container_id, 'Name': container_id, 'Image': 'abcd'},
|
||||
]
|
||||
|
||||
service = Service(name=service_name, client=mock_client, image='foo')
|
||||
net = ServiceNet(service)
|
||||
|
||||
self.assertEqual(net.id, service_name)
|
||||
self.assertEqual(net.mode, 'container:' + container_id)
|
||||
self.assertEqual(net.service_name, service_name)
|
||||
|
||||
def test_net_service_no_containers(self):
|
||||
service_name = 'web'
|
||||
mock_client = mock.create_autospec(docker.Client)
|
||||
mock_client.containers.return_value = []
|
||||
|
||||
service = Service(name=service_name, client=mock_client, image='foo')
|
||||
net = ServiceNet(service)
|
||||
|
||||
self.assertEqual(net.id, service_name)
|
||||
self.assertEqual(net.mode, None)
|
||||
self.assertEqual(net.service_name, service_name)
|
||||
|
||||
|
||||
def mock_get_image(images):
|
||||
if images:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue