diff --git a/CHANGELOG.md b/CHANGELOG.md index ba91a505b..ac2050512 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,7 @@ Change log - Values interpolated from the environment will now be converted to the proper type when used in non-string fields. -- Added support for `--labels` in `docker-compose run` +- Added support for `--label` in `docker-compose run` - Added support for `--timeout` in `docker-compose down` @@ -71,6 +71,8 @@ Change log - Fixed a bug where missing secret files would generate an empty directory in their place +- Fixed character encoding issues in the CLI's error handlers + - Added validation for the `test` field in healthchecks - Added validation for the `subnet` field in IPAM configurations diff --git a/compose/__init__.py b/compose/__init__.py index 2b363f3be..231670a5c 100644 --- a/compose/__init__.py +++ b/compose/__init__.py @@ -1,4 +1,4 @@ from __future__ import absolute_import from __future__ import unicode_literals -__version__ = '1.18.0-rc1' +__version__ = '1.18.0-rc2' diff --git a/compose/cli/errors.py b/compose/cli/errors.py index 1506aa660..82768970b 100644 --- a/compose/cli/errors.py +++ b/compose/cli/errors.py @@ -7,7 +7,6 @@ import socket from distutils.spawn import find_executable from textwrap import dedent -import six from docker.errors import APIError from requests.exceptions import ConnectionError as RequestsConnectionError from requests.exceptions import ReadTimeout @@ -15,6 +14,7 @@ from requests.exceptions import SSLError from requests.packages.urllib3.exceptions import ReadTimeoutError from ..const import API_VERSION_TO_ENGINE_VERSION +from .utils import binarystr_to_unicode from .utils import is_docker_for_mac_installed from .utils import is_mac from .utils import is_ubuntu @@ -75,7 +75,9 @@ def log_windows_pipe_error(exc): ) else: log.error( - "Windows named pipe error: {} (code: {})".format(exc.strerror, exc.winerror) + "Windows named pipe error: {} (code: {})".format( + binarystr_to_unicode(exc.strerror), exc.winerror + ) ) @@ -89,9 +91,7 @@ def log_timeout_error(timeout): def log_api_error(e, client_version): - explanation = e.explanation - if isinstance(explanation, six.binary_type): - explanation = explanation.decode('utf-8') + explanation = binarystr_to_unicode(e.explanation) if 'client is newer than server' not in explanation: log.error(explanation) @@ -106,7 +106,8 @@ def log_api_error(e, client_version): log.error( "The Docker Engine version is less than the minimum required by " "Compose. Your current project requires a Docker Engine of " - "version {version} or greater.".format(version=version)) + "version {version} or greater.".format(version=version) + ) def exit_with_error(msg): @@ -115,12 +116,17 @@ def exit_with_error(msg): def get_conn_error_message(url): - if find_executable('docker') is None: - return docker_not_found_msg("Couldn't connect to Docker daemon.") - if is_docker_for_mac_installed(): - return conn_error_docker_for_mac - if find_executable('docker-machine') is not None: - return conn_error_docker_machine + try: + if find_executable('docker') is None: + return docker_not_found_msg("Couldn't connect to Docker daemon.") + if is_docker_for_mac_installed(): + return conn_error_docker_for_mac + if find_executable('docker-machine') is not None: + return conn_error_docker_machine + except UnicodeDecodeError: + # https://github.com/docker/compose/issues/5442 + # Ignore the error and print the generic message instead. + pass return conn_error_generic.format(url=url) diff --git a/compose/cli/utils.py b/compose/cli/utils.py index 4d4fc4c18..a171d6678 100644 --- a/compose/cli/utils.py +++ b/compose/cli/utils.py @@ -10,6 +10,7 @@ import subprocess import sys import docker +import six import compose from ..const import IS_WINDOWS_PLATFORM @@ -148,3 +149,15 @@ def human_readable_file_size(size): size / float(1 << (order * 10)), suffixes[order] ) + + +def binarystr_to_unicode(s): + if not isinstance(s, six.binary_type): + return s + + if IS_WINDOWS_PLATFORM: + try: + return s.decode('windows-1250') + except UnicodeDecodeError: + pass + return s.decode('utf-8', 'replace') diff --git a/compose/config/config.py b/compose/config/config.py index 98719d6ba..51391fc7b 100644 --- a/compose/config/config.py +++ b/compose/config/config.py @@ -1153,7 +1153,7 @@ def resolve_volume_paths(working_dir, service_dict): def resolve_volume_path(working_dir, volume): if isinstance(volume, dict): - if volume.get('source', '').startswith('.') and volume['type'] == 'mount': + if volume.get('source', '').startswith('.') and volume['type'] == 'bind': volume['source'] = expand_path(working_dir, volume['source']) return volume diff --git a/script/run/run.sh b/script/run/run.sh index 441c0d806..4be14b722 100755 --- a/script/run/run.sh +++ b/script/run/run.sh @@ -15,7 +15,7 @@ set -e -VERSION="1.18.0-rc1" +VERSION="1.18.0-rc2" IMAGE="docker/compose:$VERSION" diff --git a/tests/unit/cli/errors_test.py b/tests/unit/cli/errors_test.py index 68326d1c7..7b53ed2b1 100644 --- a/tests/unit/cli/errors_test.py +++ b/tests/unit/cli/errors_test.py @@ -86,3 +86,13 @@ class TestHandleConnectionErrors(object): _, args, _ = mock_logging.error.mock_calls[0] assert "Windows named pipe error: The pipe is busy. (code: 231)" == args[0] + + @pytest.mark.skipif(not IS_WINDOWS_PLATFORM, reason='Needs pywin32') + def test_windows_pipe_error_encoding_issue(self, mock_logging): + import pywintypes + with pytest.raises(errors.ConnectionError): + with handle_connection_errors(mock.Mock(api_version='1.22')): + raise pywintypes.error(9999, 'WriteFile', 'I use weird characters \xe9') + + _, args, _ = mock_logging.error.mock_calls[0] + assert 'Windows named pipe error: I use weird characters \xe9 (code: 9999)' == args[0] diff --git a/tests/unit/config/config_test.py b/tests/unit/config/config_test.py index 7029fcb08..122ab2ef9 100644 --- a/tests/unit/config/config_test.py +++ b/tests/unit/config/config_test.py @@ -1304,6 +1304,29 @@ class ConfigTest(unittest.TestCase): assert npipe_mount.target == '/named_pipe' assert not npipe_mount.is_named_volume + def test_load_bind_mount_relative_path(self): + expected_source = 'C:\\tmp\\web' if IS_WINDOWS_PLATFORM else '/tmp/web' + base_file = config.ConfigFile( + 'base.yaml', { + 'version': '3.4', + 'services': { + 'web': { + 'image': 'busybox:latest', + 'volumes': [ + {'type': 'bind', 'source': './web', 'target': '/web'}, + ], + }, + }, + }, + ) + + details = config.ConfigDetails('/tmp', [base_file]) + config_data = config.load(details) + mount = config_data.services[0].get('volumes')[0] + assert mount.target == '/web' + assert mount.type == 'bind' + assert mount.source == expected_source + def test_config_valid_service_names(self): for valid_name in ['_', '-', '.__.', '_what-up.', 'what_.up----', 'whatup']: services = config.load(