Merge pull request #4427 from docker/bump-1.11.0-rc1

Bump 1.11.0 RC1
This commit is contained in:
Joffrey F 2017-02-06 15:02:54 -08:00 committed by GitHub
commit daed6dbb91
32 changed files with 909 additions and 118 deletions

View File

@ -1,6 +1,30 @@
Change log Change log
========== ==========
1.11.0 (2017-02-08)
-------------------
### New Features
#### Compose file version 3.1
- Introduced version 3.1 of the `docker-compose.yml` specification. This
version requires Docker Engine 1.13.0 or above. It introduces support
for secrets. See the documentation for more information
#### Compose file version 2.0 and up
- Introduced the `docker-compose top` command that displays processes running
for the different services managed by Compose.
### Bugfixes
- Fixed a bug where extending a service defining a healthcheck dictionary
would cause `docker-compose` to error out.
- Fixed an issue where the `pid` entry in a service definition was being
ignored when using multiple Compose files.
1.10.1 (2017-02-01) 1.10.1 (2017-02-01)
------------------ ------------------
@ -214,7 +238,7 @@ Bug Fixes
- Fixed a bug in Windows environment where volume mappings of the - Fixed a bug in Windows environment where volume mappings of the
host's root directory would be parsed incorrectly. host's root directory would be parsed incorrectly.
- Fixed a bug where `docker-compose config` would ouput an invalid - Fixed a bug where `docker-compose config` would output an invalid
Compose file if external networks were specified. Compose file if external networks were specified.
- Fixed an issue where unset buildargs would be assigned a string - Fixed an issue where unset buildargs would be assigned a string

View File

@ -13,6 +13,7 @@ RUN set -ex; \
ca-certificates \ ca-certificates \
curl \ curl \
libsqlite3-dev \ libsqlite3-dev \
libbz2-dev \
; \ ; \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
@ -20,40 +21,32 @@ RUN curl https://get.docker.com/builds/Linux/x86_64/docker-1.8.3 \
-o /usr/local/bin/docker && \ -o /usr/local/bin/docker && \
chmod +x /usr/local/bin/docker chmod +x /usr/local/bin/docker
# Build Python 2.7.9 from source # Build Python 2.7.13 from source
RUN set -ex; \ RUN set -ex; \
curl -L https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz | tar -xz; \ curl -L https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz | tar -xz; \
cd Python-2.7.9; \ cd Python-2.7.13; \
./configure --enable-shared; \ ./configure --enable-shared; \
make; \ make; \
make install; \ make install; \
cd ..; \ cd ..; \
rm -rf /Python-2.7.9 rm -rf /Python-2.7.13
# Build python 3.4 from source # Build python 3.4 from source
RUN set -ex; \ RUN set -ex; \
curl -L https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz | tar -xz; \ curl -L https://www.python.org/ftp/python/3.4.6/Python-3.4.6.tgz | tar -xz; \
cd Python-3.4.3; \ cd Python-3.4.6; \
./configure --enable-shared; \ ./configure --enable-shared; \
make; \ make; \
make install; \ make install; \
cd ..; \ cd ..; \
rm -rf /Python-3.4.3 rm -rf /Python-3.4.6
# Make libpython findable # Make libpython findable
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib
# Install setuptools
RUN set -ex; \
curl -L https://bootstrap.pypa.io/ez_setup.py | python
# Install pip # Install pip
RUN set -ex; \ RUN set -ex; \
curl -L https://pypi.python.org/packages/source/p/pip/pip-8.1.1.tar.gz | tar -xz; \ curl -L https://bootstrap.pypa.io/get-pip.py | python
cd pip-8.1.1; \
python setup.py install; \
cd ..; \
rm -rf pip-8.1.1
# Python3 requires a valid locale # Python3 requires a valid locale
RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '1.10.1' __version__ = '1.11.0-rc1'

View File

@ -33,7 +33,7 @@ def make_color_fn(code):
return lambda s: ansi_color(code, s) return lambda s: ansi_color(code, s)
colorama.init() colorama.init(strip=False)
for (name, code) in get_pairs(): for (name, code) in get_pairs():
globals()[name] = make_color_fn(code) globals()[name] = make_color_fn(code)

View File

@ -215,6 +215,7 @@ class TopLevelCommand(object):
scale Set number of containers for a service scale Set number of containers for a service
start Start services start Start services
stop Stop services stop Stop services
top Display the running processes
unpause Unpause services unpause Unpause services
up Create and start containers up Create and start containers
version Show the Docker-Compose version information version Show the Docker-Compose version information
@ -800,6 +801,33 @@ class TopLevelCommand(object):
containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout) containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout)
exit_if(not containers, 'No containers to restart', 1) exit_if(not containers, 'No containers to restart', 1)
def top(self, options):
"""
Display the running processes
Usage: top [SERVICE...]
"""
containers = sorted(
self.project.containers(service_names=options['SERVICE'], stopped=False) +
self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
key=attrgetter('name')
)
for idx, container in enumerate(containers):
if idx > 0:
print()
top_data = self.project.client.top(container.name)
headers = top_data.get("Titles")
rows = []
for process in top_data.get("Processes", []):
rows.append(process)
print(container.name)
print(Formatter().table(headers, rows))
def unpause(self, options): def unpause(self, options):
""" """
Unpause services. Unpause services.

View File

