diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c07e9ca..768e6c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +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) ------------------ @@ -238,7 +214,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 +596,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 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/README.md b/README.md index 35a10b90..5cf69b05 100644 --- a/README.md +++ b/README.md @@ -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/docker.github.io/blob/master/compose/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..6d2e41a2 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.10.1' 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..6677a376 100644 --- a/compose/cli/colors.py +++ b/compose/cli/colors.py @@ -33,7 +33,7 @@ def make_color_fn(code): return lambda s: ansi_color(code, s) -colorama.init(strip=False) +colorama.init() for (name, code) in get_pairs(): globals()[name] = make_color_fn(code) diff --git a/compose/cli/main.py b/compose/cli/main.py index 51ba36a0..db068272 100644 --- a/compose/cli/main.py +++ b/compose/cli/main.py @@ -14,6 +14,30 @@ from distutils.spawn import find_executable from inspect import getdoc from operator import attrgetter + +# Attempt to detect https://github.com/docker/compose/issues/4344 +try: + # A regular import statement causes PyInstaller to freak out while + # trying to load pip. This way it is simply ignored. + pip = __import__('pip') + pip_packages = pip.get_installed_distributions() + if 'docker-py' in [pkg.project_name for pkg in pip_packages]: + 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 ImportError: + # pip is not available, which indicates it's probably the binary + # distribution of Compose which is not affected + pass + + from . import errors from . import signals from .. import __version__ @@ -191,7 +215,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 @@ -777,33 +800,6 @@ class TopLevelCommand(object): 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. diff --git a/compose/config/config.py b/compose/config/config.py index 4c9cf423..7e77421e 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -12,12 +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 @@ -78,13 +76,12 @@ 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', @@ -186,6 +183,11 @@ class ConfigFile(namedtuple('_ConfigFile', 'filename config')): if version == '3': version = V3_0 + if version not in (V2_0, V2_1, V3_0): + raise ConfigurationError( + 'Version in "{}" is unsupported. {}' + .format(self.filename, VERSION_EXPLANATION)) + return version def get_service(self, name): @@ -200,11 +202,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,7 +328,6 @@ 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: @@ -344,7 +342,7 @@ def load(config_details): "`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 +356,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 +383,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 +449,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, V3_0): processed_config = dict(config_file.config) processed_config['services'] = services processed_config['volumes'] = interpolate_config_section( @@ -490,9 +465,7 @@ def process_config_file(config_file, environment, service_name=None): elif config_file.version == V1: processed_config = services else: - raise ConfigurationError( - 'Version in "{}" is unsupported. {}' - .format(config_file.filename, VERSION_EXPLANATION)) + raise Exception("Unsupported version: {}".format(repr(config_file.version))) config_file = config_file._replace(config=processed_config) validate_against_config_schema(config_file) @@ -713,15 +686,9 @@ def process_healthcheck(service_dict, service_name): 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'] + hc['interval'] = parse_nanoseconds_int(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'] + hc['timeout'] = parse_nanoseconds_int(raw['timeout']) if 'retries' in raw: hc['retries'] = raw['retries'] @@ -763,11 +730,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 @@ -858,7 +820,6 @@ def merge_service_dicts(base, override, version): 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) diff --git a/compose/config/config_schema_v2.0.json b/compose/config/config_schema_v2.0.json index 59c7b30c..77494715 100644 --- a/compose/config/config_schema_v2.0.json +++ b/compose/config/config_schema_v2.0.json @@ -276,9 +276,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..97ec5fa1 100644 --- a/compose/config/config_schema_v2.1.json +++ b/compose/config/config_schema_v2.1.json @@ -322,10 +322,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.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..7b926930 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 diff --git a/compose/config/serialize.py b/compose/config/serialize.py index 46d283f0..edf55353 100644 --- a/compose/config/serialize.py +++ b/compose/config/serialize.py @@ -32,11 +32,6 @@ 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: version = V2_1 @@ -45,7 +40,7 @@ def denormalize_config(config): 'version': version, 'services': services, 'networks': networks, - 'volumes': volumes, + 'volumes': config.volumes, } @@ -102,7 +97,4 @@ def denormalize_service_dict(service_dict, version): 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/const.py b/compose/const.py index e694dbda..1b1be5c7 100644 --- a/compose/const.py +++ b/compose/const.py @@ -5,7 +5,7 @@ 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' @@ -16,20 +16,16 @@ 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 = { @@ -37,5 +33,4 @@ API_VERSION_TO_ENGINE_VERSION = { 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/project.py b/compose/project.py index 133071e7..d99ef7c9 100644 --- a/compose/project.py +++ b/compose/project.py @@ -104,11 +104,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 +114,6 @@ class Project(object): links=links, network_mode=network_mode, volumes_from=volumes_from, - secrets=secrets, **service_dict) ) @@ -365,7 +359,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 @@ -559,33 +553,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': diff --git a/compose/service.py b/compose/service.py index 9f2fc68b..724e0565 100644 --- a/compose/service.py +++ b/compose/service.py @@ -17,7 +17,6 @@ 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 @@ -140,7 +139,6 @@ class Service(object): volumes_from=None, network_mode=None, networks=None, - secrets=None, **options ): self.name = name @@ -151,7 +149,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): @@ -695,14 +692,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 @@ -773,15 +765,6 @@ 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) 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..ec5a2039 100644 --- a/docker-compose.spec +++ b/docker-compose.spec @@ -37,11 +37,6 @@ exe = EXE(pyz, '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/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/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..c43055e7 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.12.0dev" +VERSION="1.10.1" IMAGE="docker/compose:$VERSION" 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.py b/setup.py index eafbc356..0b1d4e08 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from __future__ import absolute_import -from __future__ import print_function from __future__ import unicode_literals import codecs +import logging import os import re import sys @@ -64,9 +64,11 @@ try: 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) +except Exception: + logging.getLogger(__name__).exception( + 'Failed to compute platform dependencies. All dependencies will be ' + 'installed as a result.' + ) for key, value in extras_require.items(): if key.startswith(':'): install_requires.extend(value) diff --git a/tests/acceptance/cli_test.py b/tests/acceptance/cli_test.py index 8366ca75..7a2b7fb5 100644 --- a/tests/acceptance/cli_test.py +++ b/tests/acceptance/cli_test.py @@ -262,20 +262,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']) @@ -1234,7 +1220,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 +1232,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 +1279,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 +1296,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 +1893,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/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/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/project_test.py b/tests/integration/project_test.py index 28762cd2..ee2b7817 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,14 +8,12 @@ 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 @@ -29,16 +26,6 @@ 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 +70,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 +96,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 +112,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 +139,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 +174,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 +198,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 +469,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 +504,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 +564,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 +576,7 @@ class ProjectTest(DockerClientTestCase): 'baz': {'aliases': ['extra']}, }, }], + volumes={}, networks={ 'foo': {'driver': 'bridge'}, 'bar': {'driver': None}, @@ -622,13 +610,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 +671,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 +684,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -736,7 +726,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 +738,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -779,7 +770,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 +782,7 @@ class ProjectTest(DockerClientTestCase): } }, }], + volumes={}, networks={ 'static_test': { 'driver': 'bridge', @@ -815,7 +807,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 +818,7 @@ class ProjectTest(DockerClientTestCase): } } }], + volumes={}, networks={ 'linklocaltest': {'driver': 'bridge'} } @@ -851,13 +844,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 +866,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 +887,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 +917,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'}} } @@ -952,7 +951,7 @@ class ProjectTest(DockerClientTestCase): 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 +959,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( @@ -979,7 +979,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 +993,7 @@ class ProjectTest(DockerClientTestCase): } } }, + networks={}, ) project = Project.from_config( @@ -1105,7 +1106,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 +1114,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, + networks={}, ) project = Project.from_config( @@ -1122,14 +1124,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 +1139,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {}}, + networks={}, ) project = Project.from_config( @@ -1149,47 +1152,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 +1164,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'foobar'}}, + networks={}, ) project = Project.from_config( @@ -1211,7 +1179,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 +1187,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( name='composetest', @@ -1249,7 +1218,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 +1226,7 @@ class ProjectTest(DockerClientTestCase): 'command': 'top' }], volumes={vol_name: {'driver': 'local'}}, + networks={}, ) project = Project.from_config( name='composetest', @@ -1287,7 +1257,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 +1267,7 @@ class ProjectTest(DockerClientTestCase): volumes={ vol_name: {'external': True, 'external_name': vol_name} }, + networks=None, ) project = Project.from_config( name='composetest', @@ -1311,7 +1282,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 +1292,7 @@ class ProjectTest(DockerClientTestCase): volumes={ vol_name: {'external': True, 'external_name': vol_name} }, + networks=None, ) project = Project.from_config( name='composetest', @@ -1377,7 +1349,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 +1357,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 ) @@ -1430,7 +1402,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 ) @@ -1467,7 +1439,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 ) @@ -1503,7 +1475,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 ) @@ -1517,30 +1489,3 @@ class ProjectTest(DockerClientTestCase): 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/testcases.py b/tests/integration/testcases.py index efc1551b..230bd2d9 100644 --- a/tests/integration/testcases.py +++ b/tests/integration/testcases.py @@ -41,9 +41,9 @@ def engine_max_version(): 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'): + elif version_lt(version, '1.13'): return V2_1 return V3_0 @@ -52,9 +52,8 @@ def build_version_required_decorator(ignored_versions): 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() in ignored_versions: + skip("Engine version is too low") return return f(self, *args, **kwargs) return wrapper diff --git a/tests/unit/bundle_test.py b/tests/unit/bundle_test.py index 21bdb31b..a279cab0 100644 --- a/tests/unit/bundle_test.py +++ b/tests/unit/bundle_test.py @@ -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..f9b60bff 100644 --- a/tests/unit/cli_test.py +++ b/tests/unit/cli_test.py @@ -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..d7947a4e 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -13,14 +13,12 @@ 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 @@ -54,10 +52,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( @@ -174,9 +168,6 @@ class ConfigTest(unittest.TestCase): 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 @@ -1757,56 +1748,6 @@ class ConfigTest(unittest.TestCase): } } - def test_merge_pid(self): - # Regression: https://github.com/docker/compose/issues/4184 - base = { - 'image': 'busybox', - 'pid': 'host' - } - - override = { - 'labels': {'com.docker.compose.test': 'yes'} - } - - actual = config.merge_service_dicts(base, override, V2_0) - assert actual == { - 'image': 'busybox', - 'pid': 'host', - 'labels': {'com.docker.compose.test': 'yes'} - } - - def test_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 +1827,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): @@ -3242,19 +3098,6 @@ class ExtendsTest(unittest.TestCase): 'other': {'condition': 'service_started'} } - def test_extends_with_healthcheck(self): - service_dicts = load_from_filename('tests/fixtures/extends/healthcheck-2.yml') - assert service_sort(service_dicts) == [{ - 'name': 'demo', - 'image': 'foobar:latest', - 'healthcheck': { - 'test': ['CMD', '/health.sh'], - 'interval': 10000000000, - 'timeout': 5000000000, - 'retries': 36, - } - }] - @pytest.mark.xfail(IS_WINDOWS_PLATFORM, reason='paths use slash') class ExpandPathTest(unittest.TestCase): @@ -3527,24 +3370,3 @@ class SerializeTest(unittest.TestCase): 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/project_test.py b/tests/unit/project_test.py index 32d0adfa..9a12438f 100644 --- a/tests/unit/project_test.py +++ b/tests/unit/project_test.py @@ -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')