Explorar el Código

Merge pull request #3139 from shin-/1716-tls-flags-support

Add support for TLS config command-line options
Daniel Nephin hace 9 años
padre
commit
2be2029791

+ 11 - 4
compose/cli/command.py

@@ -12,6 +12,7 @@ from .. import config
 from ..const import API_VERSIONS
 from ..project import Project
 from .docker_client import docker_client
+from .docker_client import tls_config_from_options
 from .utils import get_version_info
 
 log = logging.getLogger(__name__)
@@ -23,6 +24,8 @@ def project_from_options(project_dir, options):
         get_config_path_from_options(options),
         project_name=options.get('--project-name'),
         verbose=options.get('--verbose'),
+        host=options.get('--host'),
+        tls_config=tls_config_from_options(options),
     )
 
 
@@ -37,8 +40,8 @@ def get_config_path_from_options(options):
     return None
 
 
-def get_client(verbose=False, version=None):
-    client = docker_client(version=version)
+def get_client(verbose=False, version=None, tls_config=None, host=None):
+    client = docker_client(version=version, tls_config=tls_config, host=host)
     if verbose:
         version_info = six.iteritems(client.version())
         log.info(get_version_info('full'))
@@ -49,7 +52,8 @@ def get_client(verbose=False, version=None):
     return client
 
 
-def get_project(project_dir, config_path=None, project_name=None, verbose=False):
+def get_project(project_dir, config_path=None, project_name=None, verbose=False,
+                host=None, tls_config=None):
     config_details = config.find(project_dir, config_path)
     project_name = get_project_name(config_details.working_dir, project_name)
     config_data = config.load(config_details)
@@ -57,7 +61,10 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False)
     api_version = os.environ.get(
         'COMPOSE_API_VERSION',
         API_VERSIONS[config_data.version])
-    client = get_client(verbose=verbose, version=api_version)
+    client = get_client(
+        verbose=verbose, version=api_version, tls_config=tls_config,
+        host=host
+    )
 
     return Project.from_config(project_name, config_data, client)
 

+ 34 - 1
compose/cli/docker_client.py

@@ -6,7 +6,9 @@ import os
 
 from docker import Client
 from docker.errors import TLSParameterError
+from docker.tls import TLSConfig
 from docker.utils import kwargs_from_env
+from requests.utils import urlparse
 
 from ..const import HTTP_TIMEOUT
 from .errors import UserError
@@ -14,7 +16,33 @@ from .errors import UserError
 log = logging.getLogger(__name__)
 
 
