Merge pull request #6455 from docker/bump-1.24.0-rc1

Bump 1.24.0-rc1
This commit is contained in:
Djordje Lukic 2019-01-14 17:54:34 +01:00 committed by GitHub
commit 82db4fd4f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 768 additions and 111 deletions

60
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,60 @@
---
name: Bug report
about: Report a bug encountered while using docker-compose
---
<!--
Welcome to the docker-compose issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to docker-compose
- For questions and general support, use https://forums.docker.com
- For documentation issues, use https://github.com/docker/docker.github.io
- For issues with the `docker stack` commands and the version 3 of the Compose file, use
https://github.com/docker/cli
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
-->
## Description of the issue
## Context information (for bug reports)
**Output of `docker-compose version`**
```
(paste here)
```
**Output of `docker version`**
```
(paste here)
```
**Output of `docker-compose config`**
(Make sure to add the relevant `-f` and other flags)
```
(paste here)
```
## Steps to reproduce the issue
1.
2.
3.
### Observed result
### Expected result
### Stacktrace / full error message
```
(paste here)
```
## Additional information
OS version / distribution, `docker-compose` install method, etc.

View File

@ -0,0 +1,29 @@
---
name: Feature request
about: Suggest an idea to improve Compose
---
<!--
Welcome to the docker-compose issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to docker-compose
- For questions and general support, use https://forums.docker.com
- For documentation issues, use https://github.com/docker/docker.github.io
- For issues with the `docker stack` commands and the version 3 of the Compose file, use
https://github.com/docker/cli
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
-->
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -0,0 +1,9 @@
---
name: Question about using Compose
about: This is not the appropriate channel
---
Please post on our forums: https://forums.docker.com for questions about using `docker-compose`.
Posts that are not a bug report or a feature/enhancement request will not be addressed on this issue tracker.

View File

@ -14,7 +14,7 @@
- id: requirements-txt-fixer
- id: trailing-whitespace
- repo: git://github.com/asottile/reorder_python_imports
sha: v0.3.5
sha: v1.3.4
hooks:
- id: reorder-python-imports
language_version: 'python2.7'

View File

@ -1,6 +1,42 @@
Change log
==========
1.24.0 (2019-01-25)
-------------------
### Features
- Added support for connecting to the Docker Engine using the `ssh` protocol.
- Added a `--all` flag to `docker-compose ps` to include stopped one-off containers
in the command's output.
### Bugfixes
- Fixed a bug where some valid credential helpers weren't properly handled by Compose
when attempting to pull images from private registries.
- Fixed an issue where the output of `docker-compose start` before containers were created
was misleading
- To match the Docker CLI behavior and to avoid confusing issues, Compose will no longer
accept whitespace in variable names sourced from environment files.
- Compose will now report a configuration error if a service attempts to declare
duplicate mount points in the volumes section.
- Fixed an issue with the containerized version of Compose that prevented users from
writing to stdin during interactive sessions started by `run` or `exec`.
- One-off containers started by `run` no longer adopt the restart policy of the service,
and are instead set to never restart.
- Fixed an issue that caused some container events to not appear in the output of
the `docker-compose events` command.
- Missing images will no longer stop the execution of `docker-compose down` commands
(a warning will be displayed instead).
1.23.2 (2018-11-28)
-------------------

View File

