mirror of
https://github.com/docker/compose.git
synced 2025-07-29 16:44:20 +02:00
commit
daed6dbb91
26
CHANGELOG.md
26
CHANGELOG.md
@ -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
|
||||||
|
25
Dockerfile
25
Dockerfile
@ -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
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
|
428
compose/config/config_schema_v3.1.json
Normal file
428
compose/config/config_schema_v3.1.json
Normal 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"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
}
|
}
|
||||||
|
@ -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':
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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]' \
|
||||||
|
@ -1 +1 @@
|
|||||||
pyinstaller==3.1.1
|
pyinstaller==3.2.1
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
9
tests/fixtures/extends/healthcheck-1.yml
vendored
Normal file
9
tests/fixtures/extends/healthcheck-1.yml
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
version: '2.1'
|
||||||
|
services:
|
||||||
|
demo:
|
||||||
|
image: foobar:latest
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "/health.sh"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 36
|
6
tests/fixtures/extends/healthcheck-2.yml
vendored
Normal file
6
tests/fixtures/extends/healthcheck-2.yml
vendored
Normal 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
1
tests/fixtures/secrets/default
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
This is the secret
|
6
tests/fixtures/top/docker-compose.yml
vendored
Normal file
6
tests/fixtures/top/docker-compose.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
service_a:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
service_b:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
2
tests/fixtures/volumes/docker-compose.yml
vendored
Normal file
2
tests/fixtures/volumes/docker-compose.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
version: '2.1'
|
||||||
|
services: {}
|
16
tests/fixtures/volumes/external-volumes.yml
vendored
Normal file
16
tests/fixtures/volumes/external-volumes.yml
vendored
Normal 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
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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):
|
||||||
|
@ -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')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user