-def docker_client(version=None):
+def tls_config_from_options(options):
+    tls = options.get('--tls', False)
+    ca_cert = options.get('--tlscacert')
+    cert = options.get('--tlscert')
+    key = options.get('--tlskey')
+    verify = options.get('--tlsverify')
+    hostname = urlparse(options.get('--host') or '').hostname
+
+    advanced_opts = any([ca_cert, cert, key, verify])
+
+    if tls is True and not advanced_opts:
+        return True
+    elif advanced_opts:
+        client_cert = None
+        if cert or key:
+            client_cert = (cert, key)
+        return TLSConfig(
+            client_cert=client_cert, verify=verify, ca_cert=ca_cert,
+            assert_hostname=(
+                hostname or not options.get('--skip-hostname-check', False)
+            )
+        )
+    else:
+        return None
+
+
+def docker_client(version=None, tls_config=None, host=None):
     """
     Returns a docker-py client configured using environment variables
     according to the same logic as the official Docker client.
@@ -31,6 +59,11 @@ def docker_client(version=None):
             "and DOCKER_CERT_PATH are set correctly.\n"
             "You might need to run `eval \"$(docker-machine env default)\"`")
 
+    if host:
+        kwargs['base_url'] = host
+    if tls_config:
+        kwargs['tls'] = tls_config
+
     if version:
         kwargs['version'] = version
 

+ 14 - 4
compose/cli/main.py

@@ -145,10 +145,20 @@ class TopLevelCommand(object):
       docker-compose -h|--help
 
     Options:
-      -f, --file FILE           Specify an alternate compose file (default: docker-compose.yml)
-      -p, --project-name NAME   Specify an alternate project name (default: directory name)
-      --verbose                 Show more output
-      -v, --version             Print version and exit
+      -f, --file FILE             Specify an alternate compose file (default: docker-compose.yml)
+      -p, --project-name NAME     Specify an alternate project name (default: directory name)
+      --verbose                   Show more output
+      -v, --version               Print version and exit
+      -H, --host HOST             Daemon socket to connect to
+
+      --tls                       Use TLS; implied by --tlsverify
+      --tlscacert CA_PATH         Trust certs signed only by this CA
+      --tlscert CLIENT_CERT_PATH  Path to TLS certificate file
+      --tlskey TLS_KEY_PATH       Path to TLS key file
+      --tlsverify                 Use TLS and verify the remote
+      --skip-hostname-check       Don't check the daemon's hostname against the name specified
+                                  in the client certificate (for example if your docker host
+                                  is an IP address)
 
     Commands:
       build              Build or rebuild services

+ 1 - 1
requirements.txt

@@ -3,7 +3,7 @@ cached-property==1.2.0
 dockerpty==0.4.1
 docopt==0.6.1
 enum34==1.0.4
-git+https://github.com/docker/docker-py.git@8c4546f8c8f52bb2923834783a17beb5bb89a724#egg=docker-py
+git+https://github.com/docker/docker-py.git@5c1c42397cf0fdb74182df2d69822b82df8f2a6a#egg=docker-py
 jsonschema==2.5.1
 requests==2.7.0
 six==1.7.3

+ 0 - 0
tests/fixtures/tls/ca.pem


+ 0 - 0
tests/fixtures/tls/cert.pem


+ 0 - 0
tests/fixtures/tls/key.key


+ 86 - 3
tests/unit/cli/docker_client_test.py

@@ -3,7 +3,11 @@ from __future__ import unicode_literals
 
 import os
 
-from compose.cli import docker_client
+import docker
+import pytest
+
+from compose.cli.docker_client import docker_client
+from compose.cli.docker_client import tls_config_from_options
 from tests import mock
 from tests import unittest
 
@@ -13,10 +17,89 @@ class DockerClientTestCase(unittest.TestCase):
     def test_docker_client_no_home(self):
         with mock.patch.dict(os.environ):
             del os.environ['HOME']
-            docker_client.docker_client()
+            docker_client()
 
     def test_docker_client_with_custom_timeout(self):
         timeout = 300
         with mock.patch('compose.cli.docker_client.HTTP_TIMEOUT', 300):
-            client = docker_client.docker_client()
+            client = docker_client()
             self.assertEqual(client.timeout, int(timeout))
+
+
+class TLSConfigTestCase(unittest.TestCase):
+    ca_cert = 'tests/fixtures/tls/ca.pem'
+    client_cert = 'tests/fixtures/tls/cert.pem'
+    key = 'tests/fixtures/tls/key.key'
+
+    def test_simple_tls(self):
+        options = {'--tls': True}
+        result = tls_config_from_options(options)
+        assert result is True
+
+    def test_tls_ca_cert(self):
+        options = {
+            '--tlscacert': self.ca_cert, '--tlsverify': True
+        }
+        result = tls_config_from_options(options)
+        assert isinstance(result, docker.tls.TLSConfig)
+        assert result.ca_cert == options['--tlscacert']
+        assert result.verify is True
+
+    def test_tls_ca_cert_explicit(self):
+        options = {
+            '--tlscacert': self.ca_cert, '--tls': True,
+            '--tlsverify': True
+        }
+        result = tls_config_from_options(options)
+        assert isinstance(result, docker.tls.TLSConfig)
+        assert result.ca_cert == options['--tlscacert']
+        assert result.verify is True
+
+    def test_tls_client_cert(self):
+        options = {
+            '--tlscert': self.client_cert, '--tlskey': self.key
+        }
+        result = tls_config_from_options(options)
+        assert isinstance(result, docker.tls.TLSConfig)
+        assert result.cert == (options['--tlscert'], options['--tlskey'])
+
+    def test_tls_client_cert_explicit(self):
+        options = {
+            '--tlscert': self.client_cert, '--tlskey': self.key,
+            '--tls': True
+        }
+        result = tls_config_from_options(options)
+        assert isinstance(result, docker.tls.TLSConfig)
+        assert result.cert == (options['--tlscert'], options['--tlskey'])
+
+    def test_tls_client_and_ca(self):
+        options = {
+            '--tlscert': self.client_cert, '--tlskey': self.key,
+            '--tlsverify': True, '--tlscacert': self.ca_cert
+        }
+        result = tls_config_from_options(options)
+        assert isinstance(result, docker.tls.TLSConfig)
+        assert result.cert == (options['--tlscert'], options['--tlskey'])
+        assert result.ca_cert == options['--tlscacert']
+        assert result.verify is True
+
+    def test_tls_client_and_ca_explicit(self):
+        options = {
+            '--tlscert': self.client_cert, '--tlskey': self.key,
+            '--tlsverify': True, '--tlscacert': self.ca_cert,
+            '--tls': True
+        }
+        result = tls_config_from_options(options)
+        assert isinstance(result, docker.tls.TLSConfig)
+        assert result.cert == (options['--tlscert'], options['--tlskey'])
+        assert result.ca_cert == options['--tlscacert']
+        assert result.verify is True
+
+    def test_tls_client_missing_key(self):
+        options = {'--tlscert': self.client_cert}
+        with pytest.raises(docker.errors.TLSParameterError):
+            tls_config_from_options(options)
+
+        options = {'--tlskey': self.key}
+        with pytest.raises(docker.errors.TLSParameterError):
+            tls_config_from_options(options)