@ -4,8 +4,7 @@ include requirements.txt
include requirements-dev.txt
include tox.ini
include *.md
exclude README.md
include README.rst
include README.md
include compose/config/*.json
include compose/GITSHA
recursive-include contrib/completion *

View File

@ -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/compose-versioning.md).
Compose has commands for managing the whole lifecycle of your application:
@ -48,9 +48,8 @@ Installation and documentation
------------------------------
- Full documentation is available on [Docker's website](https://docs.docker.com/compose/).
- If you have any questions, you can talk in real-time with other developers in the #docker-compose IRC channel on Freenode. [Click here to join using IRCCloud.](https://www.irccloud.com/invite?hostname=irc.freenode.net&channel=%23docker-compose)
- Code repository for Compose is on [GitHub](https://github.com/docker/compose)
- If you find any problems please fill out an [issue](https://github.com/docker/compose/issues/new)
- Code repository for Compose is on [GitHub](https://github.com/docker/compose).
- If you find any problems please fill out an [issue](https://github.com/docker/compose/issues/new/choose). Thank you!
Contributing
------------

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import
from __future__ import unicode_literals
__version__ = '1.23.2'
__version__ = '1.24.0-rc1'

View File

@ -67,7 +67,9 @@ def handle_connection_errors(client):
def log_windows_pipe_error(exc):
if exc.winerror == 232: # https://github.com/docker/compose/issues/5005
if exc.winerror == 2:
log.error("Couldn't connect to Docker daemon. You might need to start Docker for Windows.")
elif exc.winerror == 232: # https://github.com/docker/compose/issues/5005
log.error(
"The current Compose file version is not compatible with your engine version. "
"Please upgrade your Compose file to a more recent version, or set "

View File

@ -236,7 +236,8 @@ def watch_events(thread_map, event_stream, presenters, thread_args):
thread_map[event['id']] = build_thread(
event['container'],
next(presenters),
*thread_args)
*thread_args
)
def consume_queue(queue, cascade_stop):

View File

@ -694,6 +694,7 @@ class TopLevelCommand(object):
-q, --quiet Only display IDs
--services Display services
--filter KEY=VAL Filter services by a property
-a, --all Show all stopped containers (including those created by the run command)
"""
if options['--quiet'] and options['--services']:
raise UserError('--quiet and --services cannot be combined')
@ -706,10 +707,14 @@ class TopLevelCommand(object):
print('\n'.join(service.name for service in services))
return
containers = sorted(
self.project.containers(service_names=options['SERVICE'], stopped=True) +
self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
key=attrgetter('name'))
if options['--all']:
containers = sorted(self.project.containers(service_names=options['SERVICE'],
one_off=OneOffFilter.include, stopped=True))
else:
containers = sorted(
self.project.containers(service_names=options['SERVICE'], stopped=True) +
self.project.containers(service_names=options['SERVICE'], one_off=OneOffFilter.only),
key=attrgetter('name'))
if options['--quiet']:
for container in containers:
@ -867,7 +872,7 @@ class TopLevelCommand(object):
else:
command = service.options.get('command')
container_options = build_container_options(options, detach, command)
container_options = build_one_off_container_options(options, detach, command)
run_one_off_container(
container_options, self.project, service, options,
self.toplevel_options, self.project_dir
@ -1262,7 +1267,7 @@ def build_action_from_opts(options):
return BuildAction.none
def build_container_options(options, detach, command):
def build_one_off_container_options(options, detach, command):
container_options = {
'command': command,
'tty': not (detach or options['-T'] or not sys.stdin.isatty()),
@ -1283,8 +1288,8 @@ def build_container_options(options, detach, command):
[""] if options['--entrypoint'] == '' else options['--entrypoint']
)
if options['--rm']:
container_options['restart'] = None
# Ensure that run command remains one-off (issue #6302)
container_options['restart'] = None
if options['--user']:
container_options['user'] = options.get('--user')

View File

@ -8,6 +8,7 @@ import os
import string
import sys
from collections import namedtuple
from operator import attrgetter
import six
import yaml
@ -835,6 +836,17 @@ def finalize_service_volumes(service_dict, environment):
finalized_volumes.append(MountSpec.parse(v, normalize, win_host))
else:
finalized_volumes.append(VolumeSpec.parse(v, normalize, win_host))
duplicate_mounts = []
mounts = [v.as_volume_spec() if isinstance(v, MountSpec) else v for v in finalized_volumes]
for mount in mounts:
if list(map(attrgetter('internal'), mounts)).count(mount.internal) > 1:
duplicate_mounts.append(mount.repr())
if duplicate_mounts:
raise ConfigurationError("Duplicate mount points: [%s]" % (
', '.join(duplicate_mounts)))
service_dict['volumes'] = finalized_volumes
return service_dict
@ -1040,7 +1052,6 @@ def merge_service_dicts(base, override, version):
md.merge_mapping('environment', parse_environment)
md.merge_mapping('labels', parse_labels)
md.merge_mapping('ulimits', parse_flat_dict)
md.merge_mapping('networks', parse_networks)
md.merge_mapping('sysctls', parse_sysctls)
md.merge_mapping('depends_on', parse_depends_on)
md.merge_mapping('storage_opt', parse_flat_dict)
@ -1050,6 +1061,7 @@ def merge_service_dicts(base, override, version):
md.merge_sequence('security_opt', types.SecurityOpt.parse)
md.merge_mapping('extra_hosts', parse_extra_hosts)
md.merge_field('networks', merge_networks, default={})
for field in ['volumes', 'devices']:
md.merge_field(field, merge_path_mappings)
@ -1154,6 +1166,22 @@ def merge_deploy(base, override):
return dict(md)
def merge_networks(base, override):
merged_networks = {}
all_network_names = set(base) | set(override)
base = {k: {} for k in base} if isinstance(base, list) else base
override = {k: {} for k in override} if isinstance(override, list) else override
for network_name in all_network_names:
md = MergeDict(base.get(network_name, {}), override.get(network_name, {}))
md.merge_field('aliases', merge_unique_items_lists, [])
md.merge_field('link_local_ips', merge_unique_items_lists, [])
md.merge_scalar('priority')
md.merge_scalar('ipv4_address')
md.merge_scalar('ipv6_address')
merged_networks[network_name] = dict(md)
return merged_networks
def merge_reservations(base, override):
md = MergeDict(base, override)
md.merge_scalar('cpus')
@ -1283,7 +1311,7 @@ def resolve_volume_paths(working_dir, service_dict):
def resolve_volume_path(working_dir, volume):
if isinstance(volume, dict):
if volume.get('source', '').startswith('.') and volume['type'] == 'bind':
if volume.get('source', '').startswith(('.', '~')) and volume['type'] == 'bind':
volume['source'] = expand_path(working_dir, volume['source'])
return volume

View File

@ -5,11 +5,13 @@ import codecs
import contextlib
import logging
import os
import re
import six
from ..const import IS_WINDOWS_PLATFORM
from .errors import ConfigurationError
from .errors import EnvFileNotFound
log = logging.getLogger(__name__)
@ -17,10 +19,16 @@ log = logging.getLogger(__name__)
def split_env(env):
if isinstance(env, six.binary_type):
env = env.decode('utf-8', 'replace')
key = value = None
if '=' in env:
return env.split('=', 1)
key, value = env.split('=', 1)
else:
return env, None
key = env
if re.search(r'\s', key):
raise ConfigurationError(
"environment variable name '{}' may not contains whitespace.".format(key)
)
return key, value
def env_vars_from_file(filename):
@ -28,16 +36,19 @@ def env_vars_from_file(filename):
Read in a line delimited file of environment variables.
"""
if not os.path.exists(filename):
raise ConfigurationError("Couldn't find env file: %s" % filename)
raise EnvFileNotFound("Couldn't find env file: {}".format(filename))
elif not os.path.isfile(filename):
raise ConfigurationError("%s is not a file." % (filename))
raise EnvFileNotFound("{} is not a file.".format(filename))
env = {}
with contextlib.closing(codecs.open(filename, 'r', 'utf-8-sig')) as fileobj:
for line in fileobj:
line = line.strip()
if line and not line.startswith('#'):
k, v = split_env(line)
env[k] = v
try:
k, v = split_env(line)
env[k] = v
except ConfigurationError as e:
raise ConfigurationError('In file {}: {}'.format(filename, e.msg))
return env
@ -55,9 +66,10 @@ class Environment(dict):
env_file_path = os.path.join(base_dir, '.env')
try:
return cls(env_vars_from_file(env_file_path))
except ConfigurationError:
except EnvFileNotFound:
pass
return result
instance = _initialize()
instance.update(os.environ)
return instance

View File

@ -19,6 +19,10 @@ class ConfigurationError(Exception):
return self.msg
class EnvFileNotFound(ConfigurationError):
pass
class DependencyError(ConfigurationError):
pass

View File

@ -7,7 +7,6 @@ from .version import ComposeVersion
DEFAULT_TIMEOUT = 10
HTTP_TIMEOUT = 60
IMAGE_EVENTS = ['delete', 'import', 'load', 'pull', 'push', 'save', 'tag', 'untag']
IS_WINDOWS_PLATFORM = (sys.platform == "win32")
LABEL_CONTAINER_NUMBER = 'com.docker.compose.container-number'
LABEL_ONE_OFF = 'com.docker.compose.oneoff'

View File

@ -43,14 +43,17 @@ class GlobalLimit(object):
cls.global_limiter = Semaphore(value)
def parallel_execute_watch(events, writer, errors, results, msg, get_name):
def parallel_execute_watch(events, writer, errors, results, msg, get_name, fail_check):
""" Watch events from a parallel execution, update status and fill errors and results.
Returns exception to re-raise.
"""
error_to_reraise = None
for obj, result, exception in events:
if exception is None:
writer.write(msg, get_name(obj), 'done', green)
if fail_check is not None and fail_check(obj):
writer.write(msg, get_name(obj), 'failed', red)
else:
writer.write(msg, get_name(obj), 'done', green)
results.append(result)
elif isinstance(exception, ImageNotFound):
# This is to bubble up ImageNotFound exceptions to the client so we
@ -72,12 +75,14 @@ def parallel_execute_watch(events, writer, errors, results, msg, get_name):
return error_to_reraise
def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None):
def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None, fail_check=None):
"""Runs func on objects in parallel while ensuring that func is
ran on object only after it is ran on all its dependencies.
get_deps called on object must return a collection with its dependencies.
get_name called on object must return its name.
fail_check is an additional failure check for cases that should display as a failure
in the CLI logs, but don't raise an exception (such as attempting to start 0 containers)
"""
objects = list(objects)
stream = get_output_stream(sys.stderr)
@ -96,7 +101,9 @@ def parallel_execute(objects, func, get_name, msg, get_deps=None, limit=None):
errors = {}
results = []
error_to_reraise = parallel_execute_watch(events, writer, errors, results, msg, get_name)
error_to_reraise = parallel_execute_watch(
events, writer, errors, results, msg, get_name, fail_check
)
for obj_name, error in errors.items():
stream.write("\nERROR: for {} {}\n".format(obj_name, error))

View File

@ -98,14 +98,14 @@ def print_output_event(event, stream, is_terminal):
def get_digest_from_pull(events):
digest = None
for event in events:
status = event.get('status')
if not status or 'Digest' not in status:
continue
_, digest = status.split(':', 1)
return digest.strip()
return None
else:
digest = status.split(':', 1)[1].strip()
return digest
def get_digest_from_push(events):

View File

@ -10,13 +10,13 @@ from functools import reduce
import enum
import six
from docker.errors import APIError
from docker.utils import version_lt
from . import parallel
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 IMAGE_EVENTS
from .const import LABEL_ONE_OFF
from .const import LABEL_PROJECT
from .const import LABEL_SERVICE
@ -29,6 +29,7 @@ from .service import ContainerNetworkMode
from .service import ContainerPidMode
from .service import ConvergenceStrategy
from .service import NetworkMode
from .service import parse_repository_tag
from .service import PidMode
from .service import Service
from .service import ServiceNetworkMode
@ -279,6 +280,7 @@ class Project(object):
operator.attrgetter('name'),
'Starting',
get_deps,
fail_check=lambda obj: not obj.containers(),
)
return containers
@ -401,11 +403,13 @@ class Project(object):
detached=True,
start=False)
def events(self, service_names=None):
def _legacy_event_processor(self, service_names):
# Only for v1 files or when Compose is forced to use an older API version
def build_container_event(event, container):
time = datetime.datetime.fromtimestamp(event['time'])
time = time.replace(
microsecond=microseconds_from_time_nano(event['timeNano']))
microsecond=microseconds_from_time_nano(event['timeNano'])
)
return {
'time': time,
'type': 'container',
@ -424,17 +428,15 @@ class Project(object):
filters={'label': self.labels()},
decode=True
):
# The first part of this condition is a guard against some events
# broadcasted by swarm that don't have a status field.
# This is a guard against some events broadcasted by swarm that
# don't have a status field.
# See https://github.com/docker/compose/issues/3316
if 'status' not in event or event['status'] in IMAGE_EVENTS:
# We don't receive any image events because labels aren't applied
# to images
if 'status' not in event:
continue
# 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 container has been removed or if the event
# refers to an image
container = Container.from_id(self.client, event['id'])
except APIError:
continue
@ -442,6 +444,56 @@ class Project(object):
continue
yield build_container_event(event, container)
def events(self, service_names=None):
if version_lt(self.client.api_version, '1.22'):
# New, better event API was introduced in 1.22.
return self._legacy_event_processor(service_names)
def build_container_event(event):
container_attrs = event['Actor']['Attributes']
time = datetime.datetime.fromtimestamp(event['time'])
time = time.replace(
microsecond=microseconds_from_time_nano(event['timeNano'])
)
container = None
try:
container = Container.from_id(self.client, event['id'])
except APIError:
# Container may have been removed (e.g. if this is a destroy event)
pass
return {
'time': time,
'type': 'container',
'action': event['status'],
'id': event['Actor']['ID'],
'service': container_attrs.get(LABEL_SERVICE),
'attributes': dict([
(k, v) for k, v in container_attrs.items()
if not k.startswith('com.docker.compose.')
]),
'container': container,
}
def yield_loop(service_names):
for event in self.client.events(
filters={'label': self.labels()},
decode=True
):
# TODO: support other event types
if event.get('Type') != 'container':
continue
try:
if event['Actor']['Attributes'][LABEL_SERVICE] not in service_names:
continue
except KeyError:
continue
yield build_container_event(event)
return yield_loop(set(service_names) if service_names else self.service_names)
def up(self,
service_names=None,
start_deps=True,
@ -592,8 +644,15 @@ class Project(object):
service.pull(ignore_pull_failures, silent=silent)
def push(self, service_names=None, ignore_push_failures=False):
unique_images = set()
for service in self.get_services(service_names, include_deps=False):
service.push(ignore_push_failures)
# Considering <image> and <image:latest> as the same
repo, tag, sep = parse_repository_tag(service.image_name)
service_image_name = sep.join((repo, tag)) if tag else sep.join((repo, 'latest'))
if service_image_name not in unique_images:
service.push(ignore_push_failures)
unique_images.add(service_image_name)
def _labeled_containers(self, stopped=False, one_off=OneOffFilter.exclude):
ctnrs = list(filter(None, [

View File

@ -199,7 +199,9 @@ class Service(object):
def __repr__(self):
return '<Service: {}>'.format(self.name)
def containers(self, stopped=False, one_off=False, filters={}, labels=None):
def containers(self, stopped=False, one_off=False, filters=None, labels=None):
if filters is None:
filters = {}
filters.update({'label': self.labels(one_off=one_off) + (labels or [])})
result = list(filter(None, [
@ -1146,6 +1148,9 @@ class Service(object):
try:
self.client.remove_image(self.image_name)
return True
except ImageNotFound:
log.warning("Image %s not found.", self.image_name)
return False
except APIError as e:
log.error("Failed to remove image for service %s: %s", self.name, e)
return False

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
import codecs
import hashlib
import json
import json.decoder
import logging
import ntpath

View File

@ -354,7 +354,7 @@ _docker-compose() {
'(-): :->command' \
'(-)*:: :->option-or-argument' && ret=0
local -a relevant_compose_flags relevant_docker_flags compose_options docker_options
local -a relevant_compose_flags relevant_compose_repeatable_flags relevant_docker_flags compose_options docker_options
relevant_compose_flags=(
"--file" "-f"
@ -368,6 +368,10 @@ _docker-compose() {
"--skip-hostname-check"
)
relevant_compose_repeatable_flags=(
"--file" "-f"
)
relevant_docker_flags=(
"--host" "-H"
"--tls"
@ -385,9 +389,18 @@ _docker-compose() {
fi
fi
if [[ -n "${relevant_compose_flags[(r)$k]}" ]]; then
compose_options+=$k
if [[ -n "$opt_args[$k]" ]]; then
compose_options+=$opt_args[$k]
if [[ -n "${relevant_compose_repeatable_flags[(r)$k]}" ]]; then
values=("${(@s/:/)opt_args[$k]}")
for value in $values
do
compose_options+=$k
compose_options+=$value
done
else
compose_options+=$k
if [[ -n "$opt_args[$k]" ]]; then
compose_options+=$opt_args[$k]
fi
fi
fi
done

View File

@ -3,8 +3,8 @@ cached-property==1.3.0
certifi==2017.4.17
chardet==3.0.4
colorama==0.4.0; sys_platform == 'win32'
docker==3.6.0
docker-pycreds==0.3.0
docker==3.7.0
docker-pycreds==0.4.0
dockerpty==0.4.1
docopt==0.6.2
enum34==1.1.6; python_version < '3.4'
@ -12,10 +12,11 @@ functools32==3.2.3.post2; python_version < '3.2'
idna==2.5
ipaddress==1.0.18
jsonschema==2.6.0
paramiko==2.4.2
pypiwin32==219; sys_platform == 'win32' and python_version < '3.6'
pypiwin32==223; sys_platform == 'win32' and python_version >= '3.6'
PySocks==1.6.7
PyYAML==3.12
PyYAML==4.2b1
requests==2.20.0
six==1.10.0
texttable==0.9.1

View File

@ -2,6 +2,11 @@
#
# Write the current commit sha to the file GITSHA. This file is included in
# packaging so that `docker-compose version` can include the git sha.
#
set -e
git rev-parse --short HEAD > compose/GITSHA
# sets to 'unknown' and echoes a message if the command is not successful
DOCKER_COMPOSE_GITSHA="$(git rev-parse --short HEAD)"
if [[ "${?}" != "0" ]]; then
echo "Couldn't get revision of the git repository. Setting to 'unknown' instead"
DOCKER_COMPOSE_GITSHA="unknown"
fi
echo "${DOCKER_COMPOSE_GITSHA}" > compose/GITSHA

View File

@ -26,12 +26,6 @@ if [ -z "$(command -v jq 2> /dev/null)" ]; then
fi
if [ -z "$(command -v pandoc 2> /dev/null)" ]; then
>&2 echo "$0 requires http://pandoc.org/"
>&2 echo "Please install it and make sure it is available on your \$PATH."
exit 2
fi
API=https://api.github.com/repos
REPO=docker/compose
GITHUB_REPO=git@github.com:$REPO
@ -59,8 +53,6 @@ docker push docker/compose-tests:latest
docker push docker/compose-tests:$VERSION
echo "Uploading package 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
if [ "$(command -v twine 2> /dev/null)" ]; then

View File

@ -1,6 +1,6 @@
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)**.
If you're a Mac or Windows user, the best way to install Compose and keep it up-to-date is **[Docker Desktop for Mac and Windows](https://www.docker.com/products/docker-desktop)**.
Docker for Mac and Windows will automatically install the latest version of Docker Engine for you.
Docker Desktop will automatically install the latest version of Docker Engine for you.
Alternatively, you can use the usual commands to install or upgrade Compose:

View File

@ -9,7 +9,6 @@ import sys
import time
from distutils.core import run_setup
import pypandoc
from jinja2 import Template
from release.bintray import BintrayAPI
from release.const import BINTRAY_ORG
@ -277,9 +276,6 @@ def finalize(args):
repository.checkout_branch(br_name)
pypandoc.convert_file(
os.path.join(REPO_ROOT, 'README.md'), 'rst', outputfile=os.path.join(REPO_ROOT, 'README.rst')
)
run_setup(os.path.join(REPO_ROOT, 'setup.py'), script_args=['sdist', 'bdist_wheel'])
merge_status = pr_data.merge()

View File

@ -219,6 +219,8 @@ def get_contributors(pr_data):
commits = pr_data.get_commits()
authors = {}
for commit in commits:
if not commit.author:
continue
author = commit.author.login
authors[author] = authors.get(author, 0) + 1
return [x[0] for x in sorted(list(authors.items()), key=lambda x: x[1])]

View File

@ -39,9 +39,9 @@ fi
$VENV_PYTHONBIN -m pip install -U Jinja2==2.10 \
PyGithub==1.39 \
pypandoc==1.4 \
GitPython==2.1.9 \
requests==2.18.4 \
setuptools==40.6.2 \
twine==1.11.0
$VENV_PYTHONBIN setup.py develop

View File

@ -15,7 +15,7 @@
set -e
VERSION="1.23.2"
VERSION="1.24.0-rc1"
IMAGE="docker/compose:$VERSION"
@ -47,14 +47,14 @@ if [ -n "$HOME" ]; then
fi
# Only allocate tty if we detect one
if [ -t 0 ]; then
if [ -t 1 ]; then
if [ -t 0 -a -t 1 ]; then
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -t"
fi
else
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i"
fi
# Always set -i to support piped and terminal input in run/exec
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS -i"
# Handle userns security
if [ ! -z "$(docker info 2>/dev/null | grep userns)" ]; then
DOCKER_RUN_OPTIONS="$DOCKER_RUN_OPTIONS --userns=host"

View File

@ -32,11 +32,11 @@ def find_version(*file_paths):
install_requires = [
'cached-property >= 1.2.0, < 2',
'docopt >= 0.6.1, < 0.7',
'PyYAML >= 3.10, < 4',
'PyYAML >= 3.10, < 4.3',
'requests >= 2.6.1, != 2.11.0, != 2.12.2, != 2.18.0, < 2.21',
'texttable >= 0.9.0, < 0.10',
'websocket-client >= 0.32.0, < 1.0',
'docker >= 3.6.0, < 4.0',
'docker[ssh] >= 3.7.0, < 4.0',
'dockerpty >= 0.4.1, < 0.5',
'six >= 1.3.0, < 2',
'jsonschema >= 2.5.1, < 3',
@ -77,19 +77,26 @@ setup(
name='docker-compose',
version=find_version("compose", "__init__.py"),
description='Multi-container orchestration for Docker',
long_description=read('README.md'),
long_description_content_type='text/markdown',
url='https://www.docker.com/',
project_urls={
'Documentation': 'https://docs.docker.com/compose/overview',
'Changelog': 'https://github.com/docker/compose/blob/release/CHANGELOG.md',
'Source': 'https://github.com/docker/compose',
'Tracker': 'https://github.com/docker/compose/issues',
},
author='Docker, Inc.',
license='Apache License 2.0',
packages=find_packages(exclude=['tests.*', 'tests']),
include_package_data=True,
test_suite='nose.collector',
install_requires=install_requires,
extras_require=extras_require,
tests_require=tests_require,
entry_points="""
[console_scripts]
docker-compose=compose.cli.main:main
""",
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
entry_points={
'console_scripts': ['docker-compose=compose.cli.main:main'],
},
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',

View File

@ -4,7 +4,6 @@ from __future__ import unicode_literals
import datetime
import json
import os
import os.path
import re
import signal
@ -599,10 +598,20 @@ class CLITestCase(DockerClientTestCase):
assert 'with_build' in running.stdout
assert 'with_image' in running.stdout
def test_ps_all(self):
self.project.get_service('simple').create_container(one_off='blahblah')
result = self.dispatch(['ps'])
assert 'simple-composefile_simple_run_' not in result.stdout
result2 = self.dispatch(['ps', '--all'])
assert 'simple-composefile_simple_run_' in result2.stdout
def test_pull(self):
result = self.dispatch(['pull'])
assert 'Pulling simple' in result.stderr
assert 'Pulling another' in result.stderr
assert 'done' in result.stderr
assert 'failed' not in result.stderr
def test_pull_with_digest(self):
result = self.dispatch(['-f', 'digest.yml', 'pull', '--no-parallel'])
@ -2221,6 +2230,7 @@ class CLITestCase(DockerClientTestCase):
def test_start_no_containers(self):
result = self.dispatch(['start'], returncode=1)
assert 'failed' in result.stderr
assert 'No containers to start' in result.stderr
@v2_only()

View File

@ -171,7 +171,10 @@ class CLITestCase(unittest.TestCase):
'--workdir': None,
})
assert mock_client.create_host_config.call_args[1]['restart_policy']['Name'] == 'always'
# NOTE: The "run" command is supposed to be a one-off tool; therefore restart policy "no"
# (the default) is enforced despite explicit wish for "always" in the project
# configuration file
assert not mock_client.create_host_config.call_args[1].get('restart_policy')
command = TopLevelCommand(project)
command.run({

View File

@ -1085,8 +1085,43 @@ class ConfigTest(unittest.TestCase):
details = config.ConfigDetails('.', [base_file, override_file])
web_service = config.load(details).services[0]
assert web_service['networks'] == {
'foobar': {'aliases': ['foo', 'bar']},
'baz': None
'foobar': {'aliases': ['bar', 'foo']},
'baz': {}
}
def test_load_with_multiple_files_mismatched_networks_format_inverse_order(self):
base_file = config.ConfigFile(
'override.yaml',
{
'version': '2',
'services': {
'web': {
'networks': ['baz']
}
}
}
)
override_file = config.ConfigFile(
'base.yaml',
{
'version': '2',
'services': {
'web': {
'image': 'example/web',
'networks': {
'foobar': {'aliases': ['foo', 'bar']}
}
}
},
'networks': {'foobar': {}, 'baz': {}}
}
)
details = config.ConfigDetails('.', [base_file, override_file])
web_service = config.load(details).services[0]
assert web_service['networks'] == {
'foobar': {'aliases': ['bar', 'foo']},
'baz': {}
}
def test_load_with_multiple_files_v2(self):
@ -1336,6 +1371,32 @@ class ConfigTest(unittest.TestCase):
assert mount.type == 'bind'
assert mount.source == expected_source
def test_load_bind_mount_relative_path_with_tilde(self):
base_file = config.ConfigFile(
'base.yaml', {
'version': '3.4',
'services': {
'web': {
'image': 'busybox:latest',
'volumes': [
{'type': 'bind', 'source': '~/web', 'target': '/web'},
],
},
},
},
)
details = config.ConfigDetails('.', [base_file])
config_data = config.load(details)
mount = config_data.services[0].get('volumes')[0]
assert mount.target == '/web'
assert mount.type == 'bind'
assert (
not mount.source.startswith('~') and mount.source.endswith(
'{}web'.format(os.path.sep)
)
)
def test_config_invalid_ipam_config(self):
with pytest.raises(ConfigurationError) as excinfo:
config.load(
@ -3045,6 +3106,41 @@ class ConfigTest(unittest.TestCase):
)
config.load(config_details)
def test_config_duplicate_mount_points(self):
config1 = build_config_details(
{
'version': '3.5',
'services': {
'web': {
'image': 'busybox',
'volumes': ['/tmp/foo:/tmp/foo', '/tmp/foo:/tmp/foo:rw']
}
}
}
)
config2 = build_config_details(
{
'version': '3.5',
'services': {
'web': {
'image': 'busybox',
'volumes': ['/x:/y', '/z:/y']
}
}
}
)
with self.assertRaises(ConfigurationError) as e:
config.load(config1)
self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % (
', '.join(['/tmp/foo:/tmp/foo:rw']*2)))
with self.assertRaises(ConfigurationError) as e:
config.load(config2)
self.assertEquals(str(e.exception), 'Duplicate mount points: [%s]' % (
', '.join(['/x:/y:rw', '/z:/y:rw'])))
class NetworkModeTest(unittest.TestCase):
@ -3817,8 +3913,77 @@ class MergePortsTest(unittest.TestCase, MergeListsTest):
class MergeNetworksTest(unittest.TestCase, MergeListsTest):
config_name = 'networks'
base_config = ['frontend', 'backend']
override_config = ['monitoring']
base_config = {'default': {'aliases': ['foo.bar', 'foo.baz']}}
override_config = {'default': {'ipv4_address': '123.234.123.234'}}
def test_no_network_overrides(self):
service_dict = config.merge_service_dicts(
{self.config_name: self.base_config},
{self.config_name: self.override_config},
DEFAULT_VERSION)
assert service_dict[self.config_name] == {
'default': {
'aliases': ['foo.bar', 'foo.baz'],
'ipv4_address': '123.234.123.234'
}
}
def test_all_properties(self):
service_dict = config.merge_service_dicts(
{self.config_name: {
'default': {
'aliases': ['foo.bar', 'foo.baz'],
'link_local_ips': ['192.168.1.10', '192.168.1.11'],
'ipv4_address': '111.111.111.111',
'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-first'
}
}},
{self.config_name: {
'default': {
'aliases': ['foo.baz', 'foo.baz2'],
'link_local_ips': ['192.168.1.11', '192.168.1.12'],
'ipv4_address': '123.234.123.234',
'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-second'
}
}},
DEFAULT_VERSION)
assert service_dict[self.config_name] == {
'default': {
'aliases': ['foo.bar', 'foo.baz', 'foo.baz2'],
'link_local_ips': ['192.168.1.10', '192.168.1.11', '192.168.1.12'],
'ipv4_address': '123.234.123.234',
'ipv6_address': 'FE80:CD00:0000:0CDE:1257:0000:211E:729C-second'
}
}
def test_no_network_name_overrides(self):
service_dict = config.merge_service_dicts(
{
self.config_name: {
'default': {
'aliases': ['foo.bar', 'foo.baz'],
'ipv4_address': '123.234.123.234'
}
}
},
{
self.config_name: {
'another_network': {
'ipv4_address': '123.234.123.234'
}
}
},
DEFAULT_VERSION)
assert service_dict[self.config_name] == {
'default': {
'aliases': ['foo.bar', 'foo.baz'],
'ipv4_address': '123.234.123.234'
},
'another_network': {
'ipv4_address': '123.234.123.234'
}
}
class MergeStringsOrListsTest(unittest.TestCase):

View File

@ -9,6 +9,7 @@ import pytest
from compose.config.environment import env_vars_from_file
from compose.config.environment import Environment
from compose.config.errors import ConfigurationError
from tests import unittest
@ -52,3 +53,12 @@ class EnvironmentTest(unittest.TestCase):
assert env_vars_from_file(str(tmpdir.join('bom.env'))) == {
'PARK_BOM': '박봄'
}
def test_env_vars_from_file_whitespace(self):
tmpdir = pytest.ensuretemp('env_file')
self.addCleanup(tmpdir.remove)
with codecs.open('{}/whitespace.env'.format(str(tmpdir)), 'w', encoding='utf-8') as f:
f.write('WHITESPACE =yes\n')
with pytest.raises(ConfigurationError) as exc:
env_vars_from_file(str(tmpdir.join('whitespace.env')))
assert 'environment variable' in exc.exconly()

View File

@ -97,22 +97,24 @@ class ProgressStreamTestCase(unittest.TestCase):
tf.seek(0)
assert tf.read() == '???'
def test_get_digest_from_push(self):
digest = "sha256:abcd"
events = [
{"status": "..."},
{"status": "..."},
{"progressDetail": {}, "aux": {"Digest": digest}},
]
assert progress_stream.get_digest_from_push(events) == digest
def test_get_digest_from_push():
digest = "sha256:abcd"
events = [
{"status": "..."},
{"status": "..."},
{"progressDetail": {}, "aux": {"Digest": digest}},
]
assert progress_stream.get_digest_from_push(events) == digest
def test_get_digest_from_pull(self):
events = list()
assert progress_stream.get_digest_from_pull(events) is None
def test_get_digest_from_pull():
digest = "sha256:abcd"
events = [
{"status": "..."},
{"status": "..."},
{"status": "Digest: %s" % digest},
]
assert progress_stream.get_digest_from_pull(events) == digest
digest = "sha256:abcd"
events = [
{"status": "..."},
{"status": "..."},
{"status": "Digest: %s" % digest},
{"status": "..."},
]
assert progress_stream.get_digest_from_pull(events) == digest

View File

@ -254,9 +254,10 @@ class ProjectTest(unittest.TestCase):
[container_ids[0] + ':rw']
)
def test_events(self):
def test_events_legacy(self):
services = [Service(name='web'), Service(name='db')]
project = Project('test', services, self.mock_client)
self.mock_client.api_version = '1.21'
self.mock_client.events.return_value = iter([
{
'status': 'create',
@ -362,6 +363,175 @@ class ProjectTest(unittest.TestCase):
},
]
def test_events(self):
services = [Service(name='web'), Service(name='db')]
project = Project('test', services, self.mock_client)
self.mock_client.api_version = '1.35'
self.mock_client.events.return_value = iter([
{
'status': 'create',
'from': 'example/image',
'Type': 'container',
'Actor': {
'ID': 'abcde',
'Attributes': {
'com.docker.compose.project': 'test',
'com.docker.compose.service': 'web',
'image': 'example/image',
'name': 'test_web_1',
}
},
'id': 'abcde',
'time': 1420092061,
'timeNano': 14200920610000002000,
},
{
'status': 'attach',
'from': 'example/image',
'Type': 'container',
'Actor': {
'ID': 'abcde',
'Attributes': {
'com.docker.compose.project': 'test',
'com.docker.compose.service': 'web',
'image': 'example/image',
'name': 'test_web_1',
}
},
'id': 'abcde',
'time': 1420092061,
'timeNano': 14200920610000003000,
},
{
'status': 'create',
'from': 'example/other',
'Type': 'container',
'Actor': {
'ID': 'bdbdbd',
'Attributes': {
'image': 'example/other',
'name': 'shrewd_einstein',
}
},
'id': 'bdbdbd',
'time': 1420092061,
'timeNano': 14200920610000005000,
},
{
'status': 'create',
'from': 'example/db',
'Type': 'container',
'Actor': {
'ID': 'ababa',
'Attributes': {
'com.docker.compose.project': 'test',
'com.docker.compose.service': 'db',
'image': 'example/db',
'name': 'test_db_1',
}
},
'id': 'ababa',
'time': 1420092061,
'timeNano': 14200920610000004000,
},
{
'status': 'destroy',
'from': 'example/db',
'Type': 'container',
'Actor': {
'ID': 'eeeee',
'Attributes': {
'com.docker.compose.project': 'test',
'com.docker.compose.service': 'db',
'image': 'example/db',
'name': 'test_db_1',
}
},
'id': 'eeeee',
'time': 1420092061,
'timeNano': 14200920610000004000,
},
])
def dt_with_microseconds(dt, us):
return datetime.datetime.fromtimestamp(dt).replace(microsecond=us)
def get_container(cid):
if cid == 'eeeee':
raise NotFound(None, None, "oops")
if cid == 'abcde':
name = 'web'
labels = {LABEL_SERVICE: name}
elif cid == 'ababa':
name = 'db'
labels = {LABEL_SERVICE: name}
else:
labels = {}
name = ''
return {
'Id': cid,
'Config': {'Labels': labels},
'Name': '/project_%s_1' % name,
}
self.mock_client.inspect_container.side_effect = get_container
events = project.events()
events_list = list(events)
# Assert the return value is a generator
assert not list(events)
assert events_list == [
{
'type': 'container',
'service': 'web',
'action': 'create',
'id': 'abcde',
'attributes': {
'name': 'test_web_1',
'image': 'example/image',
},
'time': dt_with_microseconds(1420092061, 2),
'container': Container(None, get_container('abcde')),
},
{
'type': 'container',
'service': 'web',
'action': 'attach',
'id': 'abcde',
'attributes': {
'name': 'test_web_1',
'image': 'example/image',
},
'time': dt_with_microseconds(1420092061, 3),
'container': Container(None, get_container('abcde')),
},
{
'type': 'container',
'service': 'db',
'action': 'create',
'id': 'ababa',
'attributes': {
'name': 'test_db_1',
'image': 'example/db',
},
'time': dt_with_microseconds(1420092061, 4),
'container': Container(None, get_container('ababa')),
},
{
'type': 'container',
'service': 'db',
'action': 'destroy',
'id': 'eeeee',
'attributes': {
'name': 'test_db_1',
'image': 'example/db',
},
'time': dt_with_microseconds(1420092061, 4),
'container': None,
},
]
def test_net_unset(self):
project = Project.from_config(
name='test',
@ -620,3 +790,23 @@ class ProjectTest(unittest.TestCase):
self.mock_client.pull.side_effect = OperationFailedError(b'pull error')
with pytest.raises(ProjectError):
project.pull(parallel_pull=True)
def test_avoid_multiple_push(self):
service_config_latest = {'image': 'busybox:latest', 'build': '.'}
service_config_default = {'image': 'busybox', 'build': '.'}
service_config_sha = {
'image': 'busybox@sha256:38a203e1986cf79639cfb9b2e1d6e773de84002feea2d4eb006b52004ee8502d',
'build': '.'
}
svc1 = Service('busy1', **service_config_latest)
svc1_1 = Service('busy11', **service_config_latest)
svc2 = Service('busy2', **service_config_default)
svc2_1 = Service('busy21', **service_config_default)
svc3 = Service('busy3', **service_config_sha)
svc3_1 = Service('busy31', **service_config_sha)
project = Project(
'composetest', [svc1, svc1_1, svc2, svc2_1, svc3, svc3_1], self.mock_client
)
with mock.patch('compose.service.Service.push') as fake_push:
project.push()
assert fake_push.call_count == 2

View File

@ -5,6 +5,7 @@ import docker
import pytest
from docker.constants import DEFAULT_DOCKER_API_VERSION
from docker.errors import APIError
from docker.errors import ImageNotFound
from docker.errors import NotFound
from .. import mock
@ -755,6 +756,13 @@ class ServiceTest(unittest.TestCase):
mock_log.error.assert_called_once_with(
"Failed to remove image for service %s: %s", web.name, error)
def test_remove_non_existing_image(self):
self.mock_client.remove_image.side_effect = ImageNotFound('image not found')
web = Service('web', image='example', client=self.mock_client)
with mock.patch('compose.service.log', autospec=True) as mock_log:
assert not web.remove_image(ImageType.all)
mock_log.warning.assert_called_once_with("Image %s not found.", web.image_name)
def test_specifies_host_port_with_no_ports(self):
service = Service(
'foo',