Merge pull request #5844 from docker/bump-1.21.0-rc1

Bump 1.21.0 RC1
This commit is contained in:
Joffrey F 2018-04-02 14:57:03 -07:00 committed by GitHub
commit 9cc30ad0e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1331 additions and 145 deletions

View File

@ -6,11 +6,11 @@ jobs:
steps: steps:
- checkout - checkout
- run: - run:
name: install python3 name: setup script
command: brew update > /dev/null && brew upgrade python command: ./script/setup/osx
- run: - run:
name: install tox name: install tox
command: sudo pip3 install --upgrade tox==2.1.1 command: sudo pip install --upgrade tox==2.1.1
- run: - run:
name: unit tests name: unit tests
command: tox -e py27,py36 -- tests/unit command: tox -e py27,py36 -- tests/unit

View File

@ -1,6 +1,70 @@
Change log Change log
========== ==========
1.21.0 (2018-04-11)
-------------------
### New features
#### Compose file version 2.4
- Introduced version 2.4 of the `docker-compose.yml` specification.
This version requires Docker Engine 17.12.0 or above.
- Added support for the `platform` parameter in service definitions.
If supplied, the parameter is also used when performing build for the
service.
#### Compose file version 2.2 and up
- Added support for the `cpu_rt_period` and `cpu_rt_runtime` parameters
in service definitions (2.x only).
#### Compose file version 2.1 and up
- Added support for the `cpu_period` parameter in service definitions
(2.x only).
- Added support for the `isolation` parameter in service build configurations.
Additionally, the `isolation` parameter is used for builds as well if no
`build.isolation` parameter is defined. (2.x only)
#### All formats
- Added support for the `--workdir` flag in `docker-compose exec`.
- Added support for the `--compress` flag in `docker-compose build`.
- `docker-compose pull` is now performed in parallel by default. You can
opt out using the `--no-parallel` flag. The `--parallel` flag is now
deprecated and will be removed in a future version.
- Dashes and underscores in project names are no longer stripped out.
- `docker-compose build` now supports the use of Dockerfile from outside
the build context.
### Bugfixes
- Compose now checks that the volume's configuration matches the remote
volume, and errors out if a mismatch is detected.
- Fixed a bug that caused Compose to raise unexpected errors when attempting
to create several one-off containers in parallel.
- Fixed a bug with argument parsing when using `docker-machine config` to
generate TLS flags for `exec` and `run` commands.
- Fixed a bug where variable substitution with an empty default value
(e.g. `${VAR:-}`) would print an incorrect warning.
- Improved resilience when encoding of the Compose file doesn't match the
system's. Users are encouraged to use UTF-8 when possible.
- Fixed a bug where external overlay networks in Swarm would be incorrectly
recognized as inexistent by Compose, interrupting otherwise valid
operations.
1.20.1 (2018-03-21) 1.20.1 (2018-03-21)
------------------- -------------------
@ -17,7 +81,7 @@ Change log
#### Compose file version 3.6 #### Compose file version 3.6
- Introduced version 3.6 of the `docker-compose.yml` specification. - Introduced version 3.6 of the `docker-compose.yml` specification.
This version requires to be used with Docker Engine 18.02.0 or above. This version requires Docker Engine 18.02.0 or above.
- Added support for the `tmpfs.size` property in volume mappings - Added support for the `tmpfs.size` property in volume mappings

View File

@ -3,12 +3,13 @@ FROM alpine:3.6
ENV GLIBC 2.27-r0 ENV GLIBC 2.27-r0
ENV DOCKERBINS_SHA 1270dce1bd7e1838d62ae21d2505d87f16efc1d9074645571daaefdfd0c14054 ENV DOCKERBINS_SHA 1270dce1bd7e1838d62ae21d2505d87f16efc1d9074645571daaefdfd0c14054
RUN apk update && apk add --no-cache openssl ca-certificates curl && \ RUN apk update && apk add --no-cache openssl ca-certificates curl libgcc && \
curl -fsSL -o /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub && \ curl -fsSL -o /etc/apk/keys/sgerrand.rsa.pub https://raw.githubusercontent.com/sgerrand/alpine-pkg-glibc/master/sgerrand.rsa.pub && \
curl -fsSL -o glibc-$GLIBC.apk https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$GLIBC/glibc-$GLIBC.apk && \ curl -fsSL -o glibc-$GLIBC.apk https://github.com/sgerrand/alpine-pkg-glibc/releases/download/$GLIBC/glibc-$GLIBC.apk && \
apk add --no-cache glibc-$GLIBC.apk && \ apk add --no-cache glibc-$GLIBC.apk && \
ln -s /lib/libz.so.1 /usr/glibc-compat/lib/ && \ ln -s /lib/libz.so.1 /usr/glibc-compat/lib/ && \
ln -s /lib/libc.musl-x86_64.so.1 /usr/glibc-compat/lib && \ ln -s /lib/libc.musl-x86_64.so.1 /usr/glibc-compat/lib && \
ln -s /usr/lib/libgcc_s.so.1 /usr/glibc-compat/lib && \
curl -fsSL -o dockerbins.tgz "https://download.docker.com/linux/static/stable/x86_64/docker-17.12.1-ce.tgz" && \ curl -fsSL -o dockerbins.tgz "https://download.docker.com/linux/static/stable/x86_64/docker-17.12.1-ce.tgz" && \
echo "${DOCKERBINS_SHA} dockerbins.tgz" | sha256sum -c - && \ echo "${DOCKERBINS_SHA} dockerbins.tgz" | sha256sum -c - && \
tar xvf dockerbins.tgz docker/docker --strip-components 1 && \ tar xvf dockerbins.tgz docker/docker --strip-components 1 && \

View File

@ -11,12 +11,30 @@
[Org] [Org]
[Org."Core maintainers"] [Org."Core maintainers"]
people = [ people = [
"aanand", "mefyl",
"bfirsh", "mnottale",
"dnephin",
"mnowster",
"shin-", "shin-",
] ]
[Org.Alumni]
people = [
# Aanand Prasad is one of the two creators of the fig project
# which later went on to become docker-compose, and a longtime
# maintainer responsible for several keystone features
"aanand",
# Ben Firshman is also one of the fig creators and contributed
# heavily to the project's design and UX as well as the
# day-to-day maintenance
"bfirsh",
# Mazz Mosley made significant contributions to the project
# in 2015 with solid bugfixes and improved error handling
# among them
"mnowster",
# Daniel Nephin is one of the longest-running maitainers on
# the Compose project, and has contributed several major features
# including muti-file support, variable interpolation, secrets
# emulation and many more
"dnephin",
]
[people] [people]
@ -41,6 +59,16 @@
Email = "dnephin@gmail.com" Email = "dnephin@gmail.com"
GitHub = "dnephin" GitHub = "dnephin"
[people.mefyl]
Name = "Quentin Hocquet"
Email = "quentin.hocquet@docker.com"
GitHub = "mefyl"
[people.mnottale]
Name = "Matthieu Nottale"
Email = "matthieu.nottale@docker.com"
GitHub = "mnottale"
[people.mnowster] [people.mnowster]
Name = "Mazz Mosley" Name = "Mazz Mosley"
Email = "mazz@houseofmnowster.com" Email = "mazz@houseofmnowster.com"

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
__version__ = '1.20.1' __version__ = '1.21.0-rc1'

View File

@ -122,12 +122,14 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False,
) )
with errors.handle_connection_errors(client): with errors.handle_connection_errors(client):
return Project.from_config(project_name, config_data, client) return Project.from_config(
project_name, config_data, client, environment.get('DOCKER_DEFAULT_PLATFORM')
)
def get_project_name(working_dir, project_name=None, environment=None): def get_project_name(working_dir, project_name=None, environment=None):
def normalize_name(name): def normalize_name(name):
return re.sub(r'[^a-z0-9]', '', name.lower()) return re.sub(r'[^-_a-z0-9]', '', name.lower())
if not environment: if not environment:
environment = Environment.from_env_file(working_dir) environment = Environment.from_env_file(working_dir)

View File

