Use docker context interface from docker-py

Signed-off-by: Anca Iordache <anca.iordache@docker.com>
This commit is contained in:
Anca Iordache 2020-01-29 11:41:07 +01:00
parent eb20915c6e
commit 2dfd85e30e
5 changed files with 120 additions and 36 deletions

View File

@ -8,7 +8,6 @@ import re
import six
from . import errors
from . import verbose_proxy
from .. import config
from .. import parallel
from ..config.environment import Environment
@ -17,10 +16,10 @@ from ..const import LABEL_CONFIG_FILES
from ..const import LABEL_ENVIRONMENT_FILE
from ..const import LABEL_WORKING_DIR
from ..project import Project
from .docker_client import docker_client
from .docker_client import get_tls_version
from .docker_client import tls_config_from_options
from .utils import get_version_info
from .docker_client import get_client
from .docker_client import load_context
from .docker_client import make_context
from .errors import UserError
log = logging.getLogger(__name__)
@ -48,16 +47,28 @@ def project_from_options(project_dir, options, additional_options=None):
environment.silent = options.get('COMMAND', None) in SILENT_COMMANDS
set_parallel_limit(environment)
host = options.get('--host')
# get the context for the run
context = None
context_name = options.get('--context', None)
if context_name:
context = load_context(context_name)
if not context:
raise UserError("Context '{}' not found".format(context_name))
host = options.get('--host', None)
if host is not None:
if context:
raise UserError(
"-H, --host and -c, --context are mutually exclusive. Only one should be set.")
host = host.lstrip('=')
context = make_context(host, options, environment)
return get_project(
project_dir,
get_config_path_from_options(project_dir, options, environment),
project_name=options.get('--project-name'),
verbose=options.get('--verbose'),
host=host,
tls_config=tls_config_from_options(options, environment),
context=context,
environment=environment,
override_dir=override_dir,
compatibility=compatibility_from_options(project_dir, options, environment),
@ -112,25 +123,8 @@ 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,
tls_version=None):
client = docker_client(
version=version, tls_config=tls_config, host=host,
environment=environment, tls_version=get_tls_version(environment)
)
if verbose:
version_info = six.iteritems(client.version())
log.info(get_version_info('full'))
log.info("Docker base_url: %s", client.base_url)
log.info("Docker version: %s",
", ".join("%s=%s" % item for item in version_info))
return verbose_proxy.VerboseProxy('docker', client)
return client
def get_project(project_dir, config_path=None, project_name=None, verbose=False,
host=None, tls_config=None, environment=None, override_dir=None,
context=None, environment=None, override_dir=None,
compatibility=False, interpolate=True, environment_file=None):
if not environment:
environment = Environment.from_env_file(project_dir)
@ -145,8 +139,7 @@ def get_project(project_dir, config_path=None, project_name=None, verbose=False,
API_VERSIONS[config_data.version])
client = get_client(
verbose=verbose, version=api_version, tls_config=tls_config,
host=host, environment=environment
verbose=verbose, version=api_version, context=context, environment=environment
)
with errors.handle_connection_errors(client):

View File

@ -5,17 +5,22 @@ import logging
import os.path
import ssl
import six
from docker import APIClient
from docker import Context
from docker import ContextAPI
from docker import TLSConfig
from docker.errors import TLSParameterError
from docker.tls import TLSConfig
from docker.utils import kwargs_from_env
from docker.utils.config import home_dir
from . import verbose_proxy
from ..config.environment import Environment
from ..const import HTTP_TIMEOUT
from ..utils import unquote_path
from .errors import UserError
from .utils import generate_user_agent
from .utils import get_version_info
log = logging.getLogger(__name__)
@ -24,6 +29,33 @@ def default_cert_path():
return os.path.join(home_dir(), '.docker')
def make_context(host, options, environment):
tls = tls_config_from_options(options, environment)
ctx = Context("compose", host=host)
if tls:
ctx.set_endpoint("docker", host, tls, skip_tls_verify=not tls.verify)
return ctx
def load_context(name=None):
return ContextAPI.get_context(name)
def get_client(environment, verbose=False, version=None, context=None):
client = docker_client(
version=version, context=context,
environment=environment, tls_version=get_tls_version(environment)
)
if verbose:
version_info = six.iteritems(client.version())
log.info(get_version_info('full'))
log.info("Docker base_url: %s", client.base_url)
log.info("Docker version: %s",
", ".join("%s=%s" % item for item in version_info))
return verbose_proxy.VerboseProxy('docker', client)
return client
def get_tls_version(environment):
compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None)
if not compose_tls_version:
@ -87,8 +119,7 @@ def tls_config_from_options(options, environment=None):
return None
def docker_client(environment, version=None, tls_config=None, host=None,
tls_version=None):
def docker_client(environment, version=None, context=None, tls_version=None):
"""
Returns a docker-py client configured using environment variables
according to the same logic as the official Docker client.
@ -101,10 +132,21 @@ def docker_client(environment, version=None, tls_config=None, host=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 not context:
# check env for DOCKER_HOST and certs path
host = kwargs.get("base_url", None)
tls = kwargs.get("tls", None)
verify = False if not tls else tls.verify
if host:
context = Context("compose", host=host)
else:
context = ContextAPI.get_current_context()
if tls:
context.set_endpoint("docker", host=host, tls_cfg=tls, skip_tls_verify=not verify)
kwargs['base_url'] = context.Host
if context.TLSConfig:
kwargs['tls'] = context.TLSConfig
if version:
kwargs['version'] = version

View File

@ -192,6 +192,7 @@ class TopLevelCommand(object):
(default: docker-compose.yml)
-p, --project-name NAME Specify an alternate project name
(default: directory name)
-c, --context NAME Specify a context name
--verbose Show more output
--log-level LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
--no-ansi Do not print ANSI control characters

View File

@ -4,7 +4,7 @@ cached-property==1.5.1
certifi==2019.11.28
chardet==3.0.4
colorama==0.4.3; sys_platform == 'win32'
docker==4.1.0
docker==4.2.0
docker-pycreds==0.4.0
dockerpty==0.4.1
docopt==0.6.2

View File

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import unicode_literals
import os
import shutil
import unittest
from docker import ContextAPI
from tests.acceptance.cli_test import dispatch
class ContextTestCase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.docker_dir = os.path.join(os.environ.get("HOME", "/tmp"), '.docker')
if not os.path.exists(cls.docker_dir):
os.makedirs(cls.docker_dir)
f = open(os.path.join(cls.docker_dir, "config.json"), "w")
f.write("{}")
f.close()
cls.docker_config = os.path.join(cls.docker_dir, "config.json")
os.environ['DOCKER_CONFIG'] = cls.docker_config
ContextAPI.create_context("testcontext", host="tcp://doesnotexist:8000")
@classmethod
def tearDownClass(cls):
shutil.rmtree(cls.docker_dir, ignore_errors=True)
def setUp(self):
self.base_dir = 'tests/fixtures/simple-composefile'
self.override_dir = None
def dispatch(self, options, project_options=None, returncode=0, stdin=None):
return dispatch(self.base_dir, options, project_options, returncode, stdin)
def test_help(self):
result = self.dispatch(['help'], returncode=0)
assert '-c, --context NAME' in result.stdout
def test_fail_on_both_host_and_context_opt(self):
result = self.dispatch(['-H', 'unix://', '-c', 'default', 'up'], returncode=1)
assert '-H, --host and -c, --context are mutually exclusive' in result.stderr
def test_fail_run_on_inexistent_context(self):
result = self.dispatch(['-c', 'testcontext', 'up', '-d'], returncode=1)
assert "Couldn't connect to Docker daemon" in result.stderr