@ -12,10 +12,12 @@ import six
import yaml import yaml
from cached_property import cached_property from cached_property import cached_property
from . import types
from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V1 as V1
from ..const import COMPOSEFILE_V2_0 as V2_0 from ..const import COMPOSEFILE_V2_0 as V2_0
from ..const import COMPOSEFILE_V2_1 as V2_1 from ..const import COMPOSEFILE_V2_1 as V2_1
from ..const import COMPOSEFILE_V3_0 as V3_0 from ..const import COMPOSEFILE_V3_0 as V3_0
from ..const import COMPOSEFILE_V3_1 as V3_1
from ..utils import build_string_dict from ..utils import build_string_dict
from ..utils import parse_nanoseconds_int from ..utils import parse_nanoseconds_int
from ..utils import splitdrive from ..utils import splitdrive
@ -76,12 +78,13 @@ DOCKER_CONFIG_KEYS = [
'memswap_limit', 'memswap_limit',
'mem_swappiness', 'mem_swappiness',
'net', 'net',
'oom_score_adj' 'oom_score_adj',
'pid', 'pid',
'ports', 'ports',
'privileged', 'privileged',
'read_only', 'read_only',
'restart', 'restart',
'secrets',
'security_opt', 'security_opt',
'shm_size', 'shm_size',
'stdin_open', 'stdin_open',
@ -202,8 +205,11 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')):
def get_networks(self): def get_networks(self):
return {} if self.version == V1 else self.config.get('networks', {}) return {} if self.version == V1 else self.config.get('networks', {})
def get_secrets(self):
return {} if self.version < V3_1 else self.config.get('secrets', {})
class Config(namedtuple('_Config', 'version services volumes networks')):
class Config(namedtuple('_Config', 'version services volumes networks secrets')):
""" """
:param version: configuration version :param version: configuration version
:type version: int :type version: int
@ -328,6 +334,7 @@ def load(config_details):
networks = load_mapping( networks = load_mapping(
config_details.config_files, 'get_networks', 'Network' config_details.config_files, 'get_networks', 'Network'
) )
secrets = load_secrets(config_details.config_files, config_details.working_dir)
service_dicts = load_services(config_details, main_file) service_dicts = load_services(config_details, main_file)
if main_file.version != V1: if main_file.version != V1:
@ -342,7 +349,7 @@ def load(config_details):
"`docker stack deploy` to deploy to a swarm." "`docker stack deploy` to deploy to a swarm."
.format(", ".join(sorted(s['name'] for s in services_using_deploy)))) .format(", ".join(sorted(s['name'] for s in services_using_deploy))))
return Config(main_file.version, service_dicts, volumes, networks) return Config(main_file.version, service_dicts, volumes, networks, secrets)
def load_mapping(config_files, get_func, entity_type): def load_mapping(config_files, get_func, entity_type):
@ -356,22 +363,12 @@ def load_mapping(config_files, get_func, entity_type):
external = config.get('external') external = config.get('external')
if external: if external:
if len(config.keys()) > 1: validate_external(entity_type, name, config)
raise ConfigurationError(
'{} {} declared as external but specifies'
' additional attributes ({}). '.format(
entity_type,
name,
', '.join([k for k in config.keys() if k != 'external'])
)
)
if isinstance(external, dict): if isinstance(external, dict):
config['external_name'] = external.get('name') config['external_name'] = external.get('name')
else: else:
config['external_name'] = name config['external_name'] = name
mapping[name] = config
if 'driver_opts' in config: if 'driver_opts' in config:
config['driver_opts'] = build_string_dict( config['driver_opts'] = build_string_dict(
config['driver_opts'] config['driver_opts']
@ -383,6 +380,39 @@ def load_mapping(config_files, get_func, entity_type):
return mapping return mapping
def validate_external(entity_type, name, config):
if len(config.keys()) <= 1:
return
raise ConfigurationError(
"{} {} declared as external but specifies additional attributes "
"({}).".format(
entity_type, name, ', '.join(k for k in config if k != 'external')))
def load_secrets(config_files, working_dir):
mapping = {}
for config_file in config_files:
for name, config in config_file.get_secrets().items():
mapping[name] = config or {}
if not config:
continue
external = config.get('external')
if external:
validate_external('Secret', name, config)
if isinstance(external, dict):
config['external_name'] = external.get('name')
else:
config['external_name'] = name
if 'file' in config:
config['file'] = expand_path(working_dir, config['file'])
return mapping
def load_services(config_details, config_file): def load_services(config_details, config_file):
def build_service(service_name, service_dict, service_names): def build_service(service_name, service_dict, service_names):
service_config = ServiceConfig.with_abs_paths( service_config = ServiceConfig.with_abs_paths(
@ -686,9 +716,15 @@ def process_healthcheck(service_dict, service_name):
hc['test'] = raw['test'] hc['test'] = raw['test']
if 'interval' in raw: if 'interval' in raw:
hc['interval'] = parse_nanoseconds_int(raw['interval']) if not isinstance(raw['interval'], six.integer_types):
hc['interval'] = parse_nanoseconds_int(raw['interval'])
else: # Conversion has been done previously
hc['interval'] = raw['interval']
if 'timeout' in raw: if 'timeout' in raw:
hc['timeout'] = parse_nanoseconds_int(raw['timeout']) if not isinstance(raw['timeout'], six.integer_types):
hc['timeout'] = parse_nanoseconds_int(raw['timeout'])
else: # Conversion has been done previously
hc['timeout'] = raw['timeout']
if 'retries' in raw: if 'retries' in raw:
hc['retries'] = raw['retries'] hc['retries'] = raw['retries']
@ -820,6 +856,7 @@ def merge_service_dicts(base, override, version):
md.merge_mapping('sysctls', parse_sysctls) md.merge_mapping('sysctls', parse_sysctls)
md.merge_mapping('depends_on', parse_depends_on) md.merge_mapping('depends_on', parse_depends_on)
md.merge_sequence('links', ServiceLink.parse) md.merge_sequence('links', ServiceLink.parse)
md.merge_sequence('secrets', types.ServiceSecret.parse)
for field in ['volumes', 'devices']: for field in ['volumes', 'devices']:
md.merge_field(field, merge_path_mappings) md.merge_field(field, merge_path_mappings)

View File

@ -276,9 +276,9 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"properties": { "properties": {
"name": {"type": "string"} "name": {"type": "string"}
} },
}, "additionalProperties": false
"additionalProperties": false }
}, },
"additionalProperties": false "additionalProperties": false
}, },

