diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c07e9ca..17487890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,101 +1,6 @@ Change log ========== -1.11.0 (2017-02-08) -------------------- - -### New Features - -#### Compose file version 3.1 - -- Introduced version 3.1 of the `docker-compose.yml` specification. This - version requires Docker Engine 1.13.0 or above. It introduces support - for secrets. See the documentation for more information - -#### Compose file version 2.0 and up - -- Introduced the `docker-compose top` command that displays processes running - for the different services managed by Compose. - -### Bugfixes - -- Fixed a bug where extending a service defining a healthcheck dictionary - would cause `docker-compose` to error out. - -- Fixed an issue where the `pid` entry in a service definition was being - ignored when using multiple Compose files. - -1.10.1 (2017-02-01) ------------------- - -### Bugfixes - -- Fixed an issue where presence of older versions of the docker-py - package would cause unexpected crashes while running Compose - -- Fixed an issue where healthcheck dependencies would be lost when - using multiple compose files for a project - -- Fixed a few issues that made the output of the `config` command - invalid - -- Fixed an issue where adding volume labels to v3 Compose files would - result in an error - -- Fixed an issue on Windows where build context paths containing unicode - characters were being improperly encoded - -- Fixed a bug where Compose would occasionally crash while streaming logs - when containers would stop or restart - -1.10.0 (2017-01-18) -------------------- - -### New Features - -#### Compose file version 3.0 - -- Introduced version 3.0 of the `docker-compose.yml` specification. This - version requires to be used with Docker Engine 1.13 or above and is - specifically designed to work with the `docker stack` commands. - -#### Compose file version 2.1 and up - -- Healthcheck configuration can now be done in the service definition using - the `healthcheck` parameter - -- Containers dependencies can now be set up to wait on positive healthchecks - when declared using `depends_on`. See the documentation for the updated - syntax. - **Note:** This feature will not be ported to version 3 Compose files. - -- Added support for the `sysctls` parameter in service definitions - -- Added support for the `userns_mode` parameter in service definitions - -- Compose now adds identifying labels to networks and volumes it creates - -#### Compose file version 2.0 and up - -- Added support for the `stop_grace_period` option in service definitions. - -### Bugfixes - -- Colored output now works properly on Windows. - -- Fixed a bug where docker-compose run would fail to set up link aliases - in interactive mode on Windows. - -- Networks created by Compose are now always made attachable - (Compose files v2.1 and up). - -- Fixed a bug where falsy values of `COMPOSE_CONVERT_WINDOWS_PATHS` - (`0`, `false`, empty value) were being interpreted as true. - -- Fixed a bug where forward slashes in some .dockerignore patterns weren't - being parsed correctly on Windows - - 1.9.0 (2016-11-16) ----------------- @@ -238,7 +143,7 @@ Bug Fixes - Fixed a bug in Windows environment where volume mappings of the host's root directory would be parsed incorrectly. -- Fixed a bug where `docker-compose config` would output an invalid +- Fixed a bug where `docker-compose config` would ouput an invalid Compose file if external networks were specified. - Fixed an issue where unset buildargs would be assigned a string @@ -620,7 +525,7 @@ Bug Fixes: if at least one container is using the network. - When printings logs during `up` or `logs`, flush the output buffer after - each line to prevent buffering issues from hiding logs. + each line to prevent buffering issues from hideing logs. - Recreate a container if one of its dependencies is being created. Previously a container was only recreated if it's dependencies already @@ -909,7 +814,7 @@ Fig has been renamed to Docker Compose, or just Compose for short. This has seve - The command you type is now `docker-compose`, not `fig`. - You should rename your fig.yml to docker-compose.yml. -- If you’re installing via PyPI, the package is now `docker-compose`, so install it with `pip install docker-compose`. +- If you’re installing via PyPi, the package is now `docker-compose`, so install it with `pip install docker-compose`. Besides that, there’s a lot of new stuff in this release: diff --git a/Dockerfile b/Dockerfile index a03e1510..63fac3eb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,6 @@ RUN set -ex; \ ca-certificates \ curl \ libsqlite3-dev \ - libbz2-dev \ ; \ rm -rf /var/lib/apt/lists/* @@ -21,32 +20,40 @@ RUN curl https://get.docker.com/builds/Linux/x86_64/docker-1.8.3 \ -o /usr/local/bin/docker && \ chmod +x /usr/local/bin/docker -# Build Python 2.7.13 from source +# Build Python 2.7.9 from source RUN set -ex; \ - curl -L https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz | tar -xz; \ - cd Python-2.7.13; \ + curl -L https://www.python.org/ftp/python/2.7.9/Python-2.7.9.tgz | tar -xz; \ + cd Python-2.7.9; \ ./configure --enable-shared; \ make; \ make install; \ cd ..; \ - rm -rf /Python-2.7.13 + rm -rf /Python-2.7.9 # Build python 3.4 from source RUN set -ex; \ - curl -L https://www.python.org/ftp/python/3.4.6/Python-3.4.6.tgz | tar -xz; \ - cd Python-3.4.6; \ + curl -L https://www.python.org/ftp/python/3.4.3/Python-3.4.3.tgz | tar -xz; \ + cd Python-3.4.3; \ ./configure --enable-shared; \ make; \ make install; \ cd ..; \ - rm -rf /Python-3.4.6 + rm -rf /Python-3.4.3 # Make libpython findable ENV LD_LIBRARY_PATH /usr/local/lib +# Install setuptools +RUN set -ex; \ + curl -L https://bootstrap.pypa.io/ez_setup.py | python + # Install pip RUN set -ex; \ - curl -L https://bootstrap.pypa.io/get-pip.py | python + curl -L https://pypi.python.org/packages/source/p/pip/pip-8.1.1.tar.gz | tar -xz; \ + cd pip-8.1.1; \ + python setup.py install; \ + cd ..; \ + rm -rf pip-8.1.1 # Python3 requires a valid locale RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen diff --git a/Dockerfile.run b/Dockerfile.run index de46e35e..4e76d64f 100644 --- a/Dockerfile.run +++ b/Dockerfile.run @@ -1,6 +1,5 @@ FROM alpine:3.4 -ARG version RUN apk -U add \ python \ py-pip @@ -8,7 +7,7 @@ RUN apk -U add \ COPY requirements.txt /code/requirements.txt RUN pip install -r /code/requirements.txt -COPY dist/docker_compose-${version}-py2.py3-none-any.whl /code/ -RUN pip install --no-deps /code/docker_compose-${version}-py2.py3-none-any.whl +ADD dist/docker-compose-release.tar.gz /code/docker-compose +RUN pip install --no-deps /code/docker-compose/docker-compose-* ENTRYPOINT ["/usr/bin/docker-compose"] diff --git a/Jenkinsfile b/Jenkinsfile index 51136b1f..19ccb4bb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,10 +2,17 @@ def image +def checkDocs = { -> + wrappedNode(label: 'linux') { + deleteDir(); checkout(scm) + documentationChecker("docs") + } +} + def buildImage = { -> wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) { stage("build image") { - checkout(scm) + deleteDir(); checkout(scm) def imageName = "dockerbuildbot/compose:${gitCommit()}" image = docker.image(imageName) try { @@ -32,7 +39,7 @@ def runTests = { Map settings -> { -> wrappedNode(label: "ubuntu && !zfs", cleanWorkspace: true) { stage("test python=${pythonVersions} / docker=${dockerVersions}") { - checkout(scm) + deleteDir(); checkout(scm) def storageDriver = sh(script: 'docker info | awk -F \': \' \'$1 == "Storage Driver" { print $2; exit }\'', returnStdout: true).trim() echo "Using local system's storage driver: ${storageDriver}" sh """docker run \\ @@ -55,10 +62,19 @@ def runTests = { Map settings -> } } -buildImage() -// TODO: break this out into meaningful "DOCKER_VERSIONS" values instead of all +def buildAndTest = { -> + buildImage() + // TODO: break this out into meaningful "DOCKER_VERSIONS" values instead of all + parallel( + failFast: true, + all_py27: runTests(pythonVersions: "py27", dockerVersions: "all"), + all_py34: runTests(pythonVersions: "py34", dockerVersions: "all"), + ) +} + + parallel( - failFast: true, - all_py27: runTests(pythonVersions: "py27", dockerVersions: "all"), - all_py34: runTests(pythonVersions: "py34", dockerVersions: "all"), + failFast: false, + docs: checkDocs, + test: buildAndTest ) diff --git a/README.md b/README.md index 35a10b90..93550f5a 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a Compose file to configure your application's services. Then, using a single command, you create and start all the services from your configuration. To learn more about all the features of Compose -see [the list of features](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#features). +see [the list of features](https://github.com/docker/compose/blob/release/docs/overview.md#features). Compose is great for development, testing, and staging environments, as well as CI workflows. You can learn more about each case in -[Common Use Cases](https://github.com/docker/docker.github.io/blob/master/compose/overview.md#common-use-cases). +[Common Use Cases](https://github.com/docker/compose/blob/release/docs/overview.md#common-use-cases). Using Compose is basically a three-step process. @@ -35,7 +35,7 @@ A `docker-compose.yml` looks like this: image: redis For more information about the Compose file, see the -[Compose file reference](https://github.com/docker/docker.github.io/blob/master/compose/compose-file/compose-versioning.md) +[Compose file reference](https://github.com/docker/compose/blob/release/docs/compose-file.md) Compose has commands for managing the whole lifecycle of your application: diff --git a/compose/__init__.py b/compose/__init__.py index b2ca86f8..072a6a0b 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1,4 @@ from __future__ import absolute_import from __future__ import unicode_literals -__version__ = '1.12.0dev' +__version__ = '1.9.0' diff --git a/compose/bundle.py b/compose/bundle.py index 505ce91f..854cc799 100644 --- a/compose/bundle.py +++ b/compose/bundle.py @@ -202,7 +202,7 @@ def convert_service_to_bundle(name, service_dict, image_digest): return container_config -# See https://github.com/docker/swarmkit/blob/agent/exec/container/container.go#L95 +# See https://github.com/docker/swarmkit/blob//agent/exec/container/container.go#L95 def set_command_and_args(config, entrypoint, command): if isinstance(entrypoint, six.string_types): entrypoint = split_command(entrypoint) diff --git a/compose/cli/__init__.py b/compose/cli/__init__.py index c5db4455..e69de29b 100644 --- a/compose/cli/__init__.py +++ b/compose/cli/__init__.py @@ -1,37 +0,0 @@ -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -import subprocess -import sys - -# Attempt to detect https://github.com/docker/compose/issues/4344 -try: - # We don't try importing pip because it messes with package imports - # on some Linux distros (Ubuntu, Fedora) - # https://github.com/docker/compose/issues/4425 - # https://github.com/docker/compose/issues/4481 - # https://github.com/pypa/pip/blob/master/pip/_vendor/__init__.py - s_cmd = subprocess.Popen( - ['pip', 'freeze'], stderr=subprocess.PIPE, stdout=subprocess.PIPE - ) - packages = s_cmd.communicate()[0].splitlines() - dockerpy_installed = len( - list(filter(lambda p: p.startswith(b'docker-py=='), packages)) - ) > 0 - if dockerpy_installed: - from .colors import red - print( - red('ERROR:'), - "Dependency conflict: an older version of the 'docker-py' package " - "is polluting the namespace. " - "Run the following command to remedy the issue:\n" - "pip uninstall docker docker-py; pip install docker", - file=sys.stderr - ) - sys.exit(1) - -except OSError: - # pip command is not available, which indicates it's probably the binary - # distribution of Compose which is not affected - pass diff --git a/compose/cli/colors.py b/compose/cli/colors.py index f1251e43..3c18886f 100644 --- a/compose/cli/colors.py +++ b/compose/cli/colors.py @@ -1,8 +1,5 @@ from __future__ import absolute_import from __future__ import unicode_literals - -import colorama - NAMES = [ 'grey', 'red', @@ -33,7 +30,6 @@ def make_color_fn(code): return lambda s: ansi_color(code, s) -colorama.init(strip=False) for (name, code) in get_pairs(): globals()[name] = make_color_fn(code) diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index 018d2451..b196d303 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import logging -from docker import APIClient +from docker import Client from docker.errors import TLSParameterError from docker.tls import TLSConfig from docker.utils import kwargs_from_env @@ -71,4 +71,4 @@ def docker_client(environment, version=None, tls_config=None, host=None, kwargs['user_agent'] = generate_user_agent() - return APIClient(**kwargs) + return Client(**kwargs) diff --git a/compose/cli/main.py b/compose/cli/main.py index 51ba36a0..08e58e37 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -24,6 +24,7 @@ from ..config import ConfigurationError from ..config import parse_environment from ..config.environment import Environment from ..config.serialize import serialize_config +from ..const import DEFAULT_TIMEOUT from ..const import IS_WINDOWS_PLATFORM from ..errors import StreamParseError from ..progress_stream import StreamOutputError @@ -191,7 +192,6 @@ class TopLevelCommand(object): scale Set number of containers for a service start Start services stop Stop services - top Display the running processes unpause Unpause services up Create and start containers version Show the Docker-Compose version information @@ -726,7 +726,7 @@ class TopLevelCommand(object): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) for s in options['SERVICE=NUM']: if '=' not in s: @@ -760,7 +760,7 @@ class TopLevelCommand(object): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) self.project.stop(service_names=options['SERVICE'], timeout=timeout) def restart(self, options): @@ -773,37 +773,10 @@ class TopLevelCommand(object): -t, --timeout TIMEOUT Specify a shutdown timeout in seconds. (default: 10) """ - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) containers = self.project.restart(service_names=options['SERVICE'], timeout=timeout) exit_if(not containers, 'No containers to restart', 1) - def top(self, options): - """ - Display the running processes - - Usage: top [SERVICE...] - - """ - containers = sorted( - self.project.containers(service_names=options['SERVICE'], stopped=False) + - self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only), - key=attrgetter('name') - ) - - for idx, container in enumerate(containers): - if idx > 0: - print() - - top_data = self.project.client.top(container.name) - headers = top_data.get("Titles") - rows = [] - - for process in top_data.get("Processes", []): - rows.append(process) - - print(container.name) - print(Formatter().table(headers, rows)) - def unpause(self, options): """ Unpause services. @@ -858,7 +831,7 @@ class TopLevelCommand(object): start_deps = not options['--no-deps'] cascade_stop = options['--abort-on-container-exit'] service_names = options['SERVICE'] - timeout = timeout_from_opts(options) + timeout = int(options.get('--timeout') or DEFAULT_TIMEOUT) remove_orphans = options['--remove-orphans'] detached = options.get('-d') @@ -923,11 +896,6 @@ def convergence_strategy_from_opts(options): return ConvergenceStrategy.changed -def timeout_from_opts(options): - timeout = options.get('--timeout') - return None if timeout is None else int(timeout) - - def image_type_from_opt(flag, value): if not value: return ImageType.none @@ -1016,7 +984,6 @@ def run_one_off_container(container_options, project, service, options): try: try: if IS_WINDOWS_PLATFORM: - service.connect_container_to_networks(container) exit_code = call_docker(["start", "--attach", "--interactive", container.id]) else: operation = RunOperation( diff --git a/compose/config/config.py b/compose/config/config.py index 4c9cf423..9d23b34d 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -12,14 +12,10 @@ import six import yaml from cached_property import cached_property -from . import types from ..const import COMPOSEFILE_V1 as V1 from ..const import COMPOSEFILE_V2_0 as V2_0 from ..const import COMPOSEFILE_V2_1 as V2_1 -from ..const import COMPOSEFILE_V3_0 as V3_0 -from ..const import COMPOSEFILE_V3_1 as V3_1 from ..utils import build_string_dict -from ..utils import parse_nanoseconds_int from ..utils import splitdrive from .environment import env_vars_from_file from .environment import Environment @@ -68,7 +64,6 @@ DOCKER_CONFIG_KEYS = [ 'extra_hosts', 'group_add', 'hostname', - 'healthcheck', 'image', 'ipc', 'labels', @@ -78,21 +73,18 @@ DOCKER_CONFIG_KEYS = [ 'memswap_limit', 'mem_swappiness', 'net', - 'oom_score_adj', + 'oom_score_adj' 'pid', 'ports', 'privileged', 'read_only', 'restart', - 'secrets', 'security_opt', 'shm_size', 'stdin_open', 'stop_signal', - 'sysctls', 'tty', 'user', - 'userns_mode', 'volume_driver', 'volumes', 'volumes_from', @@ -183,8 +175,10 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')): if version == '2': version = V2_0 - if version == '3': - version = V3_0 + if version not in (V2_0, V2_1): + raise ConfigurationError( + 'Version in "{}" is unsupported. {}' + .format(self.filename, VERSION_EXPLANATION)) return version @@ -200,11 +194,8 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')): def get_networks(self): return {} if self.version == V1 else self.config.get('networks', {}) - def get_secrets(self): - return {} if self.version < V3_1 else self.config.get('secrets', {}) - -class Config(namedtuple('_Config', 'version services volumes networks secrets')): +class Config(namedtuple('_Config', 'version services volumes networks')): """ :param version: configuration version :type version: int @@ -329,22 +320,13 @@ def load(config_details): networks = load_mapping( config_details.config_files, 'get_networks', 'Network' ) - secrets = load_secrets(config_details.config_files, config_details.working_dir) service_dicts = load_services(config_details, main_file) if main_file.version != V1: for service_dict in service_dicts: match_named_volumes(service_dict, volumes) - services_using_deploy = [s for s in service_dicts if s.get('deploy')] - if services_using_deploy: - log.warn( - "Some services ({}) use the 'deploy' key, which will be ignored. " - "Compose does not support deploy configuration - use " - "`docker stack deploy` to deploy to a swarm." - .format(", ".join(sorted(s['name'] for s in services_using_deploy)))) - - return Config(main_file.version, service_dicts, volumes, networks, secrets) + return Config(main_file.version, service_dicts, volumes, networks) def load_mapping(config_files, get_func, entity_type): @@ -358,12 +340,22 @@ def load_mapping(config_files, get_func, entity_type): external = config.get('external') if external: - validate_external(entity_type, name, config) + if len(config.keys()) > 1: + raise ConfigurationError( + '{} {} declared as external but specifies' + ' additional attributes ({}). '.format( + entity_type, + name, + ', '.join([k for k in config.keys() if k != 'external']) + ) + ) if isinstance(external, dict): config['external_name'] = external.get('name') else: config['external_name'] = name + mapping[name] = config + if 'driver_opts' in config: config['driver_opts'] = build_string_dict( config['driver_opts'] @@ -375,39 +367,6 @@ def load_mapping(config_files, get_func, entity_type): return mapping -def validate_external(entity_type, name, config): - if len(config.keys()) <= 1: - return - - raise ConfigurationError( - "{} {} declared as external but specifies additional attributes " - "({}).".format( - entity_type, name, ', '.join(k for k in config if k != 'external'))) - - -def load_secrets(config_files, working_dir): - mapping = {} - - for config_file in config_files: - for name, config in config_file.get_secrets().items(): - mapping[name] = config or {} - if not config: - continue - - external = config.get('external') - if external: - validate_external('Secret', name, config) - if isinstance(external, dict): - config['external_name'] = external.get('name') - else: - config['external_name'] = name - - if 'file' in config: - config['file'] = expand_path(working_dir, config['file']) - - return mapping - - def load_services(config_details, config_file): def build_service(service_name, service_dict, service_names): service_config = ServiceConfig.with_abs_paths( @@ -474,7 +433,7 @@ def process_config_file(config_file, environment, service_name=None): 'service', environment) - if config_file.version in (V2_0, V2_1, V3_0, V3_1): + if config_file.version in (V2_0, V2_1): processed_config = dict(config_file.config) processed_config['services'] = services processed_config['volumes'] = interpolate_config_section( @@ -487,12 +446,9 @@ def process_config_file(config_file, environment, service_name=None): config_file.get_networks(), 'network', environment) - elif config_file.version == V1: + + if config_file.version == V1: processed_config = services - else: - raise ConfigurationError( - 'Version in "{}" is unsupported. {}' - .format(config_file.filename, VERSION_EXPLANATION)) config_file = config_file._replace(config=processed_config) validate_against_config_schema(config_file) @@ -673,59 +629,10 @@ def process_service(service_config): if 'extra_hosts' in service_dict: service_dict['extra_hosts'] = parse_extra_hosts(service_dict['extra_hosts']) - if 'sysctls' in service_dict: - service_dict['sysctls'] = build_string_dict(parse_sysctls(service_dict['sysctls'])) - - service_dict = process_depends_on(service_dict) - for field in ['dns', 'dns_search', 'tmpfs']: if field in service_dict: service_dict[field] = to_list(service_dict[field]) - service_dict = process_healthcheck(service_dict, service_config.name) - - return service_dict - - -def process_depends_on(service_dict): - if 'depends_on' in service_dict and not isinstance(service_dict['depends_on'], dict): - service_dict['depends_on'] = dict([ - (svc, {'condition': 'service_started'}) for svc in service_dict['depends_on'] - ]) - return service_dict - - -def process_healthcheck(service_dict, service_name): - if 'healthcheck' not in service_dict: - return service_dict - - hc = {} - raw = service_dict['healthcheck'] - - if raw.get('disable'): - if len(raw) > 1: - raise ConfigurationError( - 'Service "{}" defines an invalid healthcheck: ' - '"disable: true" cannot be combined with other options' - .format(service_name)) - hc['test'] = ['NONE'] - elif 'test' in raw: - hc['test'] = raw['test'] - - if 'interval' in raw: - if not isinstance(raw['interval'], six.integer_types): - hc['interval'] = parse_nanoseconds_int(raw['interval']) - else: # Conversion has been done previously - hc['interval'] = raw['interval'] - if 'timeout' in raw: - if not isinstance(raw['timeout'], six.integer_types): - hc['timeout'] = parse_nanoseconds_int(raw['timeout']) - else: # Conversion has been done previously - hc['timeout'] = raw['timeout'] - if 'retries' in raw: - hc['retries'] = raw['retries'] - - service_dict['healthcheck'] = hc return service_dict @@ -745,7 +652,7 @@ def finalize_service(service_config, service_names, version, environment): if 'volumes' in service_dict: service_dict['volumes'] = [ VolumeSpec.parse( - v, environment.get_boolean('COMPOSE_CONVERT_WINDOWS_PATHS') + v, environment.get('COMPOSE_CONVERT_WINDOWS_PATHS') ) for v in service_dict['volumes'] ] @@ -763,11 +670,6 @@ def finalize_service(service_config, service_names, version, environment): if 'restart' in service_dict: service_dict['restart'] = parse_restart_spec(service_dict['restart']) - if 'secrets' in service_dict: - service_dict['secrets'] = [ - types.ServiceSecret.parse(s) for s in service_dict['secrets'] - ] - normalize_build(service_dict, service_config.working_dir, environment) service_dict['name'] = service_config.name @@ -855,17 +757,14 @@ def merge_service_dicts(base, override, version): md.merge_mapping('labels', parse_labels) md.merge_mapping('ulimits', parse_ulimits) md.merge_mapping('networks', parse_networks) - md.merge_mapping('sysctls', parse_sysctls) - md.merge_mapping('depends_on', parse_depends_on) md.merge_sequence('links', ServiceLink.parse) - md.merge_sequence('secrets', types.ServiceSecret.parse) for field in ['volumes', 'devices']: md.merge_field(field, merge_path_mappings) for field in [ 'ports', 'cap_add', 'cap_drop', 'expose', 'external_links', - 'security_opt', 'volumes_from', + 'security_opt', 'volumes_from', 'depends_on', ]: md.merge_field(field, merge_unique_items_lists, default=[]) @@ -932,11 +831,11 @@ def merge_environment(base, override): return env -def split_kv(kvpair): - if '=' in kvpair: - return kvpair.split('=', 1) +def split_label(label): + if '=' in label: + return label.split('=', 1) else: - return kvpair, '' + return label, '' def parse_dict_or_list(split_func, type_name, arguments): @@ -957,12 +856,8 @@ def parse_dict_or_list(split_func, type_name, arguments): parse_build_arguments = functools.partial(parse_dict_or_list, split_env, 'build arguments') parse_environment = functools.partial(parse_dict_or_list, split_env, 'environment') -parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels') +parse_labels = functools.partial(parse_dict_or_list, split_label, 'labels') parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks') -parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls') -parse_depends_on = functools.partial( - parse_dict_or_list, lambda k: (k, {'condition': 'service_started'}), 'depends_on' -) def parse_ulimits(ulimits): diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index 59c7b30c..76688916 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -192,7 +192,6 @@ "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "shm_size": {"type": ["number", "string"]}, "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, "stop_signal": {"type": "string"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, @@ -276,9 +275,9 @@ "type": ["boolean", "object"], "properties": { "name": {"type": "string"} - }, - "additionalProperties": false - } + } + }, + "additionalProperties": false }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v2.1.json b/compose/config/config_schema_v2.1.json index d1ffff89..3561a8cf 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -77,28 +77,7 @@ "cpu_shares": {"type": ["number", "string"]}, "cpu_quota": {"type": ["number", "string"]}, "cpuset": {"type": "string"}, - "depends_on": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "type": "object", - "additionalProperties": false, - "properties": { - "condition": { - "type": "string", - "enum": ["service_started", "service_healthy"] - } - }, - "required": ["condition"] - } - } - } - ] - }, + "depends_on": {"$ref": "#/definitions/list_of_strings"}, "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "dns": {"$ref": "#/definitions/string_or_list"}, "dns_search": {"$ref": "#/definitions/string_or_list"}, @@ -141,7 +120,6 @@ "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, "hostname": {"type": "string"}, "image": {"type": "string"}, "ipc": {"type": "string"}, @@ -215,9 +193,7 @@ "restart": {"type": "string"}, "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, "stop_signal": {"type": "string"}, "tmpfs": {"$ref": "#/definitions/string_or_list"}, "tty": {"type": "boolean"}, @@ -241,7 +217,6 @@ } }, "user": {"type": "string"}, - "userns_mode": {"type": "string"}, "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, "volume_driver": {"type": "string"}, "volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, @@ -254,24 +229,6 @@ "additionalProperties": false }, - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "network": { "id": "#/definitions/network", "type": "object", @@ -322,10 +279,10 @@ "type": ["boolean", "object"], "properties": { "name": {"type": "string"} - }, - "additionalProperties": false + } }, - "labels": {"$ref": "#/definitions/list_or_dict"} + "labels": {"$ref": "#/definitions/list_or_dict"}, + "additionalProperties": false }, "additionalProperties": false }, diff --git a/compose/config/config_schema_v3.0.json b/compose/config/config_schema_v3.0.json deleted file mode 100644 index fbcd8bb8..00000000 --- a/compose/config/config_schema_v3.0.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.0.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - } - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/config_schema_v3.1.json b/compose/config/config_schema_v3.1.json deleted file mode 100644 index b7037485..00000000 --- a/compose/config/config_schema_v3.1.json +++ /dev/null @@ -1,428 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "config_schema_v3.1.json", - "type": "object", - "required": ["version"], - - "properties": { - "version": { - "type": "string" - }, - - "services": { - "id": "#/properties/services", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/service" - } - }, - "additionalProperties": false - }, - - "networks": { - "id": "#/properties/networks", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/network" - } - } - }, - - "volumes": { - "id": "#/properties/volumes", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/volume" - } - }, - "additionalProperties": false - }, - - "secrets": { - "id": "#/properties/secrets", - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "$ref": "#/definitions/secret" - } - }, - "additionalProperties": false - } - }, - - "additionalProperties": false, - - "definitions": { - - "service": { - "id": "#/definitions/service", - "type": "object", - - "properties": { - "deploy": {"$ref": "#/definitions/deployment"}, - "build": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "context": {"type": "string"}, - "dockerfile": {"type": "string"}, - "args": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - } - ] - }, - "cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "cgroup_parent": {"type": "string"}, - "command": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "container_name": {"type": "string"}, - "depends_on": {"$ref": "#/definitions/list_of_strings"}, - "devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "dns": {"$ref": "#/definitions/string_or_list"}, - "dns_search": {"$ref": "#/definitions/string_or_list"}, - "domainname": {"type": "string"}, - "entrypoint": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "env_file": {"$ref": "#/definitions/string_or_list"}, - "environment": {"$ref": "#/definitions/list_or_dict"}, - - "expose": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "expose" - }, - "uniqueItems": true - }, - - "external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "extra_hosts": {"$ref": "#/definitions/list_or_dict"}, - "healthcheck": {"$ref": "#/definitions/healthcheck"}, - "hostname": {"type": "string"}, - "image": {"type": "string"}, - "ipc": {"type": "string"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - - "logging": { - "type": "object", - - "properties": { - "driver": {"type": "string"}, - "options": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number", "null"]} - } - } - }, - "additionalProperties": false - }, - - "mac_address": {"type": "string"}, - "network_mode": {"type": "string"}, - - "networks": { - "oneOf": [ - {"$ref": "#/definitions/list_of_strings"}, - { - "type": "object", - "patternProperties": { - "^[a-zA-Z0-9._-]+$": { - "oneOf": [ - { - "type": "object", - "properties": { - "aliases": {"$ref": "#/definitions/list_of_strings"}, - "ipv4_address": {"type": "string"}, - "ipv6_address": {"type": "string"} - }, - "additionalProperties": false - }, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - ] - }, - "pid": {"type": ["string", "null"]}, - - "ports": { - "type": "array", - "items": { - "type": ["string", "number"], - "format": "ports" - }, - "uniqueItems": true - }, - - "privileged": {"type": "boolean"}, - "read_only": {"type": "boolean"}, - "restart": {"type": "string"}, - "security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "shm_size": {"type": ["number", "string"]}, - "secrets": { - "type": "array", - "items": { - "oneOf": [ - {"type": "string"}, - { - "type": "object", - "properties": { - "source": {"type": "string"}, - "target": {"type": "string"}, - "uid": {"type": "string"}, - "gid": {"type": "string"}, - "mode": {"type": "number"} - } - } - ] - } - }, - "sysctls": {"$ref": "#/definitions/list_or_dict"}, - "stdin_open": {"type": "boolean"}, - "stop_grace_period": {"type": "string", "format": "duration"}, - "stop_signal": {"type": "string"}, - "tmpfs": {"$ref": "#/definitions/string_or_list"}, - "tty": {"type": "boolean"}, - "ulimits": { - "type": "object", - "patternProperties": { - "^[a-z]+$": { - "oneOf": [ - {"type": "integer"}, - { - "type":"object", - "properties": { - "hard": {"type": "integer"}, - "soft": {"type": "integer"} - }, - "required": ["soft", "hard"], - "additionalProperties": false - } - ] - } - } - }, - "user": {"type": "string"}, - "userns_mode": {"type": "string"}, - "volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}, - "working_dir": {"type": "string"} - }, - "additionalProperties": false - }, - - "healthcheck": { - "id": "#/definitions/healthcheck", - "type": "object", - "additionalProperties": false, - "properties": { - "disable": {"type": "boolean"}, - "interval": {"type": "string"}, - "retries": {"type": "number"}, - "test": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - }, - "timeout": {"type": "string"} - } - }, - "deployment": { - "id": "#/definitions/deployment", - "type": ["object", "null"], - "properties": { - "mode": {"type": "string"}, - "replicas": {"type": "integer"}, - "labels": {"$ref": "#/definitions/list_or_dict"}, - "update_config": { - "type": "object", - "properties": { - "parallelism": {"type": "integer"}, - "delay": {"type": "string", "format": "duration"}, - "failure_action": {"type": "string"}, - "monitor": {"type": "string", "format": "duration"}, - "max_failure_ratio": {"type": "number"} - }, - "additionalProperties": false - }, - "resources": { - "type": "object", - "properties": { - "limits": {"$ref": "#/definitions/resource"}, - "reservations": {"$ref": "#/definitions/resource"} - } - }, - "restart_policy": { - "type": "object", - "properties": { - "condition": {"type": "string"}, - "delay": {"type": "string", "format": "duration"}, - "max_attempts": {"type": "integer"}, - "window": {"type": "string", "format": "duration"} - }, - "additionalProperties": false - }, - "placement": { - "type": "object", - "properties": { - "constraints": {"type": "array", "items": {"type": "string"}} - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - - "resource": { - "id": "#/definitions/resource", - "type": "object", - "properties": { - "cpus": {"type": "string"}, - "memory": {"type": "string"} - }, - "additionalProperties": false - }, - - "network": { - "id": "#/definitions/network", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "ipam": { - "type": "object", - "properties": { - "driver": {"type": "string"}, - "config": { - "type": "array", - "items": { - "type": "object", - "properties": { - "subnet": {"type": "string"} - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "internal": {"type": "boolean"}, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "volume": { - "id": "#/definitions/volume", - "type": ["object", "null"], - "properties": { - "driver": {"type": "string"}, - "driver_opts": { - "type": "object", - "patternProperties": { - "^.+$": {"type": ["string", "number"]} - } - }, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - }, - "additionalProperties": false - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "secret": { - "id": "#/definitions/secret", - "type": "object", - "properties": { - "file": {"type": "string"}, - "external": { - "type": ["boolean", "object"], - "properties": { - "name": {"type": "string"} - } - }, - "labels": {"$ref": "#/definitions/list_or_dict"} - }, - "additionalProperties": false - }, - - "string_or_list": { - "oneOf": [ - {"type": "string"}, - {"$ref": "#/definitions/list_of_strings"} - ] - }, - - "list_of_strings": { - "type": "array", - "items": {"type": "string"}, - "uniqueItems": true - }, - - "list_or_dict": { - "oneOf": [ - { - "type": "object", - "patternProperties": { - ".+": { - "type": ["string", "number", "null"] - } - }, - "additionalProperties": false - }, - {"type": "array", "items": {"type": "string"}, "uniqueItems": true} - ] - }, - - "constraints": { - "service": { - "id": "#/definitions/constraints/service", - "anyOf": [ - {"required": ["build"]}, - {"required": ["image"]} - ], - "properties": { - "build": { - "required": ["context"] - } - } - } - } - } -} diff --git a/compose/config/environment.py b/compose/config/environment.py index 4ba228c8..5d6b5af6 100644 --- a/compose/config/environment.py +++ b/compose/config/environment.py @@ -2,7 +2,6 @@ from __future__ import absolute_import from __future__ import unicode_literals import codecs -import contextlib import logging import os @@ -32,12 +31,11 @@ def env_vars_from_file(filename): elif not os.path.isfile(filename): raise ConfigurationError("%s is not a file." % (filename)) env = {} - with contextlib.closing(codecs.open(filename, 'r', 'utf-8')) as fileobj: - for line in fileobj: - line = line.strip() - if line and not line.startswith('#'): - k, v = split_env(line) - env[k] = v + for line in codecs.open(filename, 'r', 'utf-8'): + line = line.strip() + if line and not line.startswith('#'): + k, v = split_env(line) + env[k] = v return env @@ -107,14 +105,3 @@ class Environment(dict): super(Environment, self).get(key.upper(), *args, **kwargs) ) return super(Environment, self).get(key, *args, **kwargs) - - def get_boolean(self, key): - # Convert a value to a boolean using "common sense" rules. - # Unset, empty, "0" and "false" (i-case) yield False. - # All other values yield True. - value = self.get(key) - if not value: - return False - if value.lower() in ['0', 'false']: - return False - return True diff --git a/compose/config/errors.py b/compose/config/errors.py index 16ed01b8..d14cbbdd 100644 --- a/compose/config/errors.py +++ b/compose/config/errors.py @@ -3,8 +3,8 @@ from __future__ import unicode_literals VERSION_EXPLANATION = ( - 'You might be seeing this error because you\'re using the wrong Compose file version. ' - 'Either specify a supported version ("2.0", "2.1", "3.0") and place your ' + 'You might be seeing this error because you\'re using the wrong Compose ' + 'file version. Either specify a version of "2" (or "2.0") and place your ' 'service definitions under the `services` key, or omit the `version` key ' 'and place your service definitions at the root of the file to use ' 'version 1.\nFor more on the Compose file format versions, see ' diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 46d283f0..95b1387f 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -6,6 +6,7 @@ import yaml from compose.config import types from compose.config.config import V1 +from compose.config.config import V2_0 from compose.config.config import V2_1 @@ -32,20 +33,15 @@ def denormalize_config(config): if 'external_name' in net_conf: del net_conf['external_name'] - volumes = config.volumes.copy() - for vol_name, vol_conf in volumes.items(): - if 'external_name' in vol_conf: - del vol_conf['external_name'] - version = config.version - if version == V1: + if version not in (V2_0, V2_1): version = V2_1 return { 'version': version, 'services': services, 'networks': networks, - 'volumes': volumes, + 'volumes': config.volumes, } @@ -57,52 +53,13 @@ def serialize_config(config): width=80) -def serialize_ns_time_value(value): - result = (value, 'ns') - table = [ - (1000., 'us'), - (1000., 'ms'), - (1000., 's'), - (60., 'm'), - (60., 'h') - ] - for stage in table: - tmp = value / stage[0] - if tmp == int(value / stage[0]): - value = tmp - result = (int(value), stage[1]) - else: - break - return '{0}{1}'.format(*result) - - def denormalize_service_dict(service_dict, version): service_dict = service_dict.copy() if 'restart' in service_dict: - service_dict['restart'] = types.serialize_restart_spec( - service_dict['restart'] - ) + service_dict['restart'] = types.serialize_restart_spec(service_dict['restart']) if version == V1 and 'network_mode' not in service_dict: service_dict['network_mode'] = 'bridge' - if 'depends_on' in service_dict and version != V2_1: - service_dict['depends_on'] = sorted([ - svc for svc in service_dict['depends_on'].keys() - ]) - - if 'healthcheck' in service_dict: - if 'interval' in service_dict['healthcheck']: - service_dict['healthcheck']['interval'] = serialize_ns_time_value( - service_dict['healthcheck']['interval'] - ) - if 'timeout' in service_dict['healthcheck']: - service_dict['healthcheck']['timeout'] = serialize_ns_time_value( - service_dict['healthcheck']['timeout'] - ) - - if 'secrets' in service_dict: - service_dict['secrets'] = map(lambda s: s.repr(), service_dict['secrets']) - return service_dict diff --git a/compose/config/types.py b/compose/config/types.py index 811e6c1f..4c106747 100644 --- a/compose/config/types.py +++ b/compose/config/types.py @@ -10,8 +10,8 @@ from collections import namedtuple import six -from ..const import COMPOSEFILE_V1 as V1 -from .errors import ConfigurationError +from compose.config.config import V1 +from compose.config.errors import ConfigurationError from compose.const import IS_WINDOWS_PLATFORM from compose.utils import splitdrive @@ -234,27 +234,3 @@ class ServiceLink(namedtuple('_ServiceLink', 'target alias')): @property def merge_field(self): return self.alias - - -class ServiceSecret(namedtuple('_ServiceSecret', 'source target uid gid mode')): - - @classmethod - def parse(cls, spec): - if isinstance(spec, six.string_types): - return cls(spec, None, None, None, None) - return cls( - spec.get('source'), - spec.get('target'), - spec.get('uid'), - spec.get('gid'), - spec.get('mode'), - ) - - @property - def merge_field(self): - return self.source - - def repr(self): - return dict( - [(k, v) for k, v in self._asdict().items() if v is not None] - ) diff --git a/compose/config/validation.py b/compose/config/validation.py index 3f23f0a7..7452e984 100644 --- a/compose/config/validation.py +++ b/compose/config/validation.py @@ -180,13 +180,11 @@ def validate_links(service_config, service_names): def validate_depends_on(service_config, service_names): - deps = service_config.config.get('depends_on', {}) - for dependency in deps.keys(): + for dependency in service_config.config.get('depends_on', []): if dependency not in service_names: raise ConfigurationError( "Service '{s.name}' depends on service '{dep}' which is " - "undefined.".format(s=service_config, dep=dependency) - ) + "undefined.".format(s=service_config, dep=dependency)) def get_unsupported_config_msg(path, error_key): @@ -203,7 +201,7 @@ def anglicize_json_type(json_type): def is_service_dict_schema(schema_id): - return schema_id in ('config_schema_v1.json', '#/properties/services') + return schema_id in ('config_schema_v1.json', '#/properties/services') def handle_error_for_schema_with_id(error, path): diff --git a/compose/const.py b/compose/const.py index e694dbda..e7b1ae97 100644 --- a/compose/const.py +++ b/compose/const.py @@ -5,37 +5,27 @@ import sys DEFAULT_TIMEOUT = 10 HTTP_TIMEOUT = 60 -IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag'] +IMAGE_EVENTS = ['delete', 'import', 'pull', 'push', 'tag', 'untag'] IS_WINDOWS_PLATFORM = (sys.platform == "win32") LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number' LABEL_ONE_OFF = 'com.docker.compose.oneoff' LABEL_PROJECT = 'com.docker.compose.project' LABEL_SERVICE = 'com.docker.compose.service' -LABEL_NETWORK = 'com.docker.compose.network' LABEL_VERSION = 'com.docker.compose.version' -LABEL_VOLUME = 'com.docker.compose.volume' LABEL_CONFIG_HASH = 'com.docker.compose.config-hash' -SECRETS_PATH = '/run/secrets' - COMPOSEFILE_V1 = '1' COMPOSEFILE_V2_0 = '2.0' COMPOSEFILE_V2_1 = '2.1' -COMPOSEFILE_V3_0 = '3.0' -COMPOSEFILE_V3_1 = '3.1' API_VERSIONS = { COMPOSEFILE_V1: '1.21', COMPOSEFILE_V2_0: '1.22', COMPOSEFILE_V2_1: '1.24', - COMPOSEFILE_V3_0: '1.25', - COMPOSEFILE_V3_1: '1.25', } API_VERSION_TO_ENGINE_VERSION = { API_VERSIONS[COMPOSEFILE_V1]: '1.9.0', API_VERSIONS[COMPOSEFILE_V2_0]: '1.10.0', API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', - API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', - API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0', } diff --git a/compose/errors.py b/compose/errors.py index 415b41e7..376cc555 100644 --- a/compose/errors.py +++ b/compose/errors.py @@ -10,24 +10,3 @@ class OperationFailedError(Exception): class StreamParseError(RuntimeError): def __init__(self, reason): self.msg = reason - - -class HealthCheckException(Exception): - def __init__(self, reason): - self.msg = reason - - -class HealthCheckFailed(HealthCheckException): - def __init__(self, container_id): - super(HealthCheckFailed, self).__init__( - 'Container "{}" is unhealthy.'.format(container_id) - ) - - -class NoHealthCheckConfigured(HealthCheckException): - def __init__(self, service_name): - super(NoHealthCheckConfigured, self).__init__( - 'Service "{}" is missing a healthcheck configuration'.format( - service_name - ) - ) diff --git a/compose/network.py b/compose/network.py index d98f68d2..3b57cb94 100644 --- a/compose/network.py +++ b/compose/network.py @@ -4,14 +4,10 @@ from __future__ import unicode_literals import logging from docker.errors import NotFound -from docker.types import IPAMConfig -from docker.types import IPAMPool -from docker.utils import version_gte -from docker.utils import version_lt +from docker.utils import create_ipam_config +from docker.utils import create_ipam_pool from .config import ConfigurationError -from .const import LABEL_NETWORK -from .const import LABEL_PROJECT log = logging.getLogger(__name__) @@ -75,8 +71,7 @@ class Network(object): ipam=self.ipam, internal=self.internal, enable_ipv6=self.enable_ipv6, - labels=self._labels, - attachable=version_gte(self.client._version, '1.24') or None, + labels=self.labels, ) def remove(self): @@ -96,26 +91,15 @@ class Network(object): return self.external_name return '{0}_{1}'.format(self.project, self.name) - @property - def _labels(self): - if version_lt(self.client._version, '1.23'): - return None - labels = self.labels.copy() if self.labels else {} - labels.update({ - LABEL_PROJECT: self.project, - LABEL_NETWORK: self.name, - }) - return labels - def create_ipam_config_from_dict(ipam_dict): if not ipam_dict: return None - return IPAMConfig( + return create_ipam_config( driver=ipam_dict.get('driver'), pool_configs=[ - IPAMPool( + create_ipam_pool( subnet=config.get('subnet'), iprange=config.get('ip_range'), gateway=config.get('gateway'), diff --git a/compose/parallel.py b/compose/parallel.py index e495410c..7ac66b37 100644 --- a/compose/parallel.py +++ b/compose/parallel.py @@ -12,8 +12,6 @@ from six.moves.queue import Empty from six.moves.queue import Queue from compose.cli.signals import ShutdownException -from compose.errors import HealthCheckFailed -from compose.errors import NoHealthCheckConfigured from compose.errors import OperationFailedError from compose.utils import get_output_stream @@ -50,7 +48,7 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None): elif isinstance(exception, APIError): errors[get_name(obj)] = exception.explanation writer.write(get_name(obj), 'error') - elif isinstance(exception, (OperationFailedError, HealthCheckFailed, NoHealthCheckConfigured)): + elif isinstance(exception, OperationFailedError): errors[get_name(obj)] = exception.msg writer.write(get_name(obj), 'error') elif isinstance(exception, UpstreamError): @@ -166,27 +164,20 @@ def feed_queue(objects, func, get_deps, results, state): for obj in pending: deps = get_deps(obj) - try: - if any(dep[0] in state.failed for dep in deps): - log.debug('{} has upstream errors - not processing'.format(obj)) - results.put((obj, None, UpstreamError())) - state.failed.add(obj) - elif all( - dep not in objects or ( - dep in state.finished and (not ready_check or ready_check(dep)) - ) for dep, ready_check in deps - ): - log.debug('Starting producer thread for {}'.format(obj)) - t = Thread(target=producer, args=(obj, func, results)) - t.daemon = True - t.start() - state.started.add(obj) - except (HealthCheckFailed, NoHealthCheckConfigured) as e: - log.debug( - 'Healthcheck for service(s) upstream of {} failed - ' - 'not processing'.format(obj) - ) - results.put((obj, None, e)) + + if any(dep in state.failed for dep in deps): + log.debug('{} has upstream errors - not processing'.format(obj)) + results.put((obj, None, UpstreamError())) + state.failed.add(obj) + elif all( + dep not in objects or dep in state.finished + for dep in deps + ): + log.debug('Starting producer thread for {}'.format(obj)) + t = Thread(target=producer, args=(obj, func, results)) + t.daemon = True + t.start() + state.started.add(obj) if state.is_done(): results.put(STOP) @@ -257,3 +248,7 @@ def parallel_unpause(containers, options): def parallel_kill(containers, options): parallel_operation(containers, 'kill', options, 'Killing') + + +def parallel_restart(containers, options): + parallel_operation(containers, 'restart', options, 'Restarting') diff --git a/compose/progress_stream.py b/compose/progress_stream.py index 5314f89f..a0f5601f 100644 --- a/compose/progress_stream.py +++ b/compose/progress_stream.py @@ -32,11 +32,12 @@ def stream_output(output, stream): if not image_id: continue - if image_id not in lines: + if image_id in lines: + diff = len(lines) - lines[image_id] + else: lines[image_id] = len(lines) stream.write("\n") - - diff = len(lines) - lines[image_id] + diff = 0 # move cursor up `diff` rows stream.write("%c[%dA" % (27, diff)) diff --git a/compose/project.py b/compose/project.py index 133071e7..60647fe9 100644 --- a/compose/project.py +++ b/compose/project.py @@ -14,6 +14,7 @@ from .config import ConfigurationError from .config.config import V1 from .config.sort_services import get_container_name_from_network_mode from .config.sort_services import get_service_name_from_network_mode +from .const import DEFAULT_TIMEOUT from .const import IMAGE_EVENTS from .const import LABEL_ONE_OFF from .const import LABEL_PROJECT @@ -104,11 +105,6 @@ class Project(object): for volume_spec in service_dict.get('volumes', []) ] - secrets = get_secrets( - service_dict['name'], - service_dict.pop('secrets', None) or [], - config_data.secrets) - project.services.append( Service( service_dict.pop('name'), @@ -119,7 +115,6 @@ class Project(object): links=links, network_mode=network_mode, volumes_from=volumes_from, - secrets=secrets, **service_dict) ) @@ -233,10 +228,7 @@ class Project(object): services = self.get_services(service_names) def get_deps(service): - return { - (self.get_service(dep), config) - for dep, config in service.get_dependency_configs().items() - } + return {self.get_service(dep) for dep in service.get_dependency_names()} parallel.parallel_execute( services, @@ -252,13 +244,13 @@ class Project(object): def get_deps(container): # actually returning inversed dependencies - return {(other, None) for other in containers + return {other for other in containers if container.service in self.get_service(other.service).get_dependency_names()} parallel.parallel_execute( containers, - self.build_container_operation_with_timeout_func('stop', options), + operator.methodcaller('stop', **options), operator.attrgetter('name'), 'Stopping', get_deps) @@ -299,12 +291,7 @@ class Project(object): def restart(self, service_names=None, **options): containers = self.containers(service_names, stopped=True) - - parallel.parallel_execute( - containers, - self.build_container_operation_with_timeout_func('restart', options), - operator.attrgetter('name'), - 'Restarting') + parallel.parallel_restart(containers, options) return containers def build(self, service_names=None, no_cache=False, pull=False, force_rm=False): @@ -365,7 +352,7 @@ class Project(object): # TODO: get labels from the API v1.22 , see github issue 2618 try: - # this can fail if the container has been removed + # this can fail if the conatiner has been removed container = Container.from_id(self.client, event['id']) except APIError: continue @@ -378,7 +365,7 @@ class Project(object): start_deps=True, strategy=ConvergenceStrategy.changed, do_build=BuildAction.none, - timeout=None, + timeout=DEFAULT_TIMEOUT, detached=False, remove_orphans=False): @@ -403,10 +390,7 @@ class Project(object): ) def get_deps(service): - return { - (self.get_service(dep), config) - for dep, config in service.get_dependency_configs().items() - } + return {self.get_service(dep) for dep in service.get_dependency_names()} results, errors = parallel.parallel_execute( services, @@ -522,14 +506,6 @@ class Project(object): dep_services.append(service) return acc + dep_services - def build_container_operation_with_timeout_func(self, operation, options): - def container_operation_with_timeout(container): - if options.get('timeout') is None: - service = self.get_service(container.service) - options['timeout'] = service.stop_timeout(None) - return getattr(container, operation)(**options) - return container_operation_with_timeout - def get_volumes_from(project, service_dict): volumes_from = service_dict.pop('volumes_from', None) @@ -559,33 +535,6 @@ def get_volumes_from(project, service_dict): return [build_volume_from(vf) for vf in volumes_from] -def get_secrets(service, service_secrets, secret_defs): - secrets = [] - - for secret in service_secrets: - secret_def = secret_defs.get(secret.source) - if not secret_def: - raise ConfigurationError( - "Service \"{service}\" uses an undefined secret \"{secret}\" " - .format(service=service, secret=secret.source)) - - if secret_def.get('external_name'): - log.warn("Service \"{service}\" uses secret \"{secret}\" which is external. " - "External secrets are not available to containers created by " - "docker-compose.".format(service=service, secret=secret.source)) - continue - - if secret.uid or secret.gid or secret.mode: - log.warn("Service \"{service}\" uses secret \"{secret}\" with uid, " - "gid, or mode. These fields are not supported by this " - "implementation of the Compose file".format( - service=service, secret=secret.source)) - - secrets.append({'secret': secret, 'file': secret_def.get('file')}) - - return secrets - - def warn_for_swarm_mode(client): info = client.info() if info.get('Swarm', {}).get('LocalNodeState') == 'active': @@ -598,7 +547,9 @@ def warn_for_swarm_mode(client): "Compose does not use swarm mode to deploy services to multiple nodes in a swarm. " "All containers will be scheduled on the current node.\n\n" "To deploy your application across the swarm, " - "use `docker stack deploy`.\n" + "use the bundle feature of the Docker experimental build.\n\n" + "More info:\n" + "https://docs.docker.com/compose/bundles\n" ) diff --git a/compose/service.py b/compose/service.py index 9f2fc68b..ad426706 100644 --- a/compose/service.py +++ b/compose/service.py @@ -10,20 +10,17 @@ from operator import attrgetter import enum import six from docker.errors import APIError -from docker.errors import ImageNotFound from docker.errors import NotFound -from docker.types import LogConfig +from docker.utils import LogConfig from docker.utils.ports import build_port_bindings from docker.utils.ports import split_port from . import __version__ -from . import const from . import progress_stream from .config import DOCKER_CONFIG_KEYS from .config import merge_environment from .config.types import VolumeSpec from .const import DEFAULT_TIMEOUT -from .const import IS_WINDOWS_PLATFORM from .const import LABEL_CONFIG_HASH from .const import LABEL_CONTAINER_NUMBER from .const import LABEL_ONE_OFF @@ -31,15 +28,12 @@ from .const import LABEL_PROJECT from .const import LABEL_SERVICE from .const import LABEL_VERSION from .container import Container -from .errors import HealthCheckFailed -from .errors import NoHealthCheckConfigured from .errors import OperationFailedError from .parallel import parallel_execute from .parallel import parallel_start from .progress_stream import stream_output from .progress_stream import StreamOutputError from .utils import json_hash -from .utils import parse_seconds_float log = logging.getLogger(__name__) @@ -69,14 +63,9 @@ DOCKER_START_KEYS = [ 'restart', 'security_opt', 'shm_size', - 'sysctls', - 'userns_mode', 'volumes_from', ] -CONDITION_STARTED = 'service_started' -CONDITION_HEALTHY = 'service_healthy' - class BuildError(Exception): def __init__(self, service, reason): @@ -140,7 +129,6 @@ class Service(object): volumes_from=None, network_mode=None, networks=None, - secrets=None, **options ): self.name = name @@ -151,7 +139,6 @@ class Service(object): self.volumes_from = volumes_from or [] self.network_mode = network_mode or NetworkMode(None) self.networks = networks or {} - self.secrets = secrets or [] self.options = options def __repr__(self): @@ -182,7 +169,7 @@ class Service(object): self.start_container_if_stopped(c, **options) return containers - def scale(self, desired_num, timeout=None): + def scale(self, desired_num, timeout=DEFAULT_TIMEOUT): """ Adjusts the number of containers to the specified number and ensures they are running. @@ -209,7 +196,7 @@ class Service(object): return container def stop_and_remove(container): - container.stop(timeout=self.stop_timeout(timeout)) + container.stop(timeout=timeout) container.remove() running_containers = self.containers(stopped=False) @@ -328,8 +315,11 @@ class Service(object): def image(self): try: return self.client.inspect_image(self.image_name) - except ImageNotFound: - raise NoSuchImageError("Image '{}' not found".format(self.image_name)) + except APIError as e: + if e.response.status_code == 404 and e.explanation and 'No such image' in str(e.explanation): + raise NoSuchImageError("Image '{}' not found".format(self.image_name)) + else: + raise @property def image_name(self): @@ -384,7 +374,7 @@ class Service(object): def execute_convergence_plan(self, plan, - timeout=None, + timeout=DEFAULT_TIMEOUT, detached=False, start=True): (action, containers) = plan @@ -431,7 +421,7 @@ class Service(object): def recreate_container( self, container, - timeout=None, + timeout=DEFAULT_TIMEOUT, attach_logs=False, start_new_container=True): """Recreate a container. @@ -442,7 +432,7 @@ class Service(object): """ log.info("Recreating %s" % container.name) - container.stop(timeout=self.stop_timeout(timeout)) + container.stop(timeout=timeout) container.rename_to_tmp_name() new_container = self.create_container( previous_container=container, @@ -456,14 +446,6 @@ class Service(object): container.remove() return new_container - def stop_timeout(self, timeout): - if timeout is not None: - return timeout - timeout = parse_seconds_float(self.options.get('stop_grace_period')) - if timeout is not None: - return timeout - return DEFAULT_TIMEOUT - def start_container_if_stopped(self, container, attach_logs=False, quiet=False): if not container.is_running: if not quiet: @@ -501,10 +483,10 @@ class Service(object): link_local_ips=netdefs.get('link_local_ips', None), ) - def remove_duplicate_containers(self, timeout=None): + def remove_duplicate_containers(self, timeout=DEFAULT_TIMEOUT): for c in self.duplicate_containers(): log.info('Removing %s' % c.name) - c.stop(timeout=self.stop_timeout(timeout)) + c.stop(timeout=timeout) c.remove() def duplicate_containers(self): @@ -540,38 +522,10 @@ class Service(object): def get_dependency_names(self): net_name = self.network_mode.service_name - return ( - self.get_linked_service_names() + - self.get_volumes_from_names() + - ([net_name] if net_name else []) + - list(self.options.get('depends_on', {}).keys()) - ) - - def get_dependency_configs(self): - net_name = self.network_mode.service_name - configs = dict( - [(name, None) for name in self.get_linked_service_names()] - ) - configs.update(dict( - [(name, None) for name in self.get_volumes_from_names()] - )) - configs.update({net_name: None} if net_name else {}) - configs.update(self.options.get('depends_on', {})) - for svc, config in self.options.get('depends_on', {}).items(): - if config['condition'] == CONDITION_STARTED: - configs[svc] = lambda s: True - elif config['condition'] == CONDITION_HEALTHY: - configs[svc] = lambda s: s.is_healthy() - else: - # The config schema already prevents this, but it might be - # bypassed if Compose is called programmatically. - raise ValueError( - 'depends_on condition "{}" is invalid.'.format( - config['condition'] - ) - ) - - return configs + return (self.get_linked_service_names() + + self.get_volumes_from_names() + + ([net_name] if net_name else []) + + self.options.get('depends_on', [])) def get_linked_service_names(self): return [service.name for (service, _) in self.links] @@ -695,14 +649,9 @@ class Service(object): override_options['binds'] = binds container_options['environment'].update(affinity) - container_options['volumes'] = dict( - (v.internal, {}) for v in container_options.get('volumes') or {}) - - secret_volumes = self.get_secret_volumes() - if secret_volumes: - override_options['binds'].extend(v.repr() for v in secret_volumes) - container_options['volumes'].update( - (v.internal, {}) for v in secret_volumes) + if 'volumes' in container_options: + container_options['volumes'] = dict( + (v.internal, {}) for v in container_options['volumes']) container_options['image'] = self.image_name @@ -759,12 +708,10 @@ class Service(object): cgroup_parent=options.get('cgroup_parent'), cpu_quota=options.get('cpu_quota'), shm_size=options.get('shm_size'), - sysctls=options.get('sysctls'), tmpfs=options.get('tmpfs'), oom_score_adj=options.get('oom_score_adj'), mem_swappiness=options.get('mem_swappiness'), - group_add=options.get('group_add'), - userns_mode=options.get('userns_mode') + group_add=options.get('group_add') ) # TODO: Add as an argument to create_host_config once it's supported @@ -773,23 +720,14 @@ class Service(object): return host_config - def get_secret_volumes(self): - def build_spec(secret): - target = '{}/{}'.format( - const.SECRETS_PATH, - secret['secret'].target or secret['secret'].source) - return VolumeSpec(secret['file'], target, 'ro') - - return [build_spec(secret) for secret in self.secrets] - def build(self, no_cache=False, pull=False, force_rm=False): log.info('Building %s' % self.name) build_opts = self.options.get('build', {}) path = build_opts.get('context') - # python2 os.stat() doesn't support unicode on some UNIX, so we - # encode it to a bytestring to be safe - if not six.PY3 and not IS_WINDOWS_PLATFORM: + # python2 os.path() doesn't support unicode, so we need to encode it to + # a byte string + if not six.PY3: path = path.encode('utf8') build_output = self.client.build( @@ -920,24 +858,6 @@ class Service(object): else: log.error(six.text_type(e)) - def is_healthy(self): - """ Check that all containers for this service report healthy. - Returns false if at least one healthcheck is pending. - If an unhealthy container is detected, raise a HealthCheckFailed - exception. - """ - result = True - for ctnr in self.containers(): - ctnr.inspect() - status = ctnr.get('State.Health.Status') - if status is None: - raise NoHealthCheckConfigured(self.name) - elif status == 'starting': - result = False - elif status == 'unhealthy': - raise HealthCheckFailed(ctnr.short_id) - return result - def short_id_alias_exists(container, network): aliases = container.get( diff --git a/compose/timeparse.py b/compose/timeparse.py deleted file mode 100644 index 16ef8a6d..00000000 --- a/compose/timeparse.py +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -''' -timeparse.py -(c) Will Roberts 1 February, 2014 - -This is a vendored and modified copy of: -github.com/wroberts/pytimeparse @ cc0550d - -It has been modified to mimic the behaviour of -https://golang.org/pkg/time/#ParseDuration -''' -# MIT LICENSE -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. -from __future__ import absolute_import -from __future__ import unicode_literals - -import re - -HOURS = r'(?P[\d.]+)h' -MINS = r'(?P[\d.]+)m' -SECS = r'(?P[\d.]+)s' -MILLI = r'(?P[\d.]+)ms' -MICRO = r'(?P[\d.]+)(?:us|µs)' -NANO = r'(?P[\d.]+)ns' - - -def opt(x): - return r'(?:{x})?'.format(x=x) - - -TIMEFORMAT = r'{HOURS}{MINS}{SECS}{MILLI}{MICRO}{NANO}'.format( - HOURS=opt(HOURS), - MINS=opt(MINS), - SECS=opt(SECS), - MILLI=opt(MILLI), - MICRO=opt(MICRO), - NANO=opt(NANO), -) - -MULTIPLIERS = dict([ - ('hours', 60 * 60), - ('mins', 60), - ('secs', 1), - ('milli', 1.0 / 1000), - ('micro', 1.0 / 1000.0 / 1000), - ('nano', 1.0 / 1000.0 / 1000.0 / 1000.0), -]) - - -def timeparse(sval): - """Parse a time expression, returning it as a number of seconds. If - possible, the return value will be an `int`; if this is not - possible, the return will be a `float`. Returns `None` if a time - expression cannot be parsed from the given string. - - Arguments: - - `sval`: the string value to parse - - >>> timeparse('1m24s') - 84 - >>> timeparse('1.2 minutes') - 72 - >>> timeparse('1.2 seconds') - 1.2 - """ - match = re.match(r'\s*' + TIMEFORMAT + r'\s*$', sval, re.I) - if not match or not match.group(0).strip(): - return - - mdict = match.groupdict() - return sum( - MULTIPLIERS[k] * cast(v) for (k, v) in mdict.items() if v is not None) - - -def cast(value): - return int(value, 10) if value.isdigit() else float(value) diff --git a/compose/utils.py b/compose/utils.py index b8bdf732..8f05e308 100644 --- a/compose/utils.py +++ b/compose/utils.py @@ -11,7 +11,6 @@ import ntpath import six from .errors import StreamParseError -from .timeparse import timeparse json_decoder = json.JSONDecoder() @@ -108,21 +107,6 @@ def microseconds_from_time_nano(time_nano): return int(time_nano % 1000000000 / 1000) -def nanoseconds_from_time_seconds(time_seconds): - return time_seconds * 1000000000 - - -def parse_seconds_float(value): - return timeparse(value or '') - - -def parse_nanoseconds_int(value): - parsed = timeparse(value or '') - if parsed is None: - return None - return int(parsed * 1000000000) - - def build_string_dict(source_dict): return dict((k, str(v if v is not None else '')) for k, v in source_dict.items()) diff --git a/compose/volume.py b/compose/volume.py index ab6a88fa..1fd1d51c 100644 --- a/compose/volume.py +++ b/compose/volume.py @@ -4,11 +4,8 @@ from __future__ import unicode_literals import logging from docker.errors import NotFound -from docker.utils import version_lt from .config import ConfigurationError -from .const import LABEL_PROJECT -from .const import LABEL_VOLUME log = logging.getLogger(__name__) @@ -26,7 +23,7 @@ class Volume(object): def create(self): return self.client.create_volume( - self.full_name, self.driver, self.driver_opts, labels=self._labels + self.full_name, self.driver, self.driver_opts, labels=self.labels ) def remove(self): @@ -56,17 +53,6 @@ class Volume(object): return self.external_name return '{0}_{1}'.format(self.project, self.name) - @property - def _labels(self): - if version_lt(self.client._version, '1.23'): - return None - labels = self.labels.copy() if self.labels else {} - labels.update({ - LABEL_PROJECT: self.project, - LABEL_VOLUME: self.name, - }) - return labels - class ProjectVolumes(object): diff --git a/contrib/completion/bash/docker-compose b/contrib/completion/bash/docker-compose index 77d02b42..991f6572 100644 --- a/contrib/completion/bash/docker-compose +++ b/contrib/completion/bash/docker-compose @@ -434,18 +434,6 @@ _docker_compose_stop() { } -_docker_compose_top() { - case "$cur" in - -*) - COMPREPLY=( $( compgen -W "--help" -- "$cur" ) ) - ;; - *) - __docker_compose_services_running - ;; - esac -} - - _docker_compose_unpause() { case "$cur" in -*) @@ -511,7 +499,6 @@ _docker_compose() { scale start stop - top unpause up version diff --git a/contrib/completion/zsh/_docker-compose b/contrib/completion/zsh/_docker-compose index 66d924f7..928e28de 100644 --- a/contrib/completion/zsh/_docker-compose +++ b/contrib/completion/zsh/_docker-compose @@ -341,11 +341,6 @@ __docker-compose_subcommand() { $opts_timeout \ '*:running services:__docker-compose_runningservices' && ret=0 ;; - (top) - _arguments \ - $opts_help \ - '*:running services:__docker-compose_runningservices' && ret=0 - ;; (unpause) _arguments \ $opts_help \ @@ -391,17 +386,9 @@ _docker-compose() { integer ret=1 typeset -A opt_args - local file_description - - if [[ -n ${words[(r)-f]} || -n ${words[(r)--file]} ]] ; then - file_description="Specify an override docker-compose file (default: docker-compose.override.yml)" - else - file_description="Specify an alternate docker-compose file (default: docker-compose.yml)" - fi - _arguments -C \ '(- :)'{-h,--help}'[Get help]' \ - '*'{-f,--file}"[${file_description}]:file:_files -g '*.yml'" \ + '(-f --file)'{-f,--file}'[Specify an alternate docker-compose file (default: docker-compose.yml)]:file:_files -g "*.yml"' \ '(-p --project-name)'{-p,--project-name}'[Specify an alternate project name (default: directory name)]:project name:' \ '--verbose[Show more output]' \ '(- :)'{-v,--version}'[Print version and exit]' \ diff --git a/docker-compose.spec b/docker-compose.spec index ef0e2593..e57d6b7a 100644 --- a/docker-compose.spec +++ b/docker-compose.spec @@ -32,16 +32,6 @@ exe = EXE(pyz, 'compose/config/config_schema_v2.1.json', 'DATA' ), - ( - 'compose/config/config_schema_v3.0.json', - 'compose/config/config_schema_v3.0.json', - 'DATA' - ), - ( - 'compose/config/config_schema_v3.1.json', - 'compose/config/config_schema_v3.1.json', - 'DATA' - ), ( 'compose/GITSHA', 'compose/GITSHA', diff --git a/project/RELEASE-PROCESS.md b/project/RELEASE-PROCESS.md index c1834f2f..930af15a 100644 --- a/project/RELEASE-PROCESS.md +++ b/project/RELEASE-PROCESS.md @@ -20,30 +20,18 @@ release. As part of this script you'll be asked to: -1. Update the version in `compose/__init__.py` and `script/run/run.sh`. +1. Update the version in `docs/install.md` and `compose/__init__.py`. - If the next release will be an RC, append `-rcN`, e.g. `1.4.0-rc1`. + If the next release will be an RC, append `rcN`, e.g. `1.4.0rc1`. 2. Write release notes in `CHANGES.md`. - Almost every feature enhancement should be mentioned, with the most - visible/exciting ones first. Use descriptive sentences and give context - where appropriate. + Almost every feature enhancement should be mentioned, with the most visible/exciting ones first. Use descriptive sentences and give context where appropriate. - Bug fixes are worth mentioning if it's likely that they've affected lots - of people, or if they were regressions in the previous version. + Bug fixes are worth mentioning if it's likely that they've affected lots of people, or if they were regressions in the previous version. Improvements to the code are not worth mentioning. -3. Create a new repository on [bintray](https://bintray.com/docker-compose). - The name has to match the name of the branch (e.g. `bump-1.9.0`) and the - type should be "Generic". Other fields can be left blank. - -4. Check that the `vnext-compose` branch on - [the docs repo](https://github.com/docker/docker.github.io/) has - documentation for all the new additions in the upcoming release, and create - a PR there for what needs to be amended. - ## When a PR is merged into master that we want in the release @@ -67,8 +55,8 @@ Check out the bump branch and run the `build-binaries` script When prompted build the non-linux binaries and test them. -1. Download the osx binary from Bintray. Make sure that the latest Travis - build has finished, otherwise you'll be downloading an old binary. +1. Download the osx binary from Bintray. Make sure that the latest build has + finished, otherwise you'll be downloading an old binary. https://dl.bintray.com/docker-compose/$BRANCH_NAME/ @@ -79,24 +67,22 @@ When prompted build the non-linux binaries and test them. 3. Draft a release from the tag on GitHub (the script will open the window for you) - The tag will only be present on Github when you run the `push-release` - script in step 7, but you can pre-fill it at that point. + In the "Tag version" dropdown, select the tag you just pushed. -4. Paste in installation instructions and release notes. Here's an example - - change the Compose version and Docker version as appropriate: +4. Paste in installation instructions and release notes. Here's an example - change the Compose version and Docker version as appropriate: - If you're a Mac or Windows user, the best way to install Compose and keep it up-to-date is **[Docker for Mac and Windows](https://www.docker.com/products/docker)**. + Firstly, note that Compose 1.5.0 requires Docker 1.8.0 or later. - Note that Compose 1.9.0 requires Docker Engine 1.10.0 or later for version 2 of the Compose File format, and Docker Engine 1.9.1 or later for version 1. Docker for Mac and Windows will automatically install the latest version of Docker Engine for you. + Secondly, if you're a Mac user, the **[Docker Toolbox](https://www.docker.com/toolbox)** will install Compose 1.5.0 for you, alongside the latest versions of the Docker Engine, Machine and Kitematic. - Alternatively, you can use the usual commands to install or upgrade Compose: + Otherwise, you can use the usual commands to install/upgrade. Either download the binary: - ``` - curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose - chmod +x /usr/local/bin/docker-compose - ``` + curl -L https://github.com/docker/compose/releases/download/1.5.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose + chmod +x /usr/local/bin/docker-compose - See the [install docs](https://docs.docker.com/compose/install/) for more install options and instructions. + Or install the PyPi package: + + pip install -U docker-compose==1.5.0 Here's what's new: @@ -113,8 +99,6 @@ When prompted build the non-linux binaries and test them. ./script/release/push-release -8. Merge the bump PR. - 8. Publish the release on GitHub. 9. Check that all the binaries download (following the install instructions) and run. @@ -123,7 +107,19 @@ When prompted build the non-linux binaries and test them. ## If it’s a stable release (not an RC) -1. Close the release’s milestone. +1. Merge the bump PR. + +2. Make sure `origin/release` is updated locally: + + git fetch origin + +3. Update the `docs` branch on the upstream repo: + + git push git@github.com:docker/compose.git origin/release:docs + +4. Let the docs team know that it’s been updated so they can publish it. + +5. Close the release’s milestone. ## If it’s a minor release (1.x.0), rather than a patch release (1.x.y) diff --git a/requirements-build.txt b/requirements-build.txt index 27f610ca..3f1dbd75 100644 --- a/requirements-build.txt +++ b/requirements-build.txt @@ -1 +1 @@ -pyinstaller==3.2.1 +pyinstaller==3.1.1 diff --git a/requirements.txt b/requirements.txt index 3b06bff4..933146c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,7 @@ PyYAML==3.11 backports.ssl-match-hostname==3.5.0.1; python_version < '3' cached-property==1.2.0 -colorama==0.3.7 -docker==2.0.2 +docker-py==1.10.6 dockerpty==0.4.1 docopt==0.6.1 enum34==1.0.4; python_version < '3.4' diff --git a/script/build/image b/script/build/image index 3590ce14..bdd98f03 100755 --- a/script/build/image +++ b/script/build/image @@ -11,5 +11,6 @@ TAG=$1 VERSION="$(python setup.py --version)" ./script/build/write-git-sha -python setup.py sdist bdist_wheel -docker build --build-arg version=$VERSION -t docker/compose:$TAG -f Dockerfile.run . +python setup.py sdist +cp dist/docker-compose-$VERSION.tar.gz dist/docker-compose-release.tar.gz +docker build -t docker/compose:$TAG -f Dockerfile.run . diff --git a/script/release/make-branch b/script/release/make-branch index 7ccf3f05..bf81219d 100755 --- a/script/release/make-branch +++ b/script/release/make-branch @@ -65,8 +65,8 @@ git config "branch.${BRANCH}.release" $VERSION editor=${EDITOR:-vim} -echo "Update versions in docs/install.md, compose/__init__.py, script/run/run.sh" -$editor docs/install.md +echo "Update versions in compose/__init__.py, script/run/run.sh" +# $editor docs/install.md $editor compose/__init__.py $editor script/run/run.sh diff --git a/script/release/push-release b/script/release/push-release index 9db6f689..33d0d777 100755 --- a/script/release/push-release +++ b/script/release/push-release @@ -54,19 +54,18 @@ git push $GITHUB_REPO $VERSION echo "Uploading the docker image" docker push docker/compose:$VERSION -echo "Uploading package to PyPI" +echo "Uploading sdist to pypi" pandoc -f markdown -t rst README.md -o README.rst sed -i -e 's/logo.png?raw=true/https:\/\/github.com\/docker\/compose\/raw\/master\/logo.png?raw=true/' README.rst ./script/build/write-git-sha -python setup.py sdist bdist_wheel +python setup.py sdist if [ "$(command -v twine 2> /dev/null)" ]; then - twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz ./dist/docker_compose-${VERSION/-/}-py2.py3-none-any.whl + twine upload ./dist/docker-compose-${VERSION/-/}.tar.gz else python setup.py upload fi echo "Testing pip package" -deactivate || true virtualenv venv-test source venv-test/bin/activate pip install docker-compose==$VERSION diff --git a/script/release/utils.sh b/script/release/utils.sh index 321c1fb7..b4e5a2e6 100644 --- a/script/release/utils.sh +++ b/script/release/utils.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Util functions for release scripts +# Util functions for release scritps # set -e diff --git a/script/run/run.sh b/script/run/run.sh index 4e173894..85c7e720 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.12.0dev" +VERSION="1.9.0" IMAGE="docker/compose:$VERSION" @@ -35,7 +35,7 @@ if [ "$(pwd)" != '/' ]; then VOLUMES="-v $(pwd):$(pwd)" fi if [ -n "$COMPOSE_FILE" ]; then - compose_dir=$(realpath $(dirname $COMPOSE_FILE)) + compose_dir=$(dirname $COMPOSE_FILE) fi # TODO: also check --file argument if [ -n "$compose_dir" ]; then diff --git a/script/test/versions.py b/script/test/versions.py index 0c3b8162..45ead143 100755 --- a/script/test/versions.py +++ b/script/test/versions.py @@ -5,7 +5,7 @@ version tags for recent releases, or the default release. The default release is the most recent non-RC version. -Recent is a list of unique major.minor versions, where each is the most +Recent is a list of unqiue major.minor versions, where each is the most recent version in the series. For example, if the list of versions is: diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79cf..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py index eafbc356..672ea80e 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import -from __future__ import print_function from __future__ import unicode_literals import codecs @@ -9,7 +8,6 @@ import os import re import sys -import pkg_resources from setuptools import find_packages from setuptools import setup @@ -31,13 +29,12 @@ def find_version(*file_paths): install_requires = [ 'cached-property >= 1.2.0, < 2', - 'colorama >= 0.3.7, < 0.4', 'docopt >= 0.6.1, < 0.7', 'PyYAML >= 3.10, < 4', 'requests >= 2.6.1, != 2.11.0, < 2.12', 'texttable >= 0.8.1, < 0.9', 'websocket-client >= 0.32.0, < 1.0', - 'docker >= 2.0.2, < 3.0', + 'docker-py >= 1.10.6, < 2.0', 'dockerpty >= 0.4.1, < 0.5', 'six >= 1.3.0, < 2', 'jsonschema >= 2.5.1, < 3', @@ -51,25 +48,7 @@ tests_require = [ if sys.version_info[:2] < (3, 4): tests_require.append('mock >= 1.0.1') - -extras_require = { - ':python_version < "3.4"': ['enum34 >= 1.0.4, < 2'], - ':python_version < "3.5"': ['backports.ssl_match_hostname >= 3.5'], - ':python_version < "3.3"': ['ipaddress >= 1.0.16'], -} - - -try: - if 'bdist_wheel' not in sys.argv: - for key, value in extras_require.items(): - if key.startswith(':') and pkg_resources.evaluate_marker(key[1:]): - install_requires.extend(value) -except Exception as e: - print("Failed to compute platform dependencies: {}. ".format(e) + - "All dependencies will be installed as a result.", file=sys.stderr) - for key, value in extras_require.items(): - if key.startswith(':'): - install_requires.extend(value) + install_requires.append('enum34 >= 1.0.4, < 2') setup( @@ -83,7 +62,6 @@ setup( include_package_data=True, test_suite='nose.collector', install_requires=install_requires, - extras_require=extras_require, tests_require=tests_require, entry_points=""" [console_scripts] diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 8366ca75..f153bd95 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -21,13 +21,11 @@ from .. import mock from compose.cli.command import get_project from compose.container import Container from compose.project import OneOffFilter -from compose.utils import nanoseconds_from_time_seconds from tests.integration.testcases import DockerClientTestCase from tests.integration.testcases import get_links from tests.integration.testcases import pull_busybox from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only -from tests.integration.testcases import v3_only ProcessResult = namedtuple('ProcessResult', 'stdout stderr') @@ -262,20 +260,6 @@ class CLITestCase(DockerClientTestCase): } } - def test_config_external_volume(self): - self.base_dir = 'tests/fixtures/volumes' - result = self.dispatch(['-f', 'external-volumes.yml', 'config']) - json_result = yaml.load(result.stdout) - assert 'volumes' in json_result - assert json_result['volumes'] == { - 'foo': { - 'external': True - }, - 'bar': { - 'external': {'name': 'some_bar'} - } - } - def test_config_v1(self): self.base_dir = 'tests/fixtures/v1-config' result = self.dispatch(['config']) @@ -301,68 +285,6 @@ class CLITestCase(DockerClientTestCase): 'volumes': {}, } - @v3_only() - def test_config_v3(self): - self.base_dir = 'tests/fixtures/v3-full' - result = self.dispatch(['config']) - - assert yaml.load(result.stdout) == { - 'version': '3.0', - 'networks': {}, - 'volumes': { - 'foobar': { - 'labels': { - 'com.docker.compose.test': 'true', - }, - }, - }, - 'services': { - 'web': { - 'image': 'busybox', - 'deploy': { - 'mode': 'replicated', - 'replicas': 6, - 'labels': ['FOO=BAR'], - 'update_config': { - 'parallelism': 3, - 'delay': '10s', - 'failure_action': 'continue', - 'monitor': '60s', - 'max_failure_ratio': 0.3, - }, - 'resources': { - 'limits': { - 'cpus': '0.001', - 'memory': '50M', - }, - 'reservations': { - 'cpus': '0.0001', - 'memory': '20M', - }, - }, - 'restart_policy': { - 'condition': 'on_failure', - 'delay': '5s', - 'max_attempts': 3, - 'window': '120s', - }, - 'placement': { - 'constraints': ['node=foo'], - }, - }, - - 'healthcheck': { - 'test': 'cat /etc/passwd', - 'interval': '10s', - 'timeout': '1s', - 'retries': 5, - }, - - 'stop_grace_period': '20s', - }, - }, - } - def test_ps(self): self.project.get_service('simple').create_container() result = self.dispatch(['ps']) @@ -870,8 +792,8 @@ class CLITestCase(DockerClientTestCase): ] assert [n['Name'] for n in networks] == [network_with_label] - assert 'label_key' in networks[0]['Labels'] - assert networks[0]['Labels']['label_key'] == 'label_val' + + assert networks[0]['Labels'] == {'label_key': 'label_val'} @v2_1_only() def test_up_with_volume_labels(self): @@ -890,8 +812,8 @@ class CLITestCase(DockerClientTestCase): ] assert [v['Name'] for v in volumes] == [volume_with_label] - assert 'label_key' in volumes[0]['Labels'] - assert volumes[0]['Labels']['label_key'] == 'label_val' + + assert volumes[0]['Labels'] == {'label_key': 'label_val'} @v2_only() def test_up_no_services(self): @@ -948,50 +870,6 @@ class CLITestCase(DockerClientTestCase): assert foo_container.get('HostConfig.NetworkMode') == \ 'container:{}'.format(bar_container.id) - @v3_only() - def test_up_with_healthcheck(self): - def wait_on_health_status(container, status): - def condition(): - container.inspect() - return container.get('State.Health.Status') == status - - return wait_on_condition(condition, delay=0.5) - - self.base_dir = 'tests/fixtures/healthcheck' - self.dispatch(['up', '-d'], None) - - passes = self.project.get_service('passes') - passes_container = passes.containers()[0] - - assert passes_container.get('Config.Healthcheck') == { - "Test": ["CMD-SHELL", "/bin/true"], - "Interval": nanoseconds_from_time_seconds(1), - "Timeout": nanoseconds_from_time_seconds(30 * 60), - "Retries": 1, - } - - wait_on_health_status(passes_container, 'healthy') - - fails = self.project.get_service('fails') - fails_container = fails.containers()[0] - - assert fails_container.get('Config.Healthcheck') == { - "Test": ["CMD", "/bin/false"], - "Interval": nanoseconds_from_time_seconds(2.5), - "Retries": 2, - } - - wait_on_health_status(fails_container, 'unhealthy') - - disabled = self.project.get_service('disabled') - disabled_container = disabled.containers()[0] - - assert disabled_container.get('Config.Healthcheck') == { - "Test": ["NONE"], - } - - assert 'Health' not in disabled_container.get('State') - def test_up_with_no_deps(self): self.base_dir = 'tests/fixtures/links-composefile' self.dispatch(['up', '-d', '--no-deps', 'web'], None) @@ -1234,7 +1112,7 @@ class CLITestCase(DockerClientTestCase): container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] self.assertEqual(user, container.get('Config.User')) - def test_run_service_with_environment_overridden(self): + def test_run_service_with_environement_overridden(self): name = 'service' self.base_dir = 'tests/fixtures/environment-composefile' self.dispatch([ @@ -1246,9 +1124,9 @@ class CLITestCase(DockerClientTestCase): ]) service = self.project.get_service(name) container = service.containers(stopped=True, one_off=OneOffFilter.only)[0] - # env overridden + # env overriden self.assertEqual('notbar', container.environment['foo']) - # keep environment from yaml + # keep environement from yaml self.assertEqual('world', container.environment['hello']) # added option from command line self.assertEqual('beta', container.environment['alpha']) @@ -1293,7 +1171,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(port_range[0], "0.0.0.0:49153") self.assertEqual(port_range[1], "0.0.0.0:49154") - def test_run_service_with_explicitly_mapped_ports(self): + def test_run_service_with_explicitly_maped_ports(self): # create one off container self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch(['run', '-d', '-p', '30000:3000', '--publish', '30001:3001', 'simple']) @@ -1310,7 +1188,7 @@ class CLITestCase(DockerClientTestCase): self.assertEqual(port_short, "0.0.0.0:30000") self.assertEqual(port_full, "0.0.0.0:30001") - def test_run_service_with_explicitly_mapped_ip_ports(self): + def test_run_service_with_explicitly_maped_ip_ports(self): # create one off container self.base_dir = 'tests/fixtures/ports-composefile' self.dispatch([ @@ -1907,23 +1785,3 @@ class CLITestCase(DockerClientTestCase): "BAZ=2", ]) self.assertTrue(expected_env <= set(web.get('Config.Env'))) - - def test_top_services_not_running(self): - self.base_dir = 'tests/fixtures/top' - result = self.dispatch(['top']) - assert len(result.stdout) == 0 - - def test_top_services_running(self): - self.base_dir = 'tests/fixtures/top' - self.dispatch(['up', '-d']) - result = self.dispatch(['top']) - - self.assertIn('top_service_a', result.stdout) - self.assertIn('top_service_b', result.stdout) - self.assertNotIn('top_not_a_service', result.stdout) - - def test_top_processes_running(self): - self.base_dir = 'tests/fixtures/top' - self.dispatch(['up', '-d']) - result = self.dispatch(['top']) - assert result.stdout.count("top") == 4 diff --git a/tests/fixtures/extends/healthcheck-1.yml b/tests/fixtures/extends/healthcheck-1.yml deleted file mode 100644 index 4c311e62..00000000 --- a/tests/fixtures/extends/healthcheck-1.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: '2.1' -services: - demo: - image: foobar:latest - healthcheck: - test: ["CMD", "/health.sh"] - interval: 10s - timeout: 5s - retries: 36 diff --git a/tests/fixtures/extends/healthcheck-2.yml b/tests/fixtures/extends/healthcheck-2.yml deleted file mode 100644 index 11bc9f09..00000000 --- a/tests/fixtures/extends/healthcheck-2.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: '2.1' -services: - demo: - extends: - file: healthcheck-1.yml - service: demo diff --git a/tests/fixtures/healthcheck/docker-compose.yml b/tests/fixtures/healthcheck/docker-compose.yml deleted file mode 100644 index 2c45b8d8..00000000 --- a/tests/fixtures/healthcheck/docker-compose.yml +++ /dev/null @@ -1,24 +0,0 @@ -version: "3" -services: - passes: - image: busybox - command: top - healthcheck: - test: "/bin/true" - interval: 1s - timeout: 30m - retries: 1 - - fails: - image: busybox - command: top - healthcheck: - test: ["CMD", "/bin/false"] - interval: 2.5s - retries: 2 - - disabled: - image: busybox - command: top - healthcheck: - disable: true diff --git a/tests/fixtures/secrets/default b/tests/fixtures/secrets/default deleted file mode 100644 index f9dc2014..00000000 --- a/tests/fixtures/secrets/default +++ /dev/null @@ -1 +0,0 @@ -This is the secret diff --git a/tests/fixtures/top/docker-compose.yml b/tests/fixtures/top/docker-compose.yml deleted file mode 100644 index d632a836..00000000 --- a/tests/fixtures/top/docker-compose.yml +++ /dev/null @@ -1,6 +0,0 @@ -service_a: - image: busybox:latest - command: top -service_b: - image: busybox:latest - command: top diff --git a/tests/fixtures/v3-full/docker-compose.yml b/tests/fixtures/v3-full/docker-compose.yml deleted file mode 100644 index a1661ab9..00000000 --- a/tests/fixtures/v3-full/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: "3" -services: - web: - image: busybox - - deploy: - mode: replicated - replicas: 6 - labels: [FOO=BAR] - update_config: - parallelism: 3 - delay: 10s - failure_action: continue - monitor: 60s - max_failure_ratio: 0.3 - resources: - limits: - cpus: '0.001' - memory: 50M - reservations: - cpus: '0.0001' - memory: 20M - restart_policy: - condition: on_failure - delay: 5s - max_attempts: 3 - window: 120s - placement: - constraints: [node=foo] - - healthcheck: - test: cat /etc/passwd - interval: 10s - timeout: 1s - retries: 5 - - stop_grace_period: 20s -volumes: - foobar: - labels: - com.docker.compose.test: 'true' diff --git a/tests/fixtures/volumes/docker-compose.yml b/tests/fixtures/volumes/docker-compose.yml deleted file mode 100644 index da711ac4..00000000 --- a/tests/fixtures/volumes/docker-compose.yml +++ /dev/null @@ -1,2 +0,0 @@ -version: '2.1' -services: {} diff --git a/tests/fixtures/volumes/external-volumes.yml b/tests/fixtures/volumes/external-volumes.yml deleted file mode 100644 index 05c6c484..00000000 --- a/tests/fixtures/volumes/external-volumes.yml +++ /dev/null @@ -1,16 +0,0 @@ -version: "2.1" - -services: - web: - image: busybox - command: top - volumes: - - foo:/var/lib/ - - bar:/etc/ - -volumes: - foo: - external: true - bar: - external: - name: some_bar diff --git a/tests/integration/network_test.py b/tests/integration/network_test.py deleted file mode 100644 index 2ff610fb..00000000 --- a/tests/integration/network_test.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -from .testcases import DockerClientTestCase -from compose.const import LABEL_NETWORK -from compose.const import LABEL_PROJECT -from compose.network import Network - - -class NetworkTest(DockerClientTestCase): - def test_network_default_labels(self): - net = Network(self.client, 'composetest', 'foonet') - net.ensure() - net_data = net.inspect() - labels = net_data['Labels'] - assert labels[LABEL_NETWORK] == net.name - assert labels[LABEL_PROJECT] == net.project diff --git a/tests/integration/project_test.py b/tests/integration/project_test.py index 28762cd2..3afefb5a 100644 --- a/tests/integration/project_test.py +++ b/tests/integration/project_test.py @@ -1,7 +1,6 @@ from __future__ import absolute_import from __future__ import unicode_literals -import os.path import random import py @@ -9,36 +8,22 @@ import pytest from docker.errors import NotFound from .. import mock -from ..helpers import build_config as load_config +from ..helpers import build_config from .testcases import DockerClientTestCase from compose.config import config from compose.config import ConfigurationError -from compose.config import types from compose.config.config import V2_0 from compose.config.config import V2_1 -from compose.config.config import V3_1 from compose.config.types import VolumeFromSpec from compose.config.types import VolumeSpec from compose.const import LABEL_PROJECT from compose.const import LABEL_SERVICE from compose.container import Container -from compose.errors import HealthCheckFailed -from compose.errors import NoHealthCheckConfigured from compose.project import Project from compose.project import ProjectError from compose.service import ConvergenceStrategy from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only -from tests.integration.testcases import v3_only - - -def build_config(**kwargs): - return config.Config( - version=kwargs.get('version'), - services=kwargs.get('services'), - volumes=kwargs.get('volumes'), - networks=kwargs.get('networks'), - secrets=kwargs.get('secrets')) class ProjectTest(DockerClientTestCase): @@ -83,7 +68,7 @@ class ProjectTest(DockerClientTestCase): def test_volumes_from_service(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'data': { 'image': 'busybox:latest', 'volumes': ['/var/data'], @@ -109,7 +94,7 @@ class ProjectTest(DockerClientTestCase): ) project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'db': { 'image': 'busybox:latest', 'volumes_from': ['composetest_data_container'], @@ -125,7 +110,7 @@ class ProjectTest(DockerClientTestCase): project = Project.from_config( name='composetest', client=self.client, - config_data=load_config({ + config_data=build_config({ 'version': V2_0, 'services': { 'net': { @@ -152,7 +137,7 @@ class ProjectTest(DockerClientTestCase): def get_project(): return Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'version': V2_0, 'services': { 'web': { @@ -187,7 +172,7 @@ class ProjectTest(DockerClientTestCase): def test_net_from_service_v1(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'net': { 'image': 'busybox:latest', 'command': ["top"] @@ -211,7 +196,7 @@ class ProjectTest(DockerClientTestCase): def get_project(): return Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'web': { 'image': 'busybox:latest', 'net': 'container:composetest_net_container' @@ -482,7 +467,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_starts_depends(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'console': { 'image': 'busybox:latest', 'command': ["top"], @@ -517,7 +502,7 @@ class ProjectTest(DockerClientTestCase): def test_project_up_with_no_deps(self): project = Project.from_config( name='composetest', - config_data=load_config({ + config_data=build_config({ 'console': { 'image': 'busybox:latest', 'command': ["top"], @@ -577,7 +562,7 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_project_up_networks(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -589,6 +574,7 @@ class ProjectTest(DockerClientTestCase): 'baz': {'aliases': ['extra']}, }, }], + volumes={}, networks={ 'foo': {'driver': 'bridge'}, 'bar': {'driver': None}, @@ -622,13 +608,14 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_up_with_ipam_config(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', 'image': 'busybox:latest', 'networks': {'front': None}, }], + volumes={}, networks={ 'front': { 'driver': 'bridge', @@ -682,7 +669,7 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_up_with_network_static_addresses(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -695,6 +682,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -736,7 +724,7 @@ class ProjectTest(DockerClientTestCase): @v2_1_only() def test_up_with_enable_ipv6(self): self.require_api_version('1.23') - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -748,6 +736,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -779,7 +768,7 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_up_with_network_static_addresses_missing_subnet(self): - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -791,6 +780,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -815,7 +805,7 @@ class ProjectTest(DockerClientTestCase): @v2_1_only() def test_up_with_network_link_local_ips(self): - config_data = build_config( + config_data = config.Config( version=V2_1, services=[{ 'name': 'web', @@ -826,6 +816,7 @@ class ProjectTest(DockerClientTestCase): } } }], + volumes={}, networks={ 'linklocaltest': {'driver': 'bridge'} } @@ -851,13 +842,15 @@ class ProjectTest(DockerClientTestCase): @v2_1_only() def test_up_with_isolation(self): self.require_api_version('1.24') - config_data = build_config( + config_data = config.Config( version=V2_1, services=[{ 'name': 'web', 'image': 'busybox:latest', 'isolation': 'default' }], + volumes={}, + networks={} ) project = Project.from_config( client=self.client, @@ -871,13 +864,15 @@ class ProjectTest(DockerClientTestCase): @v2_1_only() def test_up_with_invalid_isolation(self): self.require_api_version('1.24') - config_data = build_config( + config_data = config.Config( version=V2_1, services=[{ 'name': 'web', 'image': 'busybox:latest', 'isolation': 'foobar' }], + volumes={}, + networks={} ) project = Project.from_config( client=self.client, @@ -890,13 +885,14 @@ class ProjectTest(DockerClientTestCase): @v2_only() def test_project_up_with_network_internal(self): self.require_api_version('1.23') - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', 'image': 'busybox:latest', 'networks': {'internal': None}, }], + volumes={}, networks={ 'internal': {'driver': 'bridge', 'internal': True}, }, @@ -919,13 +915,14 @@ class ProjectTest(DockerClientTestCase): network_name = 'network_with_label' - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', 'image': 'busybox:latest', 'networks': {network_name: None} }], + volumes={}, networks={ network_name: {'labels': {'label_key': 'label_val'}} } @@ -945,14 +942,14 @@ class ProjectTest(DockerClientTestCase): ] assert [n['Name'] for n in networks] == ['composetest_{}'.format(network_name)] - assert 'label_key' in networks[0]['Labels'] - assert networks[0]['Labels']['label_key'] == 'label_val' + + assert networks[0]['Labels'] == {'label_key': 'label_val'} @v2_only() def test_project_up_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -960,6 +957,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( @@ -979,7 +977,7 @@ class ProjectTest(DockerClientTestCase): volume_name = 'volume_with_label' - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -993,6 +991,7 @@ class ProjectTest(DockerClientTestCase): } } }, + networks={}, ) project = Project.from_config( @@ -1010,8 +1009,7 @@ class ProjectTest(DockerClientTestCase): assert [v['Name'] for v in volumes] == ['composetest_{}'.format(volume_name)] - assert 'label_key' in volumes[0]['Labels'] - assert volumes[0]['Labels']['label_key'] == 'label_val' + assert volumes[0]['Labels'] == {'label_key': 'label_val'} @v2_only() def test_project_up_logging_with_multiple_files(self): @@ -1105,7 +1103,7 @@ class ProjectTest(DockerClientTestCase): def test_initialize_volumes(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1113,6 +1111,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, + networks={}, ) project = Project.from_config( @@ -1122,14 +1121,14 @@ class ProjectTest(DockerClientTestCase): project.volumes.initialize() volume_data = self.client.inspect_volume(full_vol_name) - assert volume_data['Name'] == full_vol_name - assert volume_data['Driver'] == 'local' + self.assertEqual(volume_data['Name'], full_vol_name) + self.assertEqual(volume_data['Driver'], 'local') @v2_only() def test_project_up_implicit_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1137,6 +1136,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, + networks={}, ) project = Project.from_config( @@ -1149,47 +1149,11 @@ class ProjectTest(DockerClientTestCase): self.assertEqual(volume_data['Name'], full_vol_name) self.assertEqual(volume_data['Driver'], 'local') - @v3_only() - def test_project_up_with_secrets(self): - create_host_file(self.client, os.path.abspath('tests/fixtures/secrets/default')) - - config_data = build_config( - version=V3_1, - services=[{ - 'name': 'web', - 'image': 'busybox:latest', - 'command': 'cat /run/secrets/special', - 'secrets': [ - types.ServiceSecret.parse({'source': 'super', 'target': 'special'}), - ], - }], - secrets={ - 'super': { - 'file': os.path.abspath('tests/fixtures/secrets/default'), - }, - }, - ) - - project = Project.from_config( - client=self.client, - name='composetest', - config_data=config_data, - ) - project.up() - project.stop() - - containers = project.containers(stopped=True) - assert len(containers) == 1 - container, = containers - - output = container.logs() - assert output == b"This is the secret\n" - @v2_only() def test_initialize_volumes_invalid_volume_driver(self): vol_name = '{0:x}'.format(random.getrandbits(32)) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1197,6 +1161,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'foobar'}}, + networks={}, ) project = Project.from_config( @@ -1211,7 +1176,7 @@ class ProjectTest(DockerClientTestCase): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1219,6 +1184,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( name='composetest', @@ -1249,7 +1215,7 @@ class ProjectTest(DockerClientTestCase): vol_name = '{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1257,6 +1223,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( name='composetest', @@ -1287,7 +1254,7 @@ class ProjectTest(DockerClientTestCase): vol_name = 'composetest_{0:x}'.format(random.getrandbits(32)) full_vol_name = 'composetest_{0}'.format(vol_name) self.client.create_volume(vol_name) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1297,6 +1264,7 @@ class ProjectTest(DockerClientTestCase): volumes={ vol_name: {'external': True, 'external_name': vol_name} }, + networks=None, ) project = Project.from_config( name='composetest', @@ -1311,7 +1279,7 @@ class ProjectTest(DockerClientTestCase): def test_initialize_volumes_inexistent_external_volume(self): vol_name = '{0:x}'.format(random.getrandbits(32)) - config_data = build_config( + config_data = config.Config( version=V2_0, services=[{ 'name': 'web', @@ -1321,6 +1289,7 @@ class ProjectTest(DockerClientTestCase): volumes={ vol_name: {'external': True, 'external_name': vol_name} }, + networks=None, ) project = Project.from_config( name='composetest', @@ -1377,7 +1346,7 @@ class ProjectTest(DockerClientTestCase): } } - config_data = load_config(config_dict) + config_data = build_config(config_dict) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) @@ -1385,7 +1354,7 @@ class ProjectTest(DockerClientTestCase): config_dict['service2'] = config_dict['service1'] del config_dict['service1'] - config_data = load_config(config_dict) + config_data = build_config(config_dict) project = Project.from_config( name='composetest', config_data=config_data, client=self.client ) @@ -1405,142 +1374,3 @@ class ProjectTest(DockerClientTestCase): ctnr for ctnr in project._labeled_containers() if ctnr.labels.get(LABEL_SERVICE) == 'service1' ]) == 0 - - @v2_1_only() - def test_project_up_healthy_dependency(self): - config_dict = { - 'version': '2.1', - 'services': { - 'svc1': { - 'image': 'busybox:latest', - 'command': 'top', - 'healthcheck': { - 'test': 'exit 0', - 'retries': 1, - 'timeout': '10s', - 'interval': '0.1s' - }, - }, - 'svc2': { - 'image': 'busybox:latest', - 'command': 'top', - 'depends_on': { - 'svc1': {'condition': 'service_healthy'}, - } - } - } - } - config_data = load_config(config_dict) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - project.up() - containers = project.containers() - assert len(containers) == 2 - - svc1 = project.get_service('svc1') - svc2 = project.get_service('svc2') - assert 'svc1' in svc2.get_dependency_names() - assert svc1.is_healthy() - - @v2_1_only() - def test_project_up_unhealthy_dependency(self): - config_dict = { - 'version': '2.1', - 'services': { - 'svc1': { - 'image': 'busybox:latest', - 'command': 'top', - 'healthcheck': { - 'test': 'exit 1', - 'retries': 1, - 'timeout': '10s', - 'interval': '0.1s' - }, - }, - 'svc2': { - 'image': 'busybox:latest', - 'command': 'top', - 'depends_on': { - 'svc1': {'condition': 'service_healthy'}, - } - } - } - } - config_data = load_config(config_dict) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - with pytest.raises(ProjectError): - project.up() - containers = project.containers() - assert len(containers) == 1 - - svc1 = project.get_service('svc1') - svc2 = project.get_service('svc2') - assert 'svc1' in svc2.get_dependency_names() - with pytest.raises(HealthCheckFailed): - svc1.is_healthy() - - @v2_1_only() - def test_project_up_no_healthcheck_dependency(self): - config_dict = { - 'version': '2.1', - 'services': { - 'svc1': { - 'image': 'busybox:latest', - 'command': 'top', - 'healthcheck': { - 'disable': True - }, - }, - 'svc2': { - 'image': 'busybox:latest', - 'command': 'top', - 'depends_on': { - 'svc1': {'condition': 'service_healthy'}, - } - } - } - } - config_data = load_config(config_dict) - project = Project.from_config( - name='composetest', config_data=config_data, client=self.client - ) - with pytest.raises(ProjectError): - project.up() - containers = project.containers() - assert len(containers) == 1 - - svc1 = project.get_service('svc1') - svc2 = project.get_service('svc2') - assert 'svc1' in svc2.get_dependency_names() - with pytest.raises(NoHealthCheckConfigured): - svc1.is_healthy() - - -def create_host_file(client, filename): - dirname = os.path.dirname(filename) - - with open(filename, 'r') as fh: - content = fh.read() - - container = client.create_container( - 'busybox:latest', - ['sh', '-c', 'echo -n "{}" > {}'.format(content, filename)], - volumes={dirname: {}}, - host_config=client.create_host_config( - binds={dirname: {'bind': dirname, 'ro': False}}, - network_mode='none', - ), - ) - try: - client.start(container) - exitcode = client.wait(container) - - if exitcode != 0: - output = client.logs(container) - raise Exception( - "Container exited with code {}:\n{}".format(exitcode, output)) - finally: - client.remove_container(container, force=True) diff --git a/tests/integration/service_test.py b/tests/integration/service_test.py index 09758eee..a5ca81ee 100644 --- a/tests/integration/service_test.py +++ b/tests/integration/service_test.py @@ -30,7 +30,6 @@ from compose.service import ConvergencePlan from compose.service import ConvergenceStrategy from compose.service import NetworkMode from compose.service import Service -from tests.integration.testcases import v2_1_only from tests.integration.testcases import v2_only @@ -843,18 +842,6 @@ class ServiceTest(DockerClientTestCase): container = create_and_start_container(service) self.assertEqual(container.get('HostConfig.PidMode'), 'host') - @v2_1_only() - def test_userns_mode_none_defined(self): - service = self.create_service('web', userns_mode=None) - container = create_and_start_container(service) - self.assertEqual(container.get('HostConfig.UsernsMode'), '') - - @v2_1_only() - def test_userns_mode_host(self): - service = self.create_service('web', userns_mode='host') - container = create_and_start_container(service) - self.assertEqual(container.get('HostConfig.UsernsMode'), 'host') - def test_dns_no_value(self): service = self.create_service('web') container = create_and_start_container(service) diff --git a/tests/integration/testcases.py b/tests/integration/testcases.py index efc1551b..c7743fb8 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -13,7 +13,6 @@ from compose.config.config import resolve_environment from compose.config.config import V1 from compose.config.config import V2_0 from compose.config.config import V2_1 -from compose.config.config import V3_0 from compose.config.environment import Environment from compose.const import API_VERSIONS from compose.const import LABEL_PROJECT @@ -37,24 +36,21 @@ def get_links(container): def engine_max_version(): if 'DOCKER_VERSION' not in os.environ: - return V3_0 + return V2_1 version = os.environ['DOCKER_VERSION'].partition('-')[0] if version_lt(version, '1.10'): return V1 - if version_lt(version, '1.12'): + elif version_lt(version, '1.12'): return V2_0 - if version_lt(version, '1.13'): - return V2_1 - return V3_0 + return V2_1 -def build_version_required_decorator(ignored_versions): +def v2_only(): def decorator(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): - max_version = engine_max_version() - if max_version in ignored_versions: - skip("Engine version %s is too low" % max_version) + if engine_max_version() == V1: + skip("Engine version is too low") return return f(self, *args, **kwargs) return wrapper @@ -62,16 +58,17 @@ def build_version_required_decorator(ignored_versions): return decorator -def v2_only(): - return build_version_required_decorator((V1,)) - - def v2_1_only(): - return build_version_required_decorator((V1, V2_0)) + def decorator(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if engine_max_version() in (V1, V2_0): + skip('Engine version is too low') + return + return f(self, *args, **kwargs) + return wrapper - -def v3_only(): - return build_version_required_decorator((V1, V2_0, V2_1)) + return decorator class DockerClientTestCase(unittest.TestCase): diff --git a/tests/integration/volume_test.py b/tests/integration/volume_test.py index add16962..a75250ac 100644 --- a/tests/integration/volume_test.py +++ b/tests/integration/volume_test.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals from docker.errors import DockerException from .testcases import DockerClientTestCase -from compose.const import LABEL_PROJECT -from compose.const import LABEL_VOLUME from compose.volume import Volume @@ -96,11 +94,3 @@ class VolumeTest(DockerClientTestCase): assert vol.exists() is False vol.create() assert vol.exists() is True - - def test_volume_default_labels(self): - vol = self.create_volume('volume01') - vol.create() - vol_data = vol.inspect() - labels = vol_data['Labels'] - assert labels[LABEL_VOLUME] == vol.name - assert labels[LABEL_PROJECT] == vol.project diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py index 21bdb31b..223b3b07 100644 --- a/tests/unit/bundle_test.py +++ b/tests/unit/bundle_test.py @@ -15,7 +15,7 @@ from compose.config.config import Config def mock_service(): return mock.create_autospec( service.Service, - client=mock.create_autospec(docker.APIClient), + client=mock.create_autospec(docker.Client), options={}) @@ -77,8 +77,7 @@ def test_to_bundle(): version=2, services=services, volumes={'special': {}}, - networks={'extra': {}}, - secrets={}) + networks={'extra': {}}) with mock.patch('compose.bundle.log.warn', autospec=True) as mock_log: output = bundle.to_bundle(config, image_digests) diff --git a/tests/unit/cli_test.py b/tests/unit/cli_test.py index 317650cb..2c90b29b 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -97,7 +97,7 @@ class CLITestCase(unittest.TestCase): @mock.patch('compose.cli.main.RunOperation', autospec=True) @mock.patch('compose.cli.main.PseudoTerminal', autospec=True) 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.Client) project = Project.from_config( name='composetest', client=mock_client, @@ -128,7 +128,7 @@ class CLITestCase(unittest.TestCase): assert call_kwargs['logs'] is False def test_run_service_with_restart_always(self): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) project = Project.from_config( name='composetest', @@ -184,7 +184,7 @@ class CLITestCase(unittest.TestCase): mock_client.create_host_config.call_args[1].get('restart_policy') ) - def test_command_manual_and_service_ports_together(self): + def test_command_manula_and_service_ports_together(self): project = Project.from_config( name='composetest', client=None, diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index c26272d9..51c5e226 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -13,22 +13,16 @@ import pytest from ...helpers import build_config_details from compose.config import config -from compose.config import types from compose.config.config import resolve_build_args from compose.config.config import resolve_environment from compose.config.config import V1 from compose.config.config import V2_0 from compose.config.config import V2_1 -from compose.config.config import V3_0 -from compose.config.config import V3_1 from compose.config.environment import Environment from compose.config.errors import ConfigurationError from compose.config.errors import VERSION_EXPLANATION -from compose.config.serialize import denormalize_service_dict -from compose.config.serialize import serialize_ns_time_value from compose.config.types import VolumeSpec from compose.const import IS_WINDOWS_PLATFORM -from compose.utils import nanoseconds_from_time_seconds from tests import mock from tests import unittest @@ -54,10 +48,6 @@ def service_sort(services): return sorted(services, key=itemgetter('name')) -def secret_sort(secrets): - return sorted(secrets, key=itemgetter('source')) - - class ConfigTest(unittest.TestCase): def test_load(self): service_dicts = config.load( @@ -166,17 +156,9 @@ class ConfigTest(unittest.TestCase): for version in ['2', '2.0']: cfg = config.load(build_config_details({'version': version})) assert cfg.version == V2_0 - cfg = config.load(build_config_details({'version': '2.1'})) assert cfg.version == V2_1 - for version in ['3', '3.0']: - cfg = config.load(build_config_details({'version': version})) - assert cfg.version == V3_0 - - cfg = config.load(build_config_details({'version': '3.1'})) - assert cfg.version == V3_1 - def test_v1_file_version(self): cfg = config.load(build_config_details({'web': {'image': 'busybox'}})) assert cfg.version == V1 @@ -931,10 +913,7 @@ class ConfigTest(unittest.TestCase): 'build': {'context': os.path.abspath('/')}, 'image': 'example/web', 'volumes': [VolumeSpec.parse('/home/user/project:/code')], - 'depends_on': { - 'db': {'condition': 'service_started'}, - 'other': {'condition': 'service_started'}, - }, + 'depends_on': ['db', 'other'], }, { 'name': 'db', @@ -1723,90 +1702,6 @@ class ConfigTest(unittest.TestCase): } } - def test_merge_depends_on_no_override(self): - base = { - 'image': 'busybox', - 'depends_on': { - 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'} - } - } - override = {} - actual = config.merge_service_dicts(base, override, V2_1) - assert actual == base - - def test_merge_depends_on_mixed_syntax(self): - base = { - 'image': 'busybox', - 'depends_on': { - 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'} - } - } - override = { - 'depends_on': ['app3'] - } - - actual = config.merge_service_dicts(base, override, V2_1) - assert actual == { - 'image': 'busybox', - 'depends_on': { - 'app1': {'condition': 'service_started'}, - 'app2': {'condition': 'service_healthy'}, - 'app3': {'condition': 'service_started'} - } - } - - def test_merge_pid(self): - # Regression: https://github.com/docker/compose/issues/4184 - base = { - 'image': 'busybox', - 'pid': 'host' - } - - override = { - 'labels': {'com.docker.compose.test': 'yes'} - } - - actual = config.merge_service_dicts(base, override, V2_0) - assert actual == { - 'image': 'busybox', - 'pid': 'host', - 'labels': {'com.docker.compose.test': 'yes'} - } - - def test_merge_different_secrets(self): - base = { - 'image': 'busybox', - 'secrets': [ - {'source': 'src.txt'} - ] - } - override = {'secrets': ['other-src.txt']} - - actual = config.merge_service_dicts(base, override, V3_1) - assert secret_sort(actual['secrets']) == secret_sort([ - {'source': 'src.txt'}, - {'source': 'other-src.txt'} - ]) - - def test_merge_secrets_override(self): - base = { - 'image': 'busybox', - 'secrets': ['src.txt'], - } - override = { - 'secrets': [ - { - 'source': 'src.txt', - 'target': 'data.txt', - 'mode': 0o400 - } - ] - } - actual = config.merge_service_dicts(base, override, V3_1) - assert actual['secrets'] == override['secrets'] - def test_external_volume_config(self): config_details = build_config_details({ 'version': '2', @@ -1886,91 +1781,6 @@ class ConfigTest(unittest.TestCase): config.load(config_details) assert 'has neither an image nor a build context' in exc.exconly() - def test_load_secrets(self): - base_file = config.ConfigFile( - 'base.yaml', - { - 'version': '3.1', - 'services': { - 'web': { - 'image': 'example/web', - 'secrets': [ - 'one', - { - 'source': 'source', - 'target': 'target', - 'uid': '100', - 'gid': '200', - 'mode': 0o777, - }, - ], - }, - }, - 'secrets': { - 'one': {'file': 'secret.txt'}, - }, - }) - details = config.ConfigDetails('.', [base_file]) - service_dicts = config.load(details).services - expected = [ - { - 'name': 'web', - 'image': 'example/web', - 'secrets': [ - types.ServiceSecret('one', None, None, None, None), - types.ServiceSecret('source', 'target', '100', '200', 0o777), - ], - }, - ] - assert service_sort(service_dicts) == service_sort(expected) - - def test_load_secrets_multi_file(self): - base_file = config.ConfigFile( - 'base.yaml', - { - 'version': '3.1', - 'services': { - 'web': { - 'image': 'example/web', - 'secrets': ['one'], - }, - }, - 'secrets': { - 'one': {'file': 'secret.txt'}, - }, - }) - override_file = config.ConfigFile( - 'base.yaml', - { - 'version': '3.1', - 'services': { - 'web': { - 'secrets': [ - { - 'source': 'source', - 'target': 'target', - 'uid': '100', - 'gid': '200', - 'mode': 0o777, - }, - ], - }, - }, - }) - details = config.ConfigDetails('.', [base_file, override_file]) - service_dicts = config.load(details).services - expected = [ - { - 'name': 'web', - 'image': 'example/web', - 'secrets': [ - types.ServiceSecret('one', None, None, None, None), - types.ServiceSecret('source', 'target', '100', '200', 0o777), - ], - }, - ] - assert service_sort(service_dicts) == service_sort(expected) - class NetworkModeTest(unittest.TestCase): def test_network_mode_standard(self): @@ -3238,22 +3048,7 @@ class ExtendsTest(unittest.TestCase): image: example """) services = load_from_filename(str(tmpdir.join('docker-compose.yml'))) - assert service_sort(services)[2]['depends_on'] == { - 'other': {'condition': 'service_started'} - } - - def test_extends_with_healthcheck(self): - service_dicts = load_from_filename('tests/fixtures/extends/healthcheck-2.yml') - assert service_sort(service_dicts) == [{ - 'name': 'demo', - 'image': 'foobar:latest', - 'healthcheck': { - 'test': ['CMD', '/health.sh'], - 'interval': 10000000000, - 'timeout': 5000000000, - 'retries': 36, - } - }] + assert service_sort(services)[2]['depends_on'] == ['other'] @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') @@ -3370,54 +3165,6 @@ class BuildPathTest(unittest.TestCase): assert 'build path' in exc.exconly() -class HealthcheckTest(unittest.TestCase): - def test_healthcheck(self): - service_dict = make_service_dict( - 'test', - {'healthcheck': { - 'test': ['CMD', 'true'], - 'interval': '1s', - 'timeout': '1m', - 'retries': 3, - }}, - '.', - ) - - assert service_dict['healthcheck'] == { - 'test': ['CMD', 'true'], - 'interval': nanoseconds_from_time_seconds(1), - 'timeout': nanoseconds_from_time_seconds(60), - 'retries': 3, - } - - def test_disable(self): - service_dict = make_service_dict( - 'test', - {'healthcheck': { - 'disable': True, - }}, - '.', - ) - - assert service_dict['healthcheck'] == { - 'test': ['NONE'], - } - - def test_disable_with_other_config_is_invalid(self): - with pytest.raises(ConfigurationError) as excinfo: - make_service_dict( - 'invalid-healthcheck', - {'healthcheck': { - 'disable': True, - 'interval': '1s', - }}, - '.', - ) - - assert 'invalid-healthcheck' in excinfo.exconly() - assert 'disable' in excinfo.exconly() - - class GetDefaultConfigFilesTestCase(unittest.TestCase): files = [ @@ -3462,89 +3209,3 @@ def get_config_filename_for_files(filenames, subdir=None): return os.path.basename(filename) finally: shutil.rmtree(project_dir) - - -class SerializeTest(unittest.TestCase): - def test_denormalize_depends_on_v3(self): - service_dict = { - 'image': 'busybox', - 'command': 'true', - 'depends_on': { - 'service2': {'condition': 'service_started'}, - 'service3': {'condition': 'service_started'}, - } - } - - assert denormalize_service_dict(service_dict, V3_0) == { - 'image': 'busybox', - 'command': 'true', - 'depends_on': ['service2', 'service3'] - } - - def test_denormalize_depends_on_v2_1(self): - service_dict = { - 'image': 'busybox', - 'command': 'true', - 'depends_on': { - 'service2': {'condition': 'service_started'}, - 'service3': {'condition': 'service_started'}, - } - } - - assert denormalize_service_dict(service_dict, V2_1) == service_dict - - def test_serialize_time(self): - data = { - 9: '9ns', - 9000: '9us', - 9000000: '9ms', - 90000000: '90ms', - 900000000: '900ms', - 999999999: '999999999ns', - 1000000000: '1s', - 60000000000: '1m', - 60000000001: '60000000001ns', - 9000000000000: '150m', - 90000000000000: '25h', - } - - for k, v in data.items(): - assert serialize_ns_time_value(k) == v - - def test_denormalize_healthcheck(self): - service_dict = { - 'image': 'test', - 'healthcheck': { - 'test': 'exit 1', - 'interval': '1m40s', - 'timeout': '30s', - 'retries': 5 - } - } - processed_service = config.process_service(config.ServiceConfig( - '.', 'test', 'test', service_dict - )) - denormalized_service = denormalize_service_dict(processed_service, V2_1) - assert denormalized_service['healthcheck']['interval'] == '100s' - assert denormalized_service['healthcheck']['timeout'] == '30s' - - def test_denormalize_secrets(self): - service_dict = { - 'name': 'web', - 'image': 'example/web', - 'secrets': [ - types.ServiceSecret('one', None, None, None, None), - types.ServiceSecret('source', 'target', '100', '200', 0o777), - ], - } - denormalized_service = denormalize_service_dict(service_dict, V3_1) - assert secret_sort(denormalized_service['secrets']) == secret_sort([ - {'source': 'one'}, - { - 'source': 'source', - 'target': 'target', - 'uid': '100', - 'gid': '200', - 'mode': 0o777, - }, - ]) diff --git a/tests/unit/config/environment_test.py b/tests/unit/config/environment_test.py deleted file mode 100644 index 20446d2b..00000000 --- a/tests/unit/config/environment_test.py +++ /dev/null @@ -1,40 +0,0 @@ -# encoding: utf-8 -from __future__ import absolute_import -from __future__ import print_function -from __future__ import unicode_literals - -from compose.config.environment import Environment -from tests import unittest - - -class EnvironmentTest(unittest.TestCase): - def test_get_simple(self): - env = Environment({ - 'FOO': 'bar', - 'BAR': '1', - 'BAZ': '' - }) - - assert env.get('FOO') == 'bar' - assert env.get('BAR') == '1' - assert env.get('BAZ') == '' - - def test_get_undefined(self): - env = Environment({ - 'FOO': 'bar' - }) - assert env.get('FOOBAR') is None - - def test_get_boolean(self): - env = Environment({ - 'FOO': '', - 'BAR': '0', - 'BAZ': 'FALSE', - 'FOOBAR': 'true', - }) - - assert env.get_boolean('FOO') is False - assert env.get_boolean('BAR') is False - assert env.get_boolean('BAZ') is False - assert env.get_boolean('FOOBAR') is True - assert env.get_boolean('UNDEFINED') is False diff --git a/tests/unit/container_test.py b/tests/unit/container_test.py index 04f43016..62e3aa2c 100644 --- a/tests/unit/container_test.py +++ b/tests/unit/container_test.py @@ -98,7 +98,7 @@ class ContainerTest(unittest.TestCase): self.assertEqual(container.name_without_project, "custom_name_of_container") def test_inspect_if_not_inspected(self): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) container = Container(mock_client, dict(Id="the_id")) container.inspect_if_not_inspected() diff --git a/tests/unit/parallel_test.py b/tests/unit/parallel_test.py index 2a50b718..479c0f1d 100644 --- a/tests/unit/parallel_test.py +++ b/tests/unit/parallel_test.py @@ -25,7 +25,7 @@ deps = { def get_deps(obj): - return [(dep, None) for dep in deps[obj]] + return deps[obj] def test_parallel_execute(): diff --git a/tests/unit/project_test.py b/tests/unit/project_test.py index 32d0adfa..9569adc9 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -19,7 +19,7 @@ from compose.service import Service class ProjectTest(unittest.TestCase): def setUp(self): - self.mock_client = mock.create_autospec(docker.APIClient) + self.mock_client = mock.create_autospec(docker.Client) def test_from_config(self): config = Config( @@ -36,7 +36,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ) project = Project.from_config( name='composetest', @@ -65,7 +64,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ) project = Project.from_config('composetest', config, None) self.assertEqual(len(project.services), 2) @@ -172,7 +170,6 @@ class ProjectTest(unittest.TestCase): }], networks=None, volumes=None, - secrets=None, ), ) assert project.get_service('test')._get_volumes_from() == [container_id + ":rw"] @@ -205,7 +202,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) assert project.get_service('test')._get_volumes_from() == [container_name + ":rw"] @@ -231,7 +227,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) with mock.patch.object(Service, 'containers') as mock_return: @@ -365,7 +360,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) service = project.get_service('test') @@ -390,7 +384,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) service = project.get_service('test') @@ -424,7 +417,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) @@ -445,7 +437,6 @@ class ProjectTest(unittest.TestCase): ], networks=None, volumes=None, - secrets=None, ), ) @@ -466,7 +457,6 @@ class ProjectTest(unittest.TestCase): ], networks={'custom': {}}, volumes=None, - secrets=None, ), ) @@ -497,7 +487,6 @@ class ProjectTest(unittest.TestCase): }], networks=None, volumes=None, - secrets=None, ), ) self.assertEqual([c.id for c in project.containers()], ['1']) @@ -514,7 +503,6 @@ class ProjectTest(unittest.TestCase): }], networks={'default': {}}, volumes={'data': {}}, - secrets=None, ), ) self.mock_client.remove_network.side_effect = NotFound(None, None, 'oops') diff --git a/tests/unit/service_test.py b/tests/unit/service_test.py index 2d5b1761..1d5aa10f 100644 --- a/tests/unit/service_test.py +++ b/tests/unit/service_test.py @@ -34,7 +34,7 @@ from compose.service import warn_on_masked_volume class ServiceTest(unittest.TestCase): def setUp(self): - self.mock_client = mock.create_autospec(docker.APIClient) + self.mock_client = mock.create_autospec(docker.Client) def test_containers(self): service = Service('db', self.mock_client, 'myproject', image='foo') @@ -666,7 +666,7 @@ class ServiceTest(unittest.TestCase): class TestServiceNetwork(object): def test_connect_container_to_networks_short_aliase_exists(self): - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) service = Service( 'db', mock_client, @@ -751,7 +751,7 @@ class NetTestCase(unittest.TestCase): def test_network_mode_service(self): container_id = 'bbbb' service_name = 'web' - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) mock_client.containers.return_value = [ {'Id': container_id, 'Name': container_id, 'Image': 'abcd'}, ] @@ -765,7 +765,7 @@ class NetTestCase(unittest.TestCase): def test_network_mode_service_no_containers(self): service_name = 'web' - mock_client = mock.create_autospec(docker.APIClient) + mock_client = mock.create_autospec(docker.Client) mock_client.containers.return_value = [] service = Service(name=service_name, client=mock_client) @@ -783,7 +783,7 @@ def build_mount(destination, source, mode='rw'): class ServiceVolumesTest(unittest.TestCase): def setUp(self): - self.mock_client = mock.create_autospec(docker.APIClient) + self.mock_client = mock.create_autospec(docker.Client) def test_build_volume_binding(self): binding = build_volume_binding(VolumeSpec.parse('/outside:/inside', True)) diff --git a/tests/unit/timeparse_test.py b/tests/unit/timeparse_test.py deleted file mode 100644 index 9915932c..00000000 --- a/tests/unit/timeparse_test.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import absolute_import -from __future__ import unicode_literals - -from compose import timeparse - - -def test_milli(): - assert timeparse.timeparse('5ms') == 0.005 - - -def test_milli_float(): - assert timeparse.timeparse('50.5ms') == 0.0505 - - -def test_second_milli(): - assert timeparse.timeparse('200s5ms') == 200.005 - - -def test_second_milli_micro(): - assert timeparse.timeparse('200s5ms10us') == 200.00501 - - -def test_second(): - assert timeparse.timeparse('200s') == 200 - - -def test_second_as_float(): - assert timeparse.timeparse('20.5s') == 20.5 - - -def test_minute(): - assert timeparse.timeparse('32m') == 1920 - - -def test_hour_minute(): - assert timeparse.timeparse('2h32m') == 9120 - - -def test_minute_as_float(): - assert timeparse.timeparse('1.5m') == 90 - - -def test_hour_minute_second(): - assert timeparse.timeparse('5h34m56s') == 20096 - - -def test_invalid_with_space(): - assert timeparse.timeparse('5h 34m 56s') is None - - -def test_invalid_with_comma(): - assert timeparse.timeparse('5h,34m,56s') is None - - -def test_invalid_with_empty_string(): - assert timeparse.timeparse('') is None diff --git a/tests/unit/volume_test.py b/tests/unit/volume_test.py index 24829192..d7ad0792 100644 --- a/tests/unit/volume_test.py +++ b/tests/unit/volume_test.py @@ -10,7 +10,7 @@ from tests import mock @pytest.fixture def mock_client(): - return mock.create_autospec(docker.APIClient) + return mock.create_autospec(docker.Client) class TestVolume(object):