docker_client.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import logging
  2. import os.path
  3. import ssl
  4. from docker import APIClient
  5. from docker import Context
  6. from docker import ContextAPI
  7. from docker import TLSConfig
  8. from docker.errors import TLSParameterError
  9. from docker.utils import kwargs_from_env
  10. from docker.utils.config import home_dir
  11. from . import verbose_proxy
  12. from ..config.environment import Environment
  13. from ..const import HTTP_TIMEOUT
  14. from ..utils import unquote_path
  15. from .errors import UserError
  16. from .utils import generate_user_agent
  17. from .utils import get_version_info
  18. log = logging.getLogger(__name__)
  19. def default_cert_path():
  20. return os.path.join(home_dir(), '.docker')
  21. def make_context(host, options, environment):
  22. tls = tls_config_from_options(options, environment)
  23. ctx = Context("compose", host=host, tls=tls.verify if tls else False)
  24. if tls:
  25. ctx.set_endpoint("docker", host, tls, skip_tls_verify=not tls.verify)
  26. return ctx
  27. def load_context(name=None):
  28. return ContextAPI.get_context(name)
  29. def get_client(environment, verbose=False, version=None, context=None):
  30. client = docker_client(
  31. version=version, context=context,
  32. environment=environment, tls_version=get_tls_version(environment)
  33. )
  34. if verbose:
  35. version_info = client.version().items()
  36. log.info(get_version_info('full'))
  37. log.info("Docker base_url: %s", client.base_url)
  38. log.info("Docker version: %s",
  39. ", ".join("%s=%s" % item for item in version_info))
  40. return verbose_proxy.VerboseProxy('docker', client)
  41. return client
  42. def get_tls_version(environment):
  43. compose_tls_version = environment.get('COMPOSE_TLS_VERSION', None)
  44. if not compose_tls_version:
  45. return None
  46. tls_attr_name = "PROTOCOL_{}".format(compose_tls_version)
  47. if not hasattr(ssl, tls_attr_name):
  48. log.warning(
  49. 'The "{}" protocol is unavailable. You may need to update your '
  50. 'version of Python or OpenSSL. Falling back to TLSv1 (default).'
  51. .format(compose_tls_version)
  52. )
  53. return None
  54. return getattr(ssl, tls_attr_name)
  55. def tls_config_from_options(options, environment=None):
  56. environment = environment or Environment()
  57. cert_path = environment.get('DOCKER_CERT_PATH') or None
  58. tls = options.get('--tls', False)
  59. ca_cert = unquote_path(options.get('--tlscacert'))
  60. cert = unquote_path(options.get('--tlscert'))
  61. key = unquote_path(options.get('--tlskey'))
  62. # verify is a special case - with docopt `--tlsverify` = False means it
  63. # wasn't used, so we set it if either the environment or the flag is True
  64. # see https://github.com/docker/compose/issues/5632
  65. verify = options.get('--tlsverify') or environment.get_boolean('DOCKER_TLS_VERIFY')
  66. skip_hostname_check = options.get('--skip-hostname-check', False)
  67. if cert_path is not None and not any((ca_cert, cert, key)):
  68. # FIXME: Modify TLSConfig to take a cert_path argument and do this internally
  69. cert = os.path.join(cert_path, 'cert.pem')
  70. key = os.path.join(cert_path, 'key.pem')
  71. ca_cert = os.path.join(cert_path, 'ca.pem')
  72. if verify and not any((ca_cert, cert, key)):
  73. # Default location for cert files is ~/.docker
  74. ca_cert = os.path.join(default_cert_path(), 'ca.pem')
  75. cert = os.path.join(default_cert_path(), 'cert.pem')
  76. key = os.path.join(default_cert_path(), 'key.pem')
  77. tls_version = get_tls_version(environment)
  78. advanced_opts = any([ca_cert, cert, key, verify, tls_version])
  79. if tls is True and not advanced_opts:
  80. return True
  81. elif advanced_opts: # --tls is a noop
  82. client_cert = None
  83. if cert or key:
  84. client_cert = (cert, key)
  85. return TLSConfig(
  86. client_cert=client_cert, verify=verify, ca_cert=ca_cert,
  87. assert_hostname=False if skip_hostname_check else None,
  88. ssl_version=tls_version
  89. )
  90. return None
  91. def docker_client(environment, version=None, context=None, tls_version=None):
  92. """
  93. Returns a docker-py client configured using environment variables
  94. according to the same logic as the official Docker client.
  95. """
  96. try:
  97. kwargs = kwargs_from_env(environment=environment, ssl_version=tls_version)
  98. except TLSParameterError:
  99. raise UserError(
  100. "TLS configuration is invalid - make sure your DOCKER_TLS_VERIFY "
  101. "and DOCKER_CERT_PATH are set correctly.\n"
  102. "You might need to run `eval \"$(docker-machine env default)\"`")
  103. if not context:
  104. # check env for DOCKER_HOST and certs path
  105. host = kwargs.get("base_url", None)
  106. tls = kwargs.get("tls", None)
  107. verify = False if not tls else tls.verify
  108. if host:
  109. context = Context("compose", host=host, tls=verify)
  110. else:
  111. context = ContextAPI.get_current_context()
  112. if tls:
  113. context.set_endpoint("docker", host=host, tls_cfg=tls, skip_tls_verify=not verify)
  114. if not context.is_docker_host():
  115. raise UserError(
  116. "The platform targeted with the current context is not supported.\n"
  117. "Make sure the context in use targets a Docker Engine.\n")
  118. kwargs['base_url'] = context.Host
  119. if context.TLSConfig:
  120. kwargs['tls'] = context.TLSConfig
  121. if version:
  122. kwargs['version'] = version
  123. timeout = environment.get('COMPOSE_HTTP_TIMEOUT')
  124. if timeout:
  125. kwargs['timeout'] = int(timeout)
  126. else:
  127. kwargs['timeout'] = HTTP_TIMEOUT
  128. kwargs['user_agent'] = generate_user_agent()
  129. # Workaround for
  130. # https://pyinstaller.readthedocs.io/en/v3.3.1/runtime-information.html#ld-library-path-libpath-considerations
  131. if 'LD_LIBRARY_PATH_ORIG' in environment:
  132. kwargs['credstore_env'] = {
  133. 'LD_LIBRARY_PATH': environment.get('LD_LIBRARY_PATH_ORIG'),
  134. }
  135. use_paramiko_ssh = int(environment.get('COMPOSE_PARAMIKO_SSH', 0))
  136. client = APIClient(use_ssh_client=not use_paramiko_ssh, **kwargs)
  137. client._original_base_url = kwargs.get('base_url')
  138. return client