View File

@ -322,10 +322,10 @@
"type": ["boolean", "object"], "type": ["boolean", "object"],
"properties": { "properties": {
"name": {"type": "string"} "name": {"type": "string"}
} },
"additionalProperties": false
}, },
"labels": {"$ref": "#/definitions/list_or_dict"}, "labels": {"$ref": "#/definitions/list_or_dict"}
"additionalProperties": false
}, },
"additionalProperties": false "additionalProperties": false
}, },

View File

@ -0,0 +1,428 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v3.1.json",
"type": "object",
"required": ["version"],
"properties": {
"version": {
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false
},
"networks": {
"id": "#/properties/networks",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network"
}
}
},
"volumes": {
"id": "#/properties/volumes",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
},
"additionalProperties": false
},
"secrets": {
"id": "#/properties/secrets",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/secret"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"deploy": {"$ref": "#/definitions/deployment"},
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
}
]
},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"depends_on": {"$ref": "#/definitions/list_of_strings"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number", "null"]}
}
}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"network_mode": {"type": "string"},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"oneOf": [
{
"type": "object",
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"secrets": {
"type": "array",
"items": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"source": {"type": "string"},
"target": {"type": "string"},
"uid": {"type": "string"},
"gid": {"type": "string"},
"mode": {"type": "number"}
}
}
]
}
},
"sysctls": {"$ref": "#/definitions/list_or_dict"},
"stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"userns_mode": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"additionalProperties": false
},
"healthcheck": {
"id": "#/definitions/healthcheck",
"type": "object",
"additionalProperties": false,
"properties": {
"disable": {"type": "boolean"},
"interval": {"type": "string"},
"retries": {"type": "number"},
"test": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"timeout": {"type": "string"}
}
},
"deployment": {
"id": "#/definitions/deployment",
"type": ["object", "null"],
"properties": {
"mode": {"type": "string"},
"replicas": {"type": "integer"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"update_config": {
"type": "object",
"properties": {
"parallelism": {"type": "integer"},
"delay": {"type": "string", "format": "duration"},
"failure_action": {"type": "string"},
"monitor": {"type": "string", "format": "duration"},
"max_failure_ratio": {"type": "number"}
},
"additionalProperties": false
},
"resources": {
"type": "object",
"properties": {
"limits": {"$ref": "#/definitions/resource"},
"reservations": {"$ref": "#/definitions/resource"}
}
},
"restart_policy": {
"type": "object",
"properties": {
"condition": {"type": "string"},
"delay": {"type": "string", "format": "duration"},
"max_attempts": {"type": "integer"},
"window": {"type": "string", "format": "duration"}
},
"additionalProperties": false
},
"placement": {
"type": "object",
"properties": {
"constraints": {"type": "array", "items": {"type": "string"}}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"resource": {
"id": "#/definitions/resource",
"type": "object",
"properties": {
"cpus": {"type": "string"},
"memory": {"type": "string"}
},
"additionalProperties": false
},
"network": {
"id": "#/definitions/network",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array",
"items": {
"type": "object",
"properties": {
"subnet": {"type": "string"}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"internal": {"type": "boolean"},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"secret": {
"id": "#/definitions/secret",
"type": "object",
"properties": {
"file": {"type": "string"},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"labels": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs import codecs
import contextlib
import logging import logging
import os import os
@ -31,11 +32,12 @@ def env_vars_from_file(filename):
elif not os.path.isfile(filename): elif not os.path.isfile(filename):
raise ConfigurationError("%s is not a file." % (filename)) raise ConfigurationError("%s is not a file." % (filename))
env = {} env = {}
for line in codecs.open(filename, 'r', 'utf-8'): with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj:
line = line.strip() for line in fileobj:
if line and not line.startswith('#'): line = line.strip()
k, v = split_env(line) if line and not line.startswith('#'):
env[k] = v k, v = split_env(line)
env[k] = v
return env return env

View File

@ -32,6 +32,11 @@ def denormalize_config(config):
if 'external_name' in net_conf: if 'external_name' in net_conf:
del net_conf['external_name'] del net_conf['external_name']
volumes = config.volumes.copy()
for vol_name, vol_conf in volumes.items():
if 'external_name' in vol_conf:
del vol_conf['external_name']
version = config.version version = config.version
if version == V1: if version == V1:
version = V2_1 version = V2_1
@ -40,7 +45,7 @@ def denormalize_config(config):
'version': version, 'version': version,
'services': services, 'services': services,
'networks': networks, 'networks': networks,
'volumes': config.volumes, 'volumes': volumes,
} }

View File

@ -10,8 +10,8 @@ from collections import namedtuple
import six import six
from compose.config.config import V1 from ..const import COMPOSEFILE_V1 as V1
from compose.config.errors import ConfigurationError from .errors import ConfigurationError
from compose.const import IS_WINDOWS_PLATFORM from compose.const import IS_WINDOWS_PLATFORM
from compose.utils import splitdrive from compose.utils import splitdrive
@ -234,3 +234,22 @@ class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
@property @property
def merge_field(self): def merge_field(self):
return self.alias return self.alias
class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')):
@classmethod
def parse(cls, spec):
if isinstance(spec, six.string_types):
return cls(spec, None, None, None, None)
return cls(
spec.get('source'),
spec.get('target'),
spec.get('uid'),
spec.get('gid'),
spec.get('mode'),
)
@property
def merge_field(self):
return self.source

View File

@ -5,7 +5,7 @@ import sys
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT = 10
HTTP_TIMEOUT = 60 HTTP_TIMEOUT = 60
IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag'] IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag']
IS_WINDOWS_PLATFORM = (sys.platform == "win32") IS_WINDOWS_PLATFORM = (sys.platform == "win32")
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
LABEL_ONE_OFF = 'com.docker.compose.oneoff' LABEL_ONE_OFF = 'com.docker.compose.oneoff'
@ -16,16 +16,20 @@ LABEL_VERSION = 'com.docker.compose.version'
LABEL_VOLUME = 'com.docker.compose.volume' LABEL_VOLUME = 'com.docker.compose.volume'
LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash'
SECRETS_PATH = '/run/secrets'
COMPOSEFILE_V1 = '1' COMPOSEFILE_V1 = '1'
COMPOSEFILE_V2_0 = '2.0' COMPOSEFILE_V2_0 = '2.0'
COMPOSEFILE_V2_1 = '2.1' COMPOSEFILE_V2_1 = '2.1'
COMPOSEFILE_V3_0 = '3.0' COMPOSEFILE_V3_0 = '3.0'
COMPOSEFILE_V3_1 = '3.1'
API_VERSIONS = { API_VERSIONS = {
COMPOSEFILE_V1: '1.21', COMPOSEFILE_V1: '1.21',
COMPOSEFILE_V2_0: '1.22', COMPOSEFILE_V2_0: '1.22',
COMPOSEFILE_V2_1: '1.24', COMPOSEFILE_V2_1: '1.24',
COMPOSEFILE_V3_0: '1.25', COMPOSEFILE_V3_0: '1.25',
COMPOSEFILE_V3_1: '1.25',
} }
API_VERSION_TO_ENGINE_VERSION = { API_VERSION_TO_ENGINE_VERSION = {
@ -33,4 +37,5 @@ API_VERSION_TO_ENGINE_VERSION = {
API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0', API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0',
API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0',
API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0',
API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0',
} }

View File

@ -104,6 +104,11 @@ class Project(object):
for volume_spec in service_dict.get('volumes', []) for volume_spec in service_dict.get('volumes', [])
] ]
secrets = get_secrets(
service_dict['name'],
service_dict.pop('secrets', None) or [],
config_data.secrets)
project.services.append( project.services.append(
Service( Service(
service_dict.pop('name'), service_dict.pop('name'),
@ -114,6 +119,7 @@ class Project(object):
links=links, links=links,
network_mode=network_mode, network_mode=network_mode,
volumes_from=volumes_from, volumes_from=volumes_from,
secrets=secrets,
**service_dict) **service_dict)
) )
@ -553,6 +559,33 @@ def get_volumes_from(project, service_dict):
return [build_volume_from(vf) for vf in volumes_from] return [build_volume_from(vf) for vf in volumes_from]
def get_secrets(service, service_secrets, secret_defs):
secrets = []
for secret in service_secrets:
secret_def = secret_defs.get(secret.source)
if not secret_def:
raise ConfigurationError(
"Service \"{service}\" uses an undefined secret \"{secret}\" "
.format(service=service, secret=secret.source))
if secret_def.get('external_name'):
log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
"External secrets are not available to containers created by "
"docker-compose.".format(service=service, secret=secret.source))
continue
if secret.uid or secret.gid or secret.mode:
log.warn("Service \"{service}\" uses secret \"{secret}\" with uid, "
"gid, or mode. These fields are not supported by this "
"implementation of the Compose file".format(
service=service, secret=secret.source))
secrets.append({'secret': secret, 'file': secret_def.get('file')})
return secrets
def warn_for_swarm_mode(client): def warn_for_swarm_mode(client):
info = client.info() info = client.info()
if info.get('Swarm', {}).get('LocalNodeState') == 'active': if info.get('Swarm', {}).get('LocalNodeState') == 'active':

View File

@ -17,6 +17,7 @@ from docker.utils.ports import build_port_bindings
from docker.utils.ports import split_port from docker.utils.ports import split_port
from . import __version__ from . import __version__
from . import const
from . import progress_stream from . import progress_stream
from .config import DOCKER_CONFIG_KEYS from .config import DOCKER_CONFIG_KEYS
from .config import merge_environment from .config import merge_environment
@ -139,6 +140,7 @@ class Service(object):
volumes_from=None, volumes_from=None,
network_mode=None, network_mode=None,
networks=None, networks=None,
secrets=None,
**options **options
): ):
self.name = name self.name = name
@ -149,6 +151,7 @@ class Service(object):
self.volumes_from = volumes_from or [] self.volumes_from = volumes_from or []
self.network_mode = network_mode or NetworkMode(None) self.network_mode = network_mode or NetworkMode(None)
self.networks = networks or {} self.networks = networks or {}
self.secrets = secrets or []
self.options = options self.options = options
def __repr__(self): def __repr__(self):
@ -692,9 +695,14 @@ class Service(object):
override_options['binds'] = binds override_options['binds'] = binds
container_options['environment'].update(affinity) container_options['environment'].update(affinity)
if 'volumes' in container_options: container_options['volumes'] = dict(
container_options['volumes'] = dict( (v.internal, {}) for v in container_options.get('volumes') or {})
(v.internal, {}) for v in container_options['volumes'])
secret_volumes = self.get_secret_volumes()
if secret_volumes:
override_options['binds'].extend(v.repr() for v in secret_volumes)
container_options['volumes'].update(
(v.internal, {}) for v in secret_volumes)
container_options['image'] = self.image_name container_options['image'] = self.image_name
@ -765,6 +773,15 @@ class Service(object):
return host_config return host_config
def get_secret_volumes(self):
def build_spec(secret):
target = '{}/{}'.format(
const.SECRETS_PATH,
secret['secret'].target or secret['secret'].source)
return VolumeSpec(secret['file'], target, 'ro')
return [build_spec(secret) for secret in self.secrets]
def build(self, no_cache=False, pull=False, force_rm=False): def build(self, no_cache=False, pull=False, force_rm=False):
log.info('Building %s' % self.name) log.info('Building %s' % self.name)

View File

@ -434,6 +434,18 @@ _docker_compose_stop() {
} }
_docker_compose_top() {
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "--help" -- "$cur" ) )
;;
*)
__docker_compose_services_running
;;
esac
}
_docker_compose_unpause() { _docker_compose_unpause() {
case "$cur" in case "$cur" in
-*) -*)
@ -499,6 +511,7 @@ _docker_compose() {
scale scale
start start
stop stop
top
unpause unpause
up up
version version

View File

@ -341,6 +341,11 @@ __docker-compose_subcommand() {
$opts_timeout \ $opts_timeout \
'*:running services:__docker-compose_runningservices' && ret=0 '*:running services:__docker-compose_runningservices' && ret=0
;; ;;
(top)
_arguments \
$opts_help \
'*:running services:__docker-compose_runningservices' && ret=0
;;
(unpause) (unpause)
_arguments \ _arguments \
$opts_help \ $opts_help \
@ -386,9 +391,17 @@ _docker-compose() {
integer ret=1 integer ret=1
typeset -A opt_args typeset -A opt_args
local file_description
if [[ -n ${words[(r)-f]} || -n ${words[(r)--file]} ]] ; then
file_description="Specify an override docker-compose file (default: docker-compose.override.yml)"
else
file_description="Specify an alternate docker-compose file (default: docker-compose.yml)"
fi
_arguments -C \ _arguments -C \
'(- :)'{-h,--help}'[Get help]' \ '(- :)'{-h,--help}'[Get help]' \
'(-f --file)'{-f,--file}'[Specify an alternate docker-compose file (default: docker-compose.yml)]:file:_files -g "*.yml"' \ '*'{-f,--file}"[${file_description}]:file:_files -g '*.yml'" \
'(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \ '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \
'--verbose[Show more output]' \ '--verbose[Show more output]' \
'(- :)'{-v,--version}'[Print version and exit]' \ '(- :)'{-v,--version}'[Print version and exit]' \

View File

@ -1 +1 @@
pyinstaller==3.1.1 pyinstaller==3.2.1

View File

@ -15,7 +15,7 @@
set -e set -e
VERSION="1.10.1" VERSION="1.11.0-rc1"
IMAGE="docker/compose:$VERSION" IMAGE="docker/compose:$VERSION"

View File

@ -5,7 +5,7 @@ version tags for recent releases, or the default release.
The default release is the most recent non-RC version. The default release is the most recent non-RC version.
Recent is a list of unqiue major.minor versions, where each is the most Recent is a list of unique major.minor versions, where each is the most
recent version in the series. recent version in the series.
For example, if the list of versions is: For example, if the list of versions is:

View File

@ -262,6 +262,20 @@ class CLITestCase(DockerClientTestCase):
} }
} }
def test_config_external_volume(self):
self.base_dir = 'tests/fixtures/volumes'
result = self.dispatch(['-f', 'external-volumes.yml', 'config'])
json_result = yaml.load(result.stdout)
assert 'volumes' in json_result
assert json_result['volumes'] == {
'foo': {
'external': True
},
'bar': {
'external': {'name': 'some_bar'}
}
}
def test_config_v1(self): def test_config_v1(self):
self.base_dir = 'tests/fixtures/v1-config' self.base_dir = 'tests/fixtures/v1-config'
result = self.dispatch(['config']) result = self.dispatch(['config'])
@ -1893,3 +1907,23 @@ class CLITestCase(DockerClientTestCase):
"BAZ=2", "BAZ=2",
]) ])
self.assertTrue(expected_env <= set(web.get('Config.Env'))) self.assertTrue(expected_env <= set(web.get('Config.Env')))
def test_top_services_not_running(self):
self.base_dir = 'tests/fixtures/top'
result = self.dispatch(['top'])
assert len(result.stdout) == 0
def test_top_services_running(self):
self.base_dir = 'tests/fixtures/top'
self.dispatch(['up', '-d'])
result = self.dispatch(['top'])
self.assertIn('top_service_a', result.stdout)
self.assertIn('top_service_b', result.stdout)
self.assertNotIn('top_not_a_service', result.stdout)
def test_top_processes_running(self):
self.base_dir = 'tests/fixtures/top'
self.dispatch(['up', '-d'])
result = self.dispatch(['top'])
assert result.stdout.count("top") == 4

