mirror of
https://github.com/docker/compose.git
synced 2025-07-22 21:24:38 +02:00
commit
8fc6ac1899
@ -8,6 +8,7 @@ matrix:
|
|||||||
services:
|
services:
|
||||||
- docker
|
- docker
|
||||||
- os: osx
|
- os: osx
|
||||||
|
osx_image: xcode7.3
|
||||||
language: generic
|
language: generic
|
||||||
|
|
||||||
install: ./script/travis/install
|
install: ./script/travis/install
|
||||||
|
80
CHANGELOG.md
80
CHANGELOG.md
@ -1,6 +1,86 @@
|
|||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
1.18.0 (2017-12-15)
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
### New features
|
||||||
|
|
||||||
|
#### Compose file version 3.5
|
||||||
|
|
||||||
|
- Introduced version 3.5 of the `docker-compose.yml` specification.
|
||||||
|
This version requires to be used with Docker Engine 17.06.0 or above
|
||||||
|
|
||||||
|
- Added support for the `shm_size` parameter in build configurations
|
||||||
|
|
||||||
|
- Added support for the `isolation` parameter in service definitions
|
||||||
|
|
||||||
|
- Added support for custom names for network, secret and config definitions
|
||||||
|
|
||||||
|
#### Compose file version 2.3
|
||||||
|
|
||||||
|
- Added support for `extra_hosts` in build configuration
|
||||||
|
|
||||||
|
- Added support for the
|
||||||
|
[long syntax](https://docs.docker.com/compose/compose-file/#long-syntax-3)
|
||||||
|
for volume entries, as previously introduced in the 3.2 format.
|
||||||
|
Note that using this syntax will create
|
||||||
|
[mounts](https://docs.docker.com/engine/admin/volumes/bind-mounts/)
|
||||||
|
instead of volumes.
|
||||||
|
|
||||||
|
#### Compose file version 2.1 and up
|
||||||
|
|
||||||
|
- Added support for the `oom_kill_disable` parameter in service definitions
|
||||||
|
(2.x only)
|
||||||
|
|
||||||
|
- Added support for custom names for network, secret and config definitions
|
||||||
|
(2.x only)
|
||||||
|
|
||||||
|
|
||||||
|
#### All formats
|
||||||
|
|
||||||
|
- Values interpolated from the environment will now be converted to the
|
||||||
|
proper type when used in non-string fields.
|
||||||
|
|
||||||
|
- Added support for `--labels` in `docker-compose run`
|
||||||
|
|
||||||
|
- Added support for `--timeout` in `docker-compose down`
|
||||||
|
|
||||||
|
- Added support for `--memory` in `docker-compose build`
|
||||||
|
|
||||||
|
- Setting `stop_grace_period` in service definitions now also sets the
|
||||||
|
container's `stop_timeout`
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed an issue where Compose was still handling service hostname according
|
||||||
|
to legacy engine behavior, causing hostnames containing dots to be cut up
|
||||||
|
|
||||||
|
- Fixed a bug where the `X-Y:Z` syntax for ports was considered invalid
|
||||||
|
by Compose
|
||||||
|
|
||||||
|
- Fixed an issue with CLI logging causing duplicate messages and inelegant
|
||||||
|
output to occur
|
||||||
|
|
||||||
|
- Fixed a bug where the valid `${VAR:-}` syntax would cause Compose to
|
||||||
|
error out
|
||||||
|
|
||||||
|
- Fixed a bug where `env_file` entries using an UTF-8 BOM were being read
|
||||||
|
incorrectly
|
||||||
|
|
||||||
|
- Fixed a bug where missing secret files would generate an empty directory
|
||||||
|
in their place
|
||||||
|
|
||||||
|
- Added validation for the `test` field in healthchecks
|
||||||
|
|
||||||
|
- Added validation for the `subnet` field in IPAM configurations
|
||||||
|
|
||||||
|
- Added validation for `volumes` properties when using the long syntax in
|
||||||
|
service definitions
|
||||||
|
|
||||||
|
- The CLI now explicit prevents using `-d` and `--timeout` together
|
||||||
|
in `docker-compose up`
|
||||||
|
|
||||||
1.17.1 (2017-11-08)
|
1.17.1 (2017-11-08)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
|
@ -64,8 +64,8 @@ you can specify a test directory, file, module, class or method:
|
|||||||
|
|
||||||
$ script/test/default tests/unit
|
$ script/test/default tests/unit
|
||||||
$ script/test/default tests/unit/cli_test.py
|
$ script/test/default tests/unit/cli_test.py
|
||||||
$ script/test/default tests/unit/config_test.py::ConfigTest
|
$ script/test/default tests/unit/config/config_test.py::ConfigTest
|
||||||
$ script/test/default tests/unit/config_test.py::ConfigTest::test_load
|
$ script/test/default tests/unit/config/config_test.py::ConfigTest::test_load
|
||||||
|
|
||||||
## Finding things to work on
|
## Finding things to work on
|
||||||
|
|
||||||
|
@ -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.17.1'
|
__version__ = '1.18.0-rc1'
|
||||||
|
@ -14,6 +14,8 @@ from distutils.spawn import find_executable
|
|||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
|
import docker
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from . import signals
|
from . import signals
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
@ -22,6 +24,7 @@ from ..bundle import MissingDigests
|
|||||||
from ..bundle import serialize_bundle
|
from ..bundle import serialize_bundle
|
||||||
from ..config import ConfigurationError
|
from ..config import ConfigurationError
|
||||||
from ..config import parse_environment
|
from ..config import parse_environment
|
||||||
|
from ..config import parse_labels
|
||||||
from ..config import resolve_build_args
|
from ..config import resolve_build_args
|
||||||
from ..config.environment import Environment
|
from ..config.environment import Environment
|
||||||
from ..config.serialize import serialize_config
|
from ..config.serialize import serialize_config
|
||||||
@ -230,6 +233,7 @@ class TopLevelCommand(object):
|
|||||||
--force-rm Always remove intermediate containers.
|
--force-rm Always remove intermediate containers.
|
||||||
--no-cache Do not use cache when building the image.
|
--no-cache Do not use cache when building the image.
|
||||||
--pull Always attempt to pull a newer version of the image.
|
--pull Always attempt to pull a newer version of the image.
|
||||||
|
-m, --memory MEM Sets memory limit for the bulid container.
|
||||||
--build-arg key=val Set build-time variables for one service.
|
--build-arg key=val Set build-time variables for one service.
|
||||||
"""
|
"""
|
||||||
service_names = options['SERVICE']
|
service_names = options['SERVICE']
|
||||||
@ -246,6 +250,7 @@ class TopLevelCommand(object):
|
|||||||
no_cache=bool(options.get('--no-cache', False)),
|
no_cache=bool(options.get('--no-cache', False)),
|
||||||
pull=bool(options.get('--pull', False)),
|
pull=bool(options.get('--pull', False)),
|
||||||
force_rm=bool(options.get('--force-rm', False)),
|
force_rm=bool(options.get('--force-rm', False)),
|
||||||
|
memory=options.get('--memory'),
|
||||||
build_args=build_args)
|
build_args=build_args)
|
||||||
|
|
||||||
def bundle(self, config_options, options):
|
def bundle(self, config_options, options):
|
||||||
@ -369,9 +374,12 @@ class TopLevelCommand(object):
|
|||||||
attached to containers.
|
attached to containers.
|
||||||
--remove-orphans Remove containers for services not defined in the
|
--remove-orphans Remove containers for services not defined in the
|
||||||
Compose file
|
Compose file
|
||||||
|
-t, --timeout TIMEOUT Specify a shutdown timeout in seconds.
|
||||||
|
(default: 10)
|
||||||
"""
|
"""
|
||||||
image_type = image_type_from_opt('--rmi', options['--rmi'])
|
image_type = image_type_from_opt('--rmi', options['--rmi'])
|
||||||
self.project.down(image_type, options['--volumes'], options['--remove-orphans'])
|
timeout = timeout_from_opts(options)
|
||||||
|
self.project.down(image_type, options['--volumes'], options['--remove-orphans'], timeout=timeout)
|
||||||
|
|
||||||
def events(self, options):
|
def events(self, options):
|
||||||
"""
|
"""
|
||||||
@ -402,7 +410,7 @@ class TopLevelCommand(object):
|
|||||||
"""
|
"""
|
||||||
Execute a command in a running container
|
Execute a command in a running container
|
||||||
|
|
||||||
Usage: exec [options] SERVICE COMMAND [ARGS...]
|
Usage: exec [options] [-e KEY=VAL...] SERVICE COMMAND [ARGS...]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d Detached mode: Run command in the background.
|
-d Detached mode: Run command in the background.
|
||||||
@ -412,11 +420,16 @@ class TopLevelCommand(object):
|
|||||||
allocates a TTY.
|
allocates a TTY.
|
||||||
--index=index index of the container if there are multiple
|
--index=index index of the container if there are multiple
|
||||||
instances of a service [default: 1]
|
instances of a service [default: 1]
|
||||||
|
-e, --env KEY=VAL Set environment variables (can be used multiple times,
|
||||||
|
not supported in API < 1.25)
|
||||||
"""
|
"""
|
||||||
index = int(options.get('--index'))
|
index = int(options.get('--index'))
|
||||||
service = self.project.get_service(options['SERVICE'])
|
service = self.project.get_service(options['SERVICE'])
|
||||||
detach = options['-d']
|
detach = options['-d']
|
||||||
|
|
||||||
|
if options['--env'] and docker.utils.version_lt(self.project.client.api_version, '1.25'):
|
||||||
|
raise UserError("Setting environment for exec is not supported in API < 1.25'")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
container = service.get_container(number=index)
|
container = service.get_container(number=index)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -425,26 +438,7 @@ class TopLevelCommand(object):
|
|||||||
tty = not options["-T"]
|
tty = not options["-T"]
|
||||||
|
|
||||||
if IS_WINDOWS_PLATFORM and not detach:
|
if IS_WINDOWS_PLATFORM and not detach:
|
||||||
args = ["exec"]
|
sys.exit(call_docker(build_exec_command(options, container.id, command)))
|
||||||
|
|
||||||
if options["-d"]:
|
|
||||||
args += ["--detach"]
|
|
||||||
else:
|
|
||||||
args += ["--interactive"]
|
|
||||||
|
|
||||||
if not options["-T"]:
|
|
||||||
args += ["--tty"]
|
|
||||||
|
|
||||||
if options["--privileged"]:
|
|
||||||
args += ["--privileged"]
|
|
||||||
|
|
||||||
if options["--user"]:
|
|
||||||
args += ["--user", options["--user"]]
|
|
||||||
|
|
||||||
args += [container.id]
|
|
||||||
args += command
|
|
||||||
|
|
||||||
sys.exit(call_docker(args))
|
|
||||||
|
|
||||||
create_exec_options = {
|
create_exec_options = {
|
||||||
"privileged": options["--privileged"],
|
"privileged": options["--privileged"],
|
||||||
@ -453,6 +447,9 @@ class TopLevelCommand(object):
|
|||||||
"stdin": tty,
|
"stdin": tty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if docker.utils.version_gte(self.project.client.api_version, '1.25'):
|
||||||
|
create_exec_options["environment"] = options["--env"]
|
||||||
|
|
||||||
exec_id = container.create_exec(command, **create_exec_options)
|
exec_id = container.create_exec(command, **create_exec_options)
|
||||||
|
|
||||||
if detach:
|
if detach:
|
||||||
@ -729,7 +726,9 @@ class TopLevelCommand(object):
|
|||||||
running. If you do not want to start linked services, use
|
running. If you do not want to start linked services, use
|
||||||
`docker-compose run --no-deps SERVICE COMMAND [ARGS...]`.
|
`docker-compose run --no-deps SERVICE COMMAND [ARGS...]`.
|
||||||
|
|
||||||
Usage: run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] SERVICE [COMMAND] [ARGS...]
|
Usage:
|
||||||
|
run [options] [-v VOLUME...] [-p PORT...] [-e KEY=VAL...] [-l KEY=VALUE...]
|
||||||
|
SERVICE [COMMAND] [ARGS...]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d Detached mode: Run container in the background, print
|
-d Detached mode: Run container in the background, print
|
||||||
@ -737,6 +736,7 @@ class TopLevelCommand(object):
|
|||||||
--name NAME Assign a name to the container
|
--name NAME Assign a name to the container
|
||||||
--entrypoint CMD Override the entrypoint of the image.
|
--entrypoint CMD Override the entrypoint of the image.
|
||||||
-e KEY=VAL Set an environment variable (can be used multiple times)
|
-e KEY=VAL Set an environment variable (can be used multiple times)
|
||||||
|
-l, --label KEY=VAL Add or override a label (can be used multiple times)
|
||||||
-u, --user="" Run as specified username or uid
|
-u, --user="" Run as specified username or uid
|
||||||
--no-deps Don't start linked services.
|
--no-deps Don't start linked services.
|
||||||
--rm Remove container after run. Ignored in detached mode.
|
--rm Remove container after run. Ignored in detached mode.
|
||||||
@ -898,8 +898,8 @@ class TopLevelCommand(object):
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
-d Detached mode: Run containers in the background,
|
-d Detached mode: Run containers in the background,
|
||||||
print new container names.
|
print new container names. Incompatible with
|
||||||
Incompatible with --abort-on-container-exit.
|
--abort-on-container-exit and --timeout.
|
||||||
--no-color Produce monochrome output.
|
--no-color Produce monochrome output.
|
||||||
--no-deps Don't start linked services.
|
--no-deps Don't start linked services.
|
||||||
--force-recreate Recreate containers even if their configuration
|
--force-recreate Recreate containers even if their configuration
|
||||||
@ -913,7 +913,8 @@ class TopLevelCommand(object):
|
|||||||
--abort-on-container-exit Stops all containers if any container was stopped.
|
--abort-on-container-exit Stops all containers if any container was stopped.
|
||||||
Incompatible with -d.
|
Incompatible with -d.
|
||||||
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
|
-t, --timeout TIMEOUT Use this timeout in seconds for container shutdown
|
||||||
when attached or when containers are already
|
when attached or when containers are already.
|
||||||
|
Incompatible with -d.
|
||||||
running. (default: 10)
|
running. (default: 10)
|
||||||
--remove-orphans Remove containers for services not
|
--remove-orphans Remove containers for services not
|
||||||
defined in the Compose file
|
defined in the Compose file
|
||||||
@ -934,6 +935,9 @@ class TopLevelCommand(object):
|
|||||||
if detached and (cascade_stop or exit_value_from):
|
if detached and (cascade_stop or exit_value_from):
|
||||||
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
raise UserError("--abort-on-container-exit and -d cannot be combined.")
|
||||||
|
|
||||||
|
if detached and timeout:
|
||||||
|
raise UserError("-d and --timeout cannot be combined.")
|
||||||
|
|
||||||
if no_start:
|
if no_start:
|
||||||
for excluded in ['-d', '--abort-on-container-exit', '--exit-code-from']:
|
for excluded in ['-d', '--abort-on-container-exit', '--exit-code-from']:
|
||||||
if options.get(excluded):
|
if options.get(excluded):
|
||||||
@ -1131,6 +1135,9 @@ def build_container_options(options, detach, command):
|
|||||||
parse_environment(options['-e'])
|
parse_environment(options['-e'])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if options['--label']:
|
||||||
|
container_options['labels'] = parse_labels(options['--label'])
|
||||||
|
|
||||||
if options['--entrypoint']:
|
if options['--entrypoint']:
|
||||||
container_options['entrypoint'] = options.get('--entrypoint')
|
container_options['entrypoint'] = options.get('--entrypoint')
|
||||||
|
|
||||||
@ -1295,3 +1302,29 @@ def parse_scale_args(options):
|
|||||||
)
|
)
|
||||||
res[service_name] = num
|
res[service_name] = num
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def build_exec_command(options, container_id, command):
|
||||||
|
args = ["exec"]
|
||||||
|
|
||||||
|
if options["-d"]:
|
||||||
|
args += ["--detach"]
|
||||||
|
else:
|
||||||
|
args += ["--interactive"]
|
||||||
|
|
||||||
|
if not options["-T"]:
|
||||||
|
args += ["--tty"]
|
||||||
|
|
||||||
|
if options["--privileged"]:
|
||||||
|
args += ["--privileged"]
|
||||||
|
|
||||||
|
if options["--user"]:
|
||||||
|
args += ["--user", options["--user"]]
|
||||||
|
|
||||||
|
if options["--env"]:
|
||||||
|
for env_variable in options["--env"]:
|
||||||
|
args += ["--env", env_variable]
|
||||||
|
|
||||||
|
args += [container_id]
|
||||||
|
args += command
|
||||||
|
return args
|
||||||
|
@ -8,5 +8,7 @@ from .config import DOCKER_CONFIG_KEYS
|
|||||||
from .config import find
|
from .config import find
|
||||||
from .config import load
|
from .config import load
|
||||||
from .config import merge_environment
|
from .config import merge_environment
|
||||||
|
from .config import merge_labels
|
||||||
from .config import parse_environment
|
from .config import parse_environment
|
||||||
|
from .config import parse_labels
|
||||||
from .config import resolve_build_args
|
from .config import resolve_build_args
|
||||||
|
@ -35,6 +35,7 @@ from .interpolation import interpolate_environment_variables
|
|||||||
from .sort_services import get_container_name_from_network_mode
|
from .sort_services import get_container_name_from_network_mode
|
||||||
from .sort_services import get_service_name_from_network_mode
|
from .sort_services import get_service_name_from_network_mode
|
||||||
from .sort_services import sort_service_dicts
|
from .sort_services import sort_service_dicts
|
||||||
|
from .types import MountSpec
|
||||||
from .types import parse_extra_hosts
|
from .types import parse_extra_hosts
|
||||||
from .types import parse_restart_spec
|
from .types import parse_restart_spec
|
||||||
from .types import ServiceLink
|
from .types import ServiceLink
|
||||||
@ -47,6 +48,7 @@ from .validation import validate_config_section
|
|||||||
from .validation import validate_cpu
|
from .validation import validate_cpu
|
||||||
from .validation import validate_depends_on
|
from .validation import validate_depends_on
|
||||||
from .validation import validate_extends_file_path
|
from .validation import validate_extends_file_path
|
||||||
|
from .validation import validate_healthcheck
|
||||||
from .validation import validate_links
|
from .validation import validate_links
|
||||||
from .validation import validate_network_mode
|
from .validation import validate_network_mode
|
||||||
from .validation import validate_pid_mode
|
from .validation import validate_pid_mode
|
||||||
@ -90,6 +92,7 @@ DOCKER_CONFIG_KEYS = [
|
|||||||
'mem_swappiness',
|
'mem_swappiness',
|
||||||
'net',
|
'net',
|
||||||
'oom_score_adj',
|
'oom_score_adj',
|
||||||
|
'oom_kill_disable',
|
||||||
'pid',
|
'pid',
|
||||||
'ports',
|
'ports',
|
||||||
'privileged',
|
'privileged',
|
||||||
@ -407,12 +410,11 @@ def load_mapping(config_files, get_func, entity_type, working_dir=None):
|
|||||||
|
|
||||||
external = config.get('external')
|
external = config.get('external')
|
||||||
if external:
|
if external:
|
||||||
name_field = 'name' if entity_type == 'Volume' else 'external_name'
|
|
||||||
validate_external(entity_type, name, config, config_file.version)
|
validate_external(entity_type, name, config, config_file.version)
|
||||||
if isinstance(external, dict):
|
if isinstance(external, dict):
|
||||||
config[name_field] = external.get('name')
|
config['name'] = external.get('name')
|
||||||
elif not config.get('name'):
|
elif not config.get('name'):
|
||||||
config[name_field] = name
|
config['name'] = name
|
||||||
|
|
||||||
if 'driver_opts' in config:
|
if 'driver_opts' in config:
|
||||||
config['driver_opts'] = build_string_dict(
|
config['driver_opts'] = build_string_dict(
|
||||||
@ -519,13 +521,13 @@ def process_config_file(config_file, environment, service_name=None):
|
|||||||
processed_config['secrets'] = interpolate_config_section(
|
processed_config['secrets'] = interpolate_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_secrets(),
|
config_file.get_secrets(),
|
||||||
'secrets',
|
'secret',
|
||||||
environment)
|
environment)
|
||||||
if config_file.version >= const.COMPOSEFILE_V3_3:
|
if config_file.version >= const.COMPOSEFILE_V3_3:
|
||||||
processed_config['configs'] = interpolate_config_section(
|
processed_config['configs'] = interpolate_config_section(
|
||||||
config_file,
|
config_file,
|
||||||
config_file.get_configs(),
|
config_file.get_configs(),
|
||||||
'configs',
|
'config',
|
||||||
environment
|
environment
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -686,6 +688,7 @@ def validate_service(service_config, service_names, config_file):
|
|||||||
validate_pid_mode(service_config, service_names)
|
validate_pid_mode(service_config, service_names)
|
||||||
validate_depends_on(service_config, service_names)
|
validate_depends_on(service_config, service_names)
|
||||||
validate_links(service_config, service_names)
|
validate_links(service_config, service_names)
|
||||||
|
validate_healthcheck(service_config)
|
||||||
|
|
||||||
if not service_dict.get('image') and has_uppercase(service_name):
|
if not service_dict.get('image') and has_uppercase(service_name):
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
@ -724,7 +727,7 @@ def process_service(service_config):
|
|||||||
service_dict[field] = to_list(service_dict[field])
|
service_dict[field] = to_list(service_dict[field])
|
||||||
|
|
||||||
service_dict = process_blkio_config(process_ports(
|
service_dict = process_blkio_config(process_ports(
|
||||||
process_healthcheck(service_dict, service_config.name)
|
process_healthcheck(service_dict)
|
||||||
))
|
))
|
||||||
|
|
||||||
return service_dict
|
return service_dict
|
||||||
@ -788,33 +791,35 @@ def process_blkio_config(service_dict):
|
|||||||
return service_dict
|
return service_dict
|
||||||
|
|
||||||
|
|
||||||
def process_healthcheck(service_dict, service_name):
|
def process_healthcheck(service_dict):
|
||||||
if 'healthcheck' not in service_dict:
|
if 'healthcheck' not in service_dict:
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
||||||
hc = {}
|
hc = service_dict['healthcheck']
|
||||||
raw = service_dict['healthcheck']
|
|
||||||
|
|
||||||
if raw.get('disable'):
|
if 'disable' in hc:
|
||||||
if len(raw) > 1:
|
del hc['disable']
|
||||||
raise ConfigurationError(
|
|
||||||
'Service "{}" defines an invalid healthcheck: '
|
|
||||||
'"disable: true" cannot be combined with other options'
|
|
||||||
.format(service_name))
|
|
||||||
hc['test'] = ['NONE']
|
hc['test'] = ['NONE']
|
||||||
elif 'test' in raw:
|
|
||||||
hc['test'] = raw['test']
|
|
||||||
|
|
||||||
for field in ['interval', 'timeout', 'start_period']:
|
for field in ['interval', 'timeout', 'start_period']:
|
||||||
if field in raw:
|
if field not in hc or isinstance(hc[field], six.integer_types):
|
||||||
if not isinstance(raw[field], six.integer_types):
|
continue
|
||||||
hc[field] = parse_nanoseconds_int(raw[field])
|
hc[field] = parse_nanoseconds_int(hc[field])
|
||||||
else: # Conversion has been done previously
|
|
||||||
hc[field] = raw[field]
|
return service_dict
|
||||||
if 'retries' in raw:
|
|
||||||
hc['retries'] = raw['retries']
|
|
||||||
|
def finalize_service_volumes(service_dict, environment):
|
||||||
|
if 'volumes' in service_dict:
|
||||||
|
finalized_volumes = []
|
||||||
|
normalize = environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
|
||||||
|
for v in service_dict['volumes']:
|
||||||
|
if isinstance(v, dict):
|
||||||
|
finalized_volumes.append(MountSpec.parse(v, normalize))
|
||||||
|
else:
|
||||||
|
finalized_volumes.append(VolumeSpec.parse(v, normalize))
|
||||||
|
service_dict['volumes'] = finalized_volumes
|
||||||
|
|
||||||
service_dict['healthcheck'] = hc
|
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
||||||
|
|
||||||
@ -831,12 +836,7 @@ def finalize_service(service_config, service_names, version, environment):
|
|||||||
for vf in service_dict['volumes_from']
|
for vf in service_dict['volumes_from']
|
||||||
]
|
]
|
||||||
|
|
||||||
if 'volumes' in service_dict:
|
service_dict = finalize_service_volumes(service_dict, environment)
|
||||||
service_dict['volumes'] = [
|
|
||||||
VolumeSpec.parse(
|
|
||||||
v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS')
|
|
||||||
) for v in service_dict['volumes']
|
|
||||||
]
|
|
||||||
|
|
||||||
if 'net' in service_dict:
|
if 'net' in service_dict:
|
||||||
network_mode = service_dict.pop('net')
|
network_mode = service_dict.pop('net')
|
||||||
@ -1032,6 +1032,7 @@ def merge_build(output, base, override):
|
|||||||
md.merge_mapping('args', parse_build_arguments)
|
md.merge_mapping('args', parse_build_arguments)
|
||||||
md.merge_field('cache_from', merge_unique_items_lists, default=[])
|
md.merge_field('cache_from', merge_unique_items_lists, default=[])
|
||||||
md.merge_mapping('labels', parse_labels)
|
md.merge_mapping('labels', parse_labels)
|
||||||
|
md.merge_mapping('extra_hosts', parse_extra_hosts)
|
||||||
return dict(md)
|
return dict(md)
|
||||||
|
|
||||||
|
|
||||||
@ -1084,6 +1085,12 @@ def merge_environment(base, override):
|
|||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def merge_labels(base, override):
|
||||||
|
labels = parse_labels(base)
|
||||||
|
labels.update(parse_labels(override))
|
||||||
|
return labels
|
||||||
|
|
||||||
|
|
||||||
def split_kv(kvpair):
|
def split_kv(kvpair):
|
||||||
if '=' in kvpair:
|
if '=' in kvpair:
|
||||||
return kvpair.split('=', 1)
|
return kvpair.split('=', 1)
|
||||||
@ -1145,19 +1152,13 @@ def resolve_volume_paths(working_dir, service_dict):
|
|||||||
|
|
||||||
|
|
||||||
def resolve_volume_path(working_dir, volume):
|
def resolve_volume_path(working_dir, volume):
|
||||||
mount_params = None
|
|
||||||
if isinstance(volume, dict):
|
if isinstance(volume, dict):
|
||||||
container_path = volume.get('target')
|
if volume.get('source', '').startswith('.') and volume['type'] == 'mount':
|
||||||
host_path = volume.get('source')
|
volume['source'] = expand_path(working_dir, volume['source'])
|
||||||
mode = None
|
return volume
|
||||||
if host_path:
|
|
||||||
if volume.get('read_only'):
|
mount_params = None
|
||||||
mode = 'ro'
|
container_path, mount_params = split_path_mapping(volume)
|
||||||
if volume.get('volume', {}).get('nocopy'):
|
|
||||||
mode = 'nocopy'
|
|
||||||
mount_params = (host_path, mode)
|
|
||||||
else:
|
|
||||||
container_path, mount_params = split_path_mapping(volume)
|
|
||||||
|
|
||||||
if mount_params is not None:
|
if mount_params is not None:
|
||||||
host_path, mode = mount_params
|
host_path, mode = mount_params
|
||||||
|
@ -229,6 +229,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"oom_kill_disable": {"type": "boolean"},
|
||||||
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
||||||
"group_add": {
|
"group_add": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -349,7 +350,8 @@
|
|||||||
},
|
},
|
||||||
"internal": {"type": "boolean"},
|
"internal": {"type": "boolean"},
|
||||||
"enable_ipv6": {"type": "boolean"},
|
"enable_ipv6": {"type": "boolean"},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"name": {"type": "string"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
@ -235,6 +235,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"oom_kill_disable": {"type": "boolean"},
|
||||||
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
||||||
"group_add": {
|
"group_add": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -356,7 +357,8 @@
|
|||||||
},
|
},
|
||||||
"internal": {"type": "boolean"},
|
"internal": {"type": "boolean"},
|
||||||
"enable_ipv6": {"type": "boolean"},
|
"enable_ipv6": {"type": "boolean"},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"name": {"type": "string"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
@ -92,7 +92,8 @@
|
|||||||
"cache_from": {"$ref": "#/definitions/list_of_strings"},
|
"cache_from": {"$ref": "#/definitions/list_of_strings"},
|
||||||
"network": {"type": "string"},
|
"network": {"type": "string"},
|
||||||
"target": {"type": "string"},
|
"target": {"type": "string"},
|
||||||
"shm_size": {"type": ["integer", "string"]}
|
"shm_size": {"type": ["integer", "string"]},
|
||||||
|
"extra_hosts": {"$ref": "#/definitions/list_or_dict"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -237,6 +238,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"oom_kill_disable": {"type": "boolean"},
|
||||||
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
|
||||||
"group_add": {
|
"group_add": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@ -291,7 +293,39 @@
|
|||||||
},
|
},
|
||||||
"user": {"type": "string"},
|
"user": {"type": "string"},
|
||||||
"userns_mode": {"type": "string"},
|
"userns_mode": {"type": "string"},
|
||||||
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
"volumes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{"type": "string"},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"type": {"type": "string"},
|
||||||
|
"source": {"type": "string"},
|
||||||
|
"target": {"type": "string"},
|
||||||
|
"read_only": {"type": "boolean"},
|
||||||
|
"consistency": {"type": "string"},
|
||||||
|
"bind": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"propagation": {"type": "string"}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"volume": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"nocopy": {"type": "boolean"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"volume_driver": {"type": "string"},
|
"volume_driver": {"type": "string"},
|
||||||
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
"working_dir": {"type": "string"}
|
"working_dir": {"type": "string"}
|
||||||
@ -359,7 +393,8 @@
|
|||||||
},
|
},
|
||||||
"internal": {"type": "boolean"},
|
"internal": {"type": "boolean"},
|
||||||
"enable_ipv6": {"type": "boolean"},
|
"enable_ipv6": {"type": "boolean"},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
|
"name": {"type": "string"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
@ -294,7 +294,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"subnet": {"type": "string"}
|
"subnet": {"type": "string", "format": "subnet_ip_address"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -323,7 +323,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"subnet": {"type": "string"}
|
"subnet": {"type": "string", "format": "subnet_ip_address"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,7 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["type"],
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {"type": "string"},
|
"type": {"type": "string"},
|
||||||
"source": {"type": "string"},
|
"source": {"type": "string"},
|
||||||
@ -369,7 +370,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"subnet": {"type": "string"}
|
"subnet": {"type": "string", "format": "subnet_ip_address"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -278,6 +278,7 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["type"],
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {"type": "string"},
|
"type": {"type": "string"},
|
||||||
"source": {"type": "string"},
|
"source": {"type": "string"},
|
||||||
@ -412,7 +413,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"subnet": {"type": "string"}
|
"subnet": {"type": "string", "format": "subnet_ip_address"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -282,6 +282,7 @@
|
|||||||
{
|
{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["type"],
|
"required": ["type"],
|
||||||
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {"type": "string"},
|
"type": {"type": "string"},
|
||||||
"source": {"type": "string"},
|
"source": {"type": "string"},
|
||||||
@ -420,7 +421,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"subnet": {"type": "string"}
|
"subnet": {"type": "string", "format": "subnet_ip_address"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"patternProperties": {"^x-": {}},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|
||||||
"definitions": {
|
"definitions": {
|
||||||
@ -154,6 +155,7 @@
|
|||||||
"hostname": {"type": "string"},
|
"hostname": {"type": "string"},
|
||||||
"image": {"type": "string"},
|
"image": {"type": "string"},
|
||||||
"ipc": {"type": "string"},
|
"ipc": {"type": "string"},
|
||||||
|
"isolation": {"type": "string"},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
"labels": {"$ref": "#/definitions/list_or_dict"},
|
||||||
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
|
||||||
|
|
||||||
@ -299,7 +301,8 @@
|
|||||||
"nocopy": {"type": "boolean"}
|
"nocopy": {"type": "boolean"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"uniqueItems": true
|
"uniqueItems": true
|
||||||
@ -316,7 +319,7 @@
|
|||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"disable": {"type": "boolean"},
|
"disable": {"type": "boolean"},
|
||||||
"interval": {"type": "string"},
|
"interval": {"type": "string", "format": "duration"},
|
||||||
"retries": {"type": "number"},
|
"retries": {"type": "number"},
|
||||||
"test": {
|
"test": {
|
||||||
"oneOf": [
|
"oneOf": [
|
||||||
@ -324,7 +327,8 @@
|
|||||||
{"type": "array", "items": {"type": "string"}}
|
{"type": "array", "items": {"type": "string"}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"timeout": {"type": "string"}
|
"timeout": {"type": "string", "format": "duration"},
|
||||||
|
"start_period": {"type": "string", "format": "duration"}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"deployment": {
|
"deployment": {
|
||||||
@ -352,8 +356,23 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"limits": {"$ref": "#/definitions/resource"},
|
"limits": {
|
||||||
"reservations": {"$ref": "#/definitions/resource"}
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cpus": {"type": "string"},
|
||||||
|
"memory": {"type": "string"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"reservations": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cpus": {"type": "string"},
|
||||||
|
"memory": {"type": "string"},
|
||||||
|
"generic_resources": {"$ref": "#/definitions/generic_resources"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
@ -388,20 +407,30 @@
|
|||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
||||||
"resource": {
|
"generic_resources": {
|
||||||
"id": "#/definitions/resource",
|
"id": "#/definitions/generic_resources",
|
||||||
"type": "object",
|
"type": "array",
|
||||||
"properties": {
|
"items": {
|
||||||
"cpus": {"type": "string"},
|
"type": "object",
|
||||||
"memory": {"type": "string"}
|
"properties": {
|
||||||
},
|
"discrete_resource_spec": {
|
||||||
"additionalProperties": false
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {"type": "string"},
|
||||||
|
"value": {"type": "number"}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"network": {
|
"network": {
|
||||||
"id": "#/definitions/network",
|
"id": "#/definitions/network",
|
||||||
"type": ["object", "null"],
|
"type": ["object", "null"],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
"driver": {"type": "string"},
|
"driver": {"type": "string"},
|
||||||
"driver_opts": {
|
"driver_opts": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@ -418,7 +447,7 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"subnet": {"type": "string"}
|
"subnet": {"type": "string", "format": "subnet_ip_address"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
@ -468,6 +497,7 @@
|
|||||||
"id": "#/definitions/secret",
|
"id": "#/definitions/secret",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
"file": {"type": "string"},
|
"file": {"type": "string"},
|
||||||
"external": {
|
"external": {
|
||||||
"type": ["boolean", "object"],
|
"type": ["boolean", "object"],
|
||||||
@ -484,6 +514,7 @@
|
|||||||
"id": "#/definitions/config",
|
"id": "#/definitions/config",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
"file": {"type": "string"},
|
"file": {"type": "string"},
|
||||||
"external": {
|
"external": {
|
||||||
"type": ["boolean", "object"],
|
"type": ["boolean", "object"],
|
||||||
|
@ -32,7 +32,7 @@ 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 = {}
|
||||||
with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj:
|
with contextlib.closing(codecs.open(filename, 'r', 'utf-8-sig')) as fileobj:
|
||||||
for line in fileobj:
|
for line in fileobj:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line and not line.startswith('#'):
|
if line and not line.startswith('#'):
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import absolute_import
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from string import Template
|
from string import Template
|
||||||
|
|
||||||
import six
|
import six
|
||||||
@ -44,9 +45,13 @@ def interpolate_environment_variables(version, config, section, environment):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_config_path(config_key, section, name):
|
||||||
|
return '{}.{}.{}'.format(section, name, config_key)
|
||||||
|
|
||||||
|
|
||||||
def interpolate_value(name, config_key, value, section, interpolator):
|
def interpolate_value(name, config_key, value, section, interpolator):
|
||||||
try:
|
try:
|
||||||
return recursive_interpolate(value, interpolator)
|
return recursive_interpolate(value, interpolator, get_config_path(config_key, section, name))
|
||||||
except InvalidInterpolation as e:
|
except InvalidInterpolation as e:
|
||||||
raise ConfigurationError(
|
raise ConfigurationError(
|
||||||
'Invalid interpolation format for "{config_key}" option '
|
'Invalid interpolation format for "{config_key}" option '
|
||||||
@ -57,21 +62,24 @@ def interpolate_value(name, config_key, value, section, interpolator):
|
|||||||
string=e.string))
|
string=e.string))
|
||||||
|
|
||||||
|
|
||||||
def recursive_interpolate(obj, interpolator):
|
def recursive_interpolate(obj, interpolator, config_path):
|
||||||
|
def append(config_path, key):
|
||||||
|
return '{}.{}'.format(config_path, key)
|
||||||
|
|
||||||
if isinstance(obj, six.string_types):
|
if isinstance(obj, six.string_types):
|
||||||
return interpolator.interpolate(obj)
|
return converter.convert(config_path, interpolator.interpolate(obj))
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return dict(
|
return dict(
|
||||||
(key, recursive_interpolate(val, interpolator))
|
(key, recursive_interpolate(val, interpolator, append(config_path, key)))
|
||||||
for (key, val) in obj.items()
|
for (key, val) in obj.items()
|
||||||
)
|
)
|
||||||
if isinstance(obj, list):
|
if isinstance(obj, list):
|
||||||
return [recursive_interpolate(val, interpolator) for val in obj]
|
return [recursive_interpolate(val, interpolator, config_path) for val in obj]
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class TemplateWithDefaults(Template):
|
class TemplateWithDefaults(Template):
|
||||||
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]+)?'
|
idpattern = r'[_a-z][_a-z0-9]*(?::?-[^}]*)?'
|
||||||
|
|
||||||
# Modified from python2.7/string.py
|
# Modified from python2.7/string.py
|
||||||
def substitute(self, mapping):
|
def substitute(self, mapping):
|
||||||
@ -100,3 +108,81 @@ class TemplateWithDefaults(Template):
|
|||||||
class InvalidInterpolation(Exception):
|
class InvalidInterpolation(Exception):
|
||||||
def __init__(self, string):
|
def __init__(self, string):
|
||||||
self.string = string
|
self.string = string
|
||||||
|
|
||||||
|
|
||||||
|
PATH_JOKER = '[^.]+'
|
||||||
|
|
||||||
|
|
||||||
|
def re_path(*args):
|
||||||
|
return re.compile('^{}$'.format('.'.join(args)))
|
||||||
|
|
||||||
|
|
||||||
|
def re_path_basic(section, name):
|
||||||
|
return re_path(section, PATH_JOKER, name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_path(*args):
|
||||||
|
return re_path('service', PATH_JOKER, *args)
|
||||||
|
|
||||||
|
|
||||||
|
def to_boolean(s):
|
||||||
|
s = s.lower()
|
||||||
|
if s in ['y', 'yes', 'true', 'on']:
|
||||||
|
return True
|
||||||
|
elif s in ['n', 'no', 'false', 'off']:
|
||||||
|
return False
|
||||||
|
raise ValueError('"{}" is not a valid boolean value'.format(s))
|
||||||
|
|
||||||
|
|
||||||
|
def to_int(s):
|
||||||
|
# We must be able to handle octal representation for `mode` values notably
|
||||||
|
if six.PY3 and re.match('^0[0-9]+$', s.strip()):
|
||||||
|
s = '0o' + s[1:]
|
||||||
|
return int(s, base=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ConversionMap(object):
|
||||||
|
map = {
|
||||||
|
service_path('blkio_config', 'weight'): to_int,
|
||||||
|
service_path('blkio_config', 'weight_device', 'weight'): to_int,
|
||||||
|
service_path('cpus'): float,
|
||||||
|
service_path('cpu_count'): to_int,
|
||||||
|
service_path('configs', 'mode'): to_int,
|
||||||
|
service_path('secrets', 'mode'): to_int,
|
||||||
|
service_path('healthcheck', 'retries'): to_int,
|
||||||
|
service_path('healthcheck', 'disable'): to_boolean,
|
||||||
|
service_path('deploy', 'replicas'): to_int,
|
||||||
|
service_path('deploy', 'update_config', 'parallelism'): to_int,
|
||||||
|
service_path('deploy', 'update_config', 'max_failure_ratio'): float,
|
||||||
|
service_path('deploy', 'restart_policy', 'max_attempts'): to_int,
|
||||||
|
service_path('mem_swappiness'): to_int,
|
||||||
|
service_path('oom_kill_disable'): to_boolean,
|
||||||
|
service_path('oom_score_adj'): to_int,
|
||||||
|
service_path('ports', 'target'): to_int,
|
||||||
|
service_path('ports', 'published'): to_int,
|
||||||
|
service_path('scale'): to_int,
|
||||||
|
service_path('ulimits', PATH_JOKER): to_int,
|
||||||
|
service_path('ulimits', PATH_JOKER, 'soft'): to_int,
|
||||||
|
service_path('ulimits', PATH_JOKER, 'hard'): to_int,
|
||||||
|
service_path('privileged'): to_boolean,
|
||||||
|
service_path('read_only'): to_boolean,
|
||||||
|
service_path('stdin_open'): to_boolean,
|
||||||
|
service_path('tty'): to_boolean,
|
||||||
|
service_path('volumes', 'read_only'): to_boolean,
|
||||||
|
service_path('volumes', 'volume', 'nocopy'): to_boolean,
|
||||||
|
re_path_basic('network', 'attachable'): to_boolean,
|
||||||
|
re_path_basic('network', 'external'): to_boolean,
|
||||||
|
re_path_basic('network', 'internal'): to_boolean,
|
||||||
|
re_path_basic('volume', 'external'): to_boolean,
|
||||||
|
re_path_basic('secret', 'external'): to_boolean,
|
||||||
|
re_path_basic('config', 'external'): to_boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert(self, path, value):
|
||||||
|
for rexp in self.map.keys():
|
||||||
|
if rexp.match(path):
|
||||||
|
return self.map[rexp](value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
converter = ConversionMap()
|
||||||
|
@ -7,9 +7,11 @@ import yaml
|
|||||||
from compose.config import types
|
from compose.config import types
|
||||||
from compose.const import COMPOSEFILE_V1 as V1
|
from compose.const import COMPOSEFILE_V1 as V1
|
||||||
from compose.const import COMPOSEFILE_V2_1 as V2_1
|
from compose.const import COMPOSEFILE_V2_1 as V2_1
|
||||||
|
from compose.const import COMPOSEFILE_V2_3 as V2_3
|
||||||
from compose.const import COMPOSEFILE_V3_0 as V3_0
|
from compose.const import COMPOSEFILE_V3_0 as V3_0
|
||||||
from compose.const import COMPOSEFILE_V3_2 as V3_2
|
from compose.const import COMPOSEFILE_V3_2 as V3_2
|
||||||
from compose.const import COMPOSEFILE_V3_4 as V3_4
|
from compose.const import COMPOSEFILE_V3_4 as V3_4
|
||||||
|
from compose.const import COMPOSEFILE_V3_5 as V3_5
|
||||||
|
|
||||||
|
|
||||||
def serialize_config_type(dumper, data):
|
def serialize_config_type(dumper, data):
|
||||||
@ -34,6 +36,7 @@ def serialize_string(dumper, data):
|
|||||||
return representer(data)
|
return representer(data)
|
||||||
|
|
||||||
|
|
||||||
|
yaml.SafeDumper.add_representer(types.MountSpec, serialize_dict_type)
|
||||||
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
yaml.SafeDumper.add_representer(types.VolumeFromSpec, serialize_config_type)
|
||||||
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
|
yaml.SafeDumper.add_representer(types.VolumeSpec, serialize_config_type)
|
||||||
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
|
yaml.SafeDumper.add_representer(types.ServiceSecret, serialize_dict_type)
|
||||||
@ -67,7 +70,8 @@ def denormalize_config(config, image_digests=None):
|
|||||||
del conf['external_name']
|
del conf['external_name']
|
||||||
|
|
||||||
if 'name' in conf:
|
if 'name' in conf:
|
||||||
if config.version < V2_1 or (config.version >= V3_0 and config.version < V3_4):
|
if config.version < V2_1 or (
|
||||||
|
config.version >= V3_0 and config.version < v3_introduced_name_key(key)):
|
||||||
del conf['name']
|
del conf['name']
|
||||||
elif 'external' in conf:
|
elif 'external' in conf:
|
||||||
conf['external'] = True
|
conf['external'] = True
|
||||||
@ -75,6 +79,12 @@ def denormalize_config(config, image_digests=None):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def v3_introduced_name_key(key):
|
||||||
|
if key == 'volumes':
|
||||||
|
return V3_4
|
||||||
|
return V3_5
|
||||||
|
|
||||||
|
|
||||||
def serialize_config(config, image_digests=None):
|
def serialize_config(config, image_digests=None):
|
||||||
return yaml.safe_dump(
|
return yaml.safe_dump(
|
||||||
denormalize_config(config, image_digests),
|
denormalize_config(config, image_digests),
|
||||||
@ -141,5 +151,9 @@ def denormalize_service_dict(service_dict, version, image_digest=None):
|
|||||||
p.legacy_repr() if isinstance(p, types.ServicePort) else p
|
p.legacy_repr() if isinstance(p, types.ServicePort) else p
|
||||||
for p in service_dict['ports']
|
for p in service_dict['ports']
|
||||||
]
|
]
|
||||||
|
if 'volumes' in service_dict and (version < V2_3 or (version > V3_0 and version < V3_2)):
|
||||||
|
service_dict['volumes'] = [
|
||||||
|
v.legacy_repr() if isinstance(v, types.MountSpec) else v for v in service_dict['volumes']
|
||||||
|
]
|
||||||
|
|
||||||
return service_dict
|
return service_dict
|
||||||
|
@ -133,6 +133,61 @@ def normalize_path_for_engine(path):
|
|||||||
return path.replace('\\', '/')
|
return path.replace('\\', '/')
|
||||||
|
|
||||||
|
|
||||||
|
class MountSpec(object):
|
||||||
|
options_map = {
|
||||||
|
'volume': {
|
||||||
|
'nocopy': 'no_copy'
|
||||||
|
},
|
||||||
|
'bind': {
|
||||||
|
'propagation': 'propagation'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_fields = ['type', 'source', 'target', 'read_only', 'consistency']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, mount_dict, normalize=False):
|
||||||
|
if mount_dict.get('source'):
|
||||||
|
mount_dict['source'] = os.path.normpath(mount_dict['source'])
|
||||||
|
if normalize:
|
||||||
|
mount_dict['source'] = normalize_path_for_engine(mount_dict['source'])
|
||||||
|
|
||||||
|
return cls(**mount_dict)
|
||||||
|
|
||||||
|
def __init__(self, type, source=None, target=None, read_only=None, consistency=None, **kwargs):
|
||||||
|
self.type = type
|
||||||
|
self.source = source
|
||||||
|
self.target = target
|
||||||
|
self.read_only = read_only
|
||||||
|
self.consistency = consistency
|
||||||
|
self.options = None
|
||||||
|
if self.type in kwargs:
|
||||||
|
self.options = kwargs[self.type]
|
||||||
|
|
||||||
|
def as_volume_spec(self):
|
||||||
|
mode = 'ro' if self.read_only else 'rw'
|
||||||
|
return VolumeSpec(external=self.source, internal=self.target, mode=mode)
|
||||||
|
|
||||||
|
def legacy_repr(self):
|
||||||
|
return self.as_volume_spec().repr()
|
||||||
|
|
||||||
|
def repr(self):
|
||||||
|
res = {}
|
||||||
|
for field in self._fields:
|
||||||
|
if getattr(self, field, None):
|
||||||
|
res[field] = getattr(self, field)
|
||||||
|
if self.options:
|
||||||
|
res[self.type] = self.options
|
||||||
|
return res
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_named_volume(self):
|
||||||
|
return self.type == 'volume' and self.source
|
||||||
|
|
||||||
|
@property
|
||||||
|
def external(self):
|
||||||
|
return self.source
|
||||||
|
|
||||||
|
|
||||||
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
class VolumeSpec(namedtuple('_VolumeSpec', 'external internal mode')):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -238,17 +293,18 @@ class ServiceLink(namedtuple('_ServiceLink', 'target alias')):
|
|||||||
return self.alias
|
return self.alias
|
||||||
|
|
||||||
|
|
||||||
class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid mode')):
|
class ServiceConfigBase(namedtuple('_ServiceConfigBase', 'source target uid gid mode name')):
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, spec):
|
def parse(cls, spec):
|
||||||
if isinstance(spec, six.string_types):
|
if isinstance(spec, six.string_types):
|
||||||
return cls(spec, None, None, None, None)
|
return cls(spec, None, None, None, None, None)
|
||||||
return cls(
|
return cls(
|
||||||
spec.get('source'),
|
spec.get('source'),
|
||||||
spec.get('target'),
|
spec.get('target'),
|
||||||
spec.get('uid'),
|
spec.get('uid'),
|
||||||
spec.get('gid'),
|
spec.get('gid'),
|
||||||
spec.get('mode'),
|
spec.get('mode'),
|
||||||
|
spec.get('name')
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -277,11 +333,19 @@ class ServicePort(namedtuple('_ServicePort', 'target published protocol mode ext
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raise ConfigurationError('Invalid target port: {}'.format(target))
|
raise ConfigurationError('Invalid target port: {}'.format(target))
|
||||||
|
|
||||||
try:
|
if published:
|
||||||
if published:
|
if isinstance(published, six.string_types) and '-' in published: # "x-y:z" format
|
||||||
published = int(published)
|
a, b = published.split('-', 1)
|
||||||
except ValueError:
|
try:
|
||||||
raise ConfigurationError('Invalid published port: {}'.format(published))
|
int(a)
|
||||||
|
int(b)
|
||||||
|
except ValueError:
|
||||||
|
raise ConfigurationError('Invalid published port: {}'.format(published))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
published = int(published)
|
||||||
|
except ValueError:
|
||||||
|
raise ConfigurationError('Invalid published port: {}'.format(published))
|
||||||
|
|
||||||
return super(ServicePort, cls).__new__(
|
return super(ServicePort, cls).__new__(
|
||||||
cls, target, published, *args, **kwargs
|
cls, target, published, *args, **kwargs
|
||||||
|
@ -44,6 +44,31 @@ DOCKER_CONFIG_HINTS = {
|
|||||||
VALID_NAME_CHARS = '[a-zA-Z0-9\._\-]'
|
VALID_NAME_CHARS = '[a-zA-Z0-9\._\-]'
|
||||||
VALID_EXPOSE_FORMAT = r'^\d+(\-\d+)?(\/[a-zA-Z]+)?$'
|
VALID_EXPOSE_FORMAT = r'^\d+(\-\d+)?(\/[a-zA-Z]+)?$'
|
||||||
|
|
||||||
|
VALID_IPV4_SEG = r'(\d{1,2}|1\d{2}|2[0-4]\d|25[0-5])'
|
||||||
|
VALID_IPV4_ADDR = "({IPV4_SEG}\.){{3}}{IPV4_SEG}".format(IPV4_SEG=VALID_IPV4_SEG)
|
||||||
|
VALID_REGEX_IPV4_CIDR = "^{IPV4_ADDR}/(\d|[1-2]\d|3[0-2])$".format(IPV4_ADDR=VALID_IPV4_ADDR)
|
||||||
|
|
||||||
|
VALID_IPV6_SEG = r'[0-9a-fA-F]{1,4}'
|
||||||
|
VALID_REGEX_IPV6_CIDR = "".join("""
|
||||||
|
^
|
||||||
|
(
|
||||||
|
(({IPV6_SEG}:){{7}}{IPV6_SEG})|
|
||||||
|
(({IPV6_SEG}:){{1,7}}:)|
|
||||||
|
(({IPV6_SEG}:){{1,6}}(:{IPV6_SEG}){{1,1}})|
|
||||||
|
(({IPV6_SEG}:){{1,5}}(:{IPV6_SEG}){{1,2}})|
|
||||||
|
(({IPV6_SEG}:){{1,4}}(:{IPV6_SEG}){{1,3}})|
|
||||||
|
(({IPV6_SEG}:){{1,3}}(:{IPV6_SEG}){{1,4}})|
|
||||||
|
(({IPV6_SEG}:){{1,2}}(:{IPV6_SEG}){{1,5}})|
|
||||||
|
(({IPV6_SEG}:){{1,1}}(:{IPV6_SEG}){{1,6}})|
|
||||||
|
(:((:{IPV6_SEG}){{1,7}}|:))|
|
||||||
|
(fe80:(:{IPV6_SEG}){{0,4}}%[0-9a-zA-Z]{{1,}})|
|
||||||
|
(::(ffff(:0{{1,4}}){{0,1}}:){{0,1}}{IPV4_ADDR})|
|
||||||
|
(({IPV6_SEG}:){{1,4}}:{IPV4_ADDR})
|
||||||
|
)
|
||||||
|
/(\d|[1-9]\d|1[0-1]\d|12[0-8])
|
||||||
|
$
|
||||||
|
""".format(IPV6_SEG=VALID_IPV6_SEG, IPV4_ADDR=VALID_IPV4_ADDR).split())
|
||||||
|
|
||||||
|
|
||||||
@FormatChecker.cls_checks(format="ports", raises=ValidationError)
|
@FormatChecker.cls_checks(format="ports", raises=ValidationError)
|
||||||
def format_ports(instance):
|
def format_ports(instance):
|
||||||
@ -64,6 +89,16 @@ def format_expose(instance):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@FormatChecker.cls_checks("subnet_ip_address", raises=ValidationError)
|
||||||
|
def format_subnet_ip_address(instance):
|
||||||
|
if isinstance(instance, six.string_types):
|
||||||
|
if not re.match(VALID_REGEX_IPV4_CIDR, instance) and \
|
||||||
|
not re.match(VALID_REGEX_IPV6_CIDR, instance):
|
||||||
|
raise ValidationError("should use the CIDR format")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def match_named_volumes(service_dict, project_volumes):
|
def match_named_volumes(service_dict, project_volumes):
|
||||||
service_volumes = service_dict.get('volumes', [])
|
service_volumes = service_dict.get('volumes', [])
|
||||||
for volume_spec in service_volumes:
|
for volume_spec in service_volumes:
|
||||||
@ -391,7 +426,7 @@ def process_config_schema_errors(error):
|
|||||||
|
|
||||||
def validate_against_config_schema(config_file):
|
def validate_against_config_schema(config_file):
|
||||||
schema = load_jsonschema(config_file)
|
schema = load_jsonschema(config_file)
|
||||||
format_checker = FormatChecker(["ports", "expose"])
|
format_checker = FormatChecker(["ports", "expose", "subnet_ip_address"])
|
||||||
validator = Draft4Validator(
|
validator = Draft4Validator(
|
||||||
schema,
|
schema,
|
||||||
resolver=RefResolver(get_resolver_path(), schema),
|
resolver=RefResolver(get_resolver_path(), schema),
|
||||||
@ -465,3 +500,27 @@ def handle_errors(errors, format_error_func, filename):
|
|||||||
"The Compose file{file_msg} is invalid because:\n{error_msg}".format(
|
"The Compose file{file_msg} is invalid because:\n{error_msg}".format(
|
||||||
file_msg=" '{}'".format(filename) if filename else "",
|
file_msg=" '{}'".format(filename) if filename else "",
|
||||||
error_msg=error_msg))
|
error_msg=error_msg))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_healthcheck(service_config):
|
||||||
|
healthcheck = service_config.config.get('healthcheck', {})
|
||||||
|
|
||||||
|
if 'test' in healthcheck and isinstance(healthcheck['test'], list):
|
||||||
|
if len(healthcheck['test']) == 0:
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Service "{}" defines an invalid healthcheck: '
|
||||||
|
'"test" is an empty list'
|
||||||
|
.format(service_config.name))
|
||||||
|
|
||||||
|
# when disable is true config.py::process_healthcheck adds "test: ['NONE']" to service_config
|
||||||
|
elif healthcheck['test'][0] == 'NONE' and len(healthcheck) > 1:
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Service "{}" defines an invalid healthcheck: '
|
||||||
|
'"disable: true" cannot be combined with other options'
|
||||||
|
.format(service_config.name))
|
||||||
|
|
||||||
|
elif healthcheck['test'][0] not in ('NONE', 'CMD', 'CMD-SHELL'):
|
||||||
|
raise ConfigurationError(
|
||||||
|
'Service "{}" defines an invalid healthcheck: '
|
||||||
|
'when "test" is a list the first item must be either NONE, CMD or CMD-SHELL'
|
||||||
|
.format(service_config.name))
|
||||||
|
@ -25,21 +25,22 @@ OPTS_EXCEPTIONS = [
|
|||||||
|
|
||||||
class Network(object):
|
class Network(object):
|
||||||
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
def __init__(self, client, project, name, driver=None, driver_opts=None,
|
||||||
ipam=None, external_name=None, internal=False, enable_ipv6=False,
|
ipam=None, external=False, internal=False, enable_ipv6=False,
|
||||||
labels=None):
|
labels=None, custom_name=False):
|
||||||
self.client = client
|
self.client = client
|
||||||
self.project = project
|
self.project = project
|
||||||
self.name = name
|
self.name = name
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
self.driver_opts = driver_opts
|
self.driver_opts = driver_opts
|
||||||
self.ipam = create_ipam_config_from_dict(ipam)
|
self.ipam = create_ipam_config_from_dict(ipam)
|
||||||
self.external_name = external_name
|
self.external = external
|
||||||
self.internal = internal
|
self.internal = internal
|
||||||
self.enable_ipv6 = enable_ipv6
|
self.enable_ipv6 = enable_ipv6
|
||||||
self.labels = labels
|
self.labels = labels
|
||||||
|
self.custom_name = custom_name
|
||||||
|
|
||||||
def ensure(self):
|
def ensure(self):
|
||||||
if self.external_name:
|
if self.external:
|
||||||
try:
|
try:
|
||||||
self.inspect()
|
self.inspect()
|
||||||
log.debug(
|
log.debug(
|
||||||
@ -51,7 +52,7 @@ class Network(object):
|
|||||||
'Network {name} declared as external, but could'
|
'Network {name} declared as external, but could'
|
||||||
' not be found. Please create the network manually'
|
' not be found. Please create the network manually'
|
||||||
' using `{command} {name}` and try again.'.format(
|
' using `{command} {name}` and try again.'.format(
|
||||||
name=self.external_name,
|
name=self.full_name,
|
||||||
command='docker network create'
|
command='docker network create'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -83,7 +84,7 @@ class Network(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def remove(self):
|
def remove(self):
|
||||||
if self.external_name:
|
if self.external:
|
||||||
log.info("Network %s is external, skipping", self.full_name)
|
log.info("Network %s is external, skipping", self.full_name)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -95,8 +96,8 @@ class Network(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
if self.external_name:
|
if self.custom_name:
|
||||||
return self.external_name
|
return self.name
|
||||||
return '{0}_{1}'.format(self.project, self.name)
|
return '{0}_{1}'.format(self.project, self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -116,7 +117,7 @@ def create_ipam_config_from_dict(ipam_dict):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return IPAMConfig(
|
return IPAMConfig(
|
||||||
driver=ipam_dict.get('driver'),
|
driver=ipam_dict.get('driver') or 'default',
|
||||||
pool_configs=[
|
pool_configs=[
|
||||||
IPAMPool(
|
IPAMPool(
|
||||||
subnet=config.get('subnet'),
|
subnet=config.get('subnet'),
|
||||||
@ -203,14 +204,16 @@ def build_networks(name, config_data, client):
|
|||||||
network_config = config_data.networks or {}
|
network_config = config_data.networks or {}
|
||||||
networks = {
|
networks = {
|
||||||
network_name: Network(
|
network_name: Network(
|
||||||
client=client, project=name, name=network_name,
|
client=client, project=name,
|
||||||
|
name=data.get('name', network_name),
|
||||||
driver=data.get('driver'),
|
driver=data.get('driver'),
|
||||||
driver_opts=data.get('driver_opts'),
|
driver_opts=data.get('driver_opts'),
|
||||||
ipam=data.get('ipam'),
|
ipam=data.get('ipam'),
|
||||||
external_name=data.get('external_name'),
|
external=bool(data.get('external', False)),
|
||||||
internal=data.get('internal'),
|
internal=data.get('internal'),
|
||||||
enable_ipv6=data.get('enable_ipv6'),
|
enable_ipv6=data.get('enable_ipv6'),
|
||||||
labels=data.get('labels'),
|
labels=data.get('labels'),
|
||||||
|
custom_name=data.get('name') is not None,
|
||||||
)
|
)
|
||||||
for network_name, data in network_config.items()
|
for network_name, data in network_config.items()
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ log = logging.getLogger(__name__)
|
|||||||
STOP = object()
|
STOP = object()
|
||||||
|
|
||||||
|
|
||||||
def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None):
|
def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None, parent_objects=None):
|
||||||
"""Runs func on objects in parallel while ensuring that func is
|
"""Runs func on objects in parallel while ensuring that func is
|
||||||
ran on object only after it is ran on all its dependencies.
|
ran on object only after it is ran on all its dependencies.
|
||||||
|
|
||||||
@ -37,9 +37,19 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None):
|
|||||||
stream = get_output_stream(sys.stderr)
|
stream = get_output_stream(sys.stderr)
|
||||||
|
|
||||||
writer = ParallelStreamWriter(stream, msg)
|
writer = ParallelStreamWriter(stream, msg)
|
||||||
for obj in objects:
|
|
||||||
|
if parent_objects:
|
||||||
|
display_objects = list(parent_objects)
|
||||||
|
else:
|
||||||
|
display_objects = objects
|
||||||
|
|
||||||
|
for obj in display_objects:
|
||||||
writer.add_object(get_name(obj))
|
writer.add_object(get_name(obj))
|
||||||
writer.write_initial()
|
|
||||||
|
# write data in a second loop to consider all objects for width alignment
|
||||||
|
# and avoid duplicates when parent_objects exists
|
||||||
|
for obj in objects:
|
||||||
|
writer.write_initial(get_name(obj))
|
||||||
|
|
||||||
events = parallel_execute_iter(objects, func, get_deps, limit)
|
events = parallel_execute_iter(objects, func, get_deps, limit)
|
||||||
|
|
||||||
@ -237,12 +247,11 @@ class ParallelStreamWriter(object):
|
|||||||
self.lines.append(obj_index)
|
self.lines.append(obj_index)
|
||||||
self.width = max(self.width, len(obj_index))
|
self.width = max(self.width, len(obj_index))
|
||||||
|
|
||||||
def write_initial(self):
|
def write_initial(self, obj_index):
|
||||||
if self.msg is None:
|
if self.msg is None:
|
||||||
return
|
return
|
||||||
for line in self.lines:
|
self.stream.write("{} {:<{width}} ... \r\n".format(
|
||||||
self.stream.write("{} {:<{width}} ... \r\n".format(self.msg, line,
|
self.msg, self.lines[self.lines.index(obj_index)], width=self.width))
|
||||||
width=self.width))
|
|
||||||
self.stream.flush()
|
self.stream.flush()
|
||||||
|
|
||||||
def _write_ansi(self, obj_index, status):
|
def _write_ansi(self, obj_index, status):
|
||||||
|
@ -29,6 +29,7 @@ from .service import ConvergenceStrategy
|
|||||||
from .service import NetworkMode
|
from .service import NetworkMode
|
||||||
from .service import PidMode
|
from .service import PidMode
|
||||||
from .service import Service
|
from .service import Service
|
||||||
|
from .service import ServiceName
|
||||||
from .service import ServiceNetworkMode
|
from .service import ServiceNetworkMode
|
||||||
from .service import ServicePidMode
|
from .service import ServicePidMode
|
||||||
from .utils import microseconds_from_time_nano
|
from .utils import microseconds_from_time_nano
|
||||||
@ -190,6 +191,25 @@ class Project(object):
|
|||||||
service.remove_duplicate_containers()
|
service.remove_duplicate_containers()
|
||||||
return services
|
return services
|
||||||
|
|
||||||
|
def get_scaled_services(self, services, scale_override):
|
||||||
|
"""
|
||||||
|
Returns a list of this project's services as scaled ServiceName objects.
|
||||||
|
|
||||||
|
services: a list of Service objects
|
||||||
|
scale_override: a dict with the scale to apply to each service (k: service_name, v: scale)
|
||||||
|
"""
|
||||||
|
service_names = []
|
||||||
|
for service in services:
|
||||||
|
if service.name in scale_override:
|
||||||
|
scale = scale_override[service.name]
|
||||||
|
else:
|
||||||
|
scale = service.scale_num
|
||||||
|
|
||||||
|
for i in range(1, scale + 1):
|
||||||
|
service_names.append(ServiceName(self.name, service.name, i))
|
||||||
|
|
||||||
|
return service_names
|
||||||
|
|
||||||
def get_links(self, service_dict):
|
def get_links(self, service_dict):
|
||||||
links = []
|
links = []
|
||||||
if 'links' in service_dict:
|
if 'links' in service_dict:
|
||||||
@ -310,8 +330,8 @@ class Project(object):
|
|||||||
service_names, stopped=True, one_off=one_off
|
service_names, stopped=True, one_off=one_off
|
||||||
), options)
|
), options)
|
||||||
|
|
||||||
def down(self, remove_image_type, include_volumes, remove_orphans=False):
|
def down(self, remove_image_type, include_volumes, remove_orphans=False, timeout=None):
|
||||||
self.stop(one_off=OneOffFilter.include)
|
self.stop(one_off=OneOffFilter.include, timeout=timeout)
|
||||||
self.find_orphan_containers(remove_orphans)
|
self.find_orphan_containers(remove_orphans)
|
||||||
self.remove_stopped(v=include_volumes, one_off=OneOffFilter.include)
|
self.remove_stopped(v=include_volumes, one_off=OneOffFilter.include)
|
||||||
|
|
||||||
@ -337,10 +357,11 @@ class Project(object):
|
|||||||
)
|
)
|
||||||
return containers
|
return containers
|
||||||
|
|
||||||
def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, build_args=None):
|
def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, memory=None,
|
||||||
|
build_args=None):
|
||||||
for service in self.get_services(service_names):
|
for service in self.get_services(service_names):
|
||||||
if service.can_be_built():
|
if service.can_be_built():
|
||||||
service.build(no_cache, pull, force_rm, build_args)
|
service.build(no_cache, pull, force_rm, memory, build_args)
|
||||||
else:
|
else:
|
||||||
log.info('%s uses an image, skipping' % service.name)
|
log.info('%s uses an image, skipping' % service.name)
|
||||||
|
|
||||||
@ -430,15 +451,18 @@ class Project(object):
|
|||||||
for svc in services:
|
for svc in services:
|
||||||
svc.ensure_image_exists(do_build=do_build)
|
svc.ensure_image_exists(do_build=do_build)
|
||||||
plans = self._get_convergence_plans(services, strategy)
|
plans = self._get_convergence_plans(services, strategy)
|
||||||
|
scaled_services = self.get_scaled_services(services, scale_override)
|
||||||
|
|
||||||
def do(service):
|
def do(service):
|
||||||
|
|
||||||
return service.execute_convergence_plan(
|
return service.execute_convergence_plan(
|
||||||
plans[service.name],
|
plans[service.name],
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
detached=detached,
|
detached=detached,
|
||||||
scale_override=scale_override.get(service.name),
|
scale_override=scale_override.get(service.name),
|
||||||
rescale=rescale,
|
rescale=rescale,
|
||||||
start=start
|
start=start,
|
||||||
|
project_services=scaled_services
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_deps(service):
|
def get_deps(service):
|
||||||
@ -624,7 +648,7 @@ def get_secrets(service, service_secrets, secret_defs):
|
|||||||
"Service \"{service}\" uses an undefined secret \"{secret}\" "
|
"Service \"{service}\" uses an undefined secret \"{secret}\" "
|
||||||
.format(service=service, secret=secret.source))
|
.format(service=service, secret=secret.source))
|
||||||
|
|
||||||
if secret_def.get('external_name'):
|
if secret_def.get('external'):
|
||||||
log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
|
log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. "
|
||||||
"External secrets are not available to containers created by "
|
"External secrets are not available to containers created by "
|
||||||
"docker-compose.".format(service=service, secret=secret.source))
|
"docker-compose.".format(service=service, secret=secret.source))
|
||||||
|
@ -14,6 +14,9 @@ from docker.errors import APIError
|
|||||||
from docker.errors import ImageNotFound
|
from docker.errors import ImageNotFound
|
||||||
from docker.errors import NotFound
|
from docker.errors import NotFound
|
||||||
from docker.types import LogConfig
|
from docker.types import LogConfig
|
||||||
|
from docker.types import Mount
|
||||||
|
from docker.utils import version_gte
|
||||||
|
from docker.utils import version_lt
|
||||||
from docker.utils.ports import build_port_bindings
|
from docker.utils.ports import build_port_bindings
|
||||||
from docker.utils.ports import split_port
|
from docker.utils.ports import split_port
|
||||||
from docker.utils.utils import convert_tmpfs_mounts
|
from docker.utils.utils import convert_tmpfs_mounts
|
||||||
@ -23,7 +26,9 @@ 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
|
||||||
|
from .config import merge_labels
|
||||||
from .config.errors import DependencyError
|
from .config.errors import DependencyError
|
||||||
|
from .config.types import MountSpec
|
||||||
from .config.types import ServicePort
|
from .config.types import ServicePort
|
||||||
from .config.types import VolumeSpec
|
from .config.types import VolumeSpec
|
||||||
from .const import DEFAULT_TIMEOUT
|
from .const import DEFAULT_TIMEOUT
|
||||||
@ -76,6 +81,7 @@ HOST_CONFIG_KEYS = [
|
|||||||
'mem_reservation',
|
'mem_reservation',
|
||||||
'memswap_limit',
|
'memswap_limit',
|
||||||
'mem_swappiness',
|
'mem_swappiness',
|
||||||
|
'oom_kill_disable',
|
||||||
'oom_score_adj',
|
'oom_score_adj',
|
||||||
'pid',
|
'pid',
|
||||||
'pids_limit',
|
'pids_limit',
|
||||||
@ -378,11 +384,11 @@ class Service(object):
|
|||||||
|
|
||||||
return has_diverged
|
return has_diverged
|
||||||
|
|
||||||
def _execute_convergence_create(self, scale, detached, start):
|
def _execute_convergence_create(self, scale, detached, start, project_services=None):
|
||||||
i = self._next_container_number()
|
i = self._next_container_number()
|
||||||
|
|
||||||
def create_and_start(service, n):
|
def create_and_start(service, n):
|
||||||
container = service.create_container(number=n)
|
container = service.create_container(number=n, quiet=True)
|
||||||
if not detached:
|
if not detached:
|
||||||
container.attach_log_stream()
|
container.attach_log_stream()
|
||||||
if start:
|
if start:
|
||||||
@ -390,10 +396,11 @@ class Service(object):
|
|||||||
return container
|
return container
|
||||||
|
|
||||||
containers, errors = parallel_execute(
|
containers, errors = parallel_execute(
|
||||||
range(i, i + scale),
|
[ServiceName(self.project, self.name, index) for index in range(i, i + scale)],
|
||||||
lambda n: create_and_start(self, n),
|
lambda service_name: create_and_start(self, service_name.number),
|
||||||
lambda n: self.get_container_name(n),
|
lambda service_name: self.get_container_name(service_name.service, service_name.number),
|
||||||
"Creating",
|
"Creating",
|
||||||
|
parent_objects=project_services
|
||||||
)
|
)
|
||||||
for error in errors.values():
|
for error in errors.values():
|
||||||
raise OperationFailedError(error)
|
raise OperationFailedError(error)
|
||||||
@ -432,7 +439,7 @@ class Service(object):
|
|||||||
if start:
|
if start:
|
||||||
_, errors = parallel_execute(
|
_, errors = parallel_execute(
|
||||||
containers,
|
containers,
|
||||||
lambda c: self.start_container_if_stopped(c, attach_logs=not detached),
|
lambda c: self.start_container_if_stopped(c, attach_logs=not detached, quiet=True),
|
||||||
lambda c: c.name,
|
lambda c: c.name,
|
||||||
"Starting",
|
"Starting",
|
||||||
)
|
)
|
||||||
@ -459,7 +466,7 @@ class Service(object):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def execute_convergence_plan(self, plan, timeout=None, detached=False,
|
def execute_convergence_plan(self, plan, timeout=None, detached=False,
|
||||||
start=True, scale_override=None, rescale=True):
|
start=True, scale_override=None, rescale=True, project_services=None):
|
||||||
(action, containers) = plan
|
(action, containers) = plan
|
||||||
scale = scale_override if scale_override is not None else self.scale_num
|
scale = scale_override if scale_override is not None else self.scale_num
|
||||||
containers = sorted(containers, key=attrgetter('number'))
|
containers = sorted(containers, key=attrgetter('number'))
|
||||||
@ -468,7 +475,7 @@ class Service(object):
|
|||||||
|
|
||||||
if action == 'create':
|
if action == 'create':
|
||||||
return self._execute_convergence_create(
|
return self._execute_convergence_create(
|
||||||
scale, detached, start
|
scale, detached, start, project_services
|
||||||
)
|
)
|
||||||
|
|
||||||
# The create action needs always needs an initial scale, but otherwise,
|
# The create action needs always needs an initial scale, but otherwise,
|
||||||
@ -510,7 +517,6 @@ class Service(object):
|
|||||||
volumes can be copied to the new container, before the original
|
volumes can be copied to the new container, before the original
|
||||||
container is removed.
|
container is removed.
|
||||||
"""
|
"""
|
||||||
log.info("Recreating %s" % container.name)
|
|
||||||
|
|
||||||
container.stop(timeout=self.stop_timeout(timeout))
|
container.stop(timeout=self.stop_timeout(timeout))
|
||||||
container.rename_to_tmp_name()
|
container.rename_to_tmp_name()
|
||||||
@ -741,21 +747,26 @@ class Service(object):
|
|||||||
container_options.update(override_options)
|
container_options.update(override_options)
|
||||||
|
|
||||||
if not container_options.get('name'):
|
if not container_options.get('name'):
|
||||||
container_options['name'] = self.get_container_name(number, one_off)
|
container_options['name'] = self.get_container_name(self.name, number, one_off)
|
||||||
|
|
||||||
container_options.setdefault('detach', True)
|
container_options.setdefault('detach', True)
|
||||||
|
|
||||||
# If a qualified hostname was given, split it into an
|
# If a qualified hostname was given, split it into an
|
||||||
# unqualified hostname and a domainname unless domainname
|
# unqualified hostname and a domainname unless domainname
|
||||||
# was also given explicitly. This matches the behavior of
|
# was also given explicitly. This matches behavior
|
||||||
# the official Docker CLI in that scenario.
|
# until Docker Engine 1.11.0 - Docker API 1.23.
|
||||||
if ('hostname' in container_options and
|
if (version_lt(self.client.api_version, '1.23') and
|
||||||
|
'hostname' in container_options and
|
||||||
'domainname' not in container_options and
|
'domainname' not in container_options and
|
||||||
'.' in container_options['hostname']):
|
'.' in container_options['hostname']):
|
||||||
parts = container_options['hostname'].partition('.')
|
parts = container_options['hostname'].partition('.')
|
||||||
container_options['hostname'] = parts[0]
|
container_options['hostname'] = parts[0]
|
||||||
container_options['domainname'] = parts[2]
|
container_options['domainname'] = parts[2]
|
||||||
|
|
||||||
|
if (version_gte(self.client.api_version, '1.25') and
|
||||||
|
'stop_grace_period' in self.options):
|
||||||
|
container_options['stop_timeout'] = self.stop_timeout(None)
|
||||||
|
|
||||||
if 'ports' in container_options or 'expose' in self.options:
|
if 'ports' in container_options or 'expose' in self.options:
|
||||||
container_options['ports'] = build_container_ports(
|
container_options['ports'] = build_container_ports(
|
||||||
formatted_ports(container_options.get('ports', [])),
|
formatted_ports(container_options.get('ports', [])),
|
||||||
@ -770,21 +781,38 @@ class Service(object):
|
|||||||
self.options.get('environment'),
|
self.options.get('environment'),
|
||||||
override_options.get('environment'))
|
override_options.get('environment'))
|
||||||
|
|
||||||
|
container_options['labels'] = merge_labels(
|
||||||
|
self.options.get('labels'),
|
||||||
|
override_options.get('labels'))
|
||||||
|
|
||||||
|
container_volumes = []
|
||||||
|
container_mounts = []
|
||||||
|
if 'volumes' in container_options:
|
||||||
|
container_volumes = [
|
||||||
|
v for v in container_options.get('volumes') if isinstance(v, VolumeSpec)
|
||||||
|
]
|
||||||
|
container_mounts = [v for v in container_options.get('volumes') if isinstance(v, MountSpec)]
|
||||||
|
|
||||||
binds, affinity = merge_volume_bindings(
|
binds, affinity = merge_volume_bindings(
|
||||||
container_options.get('volumes') or [],
|
container_volumes, self.options.get('tmpfs') or [], previous_container,
|
||||||
self.options.get('tmpfs') or [],
|
container_mounts
|
||||||
previous_container)
|
)
|
||||||
override_options['binds'] = binds
|
override_options['binds'] = binds
|
||||||
container_options['environment'].update(affinity)
|
container_options['environment'].update(affinity)
|
||||||
|
|
||||||
container_options['volumes'] = dict(
|
container_options['volumes'] = dict((v.internal, {}) for v in container_volumes or {})
|
||||||
(v.internal, {}) for v in container_options.get('volumes') or {})
|
override_options['mounts'] = [build_mount(v) for v in container_mounts] or None
|
||||||
|
|
||||||
secret_volumes = self.get_secret_volumes()
|
secret_volumes = self.get_secret_volumes()
|
||||||
if secret_volumes:
|
if secret_volumes:
|
||||||
override_options['binds'].extend(v.repr() for v in secret_volumes)
|
if version_lt(self.client.api_version, '1.30'):
|
||||||
container_options['volumes'].update(
|
override_options['binds'].extend(v.legacy_repr() for v in secret_volumes)
|
||||||
(v.internal, {}) for v in secret_volumes)
|
container_options['volumes'].update(
|
||||||
|
(v.target, {}) for v in secret_volumes
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
override_options['mounts'] = override_options.get('mounts') or []
|
||||||
|
override_options['mounts'].extend([build_mount(v) for v in secret_volumes])
|
||||||
|
|
||||||
container_options['image'] = self.image_name
|
container_options['image'] = self.image_name
|
||||||
|
|
||||||
@ -857,6 +885,7 @@ class Service(object):
|
|||||||
sysctls=options.get('sysctls'),
|
sysctls=options.get('sysctls'),
|
||||||
pids_limit=options.get('pids_limit'),
|
pids_limit=options.get('pids_limit'),
|
||||||
tmpfs=options.get('tmpfs'),
|
tmpfs=options.get('tmpfs'),
|
||||||
|
oom_kill_disable=options.get('oom_kill_disable'),
|
||||||
oom_score_adj=options.get('oom_score_adj'),
|
oom_score_adj=options.get('oom_score_adj'),
|
||||||
mem_swappiness=options.get('mem_swappiness'),
|
mem_swappiness=options.get('mem_swappiness'),
|
||||||
group_add=options.get('group_add'),
|
group_add=options.get('group_add'),
|
||||||
@ -877,6 +906,7 @@ class Service(object):
|
|||||||
device_read_iops=blkio_config.get('device_read_iops'),
|
device_read_iops=blkio_config.get('device_read_iops'),
|
||||||
device_write_bps=blkio_config.get('device_write_bps'),
|
device_write_bps=blkio_config.get('device_write_bps'),
|
||||||
device_write_iops=blkio_config.get('device_write_iops'),
|
device_write_iops=blkio_config.get('device_write_iops'),
|
||||||
|
mounts=options.get('mounts'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_secret_volumes(self):
|
def get_secret_volumes(self):
|
||||||
@ -887,11 +917,11 @@ class Service(object):
|
|||||||
elif not os.path.isabs(target):
|
elif not os.path.isabs(target):
|
||||||
target = '{}/{}'.format(const.SECRETS_PATH, target)
|
target = '{}/{}'.format(const.SECRETS_PATH, target)
|
||||||
|
|
||||||
return VolumeSpec(secret['file'], target, 'ro')
|
return MountSpec('bind', secret['file'], target, read_only=True)
|
||||||
|
|
||||||
return [build_spec(secret) for secret in self.secrets]
|
return [build_spec(secret) for secret in self.secrets]
|
||||||
|
|
||||||
def build(self, no_cache=False, pull=False, force_rm=False, build_args_override=None):
|
def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_args_override=None):
|
||||||
log.info('Building %s' % self.name)
|
log.info('Building %s' % self.name)
|
||||||
|
|
||||||
build_opts = self.options.get('build', {})
|
build_opts = self.options.get('build', {})
|
||||||
@ -921,6 +951,10 @@ class Service(object):
|
|||||||
network_mode=build_opts.get('network', None),
|
network_mode=build_opts.get('network', None),
|
||||||
target=build_opts.get('target', None),
|
target=build_opts.get('target', None),
|
||||||
shmsize=parse_bytes(build_opts.get('shm_size')) if build_opts.get('shm_size') else None,
|
shmsize=parse_bytes(build_opts.get('shm_size')) if build_opts.get('shm_size') else None,
|
||||||
|
extra_hosts=build_opts.get('extra_hosts', None),
|
||||||
|
container_limits={
|
||||||
|
'memory': parse_bytes(memory) if memory else None
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -960,12 +994,12 @@ class Service(object):
|
|||||||
def custom_container_name(self):
|
def custom_container_name(self):
|
||||||
return self.options.get('container_name')
|
return self.options.get('container_name')
|
||||||
|
|
||||||
def get_container_name(self, number, one_off=False):
|
def get_container_name(self, service_name, number, one_off=False):
|
||||||
if self.custom_container_name and not one_off:
|
if self.custom_container_name and not one_off:
|
||||||
return self.custom_container_name
|
return self.custom_container_name
|
||||||
|
|
||||||
container_name = build_container_name(
|
container_name = build_container_name(
|
||||||
self.project, self.name, number, one_off,
|
self.project, service_name, number, one_off,
|
||||||
)
|
)
|
||||||
ext_links_origins = [l.split(':')[0] for l in self.options.get('external_links', [])]
|
ext_links_origins = [l.split(':')[0] for l in self.options.get('external_links', [])]
|
||||||
if container_name in ext_links_origins:
|
if container_name in ext_links_origins:
|
||||||
@ -1220,32 +1254,40 @@ def parse_repository_tag(repo_path):
|
|||||||
# Volumes
|
# Volumes
|
||||||
|
|
||||||
|
|
||||||
def merge_volume_bindings(volumes, tmpfs, previous_container):
|
def merge_volume_bindings(volumes, tmpfs, previous_container, mounts):
|
||||||
"""Return a list of volume bindings for a container. Container data volumes
|
"""
|
||||||
are replaced by those from the previous container.
|
Return a list of volume bindings for a container. Container data volumes
|
||||||
|
are replaced by those from the previous container.
|
||||||
|
Anonymous mounts are updated in place.
|
||||||
"""
|
"""
|
||||||
affinity = {}
|
affinity = {}
|
||||||
|
|
||||||
volume_bindings = dict(
|
volume_bindings = dict(
|
||||||
build_volume_binding(volume)
|
build_volume_binding(volume)
|
||||||
for volume in volumes
|
for volume in volumes
|
||||||
if volume.external)
|
if volume.external
|
||||||
|
)
|
||||||
|
|
||||||
if previous_container:
|
if previous_container:
|
||||||
old_volumes = get_container_data_volumes(previous_container, volumes, tmpfs)
|
old_volumes, old_mounts = get_container_data_volumes(
|
||||||
|
previous_container, volumes, tmpfs, mounts
|
||||||
|
)
|
||||||
warn_on_masked_volume(volumes, old_volumes, previous_container.service)
|
warn_on_masked_volume(volumes, old_volumes, previous_container.service)
|
||||||
volume_bindings.update(
|
volume_bindings.update(
|
||||||
build_volume_binding(volume) for volume in old_volumes)
|
build_volume_binding(volume) for volume in old_volumes
|
||||||
|
)
|
||||||
|
|
||||||
if old_volumes:
|
if old_volumes or old_mounts:
|
||||||
affinity = {'affinity:container': '=' + previous_container.id}
|
affinity = {'affinity:container': '=' + previous_container.id}
|
||||||
|
|
||||||
return list(volume_bindings.values()), affinity
|
return list(volume_bindings.values()), affinity
|
||||||
|
|
||||||
|
|
||||||
def get_container_data_volumes(container, volumes_option, tmpfs_option):
|
def get_container_data_volumes(container, volumes_option, tmpfs_option, mounts_option):
|
||||||
"""Find the container data volumes that are in `volumes_option`, and return
|
"""
|
||||||
a mapping of volume bindings for those volumes.
|
Find the container data volumes that are in `volumes_option`, and return
|
||||||
|
a mapping of volume bindings for those volumes.
|
||||||
|
Anonymous volume mounts are updated in place instead.
|
||||||
"""
|
"""
|
||||||
volumes = []
|
volumes = []
|
||||||
volumes_option = volumes_option or []
|
volumes_option = volumes_option or []
|
||||||
@ -1284,7 +1326,19 @@ def get_container_data_volumes(container, volumes_option, tmpfs_option):
|
|||||||
volume = volume._replace(external=mount['Name'])
|
volume = volume._replace(external=mount['Name'])
|
||||||
volumes.append(volume)
|
volumes.append(volume)
|
||||||
|
|
||||||
return volumes
|
updated_mounts = False
|
||||||
|
for mount in mounts_option:
|
||||||
|
if mount.type != 'volume':
|
||||||
|
continue
|
||||||
|
|
||||||
|
ctnr_mount = container_mounts.get(mount.target)
|
||||||
|
if not ctnr_mount.get('Name'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
mount.source = ctnr_mount['Name']
|
||||||
|
updated_mounts = True
|
||||||
|
|
||||||
|
return volumes, updated_mounts
|
||||||
|
|
||||||
|
|
||||||
def warn_on_masked_volume(volumes_option, container_volumes, service):
|
def warn_on_masked_volume(volumes_option, container_volumes, service):
|
||||||
@ -1331,6 +1385,18 @@ def build_volume_from(volume_from_spec):
|
|||||||
return "{}:{}".format(volume_from_spec.source.id, volume_from_spec.mode)
|
return "{}:{}".format(volume_from_spec.source.id, volume_from_spec.mode)
|
||||||
|
|
||||||
|
|
||||||
|
def build_mount(mount_spec):
|
||||||
|
kwargs = {}
|
||||||
|
if mount_spec.options:
|
||||||
|
for option, sdk_name in mount_spec.options_map[mount_spec.type].items():
|
||||||
|
if option in mount_spec.options:
|
||||||
|
kwargs[sdk_name] = mount_spec.options[option]
|
||||||
|
|
||||||
|
return Mount(
|
||||||
|
type=mount_spec.type, target=mount_spec.target, source=mount_spec.source,
|
||||||
|
read_only=mount_spec.read_only, consistency=mount_spec.consistency, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
# Labels
|
# Labels
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@ def json_stream(stream):
|
|||||||
|
|
||||||
|
|
||||||
def json_hash(obj):
|
def json_hash(obj):
|
||||||
dump = json.dumps(obj, sort_keys=True, separators=(',', ':'))
|
dump = json.dumps(obj, sort_keys=True, separators=(',', ':'), default=lambda x: x.repr())
|
||||||
h = hashlib.sha256()
|
h = hashlib.sha256()
|
||||||
h.update(dump.encode('utf8'))
|
h.update(dump.encode('utf8'))
|
||||||
return h.hexdigest()
|
return h.hexdigest()
|
||||||
|
@ -7,6 +7,7 @@ from docker.errors import NotFound
|
|||||||
from docker.utils import version_lt
|
from docker.utils import version_lt
|
||||||
|
|
||||||
from .config import ConfigurationError
|
from .config import ConfigurationError
|
||||||
|
from .config.types import VolumeSpec
|
||||||
from .const import LABEL_PROJECT
|
from .const import LABEL_PROJECT
|
||||||
from .const import LABEL_VOLUME
|
from .const import LABEL_VOLUME
|
||||||
|
|
||||||
@ -145,5 +146,9 @@ class ProjectVolumes(object):
|
|||||||
if not volume_spec.is_named_volume:
|
if not volume_spec.is_named_volume:
|
||||||
return volume_spec
|
return volume_spec
|
||||||
|
|
||||||
volume = self.volumes[volume_spec.external]
|
if isinstance(volume_spec, VolumeSpec):
|
||||||
return volume_spec._replace(external=volume.full_name)
|
volume = self.volumes[volume_spec.external]
|
||||||
|
return volume_spec._replace(external=volume.full_name)
|
||||||
|
else:
|
||||||
|
volume_spec.source = self.volumes[volume_spec.source].full_name
|
||||||
|
return volume_spec
|
||||||
|
@ -120,7 +120,7 @@ _docker_compose_build() {
|
|||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "--build-arg --force-rm --help --no-cache --pull" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "--build-arg --force-rm --help --memory --no-cache --pull" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_from_build
|
__docker_compose_services_from_build
|
||||||
@ -403,14 +403,14 @@ _docker_compose_run() {
|
|||||||
__docker_compose_nospace
|
__docker_compose_nospace
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--entrypoint|--name|--user|-u|--volume|-v|--workdir|-w)
|
--entrypoint|--label|-l|--name|--user|-u|--volume|-v|--workdir|-w)
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
case "$cur" in
|
case "$cur" in
|
||||||
-*)
|
-*)
|
||||||
COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --name --no-deps --publish -p --rm --service-ports -T --user -u --volume -v --workdir -w" -- "$cur" ) )
|
COMPREPLY=( $( compgen -W "-d --entrypoint -e --help --label -l --name --no-deps --publish -p --rm --service-ports -T --user -u --volume -v --workdir -w" -- "$cur" ) )
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
__docker_compose_services_all
|
__docker_compose_services_all
|
||||||
|
@ -196,6 +196,7 @@ __docker-compose_subcommand() {
|
|||||||
$opts_help \
|
$opts_help \
|
||||||
"*--build-arg=[Set build-time variables for one service.]:<varname>=<value>: " \
|
"*--build-arg=[Set build-time variables for one service.]:<varname>=<value>: " \
|
||||||
'--force-rm[Always remove intermediate containers.]' \
|
'--force-rm[Always remove intermediate containers.]' \
|
||||||
|
'--memory[Memory limit for the build container.]' \
|
||||||
'--no-cache[Do not use cache when building the image.]' \
|
'--no-cache[Do not use cache when building the image.]' \
|
||||||
'--pull[Always attempt to pull a newer version of the image.]' \
|
'--pull[Always attempt to pull a newer version of the image.]' \
|
||||||
'*:services:__docker-compose_services_from_build' && ret=0
|
'*:services:__docker-compose_services_from_build' && ret=0
|
||||||
|
@ -67,6 +67,11 @@ exe = EXE(pyz,
|
|||||||
'compose/config/config_schema_v3.4.json',
|
'compose/config/config_schema_v3.4.json',
|
||||||
'DATA'
|
'DATA'
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
'compose/config/config_schema_v3.5.json',
|
||||||
|
'compose/config/config_schema_v3.5.json',
|
||||||
|
'DATA'
|
||||||
|
),
|
||||||
(
|
(
|
||||||
'compose/GITSHA',
|
'compose/GITSHA',
|
||||||
'compose/GITSHA',
|
'compose/GITSHA',
|
||||||
|
@ -89,7 +89,7 @@ When prompted build the non-linux binaries and test them.
|
|||||||
Alternatively, you can use the usual commands to install or upgrade Compose:
|
Alternatively, you can use the usual commands to install or upgrade Compose:
|
||||||
|
|
||||||
```
|
```
|
||||||
curl -L https://github.com/docker/compose/releases/download/1.16.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
|
curl -L https://github.com/docker/compose/releases/download/1.16.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
|
||||||
chmod +x /usr/local/bin/docker-compose
|
chmod +x /usr/local/bin/docker-compose
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -2,20 +2,20 @@ backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
|||||||
cached-property==1.3.0
|
cached-property==1.3.0
|
||||||
certifi==2017.4.17
|
certifi==2017.4.17
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
colorama==0.3.9; sys_platform == 'win32'
|
docker==2.6.1
|
||||||
docker==2.5.1
|
|
||||||
docker-pycreds==0.2.1
|
docker-pycreds==0.2.1
|
||||||
dockerpty==0.4.1
|
dockerpty==0.4.1
|
||||||
docopt==0.6.2
|
docopt==0.6.2
|
||||||
enum34==1.1.6; python_version < '3.4'
|
enum34==1.1.6; python_version < '3.4'
|
||||||
functools32==3.2.3.post2; python_version < '3.2'
|
functools32==3.2.3.post2; python_version < '3.2'
|
||||||
|
git+git://github.com/tartley/colorama.git@bd378c725b45eba0b8e5cc091c3ca76a954c92ff; sys_platform == 'win32'
|
||||||
idna==2.5
|
idna==2.5
|
||||||
ipaddress==1.0.18
|
ipaddress==1.0.18
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
pypiwin32==219; sys_platform == 'win32'
|
pypiwin32==219; sys_platform == 'win32'
|
||||||
PySocks==1.6.7
|
PySocks==1.6.7
|
||||||
PyYAML==3.12
|
PyYAML==3.12
|
||||||
requests==2.11.1
|
requests==2.18.4
|
||||||
six==1.10.0
|
six==1.10.0
|
||||||
texttable==0.9.1
|
texttable==0.9.1
|
||||||
urllib3==1.21.1
|
urllib3==1.21.1
|
||||||
|
@ -30,3 +30,8 @@ mkdir $DESTINATION
|
|||||||
wget -O $DESTINATION/docker-compose-Darwin-x86_64 $BASE_BINTRAY_URL/docker-compose-Darwin-x86_64
|
wget -O $DESTINATION/docker-compose-Darwin-x86_64 $BASE_BINTRAY_URL/docker-compose-Darwin-x86_64
|
||||||
wget -O $DESTINATION/docker-compose-Linux-x86_64 $BASE_BINTRAY_URL/docker-compose-Linux-x86_64
|
wget -O $DESTINATION/docker-compose-Linux-x86_64 $BASE_BINTRAY_URL/docker-compose-Linux-x86_64
|
||||||
wget -O $DESTINATION/docker-compose-Windows-x86_64.exe $APPVEYOR_URL
|
wget -O $DESTINATION/docker-compose-Windows-x86_64.exe $APPVEYOR_URL
|
||||||
|
|
||||||
|
echo -e "\n\nCopy the following lines into the integrity check table in the release notes:\n\n"
|
||||||
|
cd $DESTINATION
|
||||||
|
ls | xargs sha256sum | sed 's/ / | /g' | sed -r 's/([^ |]+)/`\1`/g'
|
||||||
|
cd -
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.17.1"
|
VERSION="1.18.0-rc1"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
6
setup.py
6
setup.py
@ -33,10 +33,10 @@ install_requires = [
|
|||||||
'cached-property >= 1.2.0, < 2',
|
'cached-property >= 1.2.0, < 2',
|
||||||
'docopt >= 0.6.1, < 0.7',
|
'docopt >= 0.6.1, < 0.7',
|
||||||
'PyYAML >= 3.10, < 4',
|
'PyYAML >= 3.10, < 4',
|
||||||
'requests >= 2.6.1, != 2.11.0, < 2.12',
|
'requests >= 2.6.1, != 2.11.0, != 2.12.2, != 2.18.0, < 2.19',
|
||||||
'texttable >= 0.9.0, < 0.10',
|
'texttable >= 0.9.0, < 0.10',
|
||||||
'websocket-client >= 0.32.0, < 1.0',
|
'websocket-client >= 0.32.0, < 1.0',
|
||||||
'docker >= 2.5.1, < 3.0',
|
'docker >= 2.6.1, < 3.0',
|
||||||
'dockerpty >= 0.4.1, < 0.5',
|
'dockerpty >= 0.4.1, < 0.5',
|
||||||
'six >= 1.3.0, < 2',
|
'six >= 1.3.0, < 2',
|
||||||
'jsonschema >= 2.5.1, < 3',
|
'jsonschema >= 2.5.1, < 3',
|
||||||
@ -55,7 +55,7 @@ extras_require = {
|
|||||||
':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'],
|
':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'],
|
||||||
':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5'],
|
':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5'],
|
||||||
':python_version < "3.3"': ['ipaddress >= 1.0.16'],
|
':python_version < "3.3"': ['ipaddress >= 1.0.16'],
|
||||||
':sys_platform == "win32"': ['colorama >= 0.3.7, < 0.4'],
|
':sys_platform == "win32"': ['colorama >= 0.3.9, < 0.4'],
|
||||||
'socks': ['PySocks >= 1.5.6, != 1.5.7, < 2'],
|
'socks': ['PySocks >= 1.5.6, != 1.5.7, < 2'],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ from tests.integration.testcases import no_cluster
|
|||||||
from tests.integration.testcases import pull_busybox
|
from tests.integration.testcases import pull_busybox
|
||||||
from tests.integration.testcases import SWARM_SKIP_RM_VOLUMES
|
from tests.integration.testcases import SWARM_SKIP_RM_VOLUMES
|
||||||
from tests.integration.testcases import v2_1_only
|
from tests.integration.testcases import v2_1_only
|
||||||
|
from tests.integration.testcases import v2_2_only
|
||||||
from tests.integration.testcases import v2_only
|
from tests.integration.testcases import v2_only
|
||||||
from tests.integration.testcases import v3_only
|
from tests.integration.testcases import v3_only
|
||||||
|
|
||||||
@ -349,6 +350,22 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_config_external_network_v3_5(self):
|
||||||
|
self.base_dir = 'tests/fixtures/networks'
|
||||||
|
result = self.dispatch(['-f', 'external-networks-v3-5.yml', 'config'])
|
||||||
|
json_result = yaml.load(result.stdout)
|
||||||
|
assert 'networks' in json_result
|
||||||
|
assert json_result['networks'] == {
|
||||||
|
'foo': {
|
||||||
|
'external': True,
|
||||||
|
'name': 'some_foo',
|
||||||
|
},
|
||||||
|
'bar': {
|
||||||
|
'external': True,
|
||||||
|
'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'])
|
||||||
@ -427,13 +444,21 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
'timeout': '1s',
|
'timeout': '1s',
|
||||||
'retries': 5,
|
'retries': 5,
|
||||||
},
|
},
|
||||||
'volumes': [
|
'volumes': [{
|
||||||
'/host/path:/container/path:ro',
|
'read_only': True,
|
||||||
'foobar:/container/volumepath:rw',
|
'source': '/host/path',
|
||||||
'/anonymous',
|
'target': '/container/path',
|
||||||
'foobar:/container/volumepath2:nocopy'
|
'type': 'bind'
|
||||||
],
|
}, {
|
||||||
|
'source': 'foobar', 'target': '/container/volumepath', 'type': 'volume'
|
||||||
|
}, {
|
||||||
|
'target': '/anonymous', 'type': 'volume'
|
||||||
|
}, {
|
||||||
|
'source': 'foobar',
|
||||||
|
'target': '/container/volumepath2',
|
||||||
|
'type': 'volume',
|
||||||
|
'volume': {'nocopy': True}
|
||||||
|
}],
|
||||||
'stop_grace_period': '20s',
|
'stop_grace_period': '20s',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -583,6 +608,12 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
result = self.dispatch(['build', '--no-cache'], None)
|
result = self.dispatch(['build', '--no-cache'], None)
|
||||||
assert 'shm_size: 96' in result.stdout
|
assert 'shm_size: 96' in result.stdout
|
||||||
|
|
||||||
|
def test_build_memory_build_option(self):
|
||||||
|
pull_busybox(self.client)
|
||||||
|
self.base_dir = 'tests/fixtures/build-memory'
|
||||||
|
result = self.dispatch(['build', '--no-cache', '--memory', '96m', 'service'], None)
|
||||||
|
assert 'memory: 100663296' in result.stdout # 96 * 1024 * 1024
|
||||||
|
|
||||||
def test_bundle_with_digests(self):
|
def test_bundle_with_digests(self):
|
||||||
self.base_dir = 'tests/fixtures/bundle-with-digests/'
|
self.base_dir = 'tests/fixtures/bundle-with-digests/'
|
||||||
tmpdir = pytest.ensuretemp('cli_test_bundle')
|
tmpdir = pytest.ensuretemp('cli_test_bundle')
|
||||||
@ -719,12 +750,13 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
def test_run_one_off_with_volume_merge(self):
|
def test_run_one_off_with_volume_merge(self):
|
||||||
self.base_dir = 'tests/fixtures/simple-composefile-volume-ready'
|
self.base_dir = 'tests/fixtures/simple-composefile-volume-ready'
|
||||||
volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files'))
|
volume_path = os.path.abspath(os.path.join(os.getcwd(), self.base_dir, 'files'))
|
||||||
create_host_file(self.client, os.path.join(volume_path, 'example.txt'))
|
node = create_host_file(self.client, os.path.join(volume_path, 'example.txt'))
|
||||||
|
|
||||||
self.dispatch([
|
self.dispatch([
|
||||||
'-f', 'docker-compose.merge.yml',
|
'-f', 'docker-compose.merge.yml',
|
||||||
'run',
|
'run',
|
||||||
'-v', '{}:/data'.format(volume_path),
|
'-v', '{}:/data'.format(volume_path),
|
||||||
|
'-e', 'constraint:node=={}'.format(node if node is not None else '*'),
|
||||||
'simple',
|
'simple',
|
||||||
'test', '-f', '/data/example.txt'
|
'test', '-f', '/data/example.txt'
|
||||||
], returncode=0)
|
], returncode=0)
|
||||||
@ -774,6 +806,27 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
assert 'Removing network v2full_default' in result.stderr
|
assert 'Removing network v2full_default' in result.stderr
|
||||||
assert 'Removing network v2full_front' in result.stderr
|
assert 'Removing network v2full_front' in result.stderr
|
||||||
|
|
||||||
|
def test_down_timeout(self):
|
||||||
|
self.dispatch(['up', '-d'], None)
|
||||||
|
service = self.project.get_service('simple')
|
||||||
|
self.assertEqual(len(service.containers()), 1)
|
||||||
|
self.assertTrue(service.containers()[0].is_running)
|
||||||
|
""
|
||||||
|
|
||||||
|
self.dispatch(['down', '-t', '1'], None)
|
||||||
|
|
||||||
|
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||||
|
|
||||||
|
def test_down_signal(self):
|
||||||
|
self.base_dir = 'tests/fixtures/stop-signal-composefile'
|
||||||
|
self.dispatch(['up', '-d'], None)
|
||||||
|
service = self.project.get_service('simple')
|
||||||
|
self.assertEqual(len(service.containers()), 1)
|
||||||
|
self.assertTrue(service.containers()[0].is_running)
|
||||||
|
|
||||||
|
self.dispatch(['down', '-t', '1'], None)
|
||||||
|
self.assertEqual(len(service.containers(stopped=True)), 0)
|
||||||
|
|
||||||
def test_up_detached(self):
|
def test_up_detached(self):
|
||||||
self.dispatch(['up', '-d'])
|
self.dispatch(['up', '-d'])
|
||||||
service = self.project.get_service('simple')
|
service = self.project.get_service('simple')
|
||||||
@ -1278,18 +1331,9 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
['up', '-d', '--force-recreate', '--no-recreate'],
|
['up', '-d', '--force-recreate', '--no-recreate'],
|
||||||
returncode=1)
|
returncode=1)
|
||||||
|
|
||||||
def test_up_with_timeout(self):
|
def test_up_with_timeout_detached(self):
|
||||||
self.dispatch(['up', '-d', '-t', '1'])
|
result = self.dispatch(['up', '-d', '-t', '1'], returncode=1)
|
||||||
service = self.project.get_service('simple')
|
assert "-d and --timeout cannot be combined." in result.stderr
|
||||||
another = self.project.get_service('another')
|
|
||||||
self.assertEqual(len(service.containers()), 1)
|
|
||||||
self.assertEqual(len(another.containers()), 1)
|
|
||||||
|
|
||||||
# Ensure containers don't have stdin and stdout connected in -d mode
|
|
||||||
config = service.containers()[0].inspect()['Config']
|
|
||||||
self.assertFalse(config['AttachStderr'])
|
|
||||||
self.assertFalse(config['AttachStdout'])
|
|
||||||
self.assertFalse(config['AttachStdin'])
|
|
||||||
|
|
||||||
def test_up_handles_sigint(self):
|
def test_up_handles_sigint(self):
|
||||||
proc = start_process(self.base_dir, ['up', '-t', '2'])
|
proc = start_process(self.base_dir, ['up', '-t', '2'])
|
||||||
@ -1374,6 +1418,31 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
self.assertEqual(stdout, "operator\n")
|
self.assertEqual(stdout, "operator\n")
|
||||||
self.assertEqual(stderr, "")
|
self.assertEqual(stderr, "")
|
||||||
|
|
||||||
|
@v2_2_only()
|
||||||
|
def test_exec_service_with_environment_overridden(self):
|
||||||
|
name = 'service'
|
||||||
|
self.base_dir = 'tests/fixtures/environment-exec'
|
||||||
|
self.dispatch(['up', '-d'])
|
||||||
|
self.assertEqual(len(self.project.containers()), 1)
|
||||||
|
|
||||||
|
stdout, stderr = self.dispatch([
|
||||||
|
'exec',
|
||||||
|
'-T',
|
||||||
|
'-e', 'foo=notbar',
|
||||||
|
'--env', 'alpha=beta',
|
||||||
|
name,
|
||||||
|
'env',
|
||||||
|
])
|
||||||
|
|
||||||
|
# env overridden
|
||||||
|
assert 'foo=notbar' in stdout
|
||||||
|
# keep environment from yaml
|
||||||
|
assert 'hello=world' in stdout
|
||||||
|
# added option from command line
|
||||||
|
assert 'alpha=beta' in stdout
|
||||||
|
|
||||||
|
self.assertEqual(stderr, '')
|
||||||
|
|
||||||
def test_run_service_without_links(self):
|
def test_run_service_without_links(self):
|
||||||
self.base_dir = 'tests/fixtures/links-composefile'
|
self.base_dir = 'tests/fixtures/links-composefile'
|
||||||
self.dispatch(['run', 'console', '/bin/true'])
|
self.dispatch(['run', 'console', '/bin/true'])
|
||||||
@ -1803,6 +1872,17 @@ class CLITestCase(DockerClientTestCase):
|
|||||||
assert 'FOO=bar' in environment
|
assert 'FOO=bar' in environment
|
||||||
assert 'BAR=baz' not in environment
|
assert 'BAR=baz' not in environment
|
||||||
|
|
||||||
|
def test_run_label_flag(self):
|
||||||
|
self.base_dir = 'tests/fixtures/run-labels'
|
||||||
|
name = 'service'
|
||||||
|
self.dispatch(['run', '-l', 'default', '--label', 'foo=baz', name, '/bin/true'])
|
||||||
|
service = self.project.get_service(name)
|
||||||
|
container, = service.containers(stopped=True, one_off=OneOffFilter.only)
|
||||||
|
labels = container.labels
|
||||||
|
assert labels['default'] == ''
|
||||||
|
assert labels['foo'] == 'baz'
|
||||||
|
assert labels['hello'] == 'world'
|
||||||
|
|
||||||
def test_rm(self):
|
def test_rm(self):
|
||||||
service = self.project.get_service('simple')
|
service = self.project.get_service('simple')
|
||||||
service.create_container()
|
service.create_container()
|
||||||
|
4
tests/fixtures/build-memory/Dockerfile
vendored
Normal file
4
tests/fixtures/build-memory/Dockerfile
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
FROM busybox
|
||||||
|
|
||||||
|
# Report the memory (through the size of the group memory)
|
||||||
|
RUN echo "memory:" $(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
|
6
tests/fixtures/build-memory/docker-compose.yml
vendored
Normal file
6
tests/fixtures/build-memory/docker-compose.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
version: '3.5'
|
||||||
|
|
||||||
|
services:
|
||||||
|
service:
|
||||||
|
build:
|
||||||
|
context: .
|
10
tests/fixtures/environment-exec/docker-compose.yml
vendored
Normal file
10
tests/fixtures/environment-exec/docker-compose.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
version: "2.2"
|
||||||
|
|
||||||
|
services:
|
||||||
|
service:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
|
||||||
|
environment:
|
||||||
|
foo: bar
|
||||||
|
hello: world
|
13
tests/fixtures/environment-interpolation-with-defaults/docker-compose.yml
vendored
Normal file
13
tests/fixtures/environment-interpolation-with-defaults/docker-compose.yml
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
version: "2.1"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
# set value with default, default must be ignored
|
||||||
|
image: ${IMAGE:-alpine}
|
||||||
|
|
||||||
|
# unset value with default value
|
||||||
|
ports:
|
||||||
|
- "${HOST_PORT:-80}:8000"
|
||||||
|
|
||||||
|
# unset value with empty default
|
||||||
|
hostname: "host-${UNSET_VALUE:-}"
|
17
tests/fixtures/networks/external-networks-v3-5.yml
vendored
Normal file
17
tests/fixtures/networks/external-networks-v3-5.yml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
version: "3.5"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: busybox
|
||||||
|
command: top
|
||||||
|
networks:
|
||||||
|
- foo
|
||||||
|
- bar
|
||||||
|
|
||||||
|
networks:
|
||||||
|
foo:
|
||||||
|
external: true
|
||||||
|
name: some_foo
|
||||||
|
bar:
|
||||||
|
external:
|
||||||
|
name: some_bar
|
7
tests/fixtures/run-labels/docker-compose.yml
vendored
Normal file
7
tests/fixtures/run-labels/docker-compose.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
service:
|
||||||
|
image: busybox:latest
|
||||||
|
command: top
|
||||||
|
|
||||||
|
labels:
|
||||||
|
foo: bar
|
||||||
|
hello: world
|
@ -19,12 +19,8 @@ def build_config_details(contents, working_dir='working_dir', filename='filename
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def create_host_file(client, filename):
|
def create_custom_host_file(client, filename, content):
|
||||||
dirname = os.path.dirname(filename)
|
dirname = os.path.dirname(filename)
|
||||||
|
|
||||||
with open(filename, 'r') as fh:
|
|
||||||
content = fh.read()
|
|
||||||
|
|
||||||
container = client.create_container(
|
container = client.create_container(
|
||||||
'busybox:latest',
|
'busybox:latest',
|
||||||
['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)],
|
['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)],
|
||||||
@ -48,3 +44,10 @@ def create_host_file(client, filename):
|
|||||||
return container_info['Node']['Name']
|
return container_info['Node']['Name']
|
||||||
finally:
|
finally:
|
||||||
client.remove_container(container, force=True)
|
client.remove_container(container, force=True)
|
||||||
|
|
||||||
|
|
||||||
|
def create_host_file(client, filename):
|
||||||
|
with open(filename, 'r') as fh:
|
||||||
|
content = fh.read()
|
||||||
|
|
||||||
|
return create_custom_host_file(client, filename, content)
|
||||||
|
@ -35,6 +35,7 @@ from tests.integration.testcases import is_cluster
|
|||||||
from tests.integration.testcases import no_cluster
|
from tests.integration.testcases import no_cluster
|
||||||
from tests.integration.testcases import v2_1_only
|
from tests.integration.testcases import v2_1_only
|
||||||
from tests.integration.testcases import v2_2_only
|
from tests.integration.testcases import v2_2_only
|
||||||
|
from tests.integration.testcases import v2_3_only
|
||||||
from tests.integration.testcases import v2_only
|
from tests.integration.testcases import v2_only
|
||||||
from tests.integration.testcases import v3_only
|
from tests.integration.testcases import v3_only
|
||||||
|
|
||||||
@ -436,6 +437,26 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
self.assertNotEqual(db_container.id, old_db_id)
|
self.assertNotEqual(db_container.id, old_db_id)
|
||||||
self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
|
self.assertEqual(db_container.get('Volumes./etc'), db_volume_path)
|
||||||
|
|
||||||
|
@v2_3_only()
|
||||||
|
def test_recreate_preserves_mounts(self):
|
||||||
|
web = self.create_service('web')
|
||||||
|
db = self.create_service('db', volumes=[types.MountSpec(type='volume', target='/etc')])
|
||||||
|
project = Project('composetest', [web, db], self.client)
|
||||||
|
project.start()
|
||||||
|
assert len(project.containers()) == 0
|
||||||
|
|
||||||
|
project.up(['db'])
|
||||||
|
assert len(project.containers()) == 1
|
||||||
|
old_db_id = project.containers()[0].id
|
||||||
|
db_volume_path = project.containers()[0].get_mount('/etc')['Source']
|
||||||
|
|
||||||
|
project.up(strategy=ConvergenceStrategy.always)
|
||||||
|
assert len(project.containers()) == 2
|
||||||
|
|
||||||
|
db_container = [c for c in project.containers() if 'db' in c.name][0]
|
||||||
|
assert db_container.id != old_db_id
|
||||||
|
assert db_container.get_mount('/etc')['Source'] == db_volume_path
|
||||||
|
|
||||||
def test_project_up_with_no_recreate_running(self):
|
def test_project_up_with_no_recreate_running(self):
|
||||||
web = self.create_service('web')
|
web = self.create_service('web')
|
||||||
db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
|
db = self.create_service('db', volumes=[VolumeSpec.parse('/var/db')])
|
||||||
@ -932,6 +953,43 @@ class ProjectTest(DockerClientTestCase):
|
|||||||
assert 'LinkLocalIPs' in ipam_config
|
assert 'LinkLocalIPs' in ipam_config
|
||||||
assert ipam_config['LinkLocalIPs'] == ['169.254.8.8']
|
assert ipam_config['LinkLocalIPs'] == ['169.254.8.8']
|
||||||
|
|
||||||
|
@v2_1_only()
|
||||||
|
def test_up_with_custom_name_resources(self):
|
||||||
|
config_data = build_config(
|
||||||
|
version=V2_2,
|
||||||
|
services=[{
|
||||||
|
'name': 'web',
|
||||||
|
'volumes': [VolumeSpec.parse('foo:/container-path')],
|
||||||
|
'networks': {'foo': {}},
|
||||||
|
'image': 'busybox:latest'
|
||||||
|
}],
|
||||||
|
networks={
|
||||||
|
'foo': {
|
||||||
|
'name': 'zztop',
|
||||||
|
'labels': {'com.docker.compose.test_value': 'sharpdressedman'}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
volumes={
|
||||||
|
'foo': {
|
||||||
|
'name': 'acdc',
|
||||||
|
'labels': {'com.docker.compose.test_value': 'thefuror'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.from_config(
|
||||||
|
client=self.client,
|
||||||
|
name='composetest',
|
||||||
|
config_data=config_data
|
||||||
|
)
|
||||||
|
|
||||||
|
project.up(detached=True)
|
||||||
|
network = [n for n in self.client.networks() if n['Name'] == 'zztop'][0]
|
||||||
|
volume = [v for v in self.client.volumes()['Volumes'] if v['Name'] == 'acdc'][0]
|
||||||
|
|
||||||
|
assert network['Labels']['com.docker.compose.test_value'] == 'sharpdressedman'
|
||||||
|
assert volume['Labels']['com.docker.compose.test_value'] == 'thefuror'
|
||||||
|
|
||||||
@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')
|
||||||
|
@ -19,6 +19,7 @@ from .testcases import pull_busybox
|
|||||||
from .testcases import SWARM_SKIP_CONTAINERS_ALL
|
from .testcases import SWARM_SKIP_CONTAINERS_ALL
|
||||||
from .testcases import SWARM_SKIP_CPU_SHARES
|
from .testcases import SWARM_SKIP_CPU_SHARES
|
||||||
from compose import __version__
|
from compose import __version__
|
||||||
|
from compose.config.types import MountSpec
|
||||||
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 IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
@ -37,6 +38,7 @@ from compose.service import NetworkMode
|
|||||||
from compose.service import PidMode
|
from compose.service import PidMode
|
||||||
from compose.service import Service
|
from compose.service import Service
|
||||||
from compose.utils import parse_nanoseconds_int
|
from compose.utils import parse_nanoseconds_int
|
||||||
|
from tests.helpers import create_custom_host_file
|
||||||
from tests.integration.testcases import is_cluster
|
from tests.integration.testcases import is_cluster
|
||||||
from tests.integration.testcases import no_cluster
|
from tests.integration.testcases import no_cluster
|
||||||
from tests.integration.testcases import v2_1_only
|
from tests.integration.testcases import v2_1_only
|
||||||
@ -239,8 +241,7 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
service.start_container(container)
|
service.start_container(container)
|
||||||
self.assertEqual(set(container.get('HostConfig.SecurityOpt')), set(security_opt))
|
self.assertEqual(set(container.get('HostConfig.SecurityOpt')), set(security_opt))
|
||||||
|
|
||||||
# @pytest.mark.xfail(True, reason='Not supported on most drivers')
|
@pytest.mark.xfail(True, reason='Not supported on most drivers')
|
||||||
@pytest.mark.skipif(True, reason='https://github.com/moby/moby/issues/34270')
|
|
||||||
def test_create_container_with_storage_opt(self):
|
def test_create_container_with_storage_opt(self):
|
||||||
storage_opt = {'size': '1G'}
|
storage_opt = {'size': '1G'}
|
||||||
service = self.create_service('db', storage_opt=storage_opt)
|
service = self.create_service('db', storage_opt=storage_opt)
|
||||||
@ -248,6 +249,12 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
service.start_container(container)
|
service.start_container(container)
|
||||||
self.assertEqual(container.get('HostConfig.StorageOpt'), storage_opt)
|
self.assertEqual(container.get('HostConfig.StorageOpt'), storage_opt)
|
||||||
|
|
||||||
|
def test_create_container_with_oom_kill_disable(self):
|
||||||
|
self.require_api_version('1.20')
|
||||||
|
service = self.create_service('db', oom_kill_disable=True)
|
||||||
|
container = service.create_container()
|
||||||
|
assert container.get('HostConfig.OomKillDisable') is True
|
||||||
|
|
||||||
def test_create_container_with_mac_address(self):
|
def test_create_container_with_mac_address(self):
|
||||||
service = self.create_service('db', mac_address='02:42:ac:11:65:43')
|
service = self.create_service('db', mac_address='02:42:ac:11:65:43')
|
||||||
container = service.create_container()
|
container = service.create_container()
|
||||||
@ -271,6 +278,54 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
|
self.assertTrue(path.basename(actual_host_path) == path.basename(host_path),
|
||||||
msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
|
msg=("Last component differs: %s, %s" % (actual_host_path, host_path)))
|
||||||
|
|
||||||
|
@v2_3_only()
|
||||||
|
def test_create_container_with_host_mount(self):
|
||||||
|
host_path = '/tmp/host-path'
|
||||||
|
container_path = '/container-path'
|
||||||
|
|
||||||
|
create_custom_host_file(self.client, path.join(host_path, 'a.txt'), 'test')
|
||||||
|
|
||||||
|
service = self.create_service(
|
||||||
|
'db',
|
||||||
|
volumes=[
|
||||||
|
MountSpec(type='bind', source=host_path, target=container_path, read_only=True)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
container = service.create_container()
|
||||||
|
service.start_container(container)
|
||||||
|
mount = container.get_mount(container_path)
|
||||||
|
assert mount
|
||||||
|
assert path.basename(mount['Source']) == path.basename(host_path)
|
||||||
|
assert mount['RW'] is False
|
||||||
|
|
||||||
|
@v2_3_only()
|
||||||
|
def test_create_container_with_tmpfs_mount(self):
|
||||||
|
container_path = '/container-tmpfs'
|
||||||
|
service = self.create_service(
|
||||||
|
'db',
|
||||||
|
volumes=[MountSpec(type='tmpfs', target=container_path)]
|
||||||
|
)
|
||||||
|
container = service.create_container()
|
||||||
|
service.start_container(container)
|
||||||
|
mount = container.get_mount(container_path)
|
||||||
|
assert mount
|
||||||
|
assert mount['Type'] == 'tmpfs'
|
||||||
|
|
||||||
|
@v2_3_only()
|
||||||
|
def test_create_container_with_volume_mount(self):
|
||||||
|
container_path = '/container-volume'
|
||||||
|
volume_name = 'composetest_abcde'
|
||||||
|
self.client.create_volume(volume_name)
|
||||||
|
service = self.create_service(
|
||||||
|
'db',
|
||||||
|
volumes=[MountSpec(type='volume', source=volume_name, target=container_path)]
|
||||||
|
)
|
||||||
|
container = service.create_container()
|
||||||
|
service.start_container(container)
|
||||||
|
mount = container.get_mount(container_path)
|
||||||
|
assert mount
|
||||||
|
assert mount['Name'] == volume_name
|
||||||
|
|
||||||
def test_create_container_with_healthcheck_config(self):
|
def test_create_container_with_healthcheck_config(self):
|
||||||
one_second = parse_nanoseconds_int('1s')
|
one_second = parse_nanoseconds_int('1s')
|
||||||
healthcheck = {
|
healthcheck = {
|
||||||
@ -434,6 +489,38 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
|
|
||||||
orig_container = new_container
|
orig_container = new_container
|
||||||
|
|
||||||
|
@v2_3_only()
|
||||||
|
def test_execute_convergence_plan_recreate_twice_with_mount(self):
|
||||||
|
service = self.create_service(
|
||||||
|
'db',
|
||||||
|
volumes=[MountSpec(target='/etc', type='volume')],
|
||||||
|
entrypoint=['top'],
|
||||||
|
command=['-d', '1']
|
||||||
|
)
|
||||||
|
|
||||||
|
orig_container = service.create_container()
|
||||||
|
service.start_container(orig_container)
|
||||||
|
|
||||||
|
orig_container.inspect() # reload volume data
|
||||||
|
volume_path = orig_container.get_mount('/etc')['Source']
|
||||||
|
|
||||||
|
# Do this twice to reproduce the bug
|
||||||
|
for _ in range(2):
|
||||||
|
new_container, = service.execute_convergence_plan(
|
||||||
|
ConvergencePlan('recreate', [orig_container])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert new_container.get_mount('/etc')['Source'] == volume_path
|
||||||
|
if not is_cluster(self.client):
|
||||||
|
assert ('affinity:container==%s' % orig_container.id in
|
||||||
|
new_container.get('Config.Env'))
|
||||||
|
else:
|
||||||
|
# In Swarm, the env marker is consumed and the container should be deployed
|
||||||
|
# on the same node.
|
||||||
|
assert orig_container.get('Node.Name') == new_container.get('Node.Name')
|
||||||
|
|
||||||
|
orig_container = new_container
|
||||||
|
|
||||||
def test_execute_convergence_plan_when_containers_are_stopped(self):
|
def test_execute_convergence_plan_when_containers_are_stopped(self):
|
||||||
service = self.create_service(
|
service = self.create_service(
|
||||||
'db',
|
'db',
|
||||||
@ -828,6 +915,29 @@ class ServiceTest(DockerClientTestCase):
|
|||||||
assert service.image()
|
assert service.image()
|
||||||
assert service.image()['Config']['Labels']['com.docker.compose.test.target'] == 'one'
|
assert service.image()['Config']['Labels']['com.docker.compose.test.target'] == 'one'
|
||||||
|
|
||||||
|
@v2_3_only()
|
||||||
|
def test_build_with_extra_hosts(self):
|
||||||
|
self.require_api_version('1.27')
|
||||||
|
base_dir = tempfile.mkdtemp()
|
||||||
|
self.addCleanup(shutil.rmtree, base_dir)
|
||||||
|
|
||||||
|
with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
|
||||||
|
f.write('\n'.join([
|
||||||
|
'FROM busybox',
|
||||||
|
'RUN ping -c1 foobar',
|
||||||
|
'RUN ping -c1 baz',
|
||||||
|
]))
|
||||||
|
|
||||||
|
service = self.create_service('build_extra_hosts', build={
|
||||||
|
'context': text_type(base_dir),
|
||||||
|
'extra_hosts': {
|
||||||
|
'foobar': '127.0.0.1',
|
||||||
|
'baz': '127.0.0.1'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
service.build()
|
||||||
|
assert service.image()
|
||||||
|
|
||||||
def test_start_container_stays_unprivileged(self):
|
def test_start_container_stays_unprivileged(self):
|
||||||
service = self.create_service('web')
|
service = self.create_service('web')
|
||||||
container = create_and_start_container(service).inspect()
|
container = create_and_start_container(service).inspect()
|
||||||
|
@ -20,7 +20,7 @@ from compose.const import COMPOSEFILE_V2_2 as V2_2
|
|||||||
from compose.const import COMPOSEFILE_V2_3 as V2_3
|
from compose.const import COMPOSEFILE_V2_3 as V2_3
|
||||||
from compose.const import COMPOSEFILE_V3_0 as V3_0
|
from compose.const import COMPOSEFILE_V3_0 as V3_0
|
||||||
from compose.const import COMPOSEFILE_V3_2 as V3_2
|
from compose.const import COMPOSEFILE_V3_2 as V3_2
|
||||||
from compose.const import COMPOSEFILE_V3_3 as V3_3
|
from compose.const import COMPOSEFILE_V3_5 as V3_5
|
||||||
from compose.const import LABEL_PROJECT
|
from compose.const import LABEL_PROJECT
|
||||||
from compose.progress_stream import stream_output
|
from compose.progress_stream import stream_output
|
||||||
from compose.service import Service
|
from compose.service import Service
|
||||||
@ -47,7 +47,7 @@ def get_links(container):
|
|||||||
|
|
||||||
def engine_max_version():
|
def engine_max_version():
|
||||||
if 'DOCKER_VERSION' not in os.environ:
|
if 'DOCKER_VERSION' not in os.environ:
|
||||||
return V3_3
|
return V3_5
|
||||||
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
|
||||||
@ -57,7 +57,7 @@ def engine_max_version():
|
|||||||
return V2_1
|
return V2_1
|
||||||
if version_lt(version, '17.06'):
|
if version_lt(version, '17.06'):
|
||||||
return V3_2
|
return V3_2
|
||||||
return V3_3
|
return V3_5
|
||||||
|
|
||||||
|
|
||||||
def min_version_skip(version):
|
def min_version_skip(version):
|
||||||
|
@ -10,6 +10,7 @@ from io import StringIO
|
|||||||
import docker
|
import docker
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
from docker.constants import DEFAULT_DOCKER_API_VERSION
|
||||||
|
|
||||||
from .. import mock
|
from .. import mock
|
||||||
from .. import unittest
|
from .. import unittest
|
||||||
@ -98,6 +99,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
@mock.patch('compose.cli.main.PseudoTerminal', autospec=True)
|
||||||
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
def test_run_interactive_passes_logs_false(self, mock_pseudo_terminal, mock_run_operation):
|
||||||
mock_client = mock.create_autospec(docker.APIClient)
|
mock_client = mock.create_autospec(docker.APIClient)
|
||||||
|
mock_client.api_version = DEFAULT_DOCKER_API_VERSION
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
client=mock_client,
|
client=mock_client,
|
||||||
@ -112,6 +114,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
|
'--label': [],
|
||||||
'--user': None,
|
'--user': None,
|
||||||
'--no-deps': None,
|
'--no-deps': None,
|
||||||
'-d': False,
|
'-d': False,
|
||||||
@ -130,6 +133,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
|
|
||||||
def test_run_service_with_restart_always(self):
|
def test_run_service_with_restart_always(self):
|
||||||
mock_client = mock.create_autospec(docker.APIClient)
|
mock_client = mock.create_autospec(docker.APIClient)
|
||||||
|
mock_client.api_version = DEFAULT_DOCKER_API_VERSION
|
||||||
|
|
||||||
project = Project.from_config(
|
project = Project.from_config(
|
||||||
name='composetest',
|
name='composetest',
|
||||||
@ -147,6 +151,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
|
'--label': [],
|
||||||
'--user': None,
|
'--user': None,
|
||||||
'--no-deps': None,
|
'--no-deps': None,
|
||||||
'-d': True,
|
'-d': True,
|
||||||
@ -170,6 +175,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
|
'--label': [],
|
||||||
'--user': None,
|
'--user': None,
|
||||||
'--no-deps': None,
|
'--no-deps': None,
|
||||||
'-d': True,
|
'-d': True,
|
||||||
@ -202,6 +208,7 @@ class CLITestCase(unittest.TestCase):
|
|||||||
'SERVICE': 'service',
|
'SERVICE': 'service',
|
||||||
'COMMAND': None,
|
'COMMAND': None,
|
||||||
'-e': [],
|
'-e': [],
|
||||||
|
'--label': [],
|
||||||
'--user': None,
|
'--user': None,
|
||||||
'--no-deps': None,
|
'--no-deps': None,
|
||||||
'-d': True,
|
'-d': True,
|
||||||
|
@ -34,7 +34,6 @@ from compose.const import COMPOSEFILE_V3_1 as V3_1
|
|||||||
from compose.const import COMPOSEFILE_V3_2 as V3_2
|
from compose.const import COMPOSEFILE_V3_2 as V3_2
|
||||||
from compose.const import COMPOSEFILE_V3_3 as V3_3
|
from compose.const import COMPOSEFILE_V3_3 as V3_3
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
from compose.utils import nanoseconds_from_time_seconds
|
|
||||||
from tests import mock
|
from tests import mock
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
@ -433,6 +432,40 @@ class ConfigTest(unittest.TestCase):
|
|||||||
'label_key': 'label_val'
|
'label_key': 'label_val'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_load_config_custom_resource_names(self):
|
||||||
|
base_file = config.ConfigFile(
|
||||||
|
'base.yaml', {
|
||||||
|
'version': '3.5',
|
||||||
|
'volumes': {
|
||||||
|
'abc': {
|
||||||
|
'name': 'xyz'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'networks': {
|
||||||
|
'abc': {
|
||||||
|
'name': 'xyz'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'secrets': {
|
||||||
|
'abc': {
|
||||||
|
'name': 'xyz'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'configs': {
|
||||||
|
'abc': {
|
||||||
|
'name': 'xyz'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
details = config.ConfigDetails('.', [base_file])
|
||||||
|
loaded_config = config.load(details)
|
||||||
|
|
||||||
|
assert loaded_config.networks['abc'] == {'name': 'xyz'}
|
||||||
|
assert loaded_config.volumes['abc'] == {'name': 'xyz'}
|
||||||
|
assert loaded_config.secrets['abc']['name'] == 'xyz'
|
||||||
|
assert loaded_config.configs['abc']['name'] == 'xyz'
|
||||||
|
|
||||||
def test_load_config_volume_and_network_labels(self):
|
def test_load_config_volume_and_network_labels(self):
|
||||||
base_file = config.ConfigFile(
|
base_file = config.ConfigFile(
|
||||||
'base.yaml',
|
'base.yaml',
|
||||||
@ -1138,9 +1171,12 @@ class ConfigTest(unittest.TestCase):
|
|||||||
details = config.ConfigDetails('.', [base_file, override_file])
|
details = config.ConfigDetails('.', [base_file, override_file])
|
||||||
service_dicts = config.load(details).services
|
service_dicts = config.load(details).services
|
||||||
svc_volumes = map(lambda v: v.repr(), service_dicts[0]['volumes'])
|
svc_volumes = map(lambda v: v.repr(), service_dicts[0]['volumes'])
|
||||||
assert sorted(svc_volumes) == sorted(
|
for vol in svc_volumes:
|
||||||
['/anonymous', '/c:/b:rw', 'vol:/x:ro']
|
assert vol in [
|
||||||
)
|
'/anonymous',
|
||||||
|
'/c:/b:rw',
|
||||||
|
{'source': 'vol', 'target': '/x', 'type': 'volume', 'read_only': True}
|
||||||
|
]
|
||||||
|
|
||||||
@mock.patch.dict(os.environ)
|
@mock.patch.dict(os.environ)
|
||||||
def test_volume_mode_override(self):
|
def test_volume_mode_override(self):
|
||||||
@ -1224,6 +1260,50 @@ class ConfigTest(unittest.TestCase):
|
|||||||
assert volume.external == 'data0028'
|
assert volume.external == 'data0028'
|
||||||
assert volume.is_named_volume
|
assert volume.is_named_volume
|
||||||
|
|
||||||
|
def test_volumes_long_syntax(self):
|
||||||
|
base_file = config.ConfigFile(
|
||||||
|
'base.yaml', {
|
||||||
|
'version': '2.3',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'image': 'busybox:latest',
|
||||||
|
'volumes': [
|
||||||
|
{
|
||||||
|
'target': '/anonymous', 'type': 'volume'
|
||||||
|
}, {
|
||||||
|
'source': '/abc', 'target': '/xyz', 'type': 'bind'
|
||||||
|
}, {
|
||||||
|
'source': '\\\\.\\pipe\\abcd', 'target': '/named_pipe', 'type': 'npipe'
|
||||||
|
}, {
|
||||||
|
'type': 'tmpfs', 'target': '/tmpfs'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
details = config.ConfigDetails('.', [base_file])
|
||||||
|
config_data = config.load(details)
|
||||||
|
volumes = config_data.services[0].get('volumes')
|
||||||
|
anon_volume = [v for v in volumes if v.target == '/anonymous'][0]
|
||||||
|
tmpfs_mount = [v for v in volumes if v.type == 'tmpfs'][0]
|
||||||
|
host_mount = [v for v in volumes if v.type == 'bind'][0]
|
||||||
|
npipe_mount = [v for v in volumes if v.type == 'npipe'][0]
|
||||||
|
|
||||||
|
assert anon_volume.type == 'volume'
|
||||||
|
assert not anon_volume.is_named_volume
|
||||||
|
|
||||||
|
assert tmpfs_mount.target == '/tmpfs'
|
||||||
|
assert not tmpfs_mount.is_named_volume
|
||||||
|
|
||||||
|
assert host_mount.source == os.path.normpath('/abc')
|
||||||
|
assert host_mount.target == '/xyz'
|
||||||
|
assert not host_mount.is_named_volume
|
||||||
|
|
||||||
|
assert npipe_mount.source == '\\\\.\\pipe\\abcd'
|
||||||
|
assert npipe_mount.target == '/named_pipe'
|
||||||
|
assert not npipe_mount.is_named_volume
|
||||||
|
|
||||||
def test_config_valid_service_names(self):
|
def test_config_valid_service_names(self):
|
||||||
for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
|
for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']:
|
||||||
services = config.load(
|
services = config.load(
|
||||||
@ -2493,8 +2573,8 @@ class ConfigTest(unittest.TestCase):
|
|||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'example/web',
|
'image': 'example/web',
|
||||||
'secrets': [
|
'secrets': [
|
||||||
types.ServiceSecret('one', None, None, None, None),
|
types.ServiceSecret('one', None, None, None, None, None),
|
||||||
types.ServiceSecret('source', 'target', '100', '200', 0o777),
|
types.ServiceSecret('source', 'target', '100', '200', 0o777, None),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -2540,8 +2620,8 @@ class ConfigTest(unittest.TestCase):
|
|||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'example/web',
|
'image': 'example/web',
|
||||||
'secrets': [
|
'secrets': [
|
||||||
types.ServiceSecret('one', None, None, None, None),
|
types.ServiceSecret('one', None, None, None, None, None),
|
||||||
types.ServiceSecret('source', 'target', '100', '200', 0o777),
|
types.ServiceSecret('source', 'target', '100', '200', 0o777, None),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -2578,8 +2658,8 @@ class ConfigTest(unittest.TestCase):
|
|||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'example/web',
|
'image': 'example/web',
|
||||||
'configs': [
|
'configs': [
|
||||||
types.ServiceConfig('one', None, None, None, None),
|
types.ServiceConfig('one', None, None, None, None, None),
|
||||||
types.ServiceConfig('source', 'target', '100', '200', 0o777),
|
types.ServiceConfig('source', 'target', '100', '200', 0o777, None),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -2625,13 +2705,40 @@ class ConfigTest(unittest.TestCase):
|
|||||||
'name': 'web',
|
'name': 'web',
|
||||||
'image': 'example/web',
|
'image': 'example/web',
|
||||||
'configs': [
|
'configs': [
|
||||||
types.ServiceConfig('one', None, None, None, None),
|
types.ServiceConfig('one', None, None, None, None, None),
|
||||||
types.ServiceConfig('source', 'target', '100', '200', 0o777),
|
types.ServiceConfig('source', 'target', '100', '200', 0o777, None),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
assert service_sort(service_dicts) == service_sort(expected)
|
assert service_sort(service_dicts) == service_sort(expected)
|
||||||
|
|
||||||
|
def test_service_volume_invalid_config(self):
|
||||||
|
config_details = build_config_details(
|
||||||
|
{
|
||||||
|
'version': '3.2',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'build': {
|
||||||
|
'context': '.',
|
||||||
|
'args': None,
|
||||||
|
},
|
||||||
|
'volumes': [
|
||||||
|
{
|
||||||
|
"type": "volume",
|
||||||
|
"source": "/data",
|
||||||
|
"garbage": {
|
||||||
|
"and": "error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with pytest.raises(ConfigurationError) as exc:
|
||||||
|
config.load(config_details)
|
||||||
|
assert "services.web.volumes contains unsupported option: 'garbage'" in exc.exconly()
|
||||||
|
|
||||||
|
|
||||||
class NetworkModeTest(unittest.TestCase):
|
class NetworkModeTest(unittest.TestCase):
|
||||||
|
|
||||||
@ -2847,6 +2954,94 @@ class PortsTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SubnetTest(unittest.TestCase):
|
||||||
|
INVALID_SUBNET_TYPES = [
|
||||||
|
None,
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
]
|
||||||
|
|
||||||
|
INVALID_SUBNET_MAPPINGS = [
|
||||||
|
"",
|
||||||
|
"192.168.0.1/sdfsdfs",
|
||||||
|
"192.168.0.1/",
|
||||||
|
"192.168.0.1/33",
|
||||||
|
"192.168.0.1/01",
|
||||||
|
"192.168.0.1",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156/sdfsdfs",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156/",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156/129",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156/01",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156",
|
||||||
|
"ge80:0000:0000:0000:0204:61ff:fe9d:f156/128",
|
||||||
|
"192.168.0.1/31/31",
|
||||||
|
]
|
||||||
|
|
||||||
|
VALID_SUBNET_MAPPINGS = [
|
||||||
|
"192.168.0.1/0",
|
||||||
|
"192.168.0.1/32",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156/0",
|
||||||
|
"fe80:0000:0000:0000:0204:61ff:fe9d:f156/128",
|
||||||
|
"1:2:3:4:5:6:7:8/0",
|
||||||
|
"1::/0",
|
||||||
|
"1:2:3:4:5:6:7::/0",
|
||||||
|
"1::8/0",
|
||||||
|
"1:2:3:4:5:6::8/0",
|
||||||
|
"::/0",
|
||||||
|
"::8/0",
|
||||||
|
"::2:3:4:5:6:7:8/0",
|
||||||
|
"fe80::7:8%eth0/0",
|
||||||
|
"fe80::7:8%1/0",
|
||||||
|
"::255.255.255.255/0",
|
||||||
|
"::ffff:255.255.255.255/0",
|
||||||
|
"::ffff:0:255.255.255.255/0",
|
||||||
|
"2001:db8:3:4::192.0.2.33/0",
|
||||||
|
"64:ff9b::192.0.2.33/0",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_config_invalid_subnet_type_validation(self):
|
||||||
|
for invalid_subnet in self.INVALID_SUBNET_TYPES:
|
||||||
|
with pytest.raises(ConfigurationError) as exc:
|
||||||
|
self.check_config(invalid_subnet)
|
||||||
|
|
||||||
|
assert "contains an invalid type" in exc.value.msg
|
||||||
|
|
||||||
|
def test_config_invalid_subnet_format_validation(self):
|
||||||
|
for invalid_subnet in self.INVALID_SUBNET_MAPPINGS:
|
||||||
|
with pytest.raises(ConfigurationError) as exc:
|
||||||
|
self.check_config(invalid_subnet)
|
||||||
|
|
||||||
|
assert "should use the CIDR format" in exc.value.msg
|
||||||
|
|
||||||
|
def test_config_valid_subnet_format_validation(self):
|
||||||
|
for valid_subnet in self.VALID_SUBNET_MAPPINGS:
|
||||||
|
self.check_config(valid_subnet)
|
||||||
|
|
||||||
|
def check_config(self, subnet):
|
||||||
|
config.load(
|
||||||
|
build_config_details({
|
||||||
|
'version': '3.5',
|
||||||
|
'services': {
|
||||||
|
'web': {
|
||||||
|
'image': 'busybox'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'networks': {
|
||||||
|
'default': {
|
||||||
|
'ipam': {
|
||||||
|
'config': [
|
||||||
|
{
|
||||||
|
'subnet': subnet
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'driver': 'default'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class InterpolationTest(unittest.TestCase):
|
class InterpolationTest(unittest.TestCase):
|
||||||
|
|
||||||
@mock.patch.dict(os.environ)
|
@mock.patch.dict(os.environ)
|
||||||
@ -2894,6 +3089,28 @@ class InterpolationTest(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@mock.patch.dict(os.environ)
|
||||||
|
def test_config_file_with_environment_variable_with_defaults(self):
|
||||||
|
project_dir = 'tests/fixtures/environment-interpolation-with-defaults'
|
||||||
|
os.environ.update(
|
||||||
|
IMAGE="busybox",
|
||||||
|
)
|
||||||
|
|
||||||
|
service_dicts = config.load(
|
||||||
|
config.find(
|
||||||
|
project_dir, None, Environment.from_env_file(project_dir)
|
||||||
|
)
|
||||||
|
).services
|
||||||
|
|
||||||
|
self.assertEqual(service_dicts, [
|
||||||
|
{
|
||||||
|
'name': 'web',
|
||||||
|
'image': 'busybox',
|
||||||
|
'ports': types.ServicePort.parse('80:8000'),
|
||||||
|
'hostname': 'host-',
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
@mock.patch.dict(os.environ)
|
@mock.patch.dict(os.environ)
|
||||||
def test_unset_variable_produces_warning(self):
|
def test_unset_variable_produces_warning(self):
|
||||||
os.environ.pop('FOO', None)
|
os.environ.pop('FOO', None)
|
||||||
@ -2948,7 +3165,7 @@ class InterpolationTest(unittest.TestCase):
|
|||||||
assert config_dict.secrets == {
|
assert config_dict.secrets == {
|
||||||
'secretdata': {
|
'secretdata': {
|
||||||
'external': {'name': 'baz.bar'},
|
'external': {'name': 'baz.bar'},
|
||||||
'external_name': 'baz.bar'
|
'name': 'baz.bar'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2966,7 +3183,7 @@ class InterpolationTest(unittest.TestCase):
|
|||||||
assert config_dict.configs == {
|
assert config_dict.configs == {
|
||||||
'configdata': {
|
'configdata': {
|
||||||
'external': {'name': 'baz.bar'},
|
'external': {'name': 'baz.bar'},
|
||||||
'external_name': 'baz.bar'
|
'name': 'baz.bar'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4188,52 +4405,103 @@ class BuildPathTest(unittest.TestCase):
|
|||||||
|
|
||||||
class HealthcheckTest(unittest.TestCase):
|
class HealthcheckTest(unittest.TestCase):
|
||||||
def test_healthcheck(self):
|
def test_healthcheck(self):
|
||||||
service_dict = make_service_dict(
|
config_dict = config.load(
|
||||||
'test',
|
build_config_details({
|
||||||
{'healthcheck': {
|
'version': '2.3',
|
||||||
'test': ['CMD', 'true'],
|
'services': {
|
||||||
'interval': '1s',
|
'test': {
|
||||||
'timeout': '1m',
|
'image': 'busybox',
|
||||||
'retries': 3,
|
'healthcheck': {
|
||||||
'start_period': '10s'
|
'test': ['CMD', 'true'],
|
||||||
}},
|
'interval': '1s',
|
||||||
'.',
|
'timeout': '1m',
|
||||||
|
'retries': 3,
|
||||||
|
'start_period': '10s',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
assert service_dict['healthcheck'] == {
|
serialized_config = yaml.load(serialize_config(config_dict))
|
||||||
|
serialized_service = serialized_config['services']['test']
|
||||||
|
|
||||||
|
assert serialized_service['healthcheck'] == {
|
||||||
'test': ['CMD', 'true'],
|
'test': ['CMD', 'true'],
|
||||||
'interval': nanoseconds_from_time_seconds(1),
|
'interval': '1s',
|
||||||
'timeout': nanoseconds_from_time_seconds(60),
|
'timeout': '1m',
|
||||||
'retries': 3,
|
'retries': 3,
|
||||||
'start_period': nanoseconds_from_time_seconds(10)
|
'start_period': '10s'
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_disable(self):
|
def test_disable(self):
|
||||||
service_dict = make_service_dict(
|
config_dict = config.load(
|
||||||
'test',
|
build_config_details({
|
||||||
{'healthcheck': {
|
'version': '2.3',
|
||||||
'disable': True,
|
'services': {
|
||||||
}},
|
'test': {
|
||||||
'.',
|
'image': 'busybox',
|
||||||
|
'healthcheck': {
|
||||||
|
'disable': True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
assert service_dict['healthcheck'] == {
|
serialized_config = yaml.load(serialize_config(config_dict))
|
||||||
|
serialized_service = serialized_config['services']['test']
|
||||||
|
|
||||||
|
assert serialized_service['healthcheck'] == {
|
||||||
'test': ['NONE'],
|
'test': ['NONE'],
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_disable_with_other_config_is_invalid(self):
|
def test_disable_with_other_config_is_invalid(self):
|
||||||
with pytest.raises(ConfigurationError) as excinfo:
|
with pytest.raises(ConfigurationError) as excinfo:
|
||||||
make_service_dict(
|
config.load(
|
||||||
'invalid-healthcheck',
|
build_config_details({
|
||||||
{'healthcheck': {
|
'version': '2.3',
|
||||||
'disable': True,
|
'services': {
|
||||||
'interval': '1s',
|
'invalid-healthcheck': {
|
||||||
}},
|
'image': 'busybox',
|
||||||
'.',
|
'healthcheck': {
|
||||||
|
'disable': True,
|
||||||
|
'interval': '1s',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
assert 'invalid-healthcheck' in excinfo.exconly()
|
assert 'invalid-healthcheck' in excinfo.exconly()
|
||||||
assert 'disable' in excinfo.exconly()
|
assert '"disable: true" cannot be combined with other options' in excinfo.exconly()
|
||||||
|
|
||||||
|
def test_healthcheck_with_invalid_test(self):
|
||||||
|
with pytest.raises(ConfigurationError) as excinfo:
|
||||||
|
config.load(
|
||||||
|
build_config_details({
|
||||||
|
'version': '2.3',
|
||||||
|
'services': {
|
||||||
|
'invalid-healthcheck': {
|
||||||
|
'image': 'busybox',
|
||||||
|
'healthcheck': {
|
||||||
|
'test': ['true'],
|
||||||
|
'interval': '1s',
|
||||||
|
'timeout': '1m',
|
||||||
|
'retries': 3,
|
||||||
|
'start_period': '10s',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'invalid-healthcheck' in excinfo.exconly()
|
||||||
|
assert 'the first item must be either NONE, CMD or CMD-SHELL' in excinfo.exconly()
|
||||||
|
|
||||||
|
|
||||||
class GetDefaultConfigFilesTestCase(unittest.TestCase):
|
class GetDefaultConfigFilesTestCase(unittest.TestCase):
|
||||||
|
@ -3,6 +3,11 @@ from __future__ import absolute_import
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from compose.config.environment import env_vars_from_file
|
||||||
from compose.config.environment import Environment
|
from compose.config.environment import Environment
|
||||||
from tests import unittest
|
from tests import unittest
|
||||||
|
|
||||||
@ -38,3 +43,12 @@ class EnvironmentTest(unittest.TestCase):
|
|||||||
assert env.get_boolean('BAZ') is False
|
assert env.get_boolean('BAZ') is False
|
||||||
assert env.get_boolean('FOOBAR') is True
|
assert env.get_boolean('FOOBAR') is True
|
||||||
assert env.get_boolean('UNDEFINED') is False
|
assert env.get_boolean('UNDEFINED') is False
|
||||||
|
|
||||||
|
def test_env_vars_from_file_bom(self):
|
||||||
|
tmpdir = pytest.ensuretemp('env_file')
|
||||||
|
self.addCleanup(tmpdir.remove)
|
||||||
|
with codecs.open('{}/bom.env'.format(str(tmpdir)), 'w', encoding='utf-8') as f:
|
||||||
|
f.write('\ufeffPARK_BOM=박봄\n')
|
||||||
|
assert env_vars_from_file(str(tmpdir.join('bom.env'))) == {
|
||||||
|
'PARK_BOM': '박봄'
|
||||||
|
}
|
||||||
|
@ -9,12 +9,22 @@ from compose.config.interpolation import Interpolator
|
|||||||
from compose.config.interpolation import InvalidInterpolation
|
from compose.config.interpolation import InvalidInterpolation
|
||||||
from compose.config.interpolation import TemplateWithDefaults
|
from compose.config.interpolation import TemplateWithDefaults
|
||||||
from compose.const import COMPOSEFILE_V2_0 as V2_0
|
from compose.const import COMPOSEFILE_V2_0 as V2_0
|
||||||
from compose.const import COMPOSEFILE_V3_1 as V3_1
|
from compose.const import COMPOSEFILE_V2_3 as V2_3
|
||||||
|
from compose.const import COMPOSEFILE_V3_4 as V3_4
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_env():
|
def mock_env():
|
||||||
return Environment({'USER': 'jenny', 'FOO': 'bar'})
|
return Environment({
|
||||||
|
'USER': 'jenny',
|
||||||
|
'FOO': 'bar',
|
||||||
|
'TRUE': 'True',
|
||||||
|
'FALSE': 'OFF',
|
||||||
|
'POSINT': '50',
|
||||||
|
'NEGINT': '-200',
|
||||||
|
'FLOAT': '0.145',
|
||||||
|
'MODE': '0600',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -102,7 +112,189 @@ def test_interpolate_environment_variables_in_secrets(mock_env):
|
|||||||
},
|
},
|
||||||
'other': {},
|
'other': {},
|
||||||
}
|
}
|
||||||
value = interpolate_environment_variables(V3_1, secrets, 'volume', mock_env)
|
value = interpolate_environment_variables(V3_4, secrets, 'secret', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpolate_environment_services_convert_types_v2(mock_env):
|
||||||
|
entry = {
|
||||||
|
'service1': {
|
||||||
|
'blkio_config': {
|
||||||
|
'weight': '${POSINT}',
|
||||||
|
'weight_device': [{'file': '/dev/sda1', 'weight': '${POSINT}'}]
|
||||||
|
},
|
||||||
|
'cpus': '${FLOAT}',
|
||||||
|
'cpu_count': '$POSINT',
|
||||||
|
'healthcheck': {
|
||||||
|
'retries': '${POSINT:-3}',
|
||||||
|
'disable': '${FALSE}',
|
||||||
|
'command': 'true'
|
||||||
|
},
|
||||||
|
'mem_swappiness': '${DEFAULT:-127}',
|
||||||
|
'oom_score_adj': '${NEGINT}',
|
||||||
|
'scale': '${POSINT}',
|
||||||
|
'ulimits': {
|
||||||
|
'nproc': '${POSINT}',
|
||||||
|
'nofile': {
|
||||||
|
'soft': '${POSINT}',
|
||||||
|
'hard': '${DEFAULT:-40000}'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'privileged': '${TRUE}',
|
||||||
|
'read_only': '${DEFAULT:-no}',
|
||||||
|
'tty': '${DEFAULT:-N}',
|
||||||
|
'stdin_open': '${DEFAULT-on}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'service1': {
|
||||||
|
'blkio_config': {
|
||||||
|
'weight': 50,
|
||||||
|
'weight_device': [{'file': '/dev/sda1', 'weight': 50}]
|
||||||
|
},
|
||||||
|
'cpus': 0.145,
|
||||||
|
'cpu_count': 50,
|
||||||
|
'healthcheck': {
|
||||||
|
'retries': 50,
|
||||||
|
'disable': False,
|
||||||
|
'command': 'true'
|
||||||
|
},
|
||||||
|
'mem_swappiness': 127,
|
||||||
|
'oom_score_adj': -200,
|
||||||
|
'scale': 50,
|
||||||
|
'ulimits': {
|
||||||
|
'nproc': 50,
|
||||||
|
'nofile': {
|
||||||
|
'soft': 50,
|
||||||
|
'hard': 40000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'privileged': True,
|
||||||
|
'read_only': False,
|
||||||
|
'tty': False,
|
||||||
|
'stdin_open': True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = interpolate_environment_variables(V2_3, entry, 'service', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpolate_environment_services_convert_types_v3(mock_env):
|
||||||
|
entry = {
|
||||||
|
'service1': {
|
||||||
|
'healthcheck': {
|
||||||
|
'retries': '${POSINT:-3}',
|
||||||
|
'disable': '${FALSE}',
|
||||||
|
'command': 'true'
|
||||||
|
},
|
||||||
|
'ulimits': {
|
||||||
|
'nproc': '${POSINT}',
|
||||||
|
'nofile': {
|
||||||
|
'soft': '${POSINT}',
|
||||||
|
'hard': '${DEFAULT:-40000}'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'privileged': '${TRUE}',
|
||||||
|
'read_only': '${DEFAULT:-no}',
|
||||||
|
'tty': '${DEFAULT:-N}',
|
||||||
|
'stdin_open': '${DEFAULT-on}',
|
||||||
|
'deploy': {
|
||||||
|
'update_config': {
|
||||||
|
'parallelism': '${DEFAULT:-2}',
|
||||||
|
'max_failure_ratio': '${FLOAT}',
|
||||||
|
},
|
||||||
|
'restart_policy': {
|
||||||
|
'max_attempts': '$POSINT',
|
||||||
|
},
|
||||||
|
'replicas': '${DEFAULT-3}'
|
||||||
|
},
|
||||||
|
'ports': [{'target': '${POSINT}', 'published': '${DEFAULT:-5000}'}],
|
||||||
|
'configs': [{'mode': '${MODE}', 'source': 'config1'}],
|
||||||
|
'secrets': [{'mode': '${MODE}', 'source': 'secret1'}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'service1': {
|
||||||
|
'healthcheck': {
|
||||||
|
'retries': 50,
|
||||||
|
'disable': False,
|
||||||
|
'command': 'true'
|
||||||
|
},
|
||||||
|
'ulimits': {
|
||||||
|
'nproc': 50,
|
||||||
|
'nofile': {
|
||||||
|
'soft': 50,
|
||||||
|
'hard': 40000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'privileged': True,
|
||||||
|
'read_only': False,
|
||||||
|
'tty': False,
|
||||||
|
'stdin_open': True,
|
||||||
|
'deploy': {
|
||||||
|
'update_config': {
|
||||||
|
'parallelism': 2,
|
||||||
|
'max_failure_ratio': 0.145,
|
||||||
|
},
|
||||||
|
'restart_policy': {
|
||||||
|
'max_attempts': 50,
|
||||||
|
},
|
||||||
|
'replicas': 3
|
||||||
|
},
|
||||||
|
'ports': [{'target': 50, 'published': 5000}],
|
||||||
|
'configs': [{'mode': 0o600, 'source': 'config1'}],
|
||||||
|
'secrets': [{'mode': 0o600, 'source': 'secret1'}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = interpolate_environment_variables(V3_4, entry, 'service', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpolate_environment_network_convert_types(mock_env):
|
||||||
|
entry = {
|
||||||
|
'network1': {
|
||||||
|
'external': '${FALSE}',
|
||||||
|
'attachable': '${TRUE}',
|
||||||
|
'internal': '${DEFAULT:-false}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'network1': {
|
||||||
|
'external': False,
|
||||||
|
'attachable': True,
|
||||||
|
'internal': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = interpolate_environment_variables(V3_4, entry, 'network', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_interpolate_environment_external_resource_convert_types(mock_env):
|
||||||
|
entry = {
|
||||||
|
'resource1': {
|
||||||
|
'external': '${TRUE}',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
'resource1': {
|
||||||
|
'external': True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = interpolate_environment_variables(V3_4, entry, 'network', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
value = interpolate_environment_variables(V3_4, entry, 'volume', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
value = interpolate_environment_variables(V3_4, entry, 'secret', mock_env)
|
||||||
|
assert value == expected
|
||||||
|
value = interpolate_environment_variables(V3_4, entry, 'config', mock_env)
|
||||||
assert value == expected
|
assert value == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,11 +100,37 @@ class TestServicePort(object):
|
|||||||
'published': 25001
|
'published': 25001
|
||||||
} in reprs
|
} in reprs
|
||||||
|
|
||||||
|
def test_parse_port_publish_range(self):
|
||||||
|
ports = ServicePort.parse('4440-4450:4000')
|
||||||
|
assert len(ports) == 1
|
||||||
|
reprs = [p.repr() for p in ports]
|
||||||
|
assert {
|
||||||
|
'target': 4000,
|
||||||
|
'published': '4440-4450'
|
||||||
|
} in reprs
|
||||||
|
|
||||||
def test_parse_invalid_port(self):
|
def test_parse_invalid_port(self):
|
||||||
port_def = '4000p'
|
port_def = '4000p'
|
||||||
with pytest.raises(ConfigurationError):
|
with pytest.raises(ConfigurationError):
|
||||||
ServicePort.parse(port_def)
|
ServicePort.parse(port_def)
|
||||||
|
|
||||||
|
def test_parse_invalid_publish_range(self):
|
||||||
|
port_def = '-4000:4000'
|
||||||
|
with pytest.raises(ConfigurationError):
|
||||||
|
ServicePort.parse(port_def)
|
||||||
|
|
||||||
|
port_def = 'asdf:4000'
|
||||||
|
with pytest.raises(ConfigurationError):
|
||||||
|
ServicePort.parse(port_def)
|
||||||
|
|
||||||
|
port_def = '1234-12f:4000'
|
||||||
|
with pytest.raises(ConfigurationError):
|
||||||
|
ServicePort.parse(port_def)
|
||||||
|
|
||||||
|
port_def = '1234-1235-1239:4000'
|
||||||
|
with pytest.raises(ConfigurationError):
|
||||||
|
ServicePort.parse(port_def)
|
||||||
|
|
||||||
|
|
||||||
class TestVolumeSpec(object):
|
class TestVolumeSpec(object):
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import docker
|
import docker
|
||||||
import pytest
|
import pytest
|
||||||
|
from docker.constants import DEFAULT_DOCKER_API_VERSION
|
||||||
from docker.errors import APIError
|
from docker.errors import APIError
|
||||||
|
|
||||||
from .. import mock
|
from .. import mock
|
||||||
@ -40,6 +41,7 @@ class ServiceTest(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.mock_client = mock.create_autospec(docker.APIClient)
|
self.mock_client = mock.create_autospec(docker.APIClient)
|
||||||
|
self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
|
||||||
|
|
||||||
def test_containers(self):
|
def test_containers(self):
|
||||||
service = Service('db', self.mock_client, 'myproject', image='foo')
|
service = Service('db', self.mock_client, 'myproject', image='foo')
|
||||||
@ -145,12 +147,6 @@ class ServiceTest(unittest.TestCase):
|
|||||||
self.assertEqual(service._get_volumes_from(), [container_id + ':rw'])
|
self.assertEqual(service._get_volumes_from(), [container_id + ':rw'])
|
||||||
from_service.create_container.assert_called_once_with()
|
from_service.create_container.assert_called_once_with()
|
||||||
|
|
||||||
def test_split_domainname_none(self):
|
|
||||||
service = Service('foo', image='foo', hostname='name', client=self.mock_client)
|
|
||||||
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
|
||||||
self.assertEqual(opts['hostname'], 'name', 'hostname')
|
|
||||||
self.assertFalse('domainname' in opts, 'domainname')
|
|
||||||
|
|
||||||
def test_memory_swap_limit(self):
|
def test_memory_swap_limit(self):
|
||||||
self.mock_client.create_host_config.return_value = {}
|
self.mock_client.create_host_config.return_value = {}
|
||||||
|
|
||||||
@ -179,7 +175,7 @@ class ServiceTest(unittest.TestCase):
|
|||||||
external_links=['default_foo_1']
|
external_links=['default_foo_1']
|
||||||
)
|
)
|
||||||
with self.assertRaises(DependencyError):
|
with self.assertRaises(DependencyError):
|
||||||
service.get_container_name(1)
|
service.get_container_name('foo', 1)
|
||||||
|
|
||||||
def test_mem_reservation(self):
|
def test_mem_reservation(self):
|
||||||
self.mock_client.create_host_config.return_value = {}
|
self.mock_client.create_host_config.return_value = {}
|
||||||
@ -232,7 +228,29 @@ class ServiceTest(unittest.TestCase):
|
|||||||
{'Type': 'syslog', 'Config': {'syslog-address': 'tcp://192.168.0.42:123'}}
|
{'Type': 'syslog', 'Config': {'syslog-address': 'tcp://192.168.0.42:123'}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_stop_grace_period(self):
|
||||||
|
self.mock_client.api_version = '1.25'
|
||||||
|
self.mock_client.create_host_config.return_value = {}
|
||||||
|
service = Service(
|
||||||
|
'foo',
|
||||||
|
image='foo',
|
||||||
|
client=self.mock_client,
|
||||||
|
stop_grace_period="1m35s")
|
||||||
|
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
||||||
|
self.assertEqual(opts['stop_timeout'], 95)
|
||||||
|
|
||||||
|
def test_split_domainname_none(self):
|
||||||
|
service = Service(
|
||||||
|
'foo',
|
||||||
|
image='foo',
|
||||||
|
hostname='name.domain.tld',
|
||||||
|
client=self.mock_client)
|
||||||
|
opts = service._get_container_create_options({'image': 'foo'}, 1)
|
||||||
|
self.assertEqual(opts['hostname'], 'name.domain.tld', 'hostname')
|
||||||
|
self.assertFalse('domainname' in opts, 'domainname')
|
||||||
|
|
||||||
def test_split_domainname_fqdn(self):
|
def test_split_domainname_fqdn(self):
|
||||||
|
self.mock_client.api_version = '1.22'
|
||||||
service = Service(
|
service = Service(
|
||||||
'foo',
|
'foo',
|
||||||
hostname='name.domain.tld',
|
hostname='name.domain.tld',
|
||||||
@ -243,6 +261,7 @@ class ServiceTest(unittest.TestCase):
|
|||||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||||
|
|
||||||
def test_split_domainname_both(self):
|
def test_split_domainname_both(self):
|
||||||
|
self.mock_client.api_version = '1.22'
|
||||||
service = Service(
|
service = Service(
|
||||||
'foo',
|
'foo',
|
||||||
hostname='name',
|
hostname='name',
|
||||||
@ -254,6 +273,7 @@ class ServiceTest(unittest.TestCase):
|
|||||||
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
self.assertEqual(opts['domainname'], 'domain.tld', 'domainname')
|
||||||
|
|
||||||
def test_split_domainname_weird(self):
|
def test_split_domainname_weird(self):
|
||||||
|
self.mock_client.api_version = '1.22'
|
||||||
service = Service(
|
service = Service(
|
||||||
'foo',
|
'foo',
|
||||||
hostname='name.sub',
|
hostname='name.sub',
|
||||||
@ -478,6 +498,8 @@ class ServiceTest(unittest.TestCase):
|
|||||||
network_mode=None,
|
network_mode=None,
|
||||||
target=None,
|
target=None,
|
||||||
shmsize=None,
|
shmsize=None,
|
||||||
|
extra_hosts=None,
|
||||||
|
container_limits={'memory': None},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ensure_image_exists_no_build(self):
|
def test_ensure_image_exists_no_build(self):
|
||||||
@ -518,7 +540,9 @@ class ServiceTest(unittest.TestCase):
|
|||||||
cache_from=None,
|
cache_from=None,
|
||||||
network_mode=None,
|
network_mode=None,
|
||||||
target=None,
|
target=None,
|
||||||
shmsize=None
|
shmsize=None,
|
||||||
|
extra_hosts=None,
|
||||||
|
container_limits={'memory': None},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_build_does_not_pull(self):
|
def test_build_does_not_pull(self):
|
||||||
@ -857,6 +881,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.mock_client = mock.create_autospec(docker.APIClient)
|
self.mock_client = mock.create_autospec(docker.APIClient)
|
||||||
|
self.mock_client.api_version = DEFAULT_DOCKER_API_VERSION
|
||||||
|
|
||||||
def test_build_volume_binding(self):
|
def test_build_volume_binding(self):
|
||||||
binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True))
|
binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True))
|
||||||
@ -914,7 +939,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
|||||||
VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
|
VolumeSpec.parse('imagedata:/mnt/image/data:rw'),
|
||||||
]
|
]
|
||||||
|
|
||||||
volumes = get_container_data_volumes(container, options, ['/dev/tmpfs'])
|
volumes, _ = get_container_data_volumes(container, options, ['/dev/tmpfs'], [])
|
||||||
assert sorted(volumes) == sorted(expected)
|
assert sorted(volumes) == sorted(expected)
|
||||||
|
|
||||||
def test_merge_volume_bindings(self):
|
def test_merge_volume_bindings(self):
|
||||||
@ -950,7 +975,7 @@ class ServiceVolumesTest(unittest.TestCase):
|
|||||||
'existingvolume:/existing/volume:rw',
|
'existingvolume:/existing/volume:rw',
|
||||||
]
|
]
|
||||||
|
|
||||||
binds, affinity = merge_volume_bindings(options, ['/dev/tmpfs'], previous_container)
|
binds, affinity = merge_volume_bindings(options, ['/dev/tmpfs'], previous_container, [])
|
||||||
assert sorted(binds) == sorted(expected)
|
assert sorted(binds) == sorted(expected)
|
||||||
assert affinity == {'affinity:container': '=cdefab'}
|
assert affinity == {'affinity:container': '=cdefab'}
|
||||||
|
|
||||||
@ -1110,8 +1135,8 @@ class ServiceSecretTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
volumes = service.get_secret_volumes()
|
volumes = service.get_secret_volumes()
|
||||||
|
|
||||||
assert volumes[0].external == secret1['file']
|
assert volumes[0].source == secret1['file']
|
||||||
assert volumes[0].internal == '{}/{}'.format(SECRETS_PATH, secret1['secret'].target)
|
assert volumes[0].target == '{}/{}'.format(SECRETS_PATH, secret1['secret'].target)
|
||||||
|
|
||||||
def test_get_secret_volumes_abspath(self):
|
def test_get_secret_volumes_abspath(self):
|
||||||
secret1 = {
|
secret1 = {
|
||||||
@ -1126,8 +1151,8 @@ class ServiceSecretTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
volumes = service.get_secret_volumes()
|
volumes = service.get_secret_volumes()
|
||||||
|
|
||||||
assert volumes[0].external == secret1['file']
|
assert volumes[0].source == secret1['file']
|
||||||
assert volumes[0].internal == secret1['secret'].target
|
assert volumes[0].target == secret1['secret'].target
|
||||||
|
|
||||||
def test_get_secret_volumes_no_target(self):
|
def test_get_secret_volumes_no_target(self):
|
||||||
secret1 = {
|
secret1 = {
|
||||||
@ -1142,5 +1167,5 @@ class ServiceSecretTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
volumes = service.get_secret_volumes()
|
volumes = service.get_secret_volumes()
|
||||||
|
|
||||||
assert volumes[0].external == secret1['file']
|
assert volumes[0].source == secret1['file']
|
||||||
assert volumes[0].internal == '{}/{}'.format(SECRETS_PATH, secret1['secret'].source)
|
assert volumes[0].target == '{}/{}'.format(SECRETS_PATH, secret1['secret'].source)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user