@ -254,6 +254,7 @@ class TopLevelCommand(object):
Usage: build [options] [--build-arg key=val...] [SERVICE...] Usage: build [options] [--build-arg key=val...] [SERVICE...]
Options: Options:
--compress Compress the build context using gzip.
--force-rm Always remove intermediate containers. --force-rm Always remove intermediate containers.
--no-cache Do not use cache when building the image. --no-cache Do not use cache when building the image.
--pull Always attempt to pull a newer version of the image. --pull Always attempt to pull a newer version of the image.
@ -277,7 +278,9 @@ class TopLevelCommand(object):
pull=bool(options.get('--pull', False)), pull=bool(options.get('--pull', False)),
force_rm=bool(options.get('--force-rm', False)), force_rm=bool(options.get('--force-rm', False)),
memory=options.get('--memory'), memory=options.get('--memory'),
build_args=build_args) build_args=build_args,
gzip=options.get('--compress', False),
)
def bundle(self, options): def bundle(self, options):
""" """
@ -459,6 +462,7 @@ class TopLevelCommand(object):
instances of a service [default: 1] instances of a service [default: 1]
-e, --env KEY=VAL Set environment variables (can be used multiple times, -e, --env KEY=VAL Set environment variables (can be used multiple times,
not supported in API < 1.25) not supported in API < 1.25)
-w, --workdir DIR Path to workdir directory for this command.
""" """
environment = Environment.from_env_file(self.project_dir) environment = Environment.from_env_file(self.project_dir)
use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI') use_cli = not environment.get_boolean('COMPOSE_INTERACTIVE_NO_CLI')
@ -467,7 +471,12 @@ class TopLevelCommand(object):
detach = options.get('--detach') detach = options.get('--detach')
if options['--env'] and docker.utils.version_lt(self.project.client.api_version, '1.25'): if options['--env'] and docker.utils.version_lt(self.project.client.api_version, '1.25'):
raise UserError("Setting environment for exec is not supported in API < 1.25'") raise UserError("Setting environment for exec is not supported in API < 1.25 (%s)"
% self.project.client.api_version)
if options['--workdir'] and docker.utils.version_lt(self.project.client.api_version, '1.35'):
raise UserError("Setting workdir for exec is not supported in API < 1.35 (%s)"
% self.project.client.api_version)
try: try:
container = service.get_container(number=index) container = service.get_container(number=index)
@ -487,6 +496,7 @@ class TopLevelCommand(object):
"user": options["--user"], "user": options["--user"],
"tty": tty, "tty": tty,
"stdin": True, "stdin": True,
"workdir": options["--workdir"],
} }
if docker.utils.version_gte(self.project.client.api_version, '1.25'): if docker.utils.version_gte(self.project.client.api_version, '1.25'):
@ -704,14 +714,17 @@ class TopLevelCommand(object):
Options: Options:
--ignore-pull-failures Pull what it can and ignores images with pull failures. --ignore-pull-failures Pull what it can and ignores images with pull failures.
--parallel Pull multiple images in parallel. --parallel Deprecated, pull multiple images in parallel (enabled by default).
--no-parallel Disable parallel pulling.
-q, --quiet Pull without printing progress information -q, --quiet Pull without printing progress information
--include-deps Also pull services declared as dependencies --include-deps Also pull services declared as dependencies
""" """
if options.get('--parallel'):
log.warn('--parallel option is deprecated and will be removed in future versions.')
self.project.pull( self.project.pull(
service_names=options['SERVICE'], service_names=options['SERVICE'],
ignore_pull_failures=options.get('--ignore-pull-failures'), ignore_pull_failures=options.get('--ignore-pull-failures'),
parallel_pull=options.get('--parallel'), parallel_pull=not options.get('--no-parallel'),
silent=options.get('--quiet'), silent=options.get('--quiet'),
include_deps=options.get('--include-deps'), include_deps=options.get('--include-deps'),
) )
@ -1408,7 +1421,7 @@ def call_docker(args, dockeropts):
if verify: if verify:
tls_options.append('--tlsverify') tls_options.append('--tlsverify')
if host: if host:
tls_options.extend(['--host', host]) tls_options.extend(['--host', host.lstrip('=')])
args = [executable_path] + tls_options + args args = [executable_path] + tls_options + args
log.debug(" ".join(map(pipes.quote, args))) log.debug(" ".join(map(pipes.quote, args)))
@ -1453,6 +1466,9 @@ def build_exec_command(options, container_id, command):
for env_variable in options["--env"]: for env_variable in options["--env"]:
args += ["--env", env_variable] args += ["--env", env_variable]
if options["--workdir"]:
args += ["--workdir", options["--workdir"]]
args += [container_id] args += [container_id]
args += command args += command
return args return args

View File

@ -2,6 +2,7 @@ from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import functools import functools
import io
import logging import logging
import os import os
import string import string
@ -67,7 +68,10 @@ DOCKER_CONFIG_KEYS = [
'command', 'command',
'cpu_count', 'cpu_count',
'cpu_percent', 'cpu_percent',
'cpu_period',
'cpu_quota', 'cpu_quota',
'cpu_rt_period',
'cpu_rt_runtime',
'cpu_shares', 'cpu_shares',
'cpus', 'cpus',
'cpuset', 'cpuset',
@ -125,11 +129,12 @@ ALLOWED_KEYS = DOCKER_CONFIG_KEYS + [
'container_name', 'container_name',
'credential_spec', 'credential_spec',
'dockerfile', 'dockerfile',
'init',
'log_driver', 'log_driver',
'log_opt', 'log_opt',
'logging', 'logging',
'network_mode', 'network_mode',
'init', 'platform',
'scale', 'scale',
'stop_grace_period', 'stop_grace_period',
] ]
@ -1115,6 +1120,7 @@ def merge_build(output, base, override):
md.merge_scalar('network') md.merge_scalar('network')
md.merge_scalar('target') md.merge_scalar('target')
md.merge_scalar('shm_size') md.merge_scalar('shm_size')
md.merge_scalar('isolation')
md.merge_mapping('args', parse_build_arguments) md.merge_mapping('args', parse_build_arguments)
md.merge_field('cache_from', merge_unique_items_lists, default=[]) md.merge_field('cache_from', merge_unique_items_lists, default=[])
md.merge_mapping('labels', parse_labels) md.merge_mapping('labels', parse_labels)
@ -1428,10 +1434,15 @@ def has_uppercase(name):
return any(char in string.ascii_uppercase for char in name) return any(char in string.ascii_uppercase for char in name)
def load_yaml(filename): def load_yaml(filename, encoding=None):
try: try:
with open(filename, 'r') as fh: with io.open(filename, 'r', encoding=encoding) as fh:
return yaml.safe_load(fh) return yaml.safe_load(fh)
except (IOError, yaml.YAMLError) as e: except (IOError, yaml.YAMLError, UnicodeDecodeError) as e:
if encoding is None:
# Sometimes the user's locale sets an encoding that doesn't match
# the YAML files. Im such cases, retry once with the "default"
# UTF-8 encoding
return load_yaml(filename, encoding='utf-8')
error_name = getattr(e, '__module__', '') + '.' + e.__class__.__name__ error_name = getattr(e, '__module__', '') + '.' + e.__class__.__name__
raise ConfigurationError(u"{}: {}".format(error_name, e)) raise ConfigurationError(u"{}: {}".format(error_name, e))

View File

@ -88,7 +88,8 @@
"context": {"type": "string"}, "context": {"type": "string"},
"dockerfile": {"type": "string"}, "dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"}, "args": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/labels"} "labels": {"$ref": "#/definitions/labels"},
"isolation": {"type": "string"}
}, },
"additionalProperties": false "additionalProperties": false
} }
@ -106,6 +107,7 @@
"container_name": {"type": "string"}, "container_name": {"type": "string"},
"cpu_shares": {"type": ["number", "string"]}, "cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]}, "cpu_quota": {"type": ["number", "string"]},
"cpu_period": {"type": ["number", "string"]},
"cpuset": {"type": "string"}, "cpuset": {"type": "string"},
"depends_on": { "depends_on": {
"oneOf": [ "oneOf": [

View File

@ -90,7 +90,8 @@
"args": {"$ref": "#/definitions/list_or_dict"}, "args": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/labels"}, "labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"}, "cache_from": {"$ref": "#/definitions/list_of_strings"},
"network": {"type": "string"} "network": {"type": "string"},
"isolation": {"type": "string"}
}, },
"additionalProperties": false "additionalProperties": false
} }
@ -110,6 +111,9 @@
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
"cpu_shares": {"type": ["number", "string"]}, "cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]}, "cpu_quota": {"type": ["number", "string"]},
"cpu_period": {"type": ["number", "string"]},
"cpu_rt_period": {"type": ["number", "string"]},
"cpu_rt_runtime": {"type": ["number", "string"]},
"cpus": {"type": "number", "minimum": 0}, "cpus": {"type": "number", "minimum": 0},
"cpuset": {"type": "string"}, "cpuset": {"type": "string"},
"depends_on": { "depends_on": {

View File

@ -93,7 +93,8 @@
"network": {"type": "string"}, "network": {"type": "string"},
"target": {"type": "string"}, "target": {"type": "string"},
"shm_size": {"type": ["integer", "string"]}, "shm_size": {"type": ["integer", "string"]},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"} "extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"isolation": {"type": "string"}
}, },
"additionalProperties": false "additionalProperties": false
} }
@ -113,6 +114,9 @@
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100}, "cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
"cpu_shares": {"type": ["number", "string"]}, "cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]}, "cpu_quota": {"type": ["number", "string"]},
"cpu_period": {"type": ["number", "string"]},
"cpu_rt_period": {"type": ["number", "string"]},
"cpu_rt_runtime": {"type": ["number", "string"]},
"cpus": {"type": "number", "minimum": 0}, "cpus": {"type": "number", "minimum": 0},
"cpuset": {"type": "string"}, "cpuset": {"type": "string"},
"depends_on": { "depends_on": {

View File

@ -0,0 +1,513 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v2.4.json",
"type": "object",
"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
}
},
"patternProperties": {"^x-": {}},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"blkio_config": {
"type": "object",
"properties": {
"device_read_bps": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"device_read_iops": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"device_write_bps": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"device_write_iops": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_limit"}
},
"weight": {"type": "integer"},
"weight_device": {
"type": "array",
"items": {"$ref": "#/definitions/blkio_weight"}
}
},
"additionalProperties": false
},
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"},
"labels": {"$ref": "#/definitions/labels"},
"cache_from": {"$ref": "#/definitions/list_of_strings"},
"network": {"type": "string"},
"target": {"type": "string"},
"shm_size": {"type": ["integer", "string"]},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"isolation": {"type": "string"}
},
"additionalProperties": false
}
]
},
"cap_add": {"$ref": "#/definitions/list_of_strings"},
"cap_drop": {"$ref": "#/definitions/list_of_strings"},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"cpu_count": {"type": "integer", "minimum": 0},
"cpu_percent": {"type": "integer", "minimum": 0, "maximum": 100},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpu_period": {"type": ["number", "string"]},
"cpu_rt_period": {"type": ["number", "string"]},
"cpu_rt_runtime": {"type": ["number", "string"]},
"cpus": {"type": "number", "minimum": 0},
"cpuset": {"type": "string"},
"depends_on": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"type": "object",
"additionalProperties": false,
"properties": {
"condition": {
"type": "string",
"enum": ["service_started", "service_healthy"]
}
},
"required": ["condition"]
}
}
}
]
},
"device_cgroup_rules": {"$ref": "#/definitions/list_of_strings"},
"devices": {"$ref": "#/definitions/list_of_strings"},
"dns_opt": {
"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
},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"external_links": {"$ref": "#/definitions/list_of_strings"},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"group_add": {
"type": "array",
"items": {
"type": ["string", "number"]
},
"uniqueItems": true
},
"healthcheck": {"$ref": "#/definitions/healthcheck"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"init": {"type": ["boolean", "string"]},
"ipc": {"type": "string"},
"isolation": {"type": "string"},
"labels": {"$ref": "#/definitions/labels"},
"links": {"$ref": "#/definitions/list_of_strings"},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {"type": "object"}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"mem_reservation": {"type": ["string", "integer"]},
"mem_swappiness": {"type": "integer"},
"memswap_limit": {"type": ["number", "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"},
"link_local_ips": {"$ref": "#/definitions/list_of_strings"},
"priority": {"type": "number"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"oom_kill_disable": {"type": "boolean"},
"oom_score_adj": {"type": "integer", "minimum": -1000, "maximum": 1000},
"pid": {"type": ["string", "null"]},
"platform": {"type": "string"},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"runtime": {"type": "string"},
"scale": {"type": "integer"},
"security_opt": {"$ref": "#/definitions/list_of_strings"},
"shm_size": {"type": ["number", "string"]},
"sysctls": {"$ref": "#/definitions/list_or_dict"},
"pids_limit": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"},
"stop_grace_period": {"type": "string", "format": "duration"},
"stop_signal": {"type": "string"},
"storage_opt": {"type": "object"},
"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": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"required": ["type"],
"additionalProperties": false,
"properties": {
"type": {"type": "string"},
"source": {"type": "string"},
"target": {"type": "string"},
"read_only": {"type": "boolean"},
"consistency": {"type": "string"},
"bind": {
"type": "object",
"properties": {
"propagation": {"type": "string"}
}
},
"volume": {
"type": "object",
"properties": {
"nocopy": {"type": "boolean"}
}
},
"tmpfs": {
"type": "object",
"properties": {
"size": {"type": ["integer", "string"]}
}
}
}
}
],
"uniqueItems": true
}
},
"volume_driver": {"type": "string"},
"volumes_from": {"$ref": "#/definitions/list_of_strings"},
"working_dir": {"type": "string"}
},
"dependencies": {
"memswap_limit": ["mem_limit"]
},
"additionalProperties": false
},
"healthcheck": {
"id": "#/definitions/healthcheck",
"type": "object",
"additionalProperties": false,
"properties": {
"disable": {"type": "boolean"},
"interval": {"type": "string"},
"retries": {"type": "number"},
"start_period": {"type": "string"},
"test": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"timeout": {"type": "string"}
}
},
"network": {
"id": "#/definitions/network",
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array"
},
"options": {
"type": "object",
"patternProperties": {
"^.+$": {"type": "string"}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
},
"internal": {"type": "boolean"},
"enable_ipv6": {"type": "boolean"},
"labels": {"$ref": "#/definitions/labels"},
"name": {"type": "string"}
},
"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/labels"},
"name": {"type": "string"}
},
"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}
]
},
"labels": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": "string"
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"blkio_limit": {
"type": "object",
"properties": {
"path": {"type": "string"},
"rate": {"type": ["integer", "string"]}
},
"additionalProperties": false
},
"blkio_weight": {
"type": "object",
"properties": {
"path": {"type": "string"},
"weight": {"type": "integer"}
},
"additionalProperties": false
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}

View File

@ -10,6 +10,7 @@ import six
from .errors import ConfigurationError from .errors import ConfigurationError
from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_0 as V2_0
from compose.utils import parse_bytes from compose.utils import parse_bytes
from compose.utils import parse_nanoseconds_int
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -132,9 +133,8 @@ class TemplateWithDefaults(Template):
braced = mo.group('braced') braced = mo.group('braced')
if braced is not None: if braced is not None:
sep = mo.group('sep') sep = mo.group('sep')
result = self.process_braced_group(braced, sep, mapping) if sep:
if result: return self.process_braced_group(braced, sep, mapping)
return result
if named is not None: if named is not None:
val = mapping[named] val = mapping[named]
@ -223,6 +223,12 @@ def bytes_to_int(s):
return v return v
def to_microseconds(v):
if not isinstance(v, six.string_types):
return v
return int(parse_nanoseconds_int(v) / 1000)
class ConversionMap(object): class ConversionMap(object):
map = { map = {
service_path('blkio_config', 'weight'): to_int, service_path('blkio_config', 'weight'): to_int,
@ -230,6 +236,10 @@ class ConversionMap(object):
service_path('build', 'labels', FULL_JOKER): to_str, service_path('build', 'labels', FULL_JOKER): to_str,
service_path('cpus'): to_float, service_path('cpus'): to_float,
service_path('cpu_count'): to_int, service_path('cpu_count'): to_int,
service_path('cpu_quota'): to_microseconds,
service_path('cpu_period'): to_microseconds,
service_path('cpu_rt_period'): to_microseconds,
service_path('cpu_rt_runtime'): to_microseconds,
service_path('configs', 'mode'): to_int, service_path('configs', 'mode'): to_int,
service_path('secrets', 'mode'): to_int, service_path('secrets', 'mode'): to_int,
service_path('healthcheck', 'retries'): to_int, service_path('healthcheck', 'retries'): to_int,

View File

@ -151,9 +151,10 @@ def denormalize_service_dict(service_dict, version, image_digest=None):
service_dict['healthcheck']['start_period'] = serialize_ns_time_value( service_dict['healthcheck']['start_period'] = serialize_ns_time_value(
service_dict['healthcheck']['start_period'] service_dict['healthcheck']['start_period']
) )
if 'ports' in service_dict and version < V3_2:
if 'ports' in service_dict:
service_dict['ports'] = [ service_dict['ports'] = [
p.legacy_repr() if isinstance(p, types.ServicePort) else p p.legacy_repr() if p.external_ip or version < V3_2 else p
for p in service_dict['ports'] for p in service_dict['ports']
] ]
if 'volumes' in service_dict and (version < V2_3 or (version > V3_0 and version < V3_2)): if 'volumes' in service_dict and (version < V2_3 or (version > V3_0 and version < V3_2)):

View File

@ -27,6 +27,7 @@ COMPOSEFILE_V2_0 = ComposeVersion('2.0')
COMPOSEFILE_V2_1 = ComposeVersion('2.1') COMPOSEFILE_V2_1 = ComposeVersion('2.1')
COMPOSEFILE_V2_2 = ComposeVersion('2.2') COMPOSEFILE_V2_2 = ComposeVersion('2.2')
COMPOSEFILE_V2_3 = ComposeVersion('2.3') COMPOSEFILE_V2_3 = ComposeVersion('2.3')
COMPOSEFILE_V2_4 = ComposeVersion('2.4')
COMPOSEFILE_V3_0 = ComposeVersion('3.0') COMPOSEFILE_V3_0 = ComposeVersion('3.0')
COMPOSEFILE_V3_1 = ComposeVersion('3.1') COMPOSEFILE_V3_1 = ComposeVersion('3.1')
@ -42,6 +43,7 @@ API_VERSIONS = {
COMPOSEFILE_V2_1: '1.24', COMPOSEFILE_V2_1: '1.24',
COMPOSEFILE_V2_2: '1.25', COMPOSEFILE_V2_2: '1.25',
COMPOSEFILE_V2_3: '1.30', COMPOSEFILE_V2_3: '1.30',
COMPOSEFILE_V2_4: '1.35',
COMPOSEFILE_V3_0: '1.25', COMPOSEFILE_V3_0: '1.25',
COMPOSEFILE_V3_1: '1.25', COMPOSEFILE_V3_1: '1.25',
COMPOSEFILE_V3_2: '1.25', COMPOSEFILE_V3_2: '1.25',
@ -57,6 +59,7 @@ API_VERSION_TO_ENGINE_VERSION = {
API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0', API_VERSIONS[COMPOSEFILE_V2_1]: '1.12.0',
API_VERSIONS[COMPOSEFILE_V2_2]: '1.13.0', API_VERSIONS[COMPOSEFILE_V2_2]: '1.13.0',
API_VERSIONS[COMPOSEFILE_V2_3]: '17.06.0', API_VERSIONS[COMPOSEFILE_V2_3]: '17.06.0',
API_VERSIONS[COMPOSEFILE_V2_4]: '17.12.0',
API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_0]: '1.13.0',
API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_1]: '1.13.0',
API_VERSIONS[COMPOSEFILE_V3_2]: '1.13.0', API_VERSIONS[COMPOSEFILE_V3_2]: '1.13.0',

View File

@ -42,6 +42,11 @@ class Network(object):
def ensure(self): def ensure(self):
if self.external: if self.external:
if self.driver == 'overlay':
# Swarm nodes do not register overlay networks that were
# created on a different node unless they're in use.
# See docker/compose#4399
return
try: try:
self.inspect() self.inspect()
log.debug( log.debug(

View File

@ -77,7 +77,7 @@ class Project(object):
return labels return labels
@classmethod @classmethod
def from_config(cls, name, config_data, client): def from_config(cls, name, config_data, client, default_platform=None):
""" """
Construct a Project from a config.Config object. Construct a Project from a config.Config object.
""" """
@ -128,6 +128,7 @@ class Project(object):
volumes_from=volumes_from, volumes_from=volumes_from,
secrets=secrets, secrets=secrets,
pid_mode=pid_mode, pid_mode=pid_mode,
platform=service_dict.pop('platform', default_platform),
**service_dict) **service_dict)
) )
@ -366,10 +367,10 @@ class Project(object):
return containers return containers
def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, memory=None, def build(self, service_names=None, no_cache=False, pull=False, force_rm=False, memory=None,
build_args=None): build_args=None, gzip=False):
for service in self.get_services(service_names): for service in self.get_services(service_names):
if service.can_be_built(): if service.can_be_built():
service.build(no_cache, pull, force_rm, memory, build_args) service.build(no_cache, pull, force_rm, memory, build_args, gzip)
else: else:
log.info('%s uses an image, skipping' % service.name) log.info('%s uses an image, skipping' % service.name)
@ -551,7 +552,7 @@ class Project(object):
services, services,
pull_service, pull_service,
operator.attrgetter('name'), operator.attrgetter('name'),
'Pulling', not silent and 'Pulling' or None,
limit=5, limit=5,
) )
if len(errors): if len(errors):

View File

@ -62,7 +62,10 @@ HOST_CONFIG_KEYS = [
'cgroup_parent', 'cgroup_parent',
'cpu_count', 'cpu_count',
'cpu_percent', 'cpu_percent',
'cpu_period',
'cpu_quota', 'cpu_quota',
'cpu_rt_period',
'cpu_rt_runtime',
'cpu_shares', 'cpu_shares',
'cpus', 'cpus',
'cpuset', 'cpuset',
@ -682,15 +685,27 @@ class Service(object):
# TODO: this would benefit from github.com/docker/docker/pull/14699 # TODO: this would benefit from github.com/docker/docker/pull/14699
# to remove the need to inspect every container # to remove the need to inspect every container
def _next_container_number(self, one_off=False): def _next_container_number(self, one_off=False):
containers = filter(None, [ containers = self._fetch_containers(
Container.from_ps(self.client, container) all=True,
for container in self.client.containers( filters={'label': self.labels(one_off=one_off)}
all=True, )
filters={'label': self.labels(one_off=one_off)})
])
numbers = [c.number for c in containers] numbers = [c.number for c in containers]
return 1 if not numbers else max(numbers) + 1 return 1 if not numbers else max(numbers) + 1
def _fetch_containers(self, **fetch_options):
# Account for containers that might have been removed since we fetched
# the list.
def soft_inspect(container):
try:
return Container.from_id(self.client, container['Id'])
except NotFound:
return None
return filter(None, [
soft_inspect(container)
for container in self.client.containers(**fetch_options)
])
def _get_aliases(self, network, container=None): def _get_aliases(self, network, container=None):
return list( return list(
{self.name} | {self.name} |
@ -947,6 +962,9 @@ class Service(object):
device_write_iops=blkio_config.get('device_write_iops'), device_write_iops=blkio_config.get('device_write_iops'),
mounts=options.get('mounts'), mounts=options.get('mounts'),
device_cgroup_rules=options.get('device_cgroup_rules'), device_cgroup_rules=options.get('device_cgroup_rules'),
cpu_period=options.get('cpu_period'),
cpu_rt_period=options.get('cpu_rt_period'),
cpu_rt_runtime=options.get('cpu_rt_runtime'),
) )
def get_secret_volumes(self): def get_secret_volumes(self):
@ -961,7 +979,8 @@ class Service(object):
return [build_spec(secret) for secret in self.secrets] return [build_spec(secret) for secret in self.secrets]
def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_args_override=None): def build(self, no_cache=False, pull=False, force_rm=False, memory=None, build_args_override=None,
gzip=False):
log.info('Building %s' % self.name) log.info('Building %s' % self.name)
build_opts = self.options.get('build', {}) build_opts = self.options.get('build', {})
@ -979,6 +998,12 @@ class Service(object):
if not six.PY3 and not IS_WINDOWS_PLATFORM: if not six.PY3 and not IS_WINDOWS_PLATFORM:
path = path.encode('utf8') path = path.encode('utf8')
platform = self.options.get('platform')
if platform and version_lt(self.client.api_version, '1.35'):
raise OperationFailedError(
'Impossible to perform platform-targeted builds for API version < 1.35'
)
build_output = self.client.build( build_output = self.client.build(
path=path, path=path,
tag=self.image_name, tag=self.image_name,
@ -997,6 +1022,9 @@ class Service(object):
container_limits={ container_limits={
'memory': parse_bytes(memory) if memory else None 'memory': parse_bytes(memory) if memory else None
}, },
gzip=gzip,
isolation=build_opts.get('isolation', self.options.get('isolation', None)),
platform=platform,
) )
try: try:
@ -1098,11 +1126,20 @@ class Service(object):
return return
repo, tag, separator = parse_repository_tag(self.options['image']) repo, tag, separator = parse_repository_tag(self.options['image'])
tag = tag or 'latest' kwargs = {
'tag': tag or 'latest',
'stream': True,
'platform': self.options.get('platform'),
}
if not silent: if not silent:
log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag)) log.info('Pulling %s (%s%s%s)...' % (self.name, repo, separator, tag))
if kwargs['platform'] and version_lt(self.client.api_version, '1.35'):
raise OperationFailedError(
'Impossible to perform platform-targeted builds for API version < 1.35'
)
try: try:
output = self.client.pull(repo, tag=tag, stream=True) output = self.client.pull(repo, **kwargs)
if silent: if silent:
with open(os.devnull, 'w') as devnull: with open(os.devnull, 'w') as devnull:
return progress_stream.get_digest_from_pull( return progress_stream.get_digest_from_pull(

View File

@ -124,19 +124,7 @@ class ProjectVolumes(object):
) )
volume.create() volume.create()
else: else:
driver = volume.inspect()['Driver'] check_remote_volume_config(volume.inspect(), volume)
if volume.driver is not None and driver != volume.driver:
raise ConfigurationError(
'Configuration for volume {0} specifies driver '
'{1}, but a volume with the same name uses a '
'different driver ({3}). If you wish to use the '
'new configuration, please remove the existing '
'volume "{2}" first:\n'
'$ docker volume rm {2}'.format(
volume.name, volume.driver, volume.full_name,
volume.inspect()['Driver']
)
)
except NotFound: except NotFound:
raise ConfigurationError( raise ConfigurationError(
'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver) 'Volume %s specifies nonexistent driver %s' % (volume.name, volume.driver)
@ -152,3 +140,43 @@ class ProjectVolumes(object):
else: else:
volume_spec.source = self.volumes[volume_spec.source].full_name volume_spec.source = self.volumes[volume_spec.source].full_name
return volume_spec return volume_spec
class VolumeConfigChangedError(ConfigurationError):
def __init__(self, local, property_name, local_value, remote_value):
super(VolumeConfigChangedError, self).__init__(
'Configuration for volume {vol_name} specifies {property_name} '
'{local_value}, but a volume with the same name uses a different '
'{property_name} ({remote_value}). If you wish to use the new '
'configuration, please remove the existing volume "{full_name}" '
'first:\n$ docker volume rm {full_name}'.format(
vol_name=local.name, property_name=property_name,
local_value=local_value, remote_value=remote_value,
full_name=local.full_name
)
)
def check_remote_volume_config(remote, local):
if local.driver and remote.get('Driver') != local.driver:
raise VolumeConfigChangedError(local, 'driver', local.driver, remote.get('Driver'))
local_opts = local.driver_opts or {}
remote_opts = remote.get('Options') or {}
for k in set.union(set(remote_opts.keys()), set(local_opts.keys())):
if k.startswith('com.docker.'): # These options are set internally
continue
if remote_opts.get(k) != local_opts.get(k):
raise VolumeConfigChangedError(
local, '"{}" driver_opt'.format(k), local_opts.get(k), remote_opts.get(k),
)
local_labels = local.labels or {}
remote_labels = remote.get('Labels') or {}
for k in set.union(set(remote_labels.keys()), set(local_labels.keys())):
if k.startswith('com.docker.'): # We are only interested in user-specified labels
continue
if remote_labels.get(k) != local_labels.get(k):
log.warn(
'Volume {}: label "{}" has changed. It may need to be'
' recreated.'.format(local.name, k)
)

View File

@ -88,7 +88,7 @@ __docker-compose_get_services() {
shift shift
[[ $kind =~ (stopped|all) ]] && args=($args -a) [[ $kind =~ (stopped|all) ]] && args=($args -a)
lines=(${(f)"$(_call_program commands docker $docker_options ps $args)"}) lines=(${(f)"$(_call_program commands docker $docker_options ps --format 'table' $args)"})
services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"}) services=(${(f)"$(_call_program commands docker-compose 2>/dev/null $compose_options ps -q)"})
# Parse header line to find columns # Parse header line to find columns

View File

@ -0,0 +1,116 @@
# Self-elevate the script if required
# http://www.expta.com/2017/03/how-to-self-elevate-powershell-script.html
If (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
If ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
$CommandLine = "-File `"" + $MyInvocation.MyCommand.Path + "`" " + $MyInvocation.UnboundArguments
Start-Process -FilePath PowerShell.exe -Verb Runas -ArgumentList $CommandLine
Exit
}
}
$SectionSeparator = "--------------------------------------------------"
# Update docker-compose if required
Function UpdateDockerCompose() {
Write-Host "Updating docker-compose if required..."
Write-Host $SectionSeparator
# Find the installed docker-compose.exe location
Try {
$DockerComposePath = Get-Command docker-compose.exe -ErrorAction Stop | `
Select-Object -First 1 -ExpandProperty Definition
}
Catch {
Write-Host "Error: Could not find path to docker-compose.exe" `
-ForegroundColor Red
Return $false
}
# Prefer/enable TLS 1.2
# https://stackoverflow.com/a/48030563/153079
[Net.ServicePointManager]::SecurityProtocol = "tls12, tls11, tls"
# Query for the latest release version
Try {
$URI = "https://api.github.com/repos/docker/compose/releases/latest"
$LatestComposeVersion = [System.Version](Invoke-RestMethod -Method Get -Uri $URI).tag_name
}
Catch {
Write-Host "Error: Query for the latest docker-compose release version failed" `
-ForegroundColor Red
Return $false
}
# Check the installed version and compare with latest release
$UpdateDockerCompose = $false
Try {
$InstalledComposeVersion = `
[System.Version]((docker-compose.exe version --short) | Out-String)
If ($InstalledComposeVersion -eq $LatestComposeVersion) {
Write-Host ("Installed docker-compose version ({0}) same as latest ({1})." `
-f $InstalledComposeVersion.ToString(), $LatestComposeVersion.ToString())
}
ElseIf ($InstalledComposeVersion -lt $LatestComposeVersion) {
Write-Host ("Installed docker-compose version ({0}) older than latest ({1})." `
-f $InstalledComposeVersion.ToString(), $LatestComposeVersion.ToString())
$UpdateDockerCompose = $true
}
Else {
Write-Host ("Installed docker-compose version ({0}) newer than latest ({1})." `
-f $InstalledComposeVersion.ToString(), $LatestComposeVersion.ToString()) `
-ForegroundColor Yellow
}
}
Catch {
Write-Host `
"Warning: Couldn't get docker-compose version, assuming an update is required..." `
-ForegroundColor Yellow
$UpdateDockerCompose = $true
}
If (-Not $UpdateDockerCompose) {
# Nothing to do!
Return $false
}
# Download the latest version of docker-compose.exe
Try {
$RemoteFileName = "docker-compose-Windows-x86_64.exe"
$URI = ("https://github.com/docker/compose/releases/download/{0}/{1}" `
-f $LatestComposeVersion.ToString(), $RemoteFileName)
Invoke-WebRequest -UseBasicParsing -Uri $URI `
-OutFile $DockerComposePath
Return $true
}
Catch {
Write-Host ("Error: Failed to download the latest version of docker-compose`n{0}" `
-f $_.Exception.Message) -ForegroundColor Red
Return $false
}
Return $false
}
If (UpdateDockerCompose) {
Write-Host "Updated to latest-version of docker-compose, running update again to verify.`n"
If (UpdateDockerCompose) {
Write-Host "Error: Should not have updated twice." -ForegroundColor Red
}
}
# Assuming elevation popped up a new powershell window, pause so the user can see what happened
# https://stackoverflow.com/a/22362868/153079
Function Pause ($Message = "Press any key to continue . . . ") {
If ((Test-Path variable:psISE) -and $psISE) {
$Shell = New-Object -ComObject "WScript.Shell"
$Shell.Popup("Click OK to continue.", 0, "Script Paused", 0)
}
Else {
Write-Host "`n$SectionSeparator"
Write-Host -NoNewline $Message
[void][System.Console]::ReadKey($true)
Write-Host
}
}
Pause

View File

@ -42,6 +42,11 @@ exe = EXE(pyz,
'compose/config/config_schema_v2.3.json', 'compose/config/config_schema_v2.3.json',
'DATA' 'DATA'
), ),
(
'compose/config/config_schema_v2.4.json',
'compose/config/config_schema_v2.4.json',
'DATA'
),
( (
'compose/config/config_schema_v3.0.json', 'compose/config/config_schema_v3.0.json',
'compose/config/config_schema_v3.0.json', 'compose/config/config_schema_v3.0.json',

50
docs/issue_template.md Normal file
View File

@ -0,0 +1,50 @@
<!--
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"
```
```
Output of "docker version"
```
```
Output of "docker-compose config"
```
## Steps to reproduce the issue
1.
2.
3.
### Observed result
### Expected result
### Stacktrace / full error message
```
(if applicable)
```
## Additional information
OS version / distribution, `docker-compose` install method, etc.

View File

@ -0,0 +1,13 @@
<!--
Welcome to the docker-compose issue tracker, and thank you for your interest
in contributing to the project! Please make sure you've read the guidelines
in CONTRIBUTING.md before submitting your pull request. Contributions that
do not comply and contributions with failing tests will not be reviewed!
-->
<!-- Please make sure an issue describing the problem the PR is trying to
solve exists, or create it before submitting a PR. The maintainers will
validate if the issue should be addressed or if it is out of scope for the
project.
-->
Resolves #

View File

@ -2,7 +2,7 @@ backports.ssl-match-hostname==3.5.0.1; python_version < '3'
cached-property==1.3.0 cached-property==1.3.0
certifi==2017.4.17 certifi==2017.4.17
chardet==3.0.4 chardet==3.0.4
docker==3.1.4 docker==3.2.1
docker-pycreds==0.2.1 docker-pycreds==0.2.1
dockerpty==0.4.1 dockerpty==0.4.1
docopt==0.6.2 docopt==0.6.2

View File

@ -15,7 +15,7 @@
set -e set -e
VERSION="1.20.1" VERSION="1.21.0-rc1"
IMAGE="docker/compose:$VERSION" IMAGE="docker/compose:$VERSION"

View File

@ -73,6 +73,11 @@ class Version(namedtuple('_Version', 'major minor patch rc edition')):
return '.'.join(map(str, self[:3])) + edition + rc return '.'.join(map(str, self[:3])) + edition + rc
BLACKLIST = [ # List of versions known to be broken and should not be used
Version.parse('18.03.0-ce-rc2'),
]
def group_versions(versions): def group_versions(versions):
"""Group versions by `major.minor` releases. """Group versions by `major.minor` releases.
@ -117,7 +122,9 @@ def get_default(versions):
def get_versions(tags): def get_versions(tags):
for tag in tags: for tag in tags:
try: try:
yield Version.parse(tag['name']) v = Version.parse(tag['name'])
if v not in BLACKLIST:
yield v
except ValueError: except ValueError:
print("Skipping invalid tag: {name}".format(**tag), file=sys.stderr) print("Skipping invalid tag: {name}".format(**tag), file=sys.stderr)

View File

@ -36,7 +36,7 @@ install_requires = [
'requests >= 2.6.1, != 2.11.0, != 2.12.2, != 2.18.0, < 2.19', 'requests >= 2.6.1, != 2.11.0, != 2.12.2, != 2.18.0, < 2.19',
'texttable >= 0.9.0, < 0.10', 'texttable >= 0.9.0, < 0.10',
'websocket-client >= 0.32.0, < 1.0', 'websocket-client >= 0.32.0, < 1.0',
'docker >= 3.1.4, < 4.0', 'docker >= 3.2.1, < 4.0',
'dockerpty >= 0.4.1, < 0.5', 'dockerpty >= 0.4.1, < 0.5',
'six >= 1.3.0, < 2', 'six >= 1.3.0, < 2',
'jsonschema >= 2.5.1, < 3', 'jsonschema >= 2.5.1, < 3',

View File

@ -177,6 +177,13 @@ class CLITestCase(DockerClientTestCase):
returncode=0 returncode=0
) )
def test_shorthand_host_opt_interactive(self):
self.dispatch(
['-H={0}'.format(os.environ.get('DOCKER_HOST', 'unix://')),
'run', 'another', 'ls'],
returncode=0
)
def test_host_not_reachable(self): def test_host_not_reachable(self):
result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1) result = self.dispatch(['-H=tcp://doesnotexist:8000', 'ps'], returncode=1)
assert "Couldn't connect to Docker daemon" in result.stderr assert "Couldn't connect to Docker daemon" in result.stderr
@ -491,16 +498,16 @@ class CLITestCase(DockerClientTestCase):
def test_ps(self): def test_ps(self):
self.project.get_service('simple').create_container() self.project.get_service('simple').create_container()
result = self.dispatch(['ps']) result = self.dispatch(['ps'])
assert 'simplecomposefile_simple_1' in result.stdout assert 'simple-composefile_simple_1' in result.stdout
def test_ps_default_composefile(self): def test_ps_default_composefile(self):
self.base_dir = 'tests/fixtures/multiple-composefiles' self.base_dir = 'tests/fixtures/multiple-composefiles'
self.dispatch(['up', '-d']) self.dispatch(['up', '-d'])
result = self.dispatch(['ps']) result = self.dispatch(['ps'])
assert 'multiplecomposefiles_simple_1' in result.stdout assert 'multiple-composefiles_simple_1' in result.stdout
assert 'multiplecomposefiles_another_1' in result.stdout assert 'multiple-composefiles_another_1' in result.stdout
assert 'multiplecomposefiles_yetanother_1' not in result.stdout assert 'multiple-composefiles_yetanother_1' not in result.stdout
def test_ps_alternate_composefile(self): def test_ps_alternate_composefile(self):
config_path = os.path.abspath( config_path = os.path.abspath(
@ -511,9 +518,9 @@ class CLITestCase(DockerClientTestCase):
self.dispatch(['-f', 'compose2.yml', 'up', '-d']) self.dispatch(['-f', 'compose2.yml', 'up', '-d'])
result = self.dispatch(['-f', 'compose2.yml', 'ps']) result = self.dispatch(['-f', 'compose2.yml', 'ps'])
assert 'multiplecomposefiles_simple_1' not in result.stdout assert 'multiple-composefiles_simple_1' not in result.stdout
assert 'multiplecomposefiles_another_1' not in result.stdout assert 'multiple-composefiles_another_1' not in result.stdout
assert 'multiplecomposefiles_yetanother_1' in result.stdout assert 'multiple-composefiles_yetanother_1' in result.stdout
def test_ps_services_filter_option(self): def test_ps_services_filter_option(self):
self.base_dir = 'tests/fixtures/ps-services-filter' self.base_dir = 'tests/fixtures/ps-services-filter'
@ -545,13 +552,11 @@ class CLITestCase(DockerClientTestCase):
def test_pull(self): def test_pull(self):
result = self.dispatch(['pull']) result = self.dispatch(['pull'])
assert sorted(result.stderr.split('\n'))[1:] == [ assert 'Pulling simple' in result.stderr
'Pulling another (busybox:latest)...', assert 'Pulling another' in result.stderr
'Pulling simple (busybox:latest)...',
]
def test_pull_with_digest(self): def test_pull_with_digest(self):
result = self.dispatch(['-f', 'digest.yml', 'pull']) result = self.dispatch(['-f', 'digest.yml', 'pull', '--no-parallel'])
assert 'Pulling simple (busybox:latest)...' in result.stderr assert 'Pulling simple (busybox:latest)...' in result.stderr
assert ('Pulling digest (busybox@' assert ('Pulling digest (busybox@'
@ -561,7 +566,7 @@ class CLITestCase(DockerClientTestCase):
def test_pull_with_ignore_pull_failures(self): def test_pull_with_ignore_pull_failures(self):
result = self.dispatch([ result = self.dispatch([
'-f', 'ignore-pull-failures.yml', '-f', 'ignore-pull-failures.yml',
'pull', '--ignore-pull-failures'] 'pull', '--ignore-pull-failures', '--no-parallel']
) )
assert 'Pulling simple (busybox:latest)...' in result.stderr assert 'Pulling simple (busybox:latest)...' in result.stderr
@ -576,7 +581,7 @@ class CLITestCase(DockerClientTestCase):
def test_pull_with_parallel_failure(self): def test_pull_with_parallel_failure(self):
result = self.dispatch([ result = self.dispatch([
'-f', 'ignore-pull-failures.yml', 'pull', '--parallel'], '-f', 'ignore-pull-failures.yml', 'pull'],
returncode=1 returncode=1
) )
@ -593,14 +598,14 @@ class CLITestCase(DockerClientTestCase):
def test_pull_with_no_deps(self): def test_pull_with_no_deps(self):
self.base_dir = 'tests/fixtures/links-composefile' self.base_dir = 'tests/fixtures/links-composefile'
result = self.dispatch(['pull', 'web']) result = self.dispatch(['pull', '--no-parallel', 'web'])
assert sorted(result.stderr.split('\n'))[1:] == [ assert sorted(result.stderr.split('\n'))[1:] == [
'Pulling web (busybox:latest)...', 'Pulling web (busybox:latest)...',
] ]
def test_pull_with_include_deps(self): def test_pull_with_include_deps(self):
self.base_dir = 'tests/fixtures/links-composefile' self.base_dir = 'tests/fixtures/links-composefile'
result = self.dispatch(['pull', '--include-deps', 'web']) result = self.dispatch(['pull', '--no-parallel', '--include-deps', 'web'])
assert sorted(result.stderr.split('\n'))[1:] == [ assert sorted(result.stderr.split('\n'))[1:] == [
'Pulling db (busybox:latest)...', 'Pulling db (busybox:latest)...',
'Pulling web (busybox:latest)...', 'Pulling web (busybox:latest)...',
@ -902,18 +907,18 @@ class CLITestCase(DockerClientTestCase):
assert len(self.project.containers(one_off=OneOffFilter.only, stopped=True)) == 2 assert len(self.project.containers(one_off=OneOffFilter.only, stopped=True)) == 2
result = self.dispatch(['down', '--rmi=local', '--volumes']) result = self.dispatch(['down', '--rmi=local', '--volumes'])
assert 'Stopping v2full_web_1' in result.stderr assert 'Stopping v2-full_web_1' in result.stderr
assert 'Stopping v2full_other_1' in result.stderr assert 'Stopping v2-full_other_1' in result.stderr
assert 'Stopping v2full_web_run_2' in result.stderr assert 'Stopping v2-full_web_run_2' in result.stderr
assert 'Removing v2full_web_1' in result.stderr assert 'Removing v2-full_web_1' in result.stderr
assert 'Removing v2full_other_1' in result.stderr assert 'Removing v2-full_other_1' in result.stderr
assert 'Removing v2full_web_run_1' in result.stderr assert 'Removing v2-full_web_run_1' in result.stderr
assert 'Removing v2full_web_run_2' in result.stderr assert 'Removing v2-full_web_run_2' in result.stderr
assert 'Removing volume v2full_data' in result.stderr assert 'Removing volume v2-full_data' in result.stderr
assert 'Removing image v2full_web' in result.stderr assert 'Removing image v2-full_web' in result.stderr
assert 'Removing image busybox' not in result.stderr assert 'Removing image busybox' not in result.stderr
assert 'Removing network v2full_default' in result.stderr assert 'Removing network v2-full_default' in result.stderr
assert 'Removing network v2full_front' in result.stderr assert 'Removing network v2-full_front' in result.stderr
def test_down_timeout(self): def test_down_timeout(self):
self.dispatch(['up', '-d'], None) self.dispatch(['up', '-d'], None)
@ -1559,6 +1564,16 @@ class CLITestCase(DockerClientTestCase):
assert stdout == "operator\n" assert stdout == "operator\n"
assert stderr == "" assert stderr == ""
@v3_only()
def test_exec_workdir(self):
self.base_dir = 'tests/fixtures/links-composefile'
os.environ['COMPOSE_API_VERSION'] = '1.35'
self.dispatch(['up', '-d', 'console'])
assert len(self.project.containers()) == 1
stdout, stderr = self.dispatch(['exec', '-T', '--workdir', '/etc', 'console', 'ls'])
assert 'passwd' in stdout
@v2_2_only() @v2_2_only()
def test_exec_service_with_environment_overridden(self): def test_exec_service_with_environment_overridden(self):
name = 'service' name = 'service'
@ -1990,39 +2005,39 @@ class CLITestCase(DockerClientTestCase):
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'simplecomposefile_simple_run_1', 'simple-composefile_simple_run_1',
'running')) 'running'))
os.kill(proc.pid, signal.SIGINT) os.kill(proc.pid, signal.SIGINT)
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'simplecomposefile_simple_run_1', 'simple-composefile_simple_run_1',
'exited')) 'exited'))
def test_run_handles_sigterm(self): def test_run_handles_sigterm(self):
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'simplecomposefile_simple_run_1', 'simple-composefile_simple_run_1',
'running')) 'running'))
os.kill(proc.pid, signal.SIGTERM) os.kill(proc.pid, signal.SIGTERM)
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'simplecomposefile_simple_run_1', 'simple-composefile_simple_run_1',
'exited')) 'exited'))
def test_run_handles_sighup(self): def test_run_handles_sighup(self):
proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top']) proc = start_process(self.base_dir, ['run', '-T', 'simple', 'top'])
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'simplecomposefile_simple_run_1', 'simple-composefile_simple_run_1',
'running')) 'running'))
os.kill(proc.pid, signal.SIGHUP) os.kill(proc.pid, signal.SIGHUP)
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'simplecomposefile_simple_run_1', 'simple-composefile_simple_run_1',
'exited')) 'exited'))
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
@ -2224,7 +2239,7 @@ class CLITestCase(DockerClientTestCase):
self.dispatch(['up', '-d', 'another']) self.dispatch(['up', '-d', 'another'])
wait_on_condition(ContainerStateCondition( wait_on_condition(ContainerStateCondition(
self.project.client, self.project.client,
'logscomposefile_another_1', 'logs-composefile_another_1',
'exited')) 'exited'))
self.dispatch(['kill', 'simple']) self.dispatch(['kill', 'simple'])
@ -2233,8 +2248,8 @@ class CLITestCase(DockerClientTestCase):
assert 'hello' in result.stdout assert 'hello' in result.stdout
assert 'test' in result.stdout assert 'test' in result.stdout
assert 'logscomposefile_another_1 exited with code 0' in result.stdout assert 'logs-composefile_another_1 exited with code 0' in result.stdout
assert 'logscomposefile_simple_1 exited with code 137' in result.stdout assert 'logs-composefile_simple_1 exited with code 137' in result.stdout
def test_logs_default(self): def test_logs_default(self):
self.base_dir = 'tests/fixtures/logs-composefile' self.base_dir = 'tests/fixtures/logs-composefile'
@ -2481,7 +2496,7 @@ class CLITestCase(DockerClientTestCase):
container, = self.project.containers() container, = self.project.containers()
expected_template = ' container {} {}' expected_template = ' container {} {}'
expected_meta_info = ['image=busybox:latest', 'name=simplecomposefile_simple_1'] expected_meta_info = ['image=busybox:latest', 'name=simple-composefile_simple_1']
assert expected_template.format('create', container.id) in lines[0] assert expected_template.format('create', container.id) in lines[0]
assert expected_template.format('start', container.id) in lines[1] assert expected_template.format('start', container.id) in lines[1]
@ -2601,13 +2616,13 @@ class CLITestCase(DockerClientTestCase):
result = wait_on_process(proc, returncode=1) result = wait_on_process(proc, returncode=1)
assert 'exitcodefrom_another_1 exited with code 1' in result.stdout assert 'exit-code-from_another_1 exited with code 1' in result.stdout
def test_images(self): def test_images(self):
self.project.get_service('simple').create_container() self.project.get_service('simple').create_container()
result = self.dispatch(['images']) result = self.dispatch(['images'])
assert 'busybox' in result.stdout assert 'busybox' in result.stdout
assert 'simplecomposefile_simple_1' in result.stdout assert 'simple-composefile_simple_1' in result.stdout
def test_images_default_composefile(self): def test_images_default_composefile(self):
self.base_dir = 'tests/fixtures/multiple-composefiles' self.base_dir = 'tests/fixtures/multiple-composefiles'
@ -2615,8 +2630,8 @@ class CLITestCase(DockerClientTestCase):
result = self.dispatch(['images']) result = self.dispatch(['images'])
assert 'busybox' in result.stdout assert 'busybox' in result.stdout
assert 'multiplecomposefiles_another_1' in result.stdout assert 'multiple-composefiles_another_1' in result.stdout
assert 'multiplecomposefiles_simple_1' in result.stdout assert 'multiple-composefiles_simple_1' in result.stdout
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_images_tagless_image(self): def test_images_tagless_image(self):
@ -2636,7 +2651,7 @@ class CLITestCase(DockerClientTestCase):
self.project.get_service('foo').create_container() self.project.get_service('foo').create_container()
result = self.dispatch(['images']) result = self.dispatch(['images'])
assert '<none>' in result.stdout assert '<none>' in result.stdout
assert 'taglessimage_foo_1' in result.stdout assert 'tagless-image_foo_1' in result.stdout
def test_up_with_override_yaml(self): def test_up_with_override_yaml(self):
self.base_dir = 'tests/fixtures/override-yaml-files' self.base_dir = 'tests/fixtures/override-yaml-files'

View File

@ -1,7 +1,10 @@
from __future__ import absolute_import from __future__ import absolute_import
from __future__ import unicode_literals from __future__ import unicode_literals
import pytest
from .testcases import DockerClientTestCase from .testcases import DockerClientTestCase
from compose.config.errors import ConfigurationError
from compose.const import LABEL_NETWORK from compose.const import LABEL_NETWORK
from compose.const import LABEL_PROJECT from compose.const import LABEL_PROJECT
from compose.network import Network from compose.network import Network
@ -15,3 +18,20 @@ class NetworkTest(DockerClientTestCase):
labels = net_data['Labels'] labels = net_data['Labels']
assert labels[LABEL_NETWORK] == net.name assert labels[LABEL_NETWORK] == net.name
assert labels[LABEL_PROJECT] == net.project assert labels[LABEL_PROJECT] == net.project
def test_network_external_default_ensure(self):
net = Network(
self.client, 'composetest', 'foonet',
external=True
)
with pytest.raises(ConfigurationError):
net.ensure()
def test_network_external_overlay_ensure(self):
net = Network(
self.client, 'composetest', 'foonet',
driver='overlay', external=True
)
assert net.ensure() is None

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import json import json
import os import os
import random import random
import shutil
import tempfile import tempfile
import py import py
@ -1537,6 +1538,52 @@ class ProjectTest(DockerClientTestCase):
vol_name vol_name
) in str(e.value) ) in str(e.value)
@v2_only()
@no_cluster('inspect volume by name defect on Swarm Classic')
def test_initialize_volumes_updated_driver_opts(self):
vol_name = '{0:x}'.format(random.getrandbits(32))
full_vol_name = 'composetest_{0}'.format(vol_name)
tmpdir = tempfile.mkdtemp(prefix='compose_test_')
self.addCleanup(shutil.rmtree, tmpdir)
driver_opts = {'o': 'bind', 'device': tmpdir, 'type': 'none'}
config_data = build_config(
version=V2_0,
services=[{
'name': 'web',
'image': 'busybox:latest',
'command': 'top'
}],
volumes={
vol_name: {
'driver': 'local',
'driver_opts': driver_opts
}
},
)
project = Project.from_config(
name='composetest',
config_data=config_data, client=self.client
)
project.volumes.initialize()
volume_data = self.get_volume_data(full_vol_name)
assert volume_data['Name'].split('/')[-1] == full_vol_name
assert volume_data['Driver'] == 'local'
assert volume_data['Options'] == driver_opts
driver_opts['device'] = '/opt/data/localdata'
project = Project.from_config(
name='composetest',
config_data=config_data,
client=self.client
)
with pytest.raises(config.ConfigurationError) as e:
project.volumes.initialize()
assert 'Configuration for volume {0} specifies "device" driver_opt {1}'.format(
vol_name, driver_opts['device']
) in str(e.value)
@v2_only() @v2_only()
def test_initialize_volumes_updated_blank_driver(self): def test_initialize_volumes_updated_blank_driver(self):
vol_name = '{0:x}'.format(random.getrandbits(32)) vol_name = '{0:x}'.format(random.getrandbits(32))

View File

@ -122,10 +122,19 @@ class ServiceTest(DockerClientTestCase):
assert container.get('HostConfig.CpuShares') == 73 assert container.get('HostConfig.CpuShares') == 73
def test_create_container_with_cpu_quota(self): def test_create_container_with_cpu_quota(self):
service = self.create_service('db', cpu_quota=40000) service = self.create_service('db', cpu_quota=40000, cpu_period=150000)
container = service.create_container() container = service.create_container()
container.start() container.start()
assert container.get('HostConfig.CpuQuota') == 40000 assert container.get('HostConfig.CpuQuota') == 40000
assert container.get('HostConfig.CpuPeriod') == 150000
@pytest.mark.xfail(raises=OperationFailedError, reason='not supported by kernel')
def test_create_container_with_cpu_rt(self):
service = self.create_service('db', cpu_rt_runtime=40000, cpu_rt_period=150000)
container = service.create_container()
container.start()
assert container.get('HostConfig.CpuRealtimeRuntime') == 40000
assert container.get('HostConfig.CpuRealtimePeriod') == 150000
@v2_2_only() @v2_2_only()
def test_create_container_with_cpu_count(self): def test_create_container_with_cpu_count(self):
@ -1096,6 +1105,38 @@ class ServiceTest(DockerClientTestCase):
service.build() service.build()
assert service.image() assert service.image()
def test_build_with_gzip(self):
base_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, base_dir)
with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
f.write('\n'.join([
'FROM busybox',
'COPY . /src',
'RUN cat /src/hello.txt'
]))
with open(os.path.join(base_dir, 'hello.txt'), 'w') as f:
f.write('hello world\n')
service = self.create_service('build_gzip', build={
'context': text_type(base_dir),
})
service.build(gzip=True)
assert service.image()
@v2_1_only()
def test_build_with_isolation(self):
base_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, base_dir)
with open(os.path.join(base_dir, 'Dockerfile'), 'w') as f:
f.write('FROM busybox\n')
service = self.create_service('build_isolation', build={
'context': text_type(base_dir),
'isolation': 'default',
})
service.build()
assert service.image()
def test_start_container_stays_unprivileged(self): def test_start_container_stays_unprivileged(self):
service = self.create_service('web') service = self.create_service('web')
container = create_and_start_container(service).inspect() container = create_and_start_container(service).inspect()

View File

@ -154,3 +154,11 @@ class TestCallDocker(object):
assert fake_call.call_args[0][0] == [ assert fake_call.call_args[0][0] == [
'docker', '--host', 'tcp://mydocker.net:2333', 'ps' 'docker', '--host', 'tcp://mydocker.net:2333', 'ps'
] ]
def test_with_host_option_shorthand_equal(self):
with mock.patch('subprocess.call') as fake_call:
call_docker(['ps'], {'--host': '=tcp://mydocker.net:2333'})
assert fake_call.call_args[0][0] == [
'docker', '--host', 'tcp://mydocker.net:2333', 'ps'
]

View File

@ -30,12 +30,12 @@ class CLITestCase(unittest.TestCase):
test_dir = py._path.local.LocalPath('tests/fixtures/simple-composefile') test_dir = py._path.local.LocalPath('tests/fixtures/simple-composefile')
with test_dir.as_cwd(): with test_dir.as_cwd():
project_name = get_project_name('.') project_name = get_project_name('.')
assert 'simplecomposefile' == project_name assert 'simple-composefile' == project_name
def test_project_name_with_explicit_base_dir(self): def test_project_name_with_explicit_base_dir(self):
base_dir = 'tests/fixtures/simple-composefile' base_dir = 'tests/fixtures/simple-composefile'
project_name = get_project_name(base_dir) project_name = get_project_name(base_dir)
assert 'simplecomposefile' == project_name assert 'simple-composefile' == project_name
def test_project_name_with_explicit_uppercase_base_dir(self): def test_project_name_with_explicit_uppercase_base_dir(self):
base_dir = 'tests/fixtures/UpperCaseDir' base_dir = 'tests/fixtures/UpperCaseDir'
@ -45,7 +45,7 @@ class CLITestCase(unittest.TestCase):
def test_project_name_with_explicit_project_name(self): def test_project_name_with_explicit_project_name(self):
name = 'explicit-project-name' name = 'explicit-project-name'
project_name = get_project_name(None, project_name=name) project_name = get_project_name(None, project_name=name)
assert 'explicitprojectname' == project_name assert 'explicit-project-name' == project_name
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_project_name_from_environment_new_var(self): def test_project_name_from_environment_new_var(self):
@ -59,7 +59,7 @@ class CLITestCase(unittest.TestCase):
with mock.patch.dict(os.environ): with mock.patch.dict(os.environ):
os.environ['COMPOSE_PROJECT_NAME'] = '' os.environ['COMPOSE_PROJECT_NAME'] = ''
project_name = get_project_name(base_dir) project_name = get_project_name(base_dir)
assert 'simplecomposefile' == project_name assert 'simple-composefile' == project_name
@mock.patch.dict(os.environ) @mock.patch.dict(os.environ)
def test_project_name_with_environment_file(self): def test_project_name_with_environment_file(self):
@ -80,7 +80,7 @@ class CLITestCase(unittest.TestCase):
def test_get_project(self): def test_get_project(self):
base_dir = 'tests/fixtures/longer-filename-composefile' base_dir = 'tests/fixtures/longer-filename-composefile'
project = get_project(base_dir) project = get_project(base_dir)
assert project.name == 'longerfilenamecomposefile' assert project.name == 'longer-filename-composefile'
assert project.client assert project.client
assert project.services assert project.services

View File

@ -3,6 +3,7 @@ from __future__ import absolute_import
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import codecs
import os import os
import shutil import shutil
import tempfile import tempfile
@ -1623,6 +1624,21 @@ class ConfigTest(unittest.TestCase):
assert 'line 3, column 32' in exc.exconly() assert 'line 3, column 32' in exc.exconly()
def test_load_yaml_with_bom(self):
tmpdir = py.test.ensuretemp('bom_yaml')
self.addCleanup(tmpdir.remove)
bom_yaml = tmpdir.join('docker-compose.yml')
with codecs.open(str(bom_yaml), 'w', encoding='utf-8') as f:
f.write('''\ufeff
version: '2.3'
volumes:
park_bom:
''')
assert config.load_yaml(str(bom_yaml)) == {
'version': '2.3',
'volumes': {'park_bom': None}
}
def test_validate_extra_hosts_invalid(self): def test_validate_extra_hosts_invalid(self):
with pytest.raises(ConfigurationError) as exc: with pytest.raises(ConfigurationError) as exc:
config.load(build_config_details({ config.load(build_config_details({
@ -4927,6 +4943,18 @@ class SerializeTest(unittest.TestCase):
serialized_config = yaml.load(serialize_config(config_dict)) serialized_config = yaml.load(serialize_config(config_dict))
assert '8080:80/tcp' in serialized_config['services']['web']['ports'] assert '8080:80/tcp' in serialized_config['services']['web']['ports']
def test_serialize_ports_with_ext_ip(self):
config_dict = config.Config(version=V3_5, services=[
{
'ports': [types.ServicePort('80', '8080', None, None, '127.0.0.1')],
'image': 'alpine',
'name': 'web'
}
], volumes={}, networks={}, secrets={}, configs={})
serialized_config = yaml.load(serialize_config(config_dict))
assert '127.0.0.1:8080:80/tcp' in serialized_config['services']['web']['ports']
def test_serialize_configs(self): def test_serialize_configs(self):
service_dict = { service_dict = {
'image': 'example/web', 'image': 'example/web',

View File

@ -420,3 +420,15 @@ def test_interpolate_unicode_values():
interpol("$FOO") == '十六夜 咲夜' interpol("$FOO") == '十六夜 咲夜'
interpol("${BAR}") == '十六夜 咲夜' interpol("${BAR}") == '十六夜 咲夜'
def test_interpolate_no_fallthrough():
# Test regression on docker/compose#5829
variable_mapping = {
'TEST:-': 'hello',
'TEST-': 'hello',
}
interpol = Interpolator(TemplateWithDefaults, variable_mapping).interpolate
assert interpol('${TEST:-}') == ''
assert interpol('${TEST-}') == ''

View File

@ -13,6 +13,7 @@ from compose.config.config import Config
from compose.config.types import VolumeFromSpec from compose.config.types import VolumeFromSpec
from compose.const import COMPOSEFILE_V1 as V1 from compose.const import COMPOSEFILE_V1 as V1
from compose.const import COMPOSEFILE_V2_0 as V2_0 from compose.const import COMPOSEFILE_V2_0 as V2_0
from compose.const import COMPOSEFILE_V2_4 as V2_4
from compose.const import LABEL_SERVICE from compose.const import LABEL_SERVICE
from compose.container import Container from compose.container import Container
from compose.project import NoSuchService from compose.project import NoSuchService
@ -561,3 +562,29 @@ class ProjectTest(unittest.TestCase):
def test_no_such_service_unicode(self): def test_no_such_service_unicode(self):
assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜' assert NoSuchService('十六夜 咲夜'.encode('utf-8')).msg == 'No such service: 十六夜 咲夜'
assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜' assert NoSuchService('十六夜 咲夜').msg == 'No such service: 十六夜 咲夜'
def test_project_platform_value(self):
service_config = {
'name': 'web',
'image': 'busybox:latest',
}
config_data = Config(
version=V2_4, services=[service_config], networks={}, volumes={}, secrets=None, configs=None
)
project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
assert project.get_service('web').options.get('platform') is None
project = Project.from_config(
name='test', client=self.mock_client, config_data=config_data, default_platform='windows'
)
assert project.get_service('web').options.get('platform') == 'windows'
service_config['platform'] = 'linux/s390x'
project = Project.from_config(name='test', client=self.mock_client, config_data=config_data)
assert project.get_service('web').options.get('platform') == 'linux/s390x'
project = Project.from_config(
name='test', client=self.mock_client, config_data=config_data, default_platform='windows'
)
assert project.get_service('web').options.get('platform') == 'linux/s390x'

View File

@ -5,6 +5,7 @@ import docker
import pytest import pytest
from docker.constants import DEFAULT_DOCKER_API_VERSION from docker.constants import DEFAULT_DOCKER_API_VERSION
from docker.errors import APIError from docker.errors import APIError
from docker.errors import NotFound
from .. import mock from .. import mock
from .. import unittest from .. import unittest
@ -20,6 +21,7 @@ from compose.const import LABEL_PROJECT
from compose.const import LABEL_SERVICE from compose.const import LABEL_SERVICE
from compose.const import SECRETS_PATH from compose.const import SECRETS_PATH
from compose.container import Container from compose.container import Container
from compose.errors import OperationFailedError
from compose.parallel import ParallelStreamWriter from compose.parallel import ParallelStreamWriter
from compose.project import OneOffFilter from compose.project import OneOffFilter
from compose.service import build_ulimits from compose.service import build_ulimits
@ -399,7 +401,8 @@ class ServiceTest(unittest.TestCase):
self.mock_client.pull.assert_called_once_with( self.mock_client.pull.assert_called_once_with(
'someimage', 'someimage',
tag='sometag', tag='sometag',
stream=True) stream=True,
platform=None)
mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...') mock_log.info.assert_called_once_with('Pulling foo (someimage:sometag)...')
def test_pull_image_no_tag(self): def test_pull_image_no_tag(self):
@ -408,7 +411,8 @@ class ServiceTest(unittest.TestCase):
self.mock_client.pull.assert_called_once_with( self.mock_client.pull.assert_called_once_with(
'ababab', 'ababab',
tag='latest', tag='latest',
stream=True) stream=True,
platform=None)
@mock.patch('compose.service.log', autospec=True) @mock.patch('compose.service.log', autospec=True)
def test_pull_image_digest(self, mock_log): def test_pull_image_digest(self, mock_log):
@ -417,9 +421,30 @@ class ServiceTest(unittest.TestCase):
self.mock_client.pull.assert_called_once_with( self.mock_client.pull.assert_called_once_with(
'someimage', 'someimage',
tag='sha256:1234', tag='sha256:1234',
stream=True) stream=True,
platform=None)
mock_log.info.assert_called_once_with('Pulling foo (someimage@sha256:1234)...') mock_log.info.assert_called_once_with('Pulling foo (someimage@sha256:1234)...')
@mock.patch('compose.service.log', autospec=True)
def test_pull_image_with_platform(self, mock_log):
self.mock_client.api_version = '1.35'
service = Service(
'foo', client=self.mock_client, image='someimage:sometag', platform='windows/x86_64'
)
service.pull()
assert self.mock_client.pull.call_count == 1
call_args = self.mock_client.pull.call_args
assert call_args[1]['platform'] == 'windows/x86_64'
@mock.patch('compose.service.log', autospec=True)
def test_pull_image_with_platform_unsupported_api(self, mock_log):
self.mock_client.api_version = '1.33'
service = Service(
'foo', client=self.mock_client, image='someimage:sometag', platform='linux/arm'
)
with pytest.raises(OperationFailedError):
service.pull()
@mock.patch('compose.service.Container', autospec=True) @mock.patch('compose.service.Container', autospec=True)
def test_recreate_container(self, _): def test_recreate_container(self, _):
mock_container = mock.create_autospec(Container) mock_container = mock.create_autospec(Container)
@ -471,23 +496,8 @@ class ServiceTest(unittest.TestCase):
_, args, _ = mock_log.warn.mock_calls[0] _, args, _ = mock_log.warn.mock_calls[0]
assert 'was built because it did not already exist' in args[0] assert 'was built because it did not already exist' in args[0]
self.mock_client.build.assert_called_once_with( assert self.mock_client.build.call_count == 1
tag='default_foo', self.mock_client.build.call_args[1]['tag'] == 'default_foo'
dockerfile=None,
path='.',
pull=False,
forcerm=False,
nocache=False,
rm=True,
buildargs={},
labels=None,
cache_from=None,
network_mode=None,
target=None,
shmsize=None,
extra_hosts=None,
container_limits={'memory': None},
)
def test_ensure_image_exists_no_build(self): def test_ensure_image_exists_no_build(self):
service = Service('foo', client=self.mock_client, build={'context': '.'}) service = Service('foo', client=self.mock_client, build={'context': '.'})
@ -513,23 +523,8 @@ class ServiceTest(unittest.TestCase):
service.ensure_image_exists(do_build=BuildAction.force) service.ensure_image_exists(do_build=BuildAction.force)
assert not mock_log.warn.called assert not mock_log.warn.called
self.mock_client.build.assert_called_once_with( assert self.mock_client.build.call_count == 1
tag='default_foo', self.mock_client.build.call_args[1]['tag'] == 'default_foo'
dockerfile=None,
path='.',
pull=False,
forcerm=False,
nocache=False,
rm=True,
buildargs={},
labels=None,
cache_from=None,
network_mode=None,
target=None,
shmsize=None,
extra_hosts=None,
container_limits={'memory': None},
)
def test_build_does_not_pull(self): def test_build_does_not_pull(self):
self.mock_client.build.return_value = [ self.mock_client.build.return_value = [
@ -542,6 +537,19 @@ class ServiceTest(unittest.TestCase):
assert self.mock_client.build.call_count == 1 assert self.mock_client.build.call_count == 1
assert not self.mock_client.build.call_args[1]['pull'] assert not self.mock_client.build.call_args[1]['pull']
def test_build_does_with_platform(self):
self.mock_client.api_version = '1.35'
self.mock_client.build.return_value = [
b'{"stream": "Successfully built 12345"}',
]
service = Service('foo', client=self.mock_client, build={'context': '.'}, platform='linux')
service.build()
assert self.mock_client.build.call_count == 1
call_args = self.mock_client.build.call_args
assert call_args[1]['platform'] == 'linux'
def test_build_with_override_build_args(self): def test_build_with_override_build_args(self):
self.mock_client.build.return_value = [ self.mock_client.build.return_value = [
b'{"stream": "Successfully built 12345"}', b'{"stream": "Successfully built 12345"}',
@ -559,6 +567,33 @@ class ServiceTest(unittest.TestCase):
assert called_build_args['arg1'] == build_args['arg1'] assert called_build_args['arg1'] == build_args['arg1']
assert called_build_args['arg2'] == 'arg2' assert called_build_args['arg2'] == 'arg2'
def test_build_with_isolation_from_service_config(self):
self.mock_client.build.return_value = [
b'{"stream": "Successfully built 12345"}',
]
service = Service('foo', client=self.mock_client, build={'context': '.'}, isolation='hyperv')
service.build()
assert self.mock_client.build.call_count == 1
called_build_args = self.mock_client.build.call_args[1]
assert called_build_args['isolation'] == 'hyperv'
def test_build_isolation_from_build_override_service_config(self):
self.mock_client.build.return_value = [
b'{"stream": "Successfully built 12345"}',
]
service = Service(
'foo', client=self.mock_client, build={'context': '.', 'isolation': 'default'},
isolation='hyperv'
)
service.build()
assert self.mock_client.build.call_count == 1
called_build_args = self.mock_client.build.call_args[1]
assert called_build_args['isolation'] == 'default'
def test_config_dict(self): def test_config_dict(self):
self.mock_client.inspect_image.return_value = {'Id': 'abcd'} self.mock_client.inspect_image.return_value = {'Id': 'abcd'}
service = Service( service = Service(
@ -888,6 +923,38 @@ class ServiceTest(unittest.TestCase):
'ftp_proxy': override_options['environment']['FTP_PROXY'], 'ftp_proxy': override_options['environment']['FTP_PROXY'],
})) }))
def test_create_when_removed_containers_are_listed(self):
# This is aimed at simulating a race between the API call to list the
# containers, and the ones to inspect each of the listed containers.
# It can happen that a container has been removed after we listed it.
# containers() returns a container that is about to be removed
self.mock_client.containers.return_value = [
{'Id': 'rm_cont_id', 'Name': 'rm_cont', 'Image': 'img_id'},
]
# inspect_container() will raise a NotFound when trying to inspect
# rm_cont_id, which at this point has been removed
def inspect(name):
if name == 'rm_cont_id':
raise NotFound(message='Not Found')
if name == 'new_cont_id':
return {'Id': 'new_cont_id'}
raise NotImplementedError("incomplete mock")
self.mock_client.inspect_container.side_effect = inspect
self.mock_client.inspect_image.return_value = {'Id': 'imageid'}
self.mock_client.create_container.return_value = {'Id': 'new_cont_id'}
# We should nonetheless be able to create a new container
service = Service('foo', client=self.mock_client)
assert service.create_container().id == 'new_cont_id'
class TestServiceNetwork(unittest.TestCase): class TestServiceNetwork(unittest.TestCase):
def setUp(self): def setUp(self):