View File

@ -0,0 +1,9 @@
version: '2.1'
services:
demo:
image: foobar:latest
healthcheck:
test: ["CMD", "/health.sh"]
interval: 10s
timeout: 5s
retries: 36

View File

@ -0,0 +1,6 @@
version: '2.1'
services:
demo:
extends:
file: healthcheck-1.yml
service: demo

1
tests/fixtures/secrets/default vendored Normal file
View File

@ -0,0 +1 @@
This is the secret

6
tests/fixtures/top/docker-compose.yml vendored Normal file
View File

@ -0,0 +1,6 @@
service_a:
image: busybox:latest
command: top
service_b:
image: busybox:latest
command: top

View File

@ -0,0 +1,2 @@
version: '2.1'
services: {}

View File

@ -0,0 +1,16 @@
version: "2.1"
services:
web:
image: busybox
command: top
volumes:
- foo:/var/lib/
- bar:/etc/
volumes:
foo:
external: true
bar:
external:
name: some_bar

View File

@ -1,6 +1,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import os.path
import random import random
import py import py
@ -8,12 +9,14 @@ import pytest
from docker.errors import NotFound from docker.errors import NotFound
from .. import mock from .. import mock
from ..helpers import build_config from ..helpers import build_config as load_config
from .testcases import DockerClientTestCase from .testcases import DockerClientTestCase
from compose.config import config from compose.config import config
from compose.config import ConfigurationError from compose.config import ConfigurationError
from compose.config import types
from compose.config.config import V2_0 from compose.config.config import V2_0
from compose.config.config import V2_1 from compose.config.config import V2_1
from compose.config.config import V3_1
from compose.config.types import VolumeFromSpec from compose.config.types import VolumeFromSpec
from compose.config.types import VolumeSpec from compose.config.types import VolumeSpec
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
@ -26,6 +29,16 @@ from compose.project import ProjectError
from compose.service import ConvergenceStrategy from compose.service import ConvergenceStrategy
from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_1_only
from tests.integration.testcases import v2_only from tests.integration.testcases import v2_only
from tests.integration.testcases import v3_only
def build_config(**kwargs):
return config.Config(
version=kwargs.get('version'),
services=kwargs.get('services'),
volumes=kwargs.get('volumes'),
networks=kwargs.get('networks'),
secrets=kwargs.get('secrets'))
class ProjectTest(DockerClientTestCase): class ProjectTest(DockerClientTestCase):
@ -70,7 +83,7 @@ class ProjectTest(DockerClientTestCase):
def test_volumes_from_service(self): def test_volumes_from_service(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'data': { 'data': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes': ['/var/data'], 'volumes': ['/var/data'],
@ -96,7 +109,7 @@ class ProjectTest(DockerClientTestCase):
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'db': { 'db': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'volumes_from': ['composetest_data_container'], 'volumes_from': ['composetest_data_container'],
@ -112,7 +125,7 @@ class ProjectTest(DockerClientTestCase):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
client=self.client, client=self.client,
config_data=build_config({ config_data=load_config({
'version': V2_0, 'version': V2_0,
'services': { 'services': {
'net': { 'net': {
@ -139,7 +152,7 @@ class ProjectTest(DockerClientTestCase):
def get_project(): def get_project():
return Project.from_config( return Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'version': V2_0, 'version': V2_0,
'services': { 'services': {
'web': { 'web': {
@ -174,7 +187,7 @@ class ProjectTest(DockerClientTestCase):
def test_net_from_service_v1(self): def test_net_from_service_v1(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'net': { 'net': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"] 'command': ["top"]
@ -198,7 +211,7 @@ class ProjectTest(DockerClientTestCase):
def get_project(): def get_project():
return Project.from_config( return Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'web': { 'web': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'net': 'container:composetest_net_container' 'net': 'container:composetest_net_container'
@ -469,7 +482,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_starts_depends(self): def test_project_up_starts_depends(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"], 'command': ["top"],
@ -504,7 +517,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_with_no_deps(self): def test_project_up_with_no_deps(self):
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
config_data=build_config({ config_data=load_config({
'console': { 'console': {
'image': 'busybox:latest', 'image': 'busybox:latest',
'command': ["top"], 'command': ["top"],
@ -564,7 +577,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_project_up_networks(self): def test_project_up_networks(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -576,7 +589,6 @@ class ProjectTest(DockerClientTestCase):
'baz': {'aliases': ['extra']}, 'baz': {'aliases': ['extra']},
}, },
}], }],
volumes={},
networks={ networks={
'foo': {'driver': 'bridge'}, 'foo': {'driver': 'bridge'},
'bar': {'driver': None}, 'bar': {'driver': None},
@ -610,14 +622,13 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_up_with_ipam_config(self): def test_up_with_ipam_config(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'networks': {'front': None}, 'networks': {'front': None},
}], }],
volumes={},
networks={ networks={
'front': { 'front': {
'driver': 'bridge', 'driver': 'bridge',
@ -671,7 +682,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_up_with_network_static_addresses(self): def test_up_with_network_static_addresses(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -684,7 +695,6 @@ class ProjectTest(DockerClientTestCase):
} }
}, },
}], }],
volumes={},
networks={ networks={
'static_test': { 'static_test': {
'driver': 'bridge', 'driver': 'bridge',
@ -726,7 +736,7 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_enable_ipv6(self): def test_up_with_enable_ipv6(self):
self.require_api_version('1.23') self.require_api_version('1.23')
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -738,7 +748,6 @@ class ProjectTest(DockerClientTestCase):
} }
}, },
}], }],
volumes={},
networks={ networks={
'static_test': { 'static_test': {
'driver': 'bridge', 'driver': 'bridge',
@ -770,7 +779,7 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_up_with_network_static_addresses_missing_subnet(self): def test_up_with_network_static_addresses_missing_subnet(self):
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -782,7 +791,6 @@ class ProjectTest(DockerClientTestCase):
} }
}, },
}], }],
volumes={},
networks={ networks={
'static_test': { 'static_test': {
'driver': 'bridge', 'driver': 'bridge',
@ -807,7 +815,7 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_network_link_local_ips(self): def test_up_with_network_link_local_ips(self):
config_data = config.Config( config_data = build_config(
version=V2_1, version=V2_1,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -818,7 +826,6 @@ class ProjectTest(DockerClientTestCase):
} }
} }
}], }],
volumes={},
networks={ networks={
'linklocaltest': {'driver': 'bridge'} 'linklocaltest': {'driver': 'bridge'}
} }
@ -844,15 +851,13 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_isolation(self): def test_up_with_isolation(self):
self.require_api_version('1.24') self.require_api_version('1.24')
config_data = config.Config( config_data = build_config(
version=V2_1, version=V2_1,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'isolation': 'default' 'isolation': 'default'
}], }],
volumes={},
networks={}
) )
project = Project.from_config( project = Project.from_config(
client=self.client, client=self.client,
@ -866,15 +871,13 @@ class ProjectTest(DockerClientTestCase):
@v2_1_only() @v2_1_only()
def test_up_with_invalid_isolation(self): def test_up_with_invalid_isolation(self):
self.require_api_version('1.24') self.require_api_version('1.24')
config_data = config.Config( config_data = build_config(
version=V2_1, version=V2_1,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'isolation': 'foobar' 'isolation': 'foobar'
}], }],
volumes={},
networks={}
) )
project = Project.from_config( project = Project.from_config(
client=self.client, client=self.client,
@ -887,14 +890,13 @@ class ProjectTest(DockerClientTestCase):
@v2_only() @v2_only()
def test_project_up_with_network_internal(self): def test_project_up_with_network_internal(self):
self.require_api_version('1.23') self.require_api_version('1.23')
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'networks': {'internal': None}, 'networks': {'internal': None},
}], }],
volumes={},
networks={ networks={
'internal': {'driver': 'bridge', 'internal': True}, 'internal': {'driver': 'bridge', 'internal': True},
}, },
@ -917,14 +919,13 @@ class ProjectTest(DockerClientTestCase):
network_name = 'network_with_label' network_name = 'network_with_label'
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
'image': 'busybox:latest', 'image': 'busybox:latest',
'networks': {network_name: None} 'networks': {network_name: None}
}], }],
volumes={},
networks={ networks={
network_name: {'labels': {'label_key': 'label_val'}} network_name: {'labels': {'label_key': 'label_val'}}
} }
@ -951,7 +952,7 @@ class ProjectTest(DockerClientTestCase):
def test_project_up_volumes(self): def test_project_up_volumes(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -959,7 +960,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'local'}}, volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -979,7 +979,7 @@ class ProjectTest(DockerClientTestCase):
volume_name = 'volume_with_label' volume_name = 'volume_with_label'
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -993,7 +993,6 @@ class ProjectTest(DockerClientTestCase):
} }
} }
}, },
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1106,7 +1105,7 @@ class ProjectTest(DockerClientTestCase):
def test_initialize_volumes(self): def test_initialize_volumes(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1114,7 +1113,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {}}, volumes={vol_name: {}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1124,14 +1122,14 @@ class ProjectTest(DockerClientTestCase):
project.volumes.initialize() project.volumes.initialize()
volume_data = self.client.inspect_volume(full_vol_name) volume_data = self.client.inspect_volume(full_vol_name)
self.assertEqual(volume_data['Name'], full_vol_name) assert volume_data['Name'] == full_vol_name
self.assertEqual(volume_data['Driver'], 'local') assert volume_data['Driver'] == 'local'
@v2_only() @v2_only()
def test_project_up_implicit_volume_driver(self): def test_project_up_implicit_volume_driver(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1139,7 +1137,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {}}, volumes={vol_name: {}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1152,11 +1149,47 @@ class ProjectTest(DockerClientTestCase):
self.assertEqual(volume_data['Name'], full_vol_name) self.assertEqual(volume_data['Name'], full_vol_name)
self.assertEqual(volume_data['Driver'], 'local') self.assertEqual(volume_data['Driver'], 'local')
@v3_only()
def test_project_up_with_secrets(self):
create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default'))
config_data = build_config(
version=V3_1,
services=[{
'name': 'web',
'image': 'busybox:latest',
'command': 'cat /run/secrets/special',
'secrets': [
types.ServiceSecret.parse({'source': 'super', 'target': 'special'}),
],
}],
secrets={
'super': {
'file': os.path.abspath('tests/fixtures/secrets/default'),
},
},
)
project = Project.from_config(
client=self.client,
name='composetest',
config_data=config_data,
)
project.up()
project.stop()
containers = project.containers(stopped=True)
assert len(containers) == 1
container, = containers
output = container.logs()
assert output == b"This is the secret\n"
@v2_only() @v2_only()
def test_initialize_volumes_invalid_volume_driver(self): def test_initialize_volumes_invalid_volume_driver(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1164,7 +1197,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'foobar'}}, volumes={vol_name: {'driver': 'foobar'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
@ -1179,7 +1211,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1187,7 +1219,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'local'}}, volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1218,7 +1249,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1226,7 +1257,6 @@ class ProjectTest(DockerClientTestCase):
'command': 'top' 'command': 'top'
}], }],
volumes={vol_name: {'driver': 'local'}}, volumes={vol_name: {'driver': 'local'}},
networks={},
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1257,7 +1287,7 @@ class ProjectTest(DockerClientTestCase):
vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) vol_name = 'composetest_{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name) full_vol_name = 'composetest_{0}'.format(vol_name)
self.client.create_volume(vol_name) self.client.create_volume(vol_name)
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1267,7 +1297,6 @@ class ProjectTest(DockerClientTestCase):
volumes={ volumes={
vol_name: {'external': True, 'external_name': vol_name} vol_name: {'external': True, 'external_name': vol_name}
}, },
networks=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1282,7 +1311,7 @@ class ProjectTest(DockerClientTestCase):
def test_initialize_volumes_inexistent_external_volume(self): def test_initialize_volumes_inexistent_external_volume(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))
config_data = config.Config( config_data = build_config(
version=V2_0, version=V2_0,
services=[{ services=[{
'name': 'web', 'name': 'web',
@ -1292,7 +1321,6 @@ class ProjectTest(DockerClientTestCase):
volumes={ volumes={
vol_name: {'external': True, 'external_name': vol_name} vol_name: {'external': True, 'external_name': vol_name}
}, },
networks=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -1349,7 +1377,7 @@ class ProjectTest(DockerClientTestCase):
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1357,7 +1385,7 @@ class ProjectTest(DockerClientTestCase):
config_dict['service2'] = config_dict['service1'] config_dict['service2'] = config_dict['service1']
del config_dict['service1'] del config_dict['service1']
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1402,7 +1430,7 @@ class ProjectTest(DockerClientTestCase):
} }
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1439,7 +1467,7 @@ class ProjectTest(DockerClientTestCase):
} }
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1475,7 +1503,7 @@ class ProjectTest(DockerClientTestCase):
} }
} }
} }
config_data = build_config(config_dict) config_data = load_config(config_dict)
project = Project.from_config( project = Project.from_config(
name='composetest', config_data=config_data, client=self.client name='composetest', config_data=config_data, client=self.client
) )
@ -1489,3 +1517,30 @@ class ProjectTest(DockerClientTestCase):
assert 'svc1' in svc2.get_dependency_names() assert 'svc1' in svc2.get_dependency_names()
with pytest.raises(NoHealthCheckConfigured): with pytest.raises(NoHealthCheckConfigured):
svc1.is_healthy() svc1.is_healthy()
def create_host_file(client, filename):
dirname = os.path.dirname(filename)
with open(filename, 'r') as fh:
content = fh.read()
container = client.create_container(
'busybox:latest',
['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)],
volumes={dirname: {}},
host_config=client.create_host_config(
binds={dirname: {'bind': dirname, 'ro': False}},
network_mode='none',
),
)
try:
client.start(container)
exitcode = client.wait(container)
if exitcode != 0:
output = client.logs(container)
raise Exception(
"Container exited with code {}:\n{}".format(exitcode, output))
finally:
client.remove_container(container, force=True)

View File

@ -41,9 +41,9 @@ def engine_max_version():
version = os.environ['DOCKER_VERSION'].partition('-')[0] version = os.environ['DOCKER_VERSION'].partition('-')[0]
if version_lt(version, '1.10'): if version_lt(version, '1.10'):
return V1 return V1
elif version_lt(version, '1.12'): if version_lt(version, '1.12'):
return V2_0 return V2_0
elif version_lt(version, '1.13'): if version_lt(version, '1.13'):
return V2_1 return V2_1
return V3_0 return V3_0
@ -52,8 +52,9 @@ def build_version_required_decorator(ignored_versions):
def decorator(f): def decorator(f):
@functools.wraps(f) @functools.wraps(f)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if engine_max_version() in ignored_versions: max_version = engine_max_version()
skip("Engine version is too low") if max_version in ignored_versions:
skip("Engine version %s is too low" % max_version)
return return
return f(self, *args, **kwargs) return f(self, *args, **kwargs)
return wrapper return wrapper

View File

@ -77,7 +77,8 @@ def test_to_bundle():
version=2, version=2,
services=services, services=services,
volumes={'special': {}}, volumes={'special': {}},
networks={'extra': {}}) networks={'extra': {}},
secrets={})
with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log:
output = bundle.to_bundle(config, image_digests) output = bundle.to_bundle(config, image_digests)

