Ver Fonte

Merge pull request #4407 from docker/bump-1.10.1

Bump 1.10.1
Joffrey F há 8 anos atrás
pai
commit
5a0ef19ee0

+ 23 - 0
CHANGELOG.md

@@ -1,6 +1,29 @@
 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 - 1
compose/__init__.py

@@ -1,4 +1,4 @@
 from __future__ import absolute_import
 from __future__ import unicode_literals
 
-__version__ = '1.10.0'
+__version__ = '1.10.1'

+ 24 - 0
compose/cli/main.py

@@ -14,6 +14,30 @@ from distutils.spawn import find_executable
 from inspect import getdoc
 from operator import attrgetter
 
+
+# Attempt to detect https://github.com/docker/compose/issues/4344
+try:
+    # A regular import statement causes PyInstaller to freak out while
+    # trying to load pip. This way it is simply ignored.
+    pip = __import__('pip')
+    pip_packages = pip.get_installed_distributions()
+    if 'docker-py' in [pkg.project_name for pkg in pip_packages]:
+        from .colors import red
+        print(
+            red('ERROR:'),
+            "Dependency conflict: an older version of the 'docker-py' package "
+            "is polluting the namespace. "
+            "Run the following command to remedy the issue:\n"
+            "pip uninstall docker docker-py; pip install docker",
+            file=sys.stderr
+        )
+        sys.exit(1)
+except ImportError:
+    # pip is not available, which indicates it's probably the binary
+    # distribution of Compose which is not affected
+    pass
+
+
 from . import errors
 from . import signals
 from .. import __version__

+ 5 - 1
compose/config/config.py

@@ -818,6 +818,7 @@ def merge_service_dicts(base, override, version):
     md.merge_mapping('ulimits', parse_ulimits)
     md.merge_mapping('networks', parse_networks)
     md.merge_mapping('sysctls', parse_sysctls)
+    md.merge_mapping('depends_on', parse_depends_on)
     md.merge_sequence('links', ServiceLink.parse)
 
     for field in ['volumes', 'devices']:
@@ -825,7 +826,7 @@ def merge_service_dicts(base, override, version):
 
     for field in [
         '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=[])
 
@@ -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_networks = functools.partial(parse_dict_or_list, lambda k: (k, None), 'networks')
 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):

+ 5 - 3
compose/config/config_schema_v3.0.json

@@ -308,6 +308,7 @@
           },
           "additionalProperties": false
         },
+        "internal": {"type": "boolean"},
         "labels": {"$ref": "#/definitions/list_or_dict"}
       },
       "additionalProperties": false
@@ -328,10 +329,11 @@
           "type": ["boolean", "object"],
           "properties": {
             "name": {"type": "string"}
-          }
-        }
+          },
+          "additionalProperties": false
+        },
+        "labels": {"$ref": "#/definitions/list_or_dict"}
       },
-      "labels": {"$ref": "#/definitions/list_or_dict"},
       "additionalProperties": false
     },
 

+ 29 - 0
compose/config/serialize.py

@@ -52,6 +52,25 @@ def serialize_config(config):
         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):
     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()
         ])
 
+    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

+ 4 - 3
compose/service.py

@@ -22,6 +22,7 @@ from .config import DOCKER_CONFIG_KEYS
 from .config import merge_environment
 from .config.types import VolumeSpec
 from .const import DEFAULT_TIMEOUT
+from .const import IS_WINDOWS_PLATFORM
 from .const import LABEL_CONFIG_HASH
 from .const import LABEL_CONTAINER_NUMBER
 from .const import LABEL_ONE_OFF
@@ -769,9 +770,9 @@ class Service(object):
 
         build_opts = self.options.get('build', {})
         path = build_opts.get('context')
-        # python2 os.path() doesn't support unicode, so we need to encode it to
-        # a byte string
-        if not six.PY3:
+        # python2 os.stat() doesn't support unicode on some UNIX, so we
+        # encode it to a bytestring to be safe
+        if not six.PY3 and not IS_WINDOWS_PLATFORM:
             path = path.encode('utf8')
 
         build_output = self.client.build(

+ 1 - 1
requirements.txt

@@ -2,7 +2,7 @@ PyYAML==3.11
 backports.ssl-match-hostname==3.5.0.1; python_version < '3'
 cached-property==1.2.0
 colorama==0.3.7
-docker==2.0.1
+docker==2.0.2
 dockerpty==0.4.1
 docopt==0.6.1
 enum34==1.0.4; python_version < '3.4'

+ 1 - 1
script/run/run.sh

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

+ 1 - 1
setup.py

@@ -37,7 +37,7 @@ install_requires = [
     'requests >= 2.6.1, != 2.11.0, < 2.12',
     'texttable >= 0.8.1, < 0.9',
     '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',
     'six >= 1.3.0, < 2',
     'jsonschema >= 2.5.1, < 3',

+ 9 - 3
tests/acceptance/cli_test.py

@@ -295,7 +295,13 @@ class CLITestCase(DockerClientTestCase):
         assert yaml.load(result.stdout) == {
             'version': '3.0',
             'networks': {},
-            'volumes': {},
+            'volumes': {
+                'foobar': {
+                    'labels': {
+                        'com.docker.compose.test': 'true',
+                    },
+                },
+            },
             'services': {
                 'web': {
                     'image': 'busybox',
@@ -333,8 +339,8 @@ class CLITestCase(DockerClientTestCase):
 
                     'healthcheck': {
                         'test': 'cat /etc/passwd',
-                        'interval': 10000000000,
-                        'timeout': 1000000000,
+                        'interval': '10s',
+                        'timeout': '1s',
                         'retries': 5,
                     },
 

+ 4 - 0
tests/fixtures/v3-full/docker-compose.yml

@@ -35,3 +35,7 @@ services:
       retries: 5
 
     stop_grace_period: 20s
+volumes:
+  foobar:
+    labels:
+      com.docker.compose.test: 'true'

+ 5 - 2
tests/integration/testcases.py

@@ -13,6 +13,7 @@ from compose.config.config import resolve_environment
 from compose.config.config import V1
 from compose.config.config import V2_0
 from compose.config.config import V2_1
+from compose.config.config import V3_0
 from compose.config.environment import Environment
 from compose.const import API_VERSIONS
 from compose.const import LABEL_PROJECT
@@ -36,13 +37,15 @@ def get_links(container):
 
 def engine_max_version():
     if 'DOCKER_VERSION' not in os.environ:
-        return V2_1
+        return V3_0
     version = os.environ['DOCKER_VERSION'].partition('-')[0]
     if version_lt(version, '1.10'):
         return V1
     elif version_lt(version, '1.12'):
         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):

+ 70 - 0
tests/unit/config/config_test.py

@@ -23,6 +23,7 @@ from compose.config.environment import Environment
 from compose.config.errors import ConfigurationError
 from compose.config.errors import VERSION_EXPLANATION
 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.const import IS_WINDOWS_PLATFORM
 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):
         config_details = build_config_details({
             'version': '2',
@@ -3300,3 +3335,38 @@ class SerializeTest(unittest.TestCase):
         }
 
         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'