diff --git a/compose/cli/command.py b/compose/cli/command.py index 8ac3aff4f..1aa36cb07 100644 --- a/compose/cli/command.py +++ b/compose/cli/command.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import logging import os import re +import ssl import six @@ -46,10 +47,28 @@ def get_config_path_from_options(base_dir, options, environment): return None -def get_client(environment, verbose=False, version=None, tls_config=None, host=None): +def get_tls_version(environment): + compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None) + if not compose_tls_version: + return None + + tls_attr_name = "PROTOCOL_{}".format(compose_tls_version) + if not hasattr(ssl, tls_attr_name): + log.warn( + 'The {} protocol is unavailable. You may need to update your ' + 'version of Python or OpenSSL. Falling back to TLSv1 (default).' + ) + return None + + return getattr(ssl, tls_attr_name) + + +def get_client(environment, verbose=False, version=None, tls_config=None, host=None, + tls_version=None): + client = docker_client( version=version, tls_config=tls_config, host=host, - environment=environment + environment=environment, tls_version=get_tls_version(environment) ) if verbose: version_info = six.iteritems(client.version()) @@ -74,6 +93,7 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False, api_version = environment.get( 'COMPOSE_API_VERSION', API_VERSIONS[config_data.version]) + client = get_client( verbose=verbose, version=api_version, tls_config=tls_config, host=host, environment=environment diff --git a/compose/cli/docker_client.py b/compose/cli/docker_client.py index 0c0113bb7..3e0873c49 100644 --- a/compose/cli/docker_client.py +++ b/compose/cli/docker_client.py @@ -39,7 +39,8 @@ def tls_config_from_options(options): return None -def docker_client(environment, version=None, tls_config=None, host=None): +def docker_client(environment, version=None, tls_config=None, host=None, + tls_version=None): """ Returns a docker-py client configured using environment variables according to the same logic as the official Docker client. @@ -49,7 +50,7 @@ def docker_client(environment, version=None, tls_config=None, host=None): "Please use COMPOSE_HTTP_TIMEOUT instead.") try: - kwargs = kwargs_from_env(environment=environment) + kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version) except TLSParameterError: raise UserError( "TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY " diff --git a/docs/reference/envvars.md b/docs/reference/envvars.md index 6f7fb7919..22516debd 100644 --- a/docs/reference/envvars.md +++ b/docs/reference/envvars.md @@ -78,6 +78,11 @@ Configures the path to the `ca.pem`, `cert.pem`, and `key.pem` files used for TL Configures the time (in seconds) a request to the Docker daemon is allowed to hang before Compose considers it failed. Defaults to 60 seconds. +## COMPOSE\_TLS\_VERSION + +Configure which TLS version is used for TLS communication with the `docker` +daemon. Defaults to `TLSv1`. +Supported values are: `TLSv1`, `TLSv1_1`, `TLSv1_2`. ## Related Information diff --git a/tests/unit/cli/command_test.py b/tests/unit/cli/command_test.py index 3502d6369..28adff3f3 100644 --- a/tests/unit/cli/command_test.py +++ b/tests/unit/cli/command_test.py @@ -2,10 +2,12 @@ from __future__ import absolute_import from __future__ import unicode_literals import os +import ssl import pytest from compose.cli.command import get_config_path_from_options +from compose.cli.command import get_tls_version from compose.config.environment import Environment from compose.const import IS_WINDOWS_PLATFORM from tests import mock @@ -46,3 +48,20 @@ class TestGetConfigPathFromOptions(object): def test_no_path(self): environment = Environment.from_env_file('.') assert not get_config_path_from_options('.', {}, environment) + + +class TestGetTlsVersion(object): + def test_get_tls_version_default(self): + environment = {} + assert get_tls_version(environment) is None + + def test_get_tls_version_upgrade(self): + environment = {'COMPOSE_TLS_VERSION': 'TLSv1_2'} + assert get_tls_version(environment) == ssl.PROTOCOL_TLSv1_2 + + def test_get_tls_version_unavailable(self): + environment = {'COMPOSE_TLS_VERSION': 'TLSv5_5'} + with mock.patch('compose.cli.command.log') as mock_log: + tls_version = get_tls_version(environment) + mock_log.warn.assert_called_once_with(mock.ANY) + assert tls_version is None