View File

@ -1748,6 +1748,24 @@ class ConfigTest(unittest.TestCase):
} }
} }
def test_merge_pid(self):
# Regression: https://github.com/docker/compose/issues/4184
base = {
'image': 'busybox',
'pid': 'host'
}
override = {
'labels': {'com.docker.compose.test': 'yes'}
}
actual = config.merge_service_dicts(base, override, V2_0)
assert actual == {
'image': 'busybox',
'pid': 'host',
'labels': {'com.docker.compose.test': 'yes'}
}
def test_external_volume_config(self): def test_external_volume_config(self):
config_details = build_config_details({ config_details = build_config_details({
'version': '2', 'version': '2',
@ -3098,6 +3116,19 @@ class ExtendsTest(unittest.TestCase):
'other': {'condition': 'service_started'} 'other': {'condition': 'service_started'}
} }
def test_extends_with_healthcheck(self):
service_dicts = load_from_filename('tests/fixtures/extends/healthcheck-2.yml')
assert service_sort(service_dicts) == [{
'name': 'demo',
'image': 'foobar:latest',
'healthcheck': {
'test': ['CMD', '/health.sh'],
'interval': 10000000000,
'timeout': 5000000000,
'retries': 36,
}
}]
@pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash')
class ExpandPathTest(unittest.TestCase): class ExpandPathTest(unittest.TestCase):

View File

@ -36,6 +36,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
) )
project = Project.from_config( project = Project.from_config(
name='composetest', name='composetest',
@ -64,6 +65,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
) )
project = Project.from_config('composetest', config, None) project = Project.from_config('composetest', config, None)
self.assertEqual(len(project.services), 2) self.assertEqual(len(project.services), 2)
@ -170,6 +172,7 @@ class ProjectTest(unittest.TestCase):
}], }],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"] assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"]
@ -202,6 +205,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"] assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"]
@ -227,6 +231,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
with mock.patch.object(Service, 'containers') as mock_return: with mock.patch.object(Service, 'containers') as mock_return:
@ -360,6 +365,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
service = project.get_service('test') service = project.get_service('test')
@ -384,6 +390,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
service = project.get_service('test') service = project.get_service('test')
@ -417,6 +424,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
@ -437,6 +445,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
@ -457,6 +466,7 @@ class ProjectTest(unittest.TestCase):
], ],
networks={'custom': {}}, networks={'custom': {}},
volumes=None, volumes=None,
secrets=None,
), ),
) )
@ -487,6 +497,7 @@ class ProjectTest(unittest.TestCase):
}], }],
networks=None, networks=None,
volumes=None, volumes=None,
secrets=None,
), ),
) )
self.assertEqual([c.id for c in project.containers()], ['1']) self.assertEqual([c.id for c in project.containers()], ['1'])
@ -503,6 +514,7 @@ class ProjectTest(unittest.TestCase):
}], }],
networks={'default': {}}, networks={'default': {}},
volumes={'data': {}}, volumes={'data': {}},
secrets=None,
), ),
) )
self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops') self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops')