mirror of https://github.com/docker/compose.git
commit
5a0ef19ee0
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -1,6 +1,29 @@
|
||||||
Change log
|
Change log
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
1.10.1 (2017-02-01)
|
||||||
|
------------------
|
||||||
|
|
||||||
|
### Bugfixes
|
||||||
|
|
||||||
|
- Fixed an issue where presence of older versions of the docker-py
|
||||||
|
package would cause unexpected crashes while running Compose
|
||||||
|
|
||||||
|
- Fixed an issue where healthcheck dependencies would be lost when
|
||||||
|
using multiple compose files for a project
|
||||||
|
|
||||||
|
- Fixed a few issues that made the output of the `config` command
|
||||||
|
invalid
|
||||||
|
|
||||||
|
- Fixed an issue where adding volume labels to v3 Compose files would
|
||||||
|
result in an error
|
||||||
|
|
||||||
|
- Fixed an issue on Windows where build context paths containing unicode
|
||||||
|
characters were being improperly encoded
|
||||||
|
|
||||||
|
- Fixed a bug where Compose would occasionally crash while streaming logs
|
||||||
|
when containers would stop or restart
|
||||||
|
|
||||||
1.10.0 (2017-01-18)
|
1.10.0 (2017-01-18)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|
|
@ -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.10.0'
|
__version__ = '1.10.1'
|
||||||
|
|
|
@ -14,6 +14,30 @@ from distutils.spawn import find_executable
|
||||||
from inspect import getdoc
|
from inspect import getdoc
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
|
|
||||||
|
|
||||||
|
# Attempt to detect https://github.com/docker/compose/issues/4344
|
||||||
|
try:
|
||||||
|
# A regular import statement causes PyInstaller to freak out while
|
||||||
|
# trying to load pip. This way it is simply ignored.
|
||||||
|
pip = __import__('pip')
|
||||||
|
pip_packages = pip.get_installed_distributions()
|
||||||
|
if 'docker-py' in [pkg.project_name for pkg in pip_packages]:
|
||||||
|
from .colors import red
|
||||||
|
print(
|
||||||
|
red('ERROR:'),
|
||||||
|
"Dependency conflict: an older version of the 'docker-py' package "
|
||||||
|
"is polluting the namespace. "
|
||||||
|
"Run the following command to remedy the issue:\n"
|
||||||
|
"pip uninstall docker docker-py; pip install docker",
|
||||||
|
file=sys.stderr
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
except ImportError:
|
||||||
|
# pip is not available, which indicates it's probably the binary
|
||||||
|
# distribution of Compose which is not affected
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
from . import errors
|
from . import errors
|
||||||
from . import signals
|
from . import signals
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
|
|
@ -818,6 +818,7 @@ def merge_service_dicts(base, override, version):
|
||||||
md.merge_mapping('ulimits', parse_ulimits)
|
md.merge_mapping('ulimits', parse_ulimits)
|
||||||
md.merge_mapping('networks', parse_networks)
|
md.merge_mapping('networks', parse_networks)
|
||||||
md.merge_mapping('sysctls', parse_sysctls)
|
md.merge_mapping('sysctls', parse_sysctls)
|
||||||
|
md.merge_mapping('depends_on', parse_depends_on)
|
||||||
md.merge_sequence('links', ServiceLink.parse)
|
md.merge_sequence('links', ServiceLink.parse)
|
||||||
|
|
||||||
for field in ['volumes', 'devices']:
|
for field in ['volumes', 'devices']:
|
||||||
|
@ -825,7 +826,7 @@ def merge_service_dicts(base, override, version):
|
||||||
|
|
||||||
for field in [
|
for field in [
|
||||||
'ports', 'cap_add', 'cap_drop', 'expose', 'external_links',
|
'ports', 'cap_add', 'cap_drop', 'expose', 'external_links',
|
||||||
'security_opt', 'volumes_from', 'depends_on',
|
'security_opt', 'volumes_from',
|
||||||
]:
|
]:
|
||||||
md.merge_field(field, merge_unique_items_lists, default=[])
|
md.merge_field(field, merge_unique_items_lists, default=[])
|
||||||
|
|
||||||
|
@ -920,6 +921,9 @@ parse_environment = functools.partial(parse_dict_or_list, split_env, 'environmen
|
||||||
parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels')
|
parse_labels = functools.partial(parse_dict_or_list, split_kv, 'labels')
|
||||||
parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks')
|
parse_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks')
|
||||||
parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls')
|
parse_sysctls = functools.partial(parse_dict_or_list, split_kv, 'sysctls')
|
||||||
|
parse_depends_on = functools.partial(
|
||||||
|
parse_dict_or_list, lambda k: (k, {'condition': 'service_started'}), 'depends_on'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_ulimits(ulimits):
|
def parse_ulimits(ulimits):
|
||||||
|
|
|
@ -308,6 +308,7 @@
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"internal": {"type": "boolean"},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"}
|
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -328,10 +329,11 @@
|
||||||
"type": ["boolean", "object"],
|
"type": ["boolean", "object"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string"}
|
"name": {"type": "string"}
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||||
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,25 @@ def serialize_config(config):
|
||||||
width=80)
|
width=80)
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_ns_time_value(value):
|
||||||
|
result = (value, 'ns')
|
||||||
|
table = [
|
||||||
|
(1000., 'us'),
|
||||||
|
(1000., 'ms'),
|
||||||
|
(1000., 's'),
|
||||||
|
(60., 'm'),
|
||||||
|
(60., 'h')
|
||||||
|
]
|
||||||
|
for stage in table:
|
||||||
|
tmp = value / stage[0]
|
||||||
|
if tmp == int(value / stage[0]):
|
||||||
|
value = tmp
|
||||||
|
result = (int(value), stage[1])
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
return '{0}{1}'.format(*result)
|
||||||
|
|
||||||
|
|
||||||
def denormalize_service_dict(service_dict, version):
|
def denormalize_service_dict(service_dict, version):
|
||||||
service_dict = service_dict.copy()
|
service_dict = service_dict.copy()
|
||||||
|
|
||||||
|
@ -68,4 +87,14 @@ def denormalize_service_dict(service_dict, version):
|
||||||
svc for svc in service_dict['depends_on'].keys()
|
svc for svc in service_dict['depends_on'].keys()
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if 'healthcheck' in service_dict:
|
||||||
|
if 'interval' in service_dict['healthcheck']:
|
||||||
|
service_dict['healthcheck']['interval'] = serialize_ns_time_value(
|
||||||
|
service_dict['healthcheck']['interval']
|
||||||
|
)
|
||||||
|
if 'timeout' in service_dict['healthcheck']:
|
||||||
|
service_dict['healthcheck']['timeout'] = serialize_ns_time_value(
|
||||||
|
service_dict['healthcheck']['timeout']
|
||||||
|
)
|
||||||
|
|
||||||
return service_dict
|
return service_dict
|
||||||
|
|
|
@ -22,6 +22,7 @@ from .config import DOCKER_CONFIG_KEYS
|
||||||
from .config import merge_environment
|
from .config import merge_environment
|
||||||
from .config.types import VolumeSpec
|
from .config.types import VolumeSpec
|
||||||
from .const import DEFAULT_TIMEOUT
|
from .const import DEFAULT_TIMEOUT
|
||||||
|
from .const import IS_WINDOWS_PLATFORM
|
||||||
from .const import LABEL_CONFIG_HASH
|
from .const import LABEL_CONFIG_HASH
|
||||||
from .const import LABEL_CONTAINER_NUMBER
|
from .const import LABEL_CONTAINER_NUMBER
|
||||||
from .const import LABEL_ONE_OFF
|
from .const import LABEL_ONE_OFF
|
||||||
|
@ -769,9 +770,9 @@ class Service(object):
|
||||||
|
|
||||||
build_opts = self.options.get('build', {})
|
build_opts = self.options.get('build', {})
|
||||||
path = build_opts.get('context')
|
path = build_opts.get('context')
|
||||||
# python2 os.path() doesn't support unicode, so we need to encode it to
|
# python2 os.stat() doesn't support unicode on some UNIX, so we
|
||||||
# a byte string
|
# encode it to a bytestring to be safe
|
||||||
if not six.PY3:
|
if not six.PY3 and not IS_WINDOWS_PLATFORM:
|
||||||
path = path.encode('utf8')
|
path = path.encode('utf8')
|
||||||
|
|
||||||
build_output = self.client.build(
|
build_output = self.client.build(
|
||||||
|
|
|
@ -2,7 +2,7 @@ PyYAML==3.11
|
||||||
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
backports.ssl-match-hostname==3.5.0.1; python_version < '3'
|
||||||
cached-property==1.2.0
|
cached-property==1.2.0
|
||||||
colorama==0.3.7
|
colorama==0.3.7
|
||||||
docker==2.0.1
|
docker==2.0.2
|
||||||
dockerpty==0.4.1
|
dockerpty==0.4.1
|
||||||
docopt==0.6.1
|
docopt==0.6.1
|
||||||
enum34==1.0.4; python_version < '3.4'
|
enum34==1.0.4; python_version < '3.4'
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
VERSION="1.10.0"
|
VERSION="1.10.1"
|
||||||
IMAGE="docker/compose:$VERSION"
|
IMAGE="docker/compose:$VERSION"
|
||||||
|
|
||||||
|
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -37,7 +37,7 @@ install_requires = [
|
||||||
'requests >= 2.6.1, != 2.11.0, < 2.12',
|
'requests >= 2.6.1, != 2.11.0, < 2.12',
|
||||||
'texttable >= 0.8.1, < 0.9',
|
'texttable >= 0.8.1, < 0.9',
|
||||||
'websocket-client >= 0.32.0, < 1.0',
|
'websocket-client >= 0.32.0, < 1.0',
|
||||||
'docker >= 2.0.1, < 3.0',
|
'docker >= 2.0.2, < 3.0',
|
||||||
'dockerpty >= 0.4.1, < 0.5',
|
'dockerpty >= 0.4.1, < 0.5',
|
||||||
'six >= 1.3.0, < 2',
|
'six >= 1.3.0, < 2',
|
||||||
'jsonschema >= 2.5.1, < 3',
|
'jsonschema >= 2.5.1, < 3',
|
||||||
|
|
|
@ -295,7 +295,13 @@ class CLITestCase(DockerClientTestCase):
|
||||||
assert yaml.load(result.stdout) == {
|
assert yaml.load(result.stdout) == {
|
||||||
'version': '3.0',
|
'version': '3.0',
|
||||||
'networks': {},
|
'networks': {},
|
||||||
'volumes': {},
|
'volumes': {
|
||||||
|
'foobar': {
|
||||||
|
'labels': {
|
||||||
|
'com.docker.compose.test': 'true',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
'services': {
|
'services': {
|
||||||
'web': {
|
'web': {
|
||||||
'image': 'busybox',
|
'image': 'busybox',
|
||||||
|
@ -333,8 +339,8 @@ class CLITestCase(DockerClientTestCase):
|
||||||
|
|
||||||
'healthcheck': {
|
'healthcheck': {
|
||||||
'test': 'cat /etc/passwd',
|
'test': 'cat /etc/passwd',
|
||||||
'interval': 10000000000,
|
'interval': '10s',
|
||||||
'timeout': 1000000000,
|
'timeout': '1s',
|
||||||
'retries': 5,
|
'retries': 5,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -35,3 +35,7 @@ services:
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
stop_grace_period: 20s
|
stop_grace_period: 20s
|
||||||
|
volumes:
|
||||||
|
foobar:
|
||||||
|
labels:
|
||||||
|
com.docker.compose.test: 'true'
|
||||||
|
|
|
@ -13,6 +13,7 @@ from compose.config.config import resolve_environment
|
||||||
from compose.config.config import V1
|
from compose.config.config import V1
|
||||||
from compose.config.config import V2_0
|
from compose.config.config import V2_0
|
||||||
from compose.config.config import V2_1
|
from compose.config.config import V2_1
|
||||||
|
from compose.config.config import V3_0
|
||||||
from compose.config.environment import Environment
|
from compose.config.environment import Environment
|
||||||
from compose.const import API_VERSIONS
|
from compose.const import API_VERSIONS
|
||||||
from compose.const import LABEL_PROJECT
|
from compose.const import LABEL_PROJECT
|
||||||
|
@ -36,13 +37,15 @@ def get_links(container):
|
||||||
|
|
||||||
def engine_max_version():
|
def engine_max_version():
|
||||||
if 'DOCKER_VERSION' not in os.environ:
|
if 'DOCKER_VERSION' not in os.environ:
|
||||||
return V2_1
|
return V3_0
|
||||||
version = os.environ['DOCKER_VERSION'].partition('-')[0]
|
version = os.environ['DOCKER_VERSION'].partition('-')[0]
|
||||||
if version_lt(version, '1.10'):
|
if version_lt(version, '1.10'):
|
||||||
return V1
|
return V1
|
||||||
elif version_lt(version, '1.12'):
|
elif version_lt(version, '1.12'):
|
||||||
return V2_0
|
return V2_0
|
||||||
|
elif version_lt(version, '1.13'):
|
||||||
return V2_1
|
return V2_1
|
||||||
|
return V3_0
|
||||||
|
|
||||||
|
|
||||||
def build_version_required_decorator(ignored_versions):
|
def build_version_required_decorator(ignored_versions):
|
||||||
|
|
|
@ -23,6 +23,7 @@ from compose.config.environment import Environment
|
||||||
from compose.config.errors import ConfigurationError
|
from compose.config.errors import ConfigurationError
|
||||||
from compose.config.errors import VERSION_EXPLANATION
|
from compose.config.errors import VERSION_EXPLANATION
|
||||||
from compose.config.serialize import denormalize_service_dict
|
from compose.config.serialize import denormalize_service_dict
|
||||||
|
from compose.config.serialize import serialize_ns_time_value
|
||||||
from compose.config.types import VolumeSpec
|
from compose.config.types import VolumeSpec
|
||||||
from compose.const import IS_WINDOWS_PLATFORM
|
from compose.const import IS_WINDOWS_PLATFORM
|
||||||
from compose.utils import nanoseconds_from_time_seconds
|
from compose.utils import nanoseconds_from_time_seconds
|
||||||
|
@ -1713,6 +1714,40 @@ class ConfigTest(unittest.TestCase):
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def test_merge_depends_on_no_override(self):
|
||||||
|
base = {
|
||||||
|
'image': 'busybox',
|
||||||
|
'depends_on': {
|
||||||
|
'app1': {'condition': 'service_started'},
|
||||||
|
'app2': {'condition': 'service_healthy'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override = {}
|
||||||
|
actual = config.merge_service_dicts(base, override, V2_1)
|
||||||
|
assert actual == base
|
||||||
|
|
||||||
|
def test_merge_depends_on_mixed_syntax(self):
|
||||||
|
base = {
|
||||||
|
'image': 'busybox',
|
||||||
|
'depends_on': {
|
||||||
|
'app1': {'condition': 'service_started'},
|
||||||
|
'app2': {'condition': 'service_healthy'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override = {
|
||||||
|
'depends_on': ['app3']
|
||||||
|
}
|
||||||
|
|
||||||
|
actual = config.merge_service_dicts(base, override, V2_1)
|
||||||
|
assert actual == {
|
||||||
|
'image': 'busybox',
|
||||||
|
'depends_on': {
|
||||||
|
'app1': {'condition': 'service_started'},
|
||||||
|
'app2': {'condition': 'service_healthy'},
|
||||||
|
'app3': {'condition': 'service_started'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def test_external_volume_config(self):
|
def test_external_volume_config(self):
|
||||||
config_details = build_config_details({
|
config_details = build_config_details({
|
||||||
'version': '2',
|
'version': '2',
|
||||||
|
@ -3300,3 +3335,38 @@ class SerializeTest(unittest.TestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
assert denormalize_service_dict(service_dict, V2_1) == service_dict
|
assert denormalize_service_dict(service_dict, V2_1) == service_dict
|
||||||
|
|
||||||
|
def test_serialize_time(self):
|
||||||
|
data = {
|
||||||
|
9: '9ns',
|
||||||
|
9000: '9us',
|
||||||
|
9000000: '9ms',
|
||||||
|
90000000: '90ms',
|
||||||
|
900000000: '900ms',
|
||||||
|
999999999: '999999999ns',
|
||||||
|
1000000000: '1s',
|
||||||
|
60000000000: '1m',
|
||||||
|
60000000001: '60000000001ns',
|
||||||
|
9000000000000: '150m',
|
||||||
|
90000000000000: '25h',
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v in data.items():
|
||||||
|
assert serialize_ns_time_value(k) == v
|
||||||
|
|
||||||
|
def test_denormalize_healthcheck(self):
|
||||||
|
service_dict = {
|
||||||
|
'image': 'test',
|
||||||
|
'healthcheck': {
|
||||||
|
'test': 'exit 1',
|
||||||
|
'interval': '1m40s',
|
||||||
|
'timeout': '30s',
|
||||||
|
'retries': 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processed_service = config.process_service(config.ServiceConfig(
|
||||||
|
'.', 'test', 'test', service_dict
|
||||||
|
))
|
||||||
|
denormalized_service = denormalize_service_dict(processed_service, V2_1)
|
||||||
|
assert denormalized_service['healthcheck']['interval'] == '100s'
|
||||||
|
assert denormalized_service['healthcheck']['timeout'] == '30s'
|
||||||
|
|
Loading…
Reference in New Issue