Merge pull request #4407 from docker/bump-1.10.1

Bump 1.10.1
This commit is contained in:
Joffrey F 2017-02-01 14:09:38 -08:00 committed by GitHub
commit 5a0ef19ee0
14 changed files with 182 additions and 16 deletions

View File

@ -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)
------------------- -------------------

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.10.0' __version__ = '1.10.1'

View File

@ -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__

View File

@ -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):

View File

@ -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"}
} },
} "additionalProperties": false
},
"labels": {"$ref": "#/definitions/list_or_dict"}
}, },
"labels": {"$ref": "#/definitions/list_or_dict"},
"additionalProperties": false "additionalProperties": false
}, },

View File

@ -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

View File

@ -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(

View File

@ -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'

View File

@ -15,7 +15,7 @@
set -e set -e
VERSION="1.10.0" VERSION="1.10.1"
IMAGE="docker/compose:$VERSION" IMAGE="docker/compose:$VERSION"

View File

@ -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',

View File

@ -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,
}, },

View File

@ -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'

View File

@ -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
return V2_1 elif version_lt(version, '1.13'):
return V2_1
return V3_0
def build_version_required_decorator(ignored_versions): def build_version_required_decorator(ignored_versions):

View File

@ -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'