Reformat code with black and isort.

This commit is contained in:
Felix Fontein
2025-10-06 18:34:59 +02:00
parent f45232635c
commit d65d37e9e9
132 changed files with 17581 additions and 14729 deletions
+17 -8
View File
@@ -18,8 +18,13 @@ URLLIB3_IMPORT_ERROR = None
try:
from requests import Session # noqa: F401, pylint: disable=unused-import
from requests.adapters import HTTPAdapter # noqa: F401, pylint: disable=unused-import
from requests.exceptions import HTTPError, InvalidSchema # noqa: F401, pylint: disable=unused-import
from requests.adapters import ( # noqa: F401, pylint: disable=unused-import
HTTPAdapter,
)
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
HTTPError,
InvalidSchema,
)
except ImportError:
REQUESTS_IMPORT_ERROR = traceback.format_exc()
@@ -38,11 +43,15 @@ except ImportError:
try:
from requests.packages import urllib3 # pylint: disable=unused-import
from requests.packages.urllib3 import connection as urllib3_connection # pylint: disable=unused-import
from requests.packages.urllib3 import (
connection as urllib3_connection, # pylint: disable=unused-import
)
except ImportError:
try:
import urllib3 # pylint: disable=unused-import
from urllib3 import connection as urllib3_connection # pylint: disable=unused-import
from urllib3 import (
connection as urllib3_connection, # pylint: disable=unused-import
)
except ImportError:
URLLIB3_IMPORT_ERROR = traceback.format_exc()
@@ -77,11 +86,11 @@ def fail_on_missing_imports():
from .errors import MissingRequirementException
raise MissingRequirementException(
'You have to install requests',
'requests', REQUESTS_IMPORT_ERROR)
"You have to install requests", "requests", REQUESTS_IMPORT_ERROR
)
if URLLIB3_IMPORT_ERROR is not None:
from .errors import MissingRequirementException
raise MissingRequirementException(
'You have to install urllib3',
'urllib3', URLLIB3_IMPORT_ERROR)
"You have to install urllib3", "urllib3", URLLIB3_IMPORT_ERROR
)
+214 -148
View File
@@ -16,37 +16,45 @@ from functools import partial
from urllib.parse import quote
from .. import auth
from .._import_helper import fail_on_missing_imports
from .._import_helper import HTTPError as _HTTPError
from .._import_helper import InvalidSchema as _InvalidSchema
from .._import_helper import Session as _Session
from ..constants import (DEFAULT_NUM_POOLS, DEFAULT_NUM_POOLS_SSH,
DEFAULT_MAX_POOL_SIZE, DEFAULT_TIMEOUT_SECONDS,
DEFAULT_USER_AGENT, IS_WINDOWS_PLATFORM,
MINIMUM_DOCKER_API_VERSION, STREAM_HEADER_SIZE_BYTES,
DEFAULT_DATA_CHUNK_SIZE)
from ..errors import (DockerException, InvalidVersion, TLSParameterError, MissingRequirementException,
create_api_error_from_http_exception)
from .._import_helper import fail_on_missing_imports
from ..constants import (
DEFAULT_DATA_CHUNK_SIZE,
DEFAULT_MAX_POOL_SIZE,
DEFAULT_NUM_POOLS,
DEFAULT_NUM_POOLS_SSH,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_USER_AGENT,
IS_WINDOWS_PLATFORM,
MINIMUM_DOCKER_API_VERSION,
STREAM_HEADER_SIZE_BYTES,
)
from ..errors import (
DockerException,
InvalidVersion,
MissingRequirementException,
TLSParameterError,
create_api_error_from_http_exception,
)
from ..tls import TLSConfig
from ..transport.npipeconn import NpipeHTTPAdapter
from ..transport.npipesocket import PYWIN32_IMPORT_ERROR
from ..transport.unixconn import UnixHTTPAdapter
from ..transport.sshconn import SSHHTTPAdapter, PARAMIKO_IMPORT_ERROR
from ..transport.sshconn import PARAMIKO_IMPORT_ERROR, SSHHTTPAdapter
from ..transport.ssladapter import SSLHTTPAdapter
from ..utils import config, utils, json_stream
from ..transport.unixconn import UnixHTTPAdapter
from ..utils import config, json_stream, utils
from ..utils.decorators import check_resource, update_headers
from ..utils.proxy import ProxyConfig
from ..utils.socket import consume_socket_output, demux_adaptor, frames_iter
from .daemon import DaemonApiMixin
log = logging.getLogger(__name__)
class APIClient(
_Session,
DaemonApiMixin):
class APIClient(_Session, DaemonApiMixin):
"""
A low-level client for the Docker Engine API.
@@ -85,139 +93,155 @@ class APIClient(
to save in the pool.
"""
__attrs__ = _Session.__attrs__ + ['_auth_configs',
'_general_configs',
'_version',
'base_url',
'timeout']
__attrs__ = _Session.__attrs__ + [
"_auth_configs",
"_general_configs",
"_version",
"base_url",
"timeout",
]
def __init__(self, base_url=None, version=None,
timeout=DEFAULT_TIMEOUT_SECONDS, tls=False,
user_agent=DEFAULT_USER_AGENT, num_pools=None,
credstore_env=None, use_ssh_client=False,
max_pool_size=DEFAULT_MAX_POOL_SIZE):
def __init__(
self,
base_url=None,
version=None,
timeout=DEFAULT_TIMEOUT_SECONDS,
tls=False,
user_agent=DEFAULT_USER_AGENT,
num_pools=None,
credstore_env=None,
use_ssh_client=False,
max_pool_size=DEFAULT_MAX_POOL_SIZE,
):
super(APIClient, self).__init__()
fail_on_missing_imports()
if tls and not base_url:
raise TLSParameterError(
'If using TLS, the base_url argument must be provided.'
"If using TLS, the base_url argument must be provided."
)
self.base_url = base_url
self.timeout = timeout
self.headers['User-Agent'] = user_agent
self.headers["User-Agent"] = user_agent
self._general_configs = config.load_general_config()
proxy_config = self._general_configs.get('proxies', {})
proxy_config = self._general_configs.get("proxies", {})
try:
proxies = proxy_config[base_url]
except KeyError:
proxies = proxy_config.get('default', {})
proxies = proxy_config.get("default", {})
self._proxy_configs = ProxyConfig.from_dict(proxies)
self._auth_configs = auth.load_config(
config_dict=self._general_configs, credstore_env=credstore_env,
config_dict=self._general_configs,
credstore_env=credstore_env,
)
self.credstore_env = credstore_env
base_url = utils.parse_host(
base_url, IS_WINDOWS_PLATFORM, tls=bool(tls)
)
base_url = utils.parse_host(base_url, IS_WINDOWS_PLATFORM, tls=bool(tls))
# SSH has a different default for num_pools to all other adapters
num_pools = num_pools or DEFAULT_NUM_POOLS_SSH if \
base_url.startswith('ssh://') else DEFAULT_NUM_POOLS
num_pools = (
num_pools or DEFAULT_NUM_POOLS_SSH
if base_url.startswith("ssh://")
else DEFAULT_NUM_POOLS
)
if base_url.startswith('http+unix://'):
if base_url.startswith("http+unix://"):
self._custom_adapter = UnixHTTPAdapter(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size
base_url,
timeout,
pool_connections=num_pools,
max_pool_size=max_pool_size,
)
self.mount('http+docker://', self._custom_adapter)
self._unmount('http://', 'https://')
self.mount("http+docker://", self._custom_adapter)
self._unmount("http://", "https://")
# host part of URL should be unused, but is resolved by requests
# module in proxy_bypass_macosx_sysconf()
self.base_url = 'http+docker://localhost'
elif base_url.startswith('npipe://'):
self.base_url = "http+docker://localhost"
elif base_url.startswith("npipe://"):
if not IS_WINDOWS_PLATFORM:
raise DockerException(
'The npipe:// protocol is only supported on Windows'
"The npipe:// protocol is only supported on Windows"
)
if PYWIN32_IMPORT_ERROR is not None:
raise MissingRequirementException(
'Install pypiwin32 package to enable npipe:// support',
'pywin32',
PYWIN32_IMPORT_ERROR)
"Install pypiwin32 package to enable npipe:// support",
"pywin32",
PYWIN32_IMPORT_ERROR,
)
self._custom_adapter = NpipeHTTPAdapter(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size
base_url,
timeout,
pool_connections=num_pools,
max_pool_size=max_pool_size,
)
self.mount('http+docker://', self._custom_adapter)
self.base_url = 'http+docker://localnpipe'
elif base_url.startswith('ssh://'):
self.mount("http+docker://", self._custom_adapter)
self.base_url = "http+docker://localnpipe"
elif base_url.startswith("ssh://"):
if PARAMIKO_IMPORT_ERROR is not None and not use_ssh_client:
raise MissingRequirementException(
'Install paramiko package to enable ssh:// support',
'paramiko',
PARAMIKO_IMPORT_ERROR)
"Install paramiko package to enable ssh:// support",
"paramiko",
PARAMIKO_IMPORT_ERROR,
)
self._custom_adapter = SSHHTTPAdapter(
base_url, timeout, pool_connections=num_pools,
max_pool_size=max_pool_size, shell_out=use_ssh_client
base_url,
timeout,
pool_connections=num_pools,
max_pool_size=max_pool_size,
shell_out=use_ssh_client,
)
self.mount('http+docker://ssh', self._custom_adapter)
self._unmount('http://', 'https://')
self.base_url = 'http+docker://ssh'
self.mount("http+docker://ssh", self._custom_adapter)
self._unmount("http://", "https://")
self.base_url = "http+docker://ssh"
else:
# Use SSLAdapter for the ability to specify SSL version
if isinstance(tls, TLSConfig):
tls.configure_client(self)
elif tls:
self._custom_adapter = SSLHTTPAdapter(
pool_connections=num_pools)
self.mount('https://', self._custom_adapter)
self._custom_adapter = SSLHTTPAdapter(pool_connections=num_pools)
self.mount("https://", self._custom_adapter)
self.base_url = base_url
# version detection needs to be after unix adapter mounting
if version is None or (isinstance(version, str) and version.lower() == 'auto'):
if version is None or (isinstance(version, str) and version.lower() == "auto"):
self._version = self._retrieve_server_version()
else:
self._version = version
if not isinstance(self._version, str):
raise DockerException(
f'Version parameter must be a string or None. Found {type(version).__name__}'
f"Version parameter must be a string or None. Found {type(version).__name__}"
)
if utils.version_lt(self._version, MINIMUM_DOCKER_API_VERSION):
raise InvalidVersion(
f'API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library.'
f"API versions below {MINIMUM_DOCKER_API_VERSION} are no longer supported by this library."
)
def _retrieve_server_version(self):
try:
version_result = self.version(api_version=False)
except Exception as e:
raise DockerException(
f'Error while fetching server API version: {e}'
)
raise DockerException(f"Error while fetching server API version: {e}")
try:
return version_result["ApiVersion"]
except KeyError:
raise DockerException(
'Invalid response from docker daemon: key "ApiVersion"'
' is missing.'
'Invalid response from docker daemon: key "ApiVersion"' " is missing."
)
except Exception as e:
raise DockerException(
f'Error while fetching server API version: {e}. Response seems to be broken.'
f"Error while fetching server API version: {e}. Response seems to be broken."
)
def _set_request_timeout(self, kwargs):
"""Prepare the kwargs for an HTTP request by inserting the timeout
parameter, if not already present."""
kwargs.setdefault('timeout', self.timeout)
kwargs.setdefault("timeout", self.timeout)
return kwargs
@update_headers
@@ -244,16 +268,16 @@ class APIClient(
for arg in args:
if not isinstance(arg, str):
raise ValueError(
f'Expected a string but found {arg} ({type(arg)}) instead'
f"Expected a string but found {arg} ({type(arg)}) instead"
)
quote_f = partial(quote, safe="/:")
args = map(quote_f, args)
if kwargs.get('versioned_api', True):
return f'{self.base_url}/v{self._version}{pathfmt.format(*args)}'
if kwargs.get("versioned_api", True):
return f"{self.base_url}/v{self._version}{pathfmt.format(*args)}"
else:
return f'{self.base_url}{pathfmt.format(*args)}'
return f"{self.base_url}{pathfmt.format(*args)}"
def _raise_for_status(self, response):
"""Raises stored :class:`APIError`, if one occurred."""
@@ -264,7 +288,7 @@ class APIClient(
def _result(self, response, json=False, binary=False):
if json and binary:
raise AssertionError('json and binary must not be both True')
raise AssertionError("json and binary must not be both True")
self._raise_for_status(response)
if json:
@@ -284,23 +308,19 @@ class APIClient(
elif data is not None:
data2 = data
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Content-Type'] = 'application/json'
if "headers" not in kwargs:
kwargs["headers"] = {}
kwargs["headers"]["Content-Type"] = "application/json"
return self._post(url, data=json.dumps(data2), **kwargs)
def _attach_params(self, override=None):
return override or {
'stdout': 1,
'stderr': 1,
'stream': 1
}
return override or {"stdout": 1, "stderr": 1, "stream": 1}
def _get_raw_response_socket(self, response):
self._raise_for_status(response)
if self.base_url == "http+docker://localnpipe":
sock = response.raw._fp.fp.raw.sock
elif self.base_url.startswith('http+docker://ssh'):
elif self.base_url.startswith("http+docker://ssh"):
sock = response.raw._fp.fp.channel
else:
sock = response.raw._fp.fp.raw
@@ -348,8 +368,8 @@ class APIClient(
while True:
if buf_length - walker < STREAM_HEADER_SIZE_BYTES:
break
header = buf[walker:walker + STREAM_HEADER_SIZE_BYTES]
dummy, length = struct.unpack_from('>BxxxL', header)
header = buf[walker : walker + STREAM_HEADER_SIZE_BYTES]
dummy, length = struct.unpack_from(">BxxxL", header)
start = walker + STREAM_HEADER_SIZE_BYTES
end = start + length
walker = end
@@ -368,7 +388,7 @@ class APIClient(
header = response.raw.read(STREAM_HEADER_SIZE_BYTES)
if not header:
break
dummy, length = struct.unpack('>BxxxL', header)
dummy, length = struct.unpack(">BxxxL", header)
if not length:
continue
data = response.raw.read(length)
@@ -377,7 +397,7 @@ class APIClient(
yield data
def _stream_raw_result(self, response, chunk_size=1, decode=True):
''' Stream result for TTY-enabled container and raw binary data'''
"""Stream result for TTY-enabled container and raw binary data"""
self._raise_for_status(response)
# Disable timeout on the underlying socket to prevent
@@ -413,7 +433,7 @@ class APIClient(
response.close()
def _disable_socket_timeout(self, socket):
""" Depending on the combination of python version and whether we are
"""Depending on the combination of python version and whether we are
connecting over http or https, we might need to access _sock, which
may or may not exist; or we may need to just settimeout on socket
itself, which also may or may not have settimeout on it. To avoid
@@ -423,15 +443,15 @@ class APIClient(
you run the risk of changing a socket that was non-blocking to
blocking, for example when using gevent.
"""
sockets = [socket, getattr(socket, '_sock', None)]
sockets = [socket, getattr(socket, "_sock", None)]
for s in sockets:
if not hasattr(s, 'settimeout'):
if not hasattr(s, "settimeout"):
continue
timeout = -1
if hasattr(s, 'gettimeout'):
if hasattr(s, "gettimeout"):
timeout = s.gettimeout()
# Do not change the timeout if it is already disabled.
@@ -440,10 +460,10 @@ class APIClient(
s.settimeout(None)
@check_resource('container')
@check_resource("container")
def _check_is_tty(self, container):
cont = self.inspect_container(container)
return cont['Config']['Tty']
return cont["Config"]["Tty"]
def _get_result(self, container, stream, res):
return self._get_result_tty(stream, res, self._check_is_tty(container))
@@ -452,17 +472,18 @@ class APIClient(
# We should also use raw streaming (without keep-alive)
# if we are dealing with a tty-enabled container.
if is_tty:
return self._stream_raw_result(res) if stream else \
self._result(res, binary=True)
return (
self._stream_raw_result(res)
if stream
else self._result(res, binary=True)
)
self._raise_for_status(res)
sep = b''
sep = b""
if stream:
return self._multiplexed_response_stream_helper(res)
else:
return sep.join(
list(self._multiplexed_buffer_helper(res))
)
return sep.join(list(self._multiplexed_buffer_helper(res)))
def _unmount(self, *args):
for proto in args:
@@ -498,15 +519,13 @@ class APIClient(
)
def _set_auth_headers(self, headers):
log.debug('Looking for auth config')
log.debug("Looking for auth config")
# If we do not have any auth data so far, try reloading the config
# file one more time in case anything showed up in there.
if not self._auth_configs or self._auth_configs.is_empty:
log.debug("No auth config in memory - loading from filesystem")
self._auth_configs = auth.load_config(
credstore_env=self.credstore_env
)
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
# Send the full auth configuration (if any exists), since the build
# could use any (or all) of the registries.
@@ -514,87 +533,134 @@ class APIClient(
auth_data = self._auth_configs.get_all_credentials()
# See https://github.com/docker/docker-py/issues/1683
if (auth.INDEX_URL not in auth_data and
auth.INDEX_NAME in auth_data):
if auth.INDEX_URL not in auth_data and auth.INDEX_NAME in auth_data:
auth_data[auth.INDEX_URL] = auth_data.get(auth.INDEX_NAME, {})
log.debug(
'Sending auth config (%s)',
', '.join(repr(k) for k in auth_data.keys())
"Sending auth config (%s)", ", ".join(repr(k) for k in auth_data.keys())
)
if auth_data:
headers['X-Registry-Config'] = auth.encode_header(
auth_data
)
headers["X-Registry-Config"] = auth.encode_header(auth_data)
else:
log.debug('No auth config found')
log.debug("No auth config found")
def get_binary(self, pathfmt, *args, **kwargs):
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs), binary=True)
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
binary=True,
)
def get_json(self, pathfmt, *args, **kwargs):
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
json=True,
)
def get_text(self, pathfmt, *args, **kwargs):
return self._result(self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs))
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def get_raw_stream(self, pathfmt, *args, **kwargs):
chunk_size = kwargs.pop('chunk_size', DEFAULT_DATA_CHUNK_SIZE)
res = self._get(self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs)
chunk_size = kwargs.pop("chunk_size", DEFAULT_DATA_CHUNK_SIZE)
res = self._get(
self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs
)
self._raise_for_status(res)
return self._stream_raw_result(res, chunk_size, False)
def delete_call(self, pathfmt, *args, **kwargs):
self._raise_for_status(self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs))
self._raise_for_status(
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def delete_json(self, pathfmt, *args, **kwargs):
return self._result(self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
return self._result(
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs),
json=True,
)
def post_call(self, pathfmt, *args, **kwargs):
self._raise_for_status(self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs))
self._raise_for_status(
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def post_json(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
self._raise_for_status(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs))
data = kwargs.pop("data", None)
self._raise_for_status(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
)
)
def post_json_to_binary(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
return self._result(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs), binary=True)
data = kwargs.pop("data", None)
return self._result(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
),
binary=True,
)
def post_json_to_json(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
return self._result(self._post_json(self._url(pathfmt, *args, versioned_api=True), data, **kwargs), json=True)
data = kwargs.pop("data", None)
return self._result(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
),
json=True,
)
def post_json_to_text(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
data = kwargs.pop("data", None)
def post_json_to_stream_socket(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
headers = (kwargs.pop('headers', None) or {}).copy()
headers.update({
'Connection': 'Upgrade',
'Upgrade': 'tcp',
})
data = kwargs.pop("data", None)
headers = (kwargs.pop("headers", None) or {}).copy()
headers.update(
{
"Connection": "Upgrade",
"Upgrade": "tcp",
}
)
return self._get_raw_response_socket(
self._post_json(self._url(pathfmt, *args, versioned_api=True), data, headers=headers, stream=True, **kwargs))
self._post_json(
self._url(pathfmt, *args, versioned_api=True),
data,
headers=headers,
stream=True,
**kwargs,
)
)
def post_json_to_stream(self, pathfmt, *args, **kwargs):
data = kwargs.pop('data', None)
headers = (kwargs.pop('headers', None) or {}).copy()
headers.update({
'Connection': 'Upgrade',
'Upgrade': 'tcp',
})
stream = kwargs.pop('stream', False)
demux = kwargs.pop('demux', False)
tty = kwargs.pop('tty', False)
data = kwargs.pop("data", None)
headers = (kwargs.pop("headers", None) or {}).copy()
headers.update(
{
"Connection": "Upgrade",
"Upgrade": "tcp",
}
)
stream = kwargs.pop("stream", False)
demux = kwargs.pop("demux", False)
tty = kwargs.pop("tty", False)
return self._read_from_socket(
self._post_json(self._url(pathfmt, *args, versioned_api=True), data, headers=headers, stream=True, **kwargs),
self._post_json(
self._url(pathfmt, *args, versioned_api=True),
data,
headers=headers,
stream=True,
**kwargs,
),
stream,
tty=tty,
demux=demux
demux=demux,
)
def post_to_json(self, pathfmt, *args, **kwargs):
return self._result(self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs), json=True)
return self._result(
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs),
json=True,
)
+19 -15
View File
@@ -16,7 +16,7 @@ from ..utils.decorators import minimum_version
class DaemonApiMixin(object):
@minimum_version('1.25')
@minimum_version("1.25")
def df(self):
"""
Get data usage information.
@@ -29,7 +29,7 @@ class DaemonApiMixin(object):
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
url = self._url('/system/df')
url = self._url("/system/df")
return self._result(self._get(url), True)
def info(self):
@@ -46,8 +46,15 @@ class DaemonApiMixin(object):
"""
return self._result(self._get(self._url("/info")), True)
def login(self, username, password=None, email=None, registry=None,
reauth=False, dockercfg_path=None):
def login(
self,
username,
password=None,
email=None,
registry=None,
reauth=False,
dockercfg_path=None,
):
"""
Authenticate with a registry. Similar to the ``docker login`` command.
@@ -80,25 +87,22 @@ class DaemonApiMixin(object):
dockercfg_path, credstore_env=self.credstore_env
)
elif not self._auth_configs or self._auth_configs.is_empty:
self._auth_configs = auth.load_config(
credstore_env=self.credstore_env
)
self._auth_configs = auth.load_config(credstore_env=self.credstore_env)
authcfg = self._auth_configs.resolve_authconfig(registry)
# If we found an existing auth config for this registry and username
# combination, we can return it immediately unless reauth is requested.
if authcfg and authcfg.get('username', None) == username \
and not reauth:
if authcfg and authcfg.get("username", None) == username and not reauth:
return authcfg
req_data = {
'username': username,
'password': password,
'email': email,
'serveraddress': registry,
"username": username,
"password": password,
"email": email,
"serveraddress": registry,
}
response = self._post_json(self._url('/auth'), data=req_data)
response = self._post_json(self._url("/auth"), data=req_data)
if response.status_code == 200:
self._auth_configs.add_auth(registry or auth.INDEX_NAME, req_data)
return self._result(response, json=True)
@@ -115,7 +119,7 @@ class DaemonApiMixin(object):
:py:class:`docker.errors.APIError`
If the server returns an error.
"""
return self._result(self._get(self._url('/_ping'))) == 'OK'
return self._result(self._get(self._url("/_ping"))) == "OK"
def version(self, api_version=True):
"""
+81 -89
View File
@@ -14,44 +14,43 @@ import json
import logging
from . import errors
from .credentials.errors import CredentialsNotFound, StoreError
from .credentials.store import Store
from .credentials.errors import StoreError, CredentialsNotFound
from .utils import config
INDEX_NAME = 'docker.io'
INDEX_URL = f'https://index.{INDEX_NAME}/v1/'
TOKEN_USERNAME = '<token>'
INDEX_NAME = "docker.io"
INDEX_URL = f"https://index.{INDEX_NAME}/v1/"
TOKEN_USERNAME = "<token>"
log = logging.getLogger(__name__)
def resolve_repository_name(repo_name):
if '://' in repo_name:
if "://" in repo_name:
raise errors.InvalidRepository(
f'Repository name cannot contain a scheme ({repo_name})'
f"Repository name cannot contain a scheme ({repo_name})"
)
index_name, remote_name = split_repo_name(repo_name)
if index_name[0] == '-' or index_name[-1] == '-':
if index_name[0] == "-" or index_name[-1] == "-":
raise errors.InvalidRepository(
f'Invalid index name ({index_name}). Cannot begin or end with a hyphen.'
f"Invalid index name ({index_name}). Cannot begin or end with a hyphen."
)
return resolve_index_name(index_name), remote_name
def resolve_index_name(index_name):
index_name = convert_to_hostname(index_name)
if index_name == 'index.' + INDEX_NAME:
if index_name == "index." + INDEX_NAME:
index_name = INDEX_NAME
return index_name
def get_config_header(client, registry):
log.debug('Looking for auth config')
log.debug("Looking for auth config")
if not client._auth_configs or client._auth_configs.is_empty:
log.debug(
"No auth config in memory - loading from filesystem"
)
log.debug("No auth config in memory - loading from filesystem")
client._auth_configs = load_config(credstore_env=client.credstore_env)
authcfg = resolve_authconfig(
client._auth_configs, registry, credstore_env=client.credstore_env
@@ -60,18 +59,18 @@ def get_config_header(client, registry):
# specific registry as we can have a readonly pull. Just
# put the header if we can.
if authcfg:
log.debug('Found auth config')
log.debug("Found auth config")
# auth_config needs to be a dict in the format used by
# auth.py username , password, serveraddress, email
return encode_header(authcfg)
log.debug('No auth config found')
log.debug("No auth config found")
return None
def split_repo_name(repo_name):
parts = repo_name.split('/', 1)
parts = repo_name.split("/", 1)
if len(parts) == 1 or (
'.' not in parts[0] and ':' not in parts[0] and parts[0] != 'localhost'
"." not in parts[0] and ":" not in parts[0] and parts[0] != "localhost"
):
# This is a docker index repo (ex: username/foobar or ubuntu)
return INDEX_NAME, repo_name
@@ -86,8 +85,8 @@ def get_credential_store(authconfig, registry):
class AuthConfig(dict):
def __init__(self, dct, credstore_env=None):
if 'auths' not in dct:
dct['auths'] = {}
if "auths" not in dct:
dct["auths"] = {}
self.update(dct)
self._credstore_env = credstore_env
self._stores = {}
@@ -109,39 +108,42 @@ class AuthConfig(dict):
conf = {}
for registry, entry in entries.items():
if not isinstance(entry, dict):
log.debug('Config entry for key %s is not auth config', registry)
log.debug("Config entry for key %s is not auth config", registry)
# We sometimes fall back to parsing the whole config as if it
# was the auth config by itself, for legacy purposes. In that
# case, we fail silently and return an empty conf if any of the
# keys is not formatted properly.
if raise_on_error:
raise errors.InvalidConfigFile(
f'Invalid configuration for registry {registry}'
f"Invalid configuration for registry {registry}"
)
return {}
if 'identitytoken' in entry:
log.debug('Found an IdentityToken entry for registry %s', registry)
conf[registry] = {
'IdentityToken': entry['identitytoken']
}
if "identitytoken" in entry:
log.debug("Found an IdentityToken entry for registry %s", registry)
conf[registry] = {"IdentityToken": entry["identitytoken"]}
continue # Other values are irrelevant if we have a token
if 'auth' not in entry:
if "auth" not in entry:
# Starting with engine v1.11 (API 1.23), an empty dictionary is
# a valid value in the auths config.
# https://github.com/docker/compose/issues/3265
log.debug('Auth data for %s is absent. Client might be using a credentials store instead.', registry)
log.debug(
"Auth data for %s is absent. Client might be using a credentials store instead.",
registry,
)
conf[registry] = {}
continue
username, password = decode_auth(entry['auth'])
log.debug('Found entry (registry=%s, username=%s)', repr(registry), repr(username))
username, password = decode_auth(entry["auth"])
log.debug(
"Found entry (registry=%s, username=%s)", repr(registry), repr(username)
)
conf[registry] = {
'username': username,
'password': password,
'email': entry.get('email'),
'serveraddress': registry,
"username": username,
"password": password,
"email": entry.get("email"),
"serveraddress": registry,
}
return conf
@@ -171,19 +173,17 @@ class AuthConfig(dict):
return cls(_load_legacy_config(config_file), credstore_env)
res = {}
if config_dict.get('auths'):
if config_dict.get("auths"):
log.debug("Found 'auths' section")
res.update({
'auths': cls.parse_auth(
config_dict.pop('auths'), raise_on_error=True
)
})
if config_dict.get('credsStore'):
res.update(
{"auths": cls.parse_auth(config_dict.pop("auths"), raise_on_error=True)}
)
if config_dict.get("credsStore"):
log.debug("Found 'credsStore' section")
res.update({'credsStore': config_dict.pop('credsStore')})
if config_dict.get('credHelpers'):
res.update({"credsStore": config_dict.pop("credsStore")})
if config_dict.get("credHelpers"):
log.debug("Found 'credHelpers' section")
res.update({'credHelpers': config_dict.pop('credHelpers')})
res.update({"credHelpers": config_dict.pop("credHelpers")})
if res:
return cls(res, credstore_env)
@@ -191,25 +191,23 @@ class AuthConfig(dict):
"Could not find auth-related section ; attempting to interpret "
"as auth-only file"
)
return cls({'auths': cls.parse_auth(config_dict)}, credstore_env)
return cls({"auths": cls.parse_auth(config_dict)}, credstore_env)
@property
def auths(self):
return self.get('auths', {})
return self.get("auths", {})
@property
def creds_store(self):
return self.get('credsStore', None)
return self.get("credsStore", None)
@property
def cred_helpers(self):
return self.get('credHelpers', {})
return self.get("credHelpers", {})
@property
def is_empty(self):
return (
not self.auths and not self.creds_store and not self.cred_helpers
)
return not self.auths and not self.creds_store and not self.cred_helpers
def resolve_authconfig(self, registry=None):
"""
@@ -226,7 +224,7 @@ class AuthConfig(dict):
cfg = self._resolve_authconfig_credstore(registry, store_name)
if cfg is not None:
return cfg
log.debug('No entry in credstore - fetching from auth dict')
log.debug("No entry in credstore - fetching from auth dict")
# Default to the public index server
registry = resolve_index_name(registry) if registry else INDEX_NAME
@@ -254,29 +252,27 @@ class AuthConfig(dict):
try:
data = store.get(registry)
res = {
'ServerAddress': registry,
"ServerAddress": registry,
}
if data['Username'] == TOKEN_USERNAME:
res['IdentityToken'] = data['Secret']
if data["Username"] == TOKEN_USERNAME:
res["IdentityToken"] = data["Secret"]
else:
res.update({
'Username': data['Username'],
'Password': data['Secret'],
})
res.update(
{
"Username": data["Username"],
"Password": data["Secret"],
}
)
return res
except CredentialsNotFound:
log.debug('No entry found')
log.debug("No entry found")
return None
except StoreError as e:
raise errors.DockerException(
f'Credentials store error: {e}'
)
raise errors.DockerException(f"Credentials store error: {e}")
def _get_store_instance(self, name):
if name not in self._stores:
self._stores[name] = Store(
name, environment=self._credstore_env
)
self._stores[name] = Store(name, environment=self._credstore_env)
return self._stores[name]
def get_credential_store(self, registry):
@@ -291,22 +287,18 @@ class AuthConfig(dict):
# Retrieve all credentials from the default store
store = self._get_store_instance(self.creds_store)
for k in store.list().keys():
auth_data[k] = self._resolve_authconfig_credstore(
k, self.creds_store
)
auth_data[k] = self._resolve_authconfig_credstore(k, self.creds_store)
auth_data[convert_to_hostname(k)] = auth_data[k]
# credHelpers entries take priority over all others
for reg, store_name in self.cred_helpers.items():
auth_data[reg] = self._resolve_authconfig_credstore(
reg, store_name
)
auth_data[reg] = self._resolve_authconfig_credstore(reg, store_name)
auth_data[convert_to_hostname(reg)] = auth_data[reg]
return auth_data
def add_auth(self, reg, data):
self['auths'][reg] = data
self["auths"][reg] = data
def resolve_authconfig(authconfig, registry=None, credstore_env=None):
@@ -316,19 +308,19 @@ def resolve_authconfig(authconfig, registry=None, credstore_env=None):
def convert_to_hostname(url):
return url.replace('http://', '').replace('https://', '').split('/', 1)[0]
return url.replace("http://", "").replace("https://", "").split("/", 1)[0]
def decode_auth(auth):
if isinstance(auth, str):
auth = auth.encode('ascii')
auth = auth.encode("ascii")
s = base64.b64decode(auth)
login, pwd = s.split(b':', 1)
return login.decode('utf8'), pwd.decode('utf8')
login, pwd = s.split(b":", 1)
return login.decode("utf8"), pwd.decode("utf8")
def encode_header(auth):
auth_json = json.dumps(auth).encode('ascii')
auth_json = json.dumps(auth).encode("ascii")
return base64.urlsafe_b64encode(auth_json)
@@ -358,22 +350,22 @@ def _load_legacy_config(config_file):
data = []
with open(config_file) as f:
for line in f.readlines():
data.append(line.strip().split(' = ')[1])
data.append(line.strip().split(" = ")[1])
if len(data) < 2:
# Not enough data
raise errors.InvalidConfigFile(
'Invalid or empty configuration file!'
)
raise errors.InvalidConfigFile("Invalid or empty configuration file!")
username, password = decode_auth(data[0])
return {'auths': {
INDEX_NAME: {
'username': username,
'password': password,
'email': data[1],
'serveraddress': INDEX_URL,
return {
"auths": {
INDEX_NAME: {
"username": username,
"password": password,
"email": data[1],
"serveraddress": INDEX_URL,
}
}
}}
}
except Exception as e:
log.debug(e)
pass
+7 -13
View File
@@ -11,26 +11,20 @@ from __future__ import annotations
import sys
MINIMUM_DOCKER_API_VERSION = '1.21'
MINIMUM_DOCKER_API_VERSION = "1.21"
DEFAULT_TIMEOUT_SECONDS = 60
STREAM_HEADER_SIZE_BYTES = 8
CONTAINER_LIMITS_KEYS = [
'memory', 'memswap', 'cpushares', 'cpusetcpus'
]
CONTAINER_LIMITS_KEYS = ["memory", "memswap", "cpushares", "cpusetcpus"]
DEFAULT_HTTP_HOST = "127.0.0.1"
DEFAULT_UNIX_SOCKET = "http+unix:///var/run/docker.sock"
DEFAULT_NPIPE = 'npipe:////./pipe/docker_engine'
DEFAULT_NPIPE = "npipe:////./pipe/docker_engine"
BYTE_UNITS = {
'b': 1,
'k': 1024,
'm': 1024 * 1024,
'g': 1024 * 1024 * 1024
}
BYTE_UNITS = {"b": 1, "k": 1024, "m": 1024 * 1024, "g": 1024 * 1024 * 1024}
IS_WINDOWS_PLATFORM = (sys.platform == 'win32')
WINDOWS_LONGPATH_PREFIX = '\\\\?\\'
IS_WINDOWS_PLATFORM = sys.platform == "win32"
WINDOWS_LONGPATH_PREFIX = "\\\\?\\"
DEFAULT_USER_AGENT = "ansible-community.docker"
DEFAULT_NUM_POOLS = 25
+22 -14
View File
@@ -13,7 +13,6 @@ import json
import os
from .. import errors
from .config import (
METAFILE,
get_current_context_name,
@@ -25,9 +24,11 @@ from .context import Context
def create_default_context():
host = None
if os.environ.get('DOCKER_HOST'):
host = os.environ.get('DOCKER_HOST')
return Context("default", "swarm", host, description="Current DOCKER_HOST based configuration")
if os.environ.get("DOCKER_HOST"):
host = os.environ.get("DOCKER_HOST")
return Context(
"default", "swarm", host, description="Current DOCKER_HOST based configuration"
)
class ContextAPI(object):
@@ -35,6 +36,7 @@ class ContextAPI(object):
Contains methods for context management:
create, list, remove, get, inspect.
"""
DEFAULT_CONTEXT = None
@classmethod
@@ -47,8 +49,14 @@ class ContextAPI(object):
@classmethod
def create_context(
cls, name, orchestrator=None, host=None, tls_cfg=None,
default_namespace=None, skip_tls_verify=False):
cls,
name,
orchestrator=None,
host=None,
tls_cfg=None,
default_namespace=None,
skip_tls_verify=False,
):
"""Creates a new context.
Returns:
(Context): a Context object.
@@ -79,8 +87,7 @@ class ContextAPI(object):
if not name:
raise errors.MissingContextParameter("name")
if name == "default":
raise errors.ContextException(
'"default" is a reserved context name')
raise errors.ContextException('"default" is a reserved context name')
ctx = Context.load_context(name)
if ctx:
raise errors.ContextAlreadyExists(name)
@@ -89,9 +96,12 @@ class ContextAPI(object):
endpoint = orchestrator
ctx = Context(name, orchestrator)
ctx.set_endpoint(
endpoint, host, tls_cfg,
endpoint,
host,
tls_cfg,
skip_tls_verify=skip_tls_verify,
def_namespace=default_namespace)
def_namespace=default_namespace,
)
ctx.save()
return ctx
@@ -173,8 +183,7 @@ class ContextAPI(object):
err = write_context_name_to_docker_config(name)
if err:
raise errors.ContextException(
f'Failed to set current context: {err}')
raise errors.ContextException(f"Failed to set current context: {err}")
@classmethod
def remove_context(cls, name):
@@ -200,8 +209,7 @@ class ContextAPI(object):
if not name:
raise errors.MissingContextParameter("name")
if name == "default":
raise errors.ContextException(
'context "default" cannot be removed')
raise errors.ContextException('context "default" cannot be removed')
ctx = Context.load_context(name)
if not ctx:
raise errors.ContextNotFound(name)
+10 -6
View File
@@ -17,19 +17,23 @@ from ..constants import DEFAULT_UNIX_SOCKET, IS_WINDOWS_PLATFORM
from ..utils.config import find_config_file, get_default_config_file
from ..utils.utils import parse_host
METAFILE = "meta.json"
def get_current_context_name_with_source():
if os.environ.get('DOCKER_HOST'):
if os.environ.get("DOCKER_HOST"):
return "default", "DOCKER_HOST environment variable set"
if os.environ.get('DOCKER_CONTEXT'):
return os.environ['DOCKER_CONTEXT'], "DOCKER_CONTEXT environment variable set"
if os.environ.get("DOCKER_CONTEXT"):
return os.environ["DOCKER_CONTEXT"], "DOCKER_CONTEXT environment variable set"
docker_cfg_path = find_config_file()
if docker_cfg_path:
try:
with open(docker_cfg_path) as f:
return json.load(f).get("currentContext", "default"), f"configuration file {docker_cfg_path}"
return (
json.load(f).get("currentContext", "default"),
f"configuration file {docker_cfg_path}",
)
except Exception:
pass
return "default", "fallback value"
@@ -40,7 +44,7 @@ def get_current_context_name():
def write_context_name_to_docker_config(name=None):
if name == 'default':
if name == "default":
name = None
docker_cfg_path = find_config_file()
config = {}
@@ -67,7 +71,7 @@ def write_context_name_to_docker_config(name=None):
def get_context_id(name):
return hashlib.sha256(name.encode('utf-8')).hexdigest()
return hashlib.sha256(name.encode("utf-8")).hexdigest()
def get_context_dir():
+56 -39
View File
@@ -15,7 +15,6 @@ from shutil import copyfile, rmtree
from ..errors import ContextException
from ..tls import TLSConfig
from .config import (
get_context_host,
get_meta_dir,
@@ -30,8 +29,16 @@ IN_MEMORY = "IN MEMORY"
class Context(object):
"""A context."""
def __init__(self, name, orchestrator=None, host=None, endpoints=None,
skip_tls_verify=False, tls=False, description=None):
def __init__(
self,
name,
orchestrator=None,
host=None,
endpoints=None,
skip_tls_verify=False,
tls=False,
description=None,
):
if not name:
raise Exception("Name not provided")
self.name = name
@@ -45,9 +52,11 @@ class Context(object):
if not endpoints:
# set default docker endpoint if no endpoint is set
default_endpoint = "docker" if (
not orchestrator or orchestrator == "swarm"
) else orchestrator
default_endpoint = (
"docker"
if (not orchestrator or orchestrator == "swarm")
else orchestrator
)
self.endpoints = {
default_endpoint: {
@@ -69,17 +78,24 @@ class Context(object):
if k != "docker":
continue
self.endpoints[k]["Host"] = v.get("Host", get_context_host(
host, skip_tls_verify or tls))
self.endpoints[k]["SkipTLSVerify"] = bool(v.get(
"SkipTLSVerify", skip_tls_verify))
self.endpoints[k]["Host"] = v.get(
"Host", get_context_host(host, skip_tls_verify or tls)
)
self.endpoints[k]["SkipTLSVerify"] = bool(
v.get("SkipTLSVerify", skip_tls_verify)
)
def set_endpoint(
self, name="docker", host=None, tls_cfg=None,
skip_tls_verify=False, def_namespace=None):
self,
name="docker",
host=None,
tls_cfg=None,
skip_tls_verify=False,
def_namespace=None,
):
self.endpoints[name] = {
"Host": get_context_host(host, not skip_tls_verify or tls_cfg is not None),
"SkipTLSVerify": skip_tls_verify
"SkipTLSVerify": skip_tls_verify,
}
if def_namespace:
self.endpoints[name]["DefaultNamespace"] = def_namespace
@@ -98,7 +114,8 @@ class Context(object):
meta["Name"],
orchestrator=meta["Metadata"].get("StackOrchestrator", None),
endpoints=meta.get("Endpoints", None),
description=meta["Metadata"].get('Description'))
description=meta["Metadata"].get("Description"),
)
instance.context_type = meta["Metadata"].get("Type", None)
instance._load_certs()
instance.meta_path = get_meta_dir(name)
@@ -127,9 +144,11 @@ class Context(object):
if k != "docker":
continue
metadata["Endpoints"][k]["Host"] = v.get(
"Host", get_context_host(None, False))
"Host", get_context_host(None, False)
)
metadata["Endpoints"][k]["SkipTLSVerify"] = bool(
v.get("SkipTLSVerify", True))
v.get("SkipTLSVerify", True)
)
return metadata
@@ -152,10 +171,14 @@ class Context(object):
if all([cert, key]) or ca_cert:
verify = None
if endpoint == "docker" and not self.endpoints["docker"].get(
"SkipTLSVerify", False):
"SkipTLSVerify", False
):
verify = True
certs[endpoint] = TLSConfig(
client_cert=(cert, key) if cert and key else None, ca_cert=ca_cert, verify=verify)
client_cert=(cert, key) if cert and key else None,
ca_cert=ca_cert,
verify=verify,
)
self.tls_cfg = certs
self.tls_path = tls_dir
@@ -173,15 +196,20 @@ class Context(object):
ca_file = tls.ca_cert
if ca_file:
copyfile(ca_file, os.path.join(
tls_dir, endpoint, os.path.basename(ca_file)))
copyfile(
ca_file, os.path.join(tls_dir, endpoint, os.path.basename(ca_file))
)
if tls.cert:
cert_file, key_file = tls.cert
copyfile(cert_file, os.path.join(
tls_dir, endpoint, os.path.basename(cert_file)))
copyfile(key_file, os.path.join(
tls_dir, endpoint, os.path.basename(key_file)))
copyfile(
cert_file,
os.path.join(tls_dir, endpoint, os.path.basename(cert_file)),
)
copyfile(
key_file,
os.path.join(tls_dir, endpoint, os.path.basename(key_file)),
)
self.meta_path = get_meta_dir(self.name)
self.tls_path = get_tls_dir(self.name)
@@ -230,11 +258,7 @@ class Context(object):
meta = {}
if self.orchestrator:
meta = {"StackOrchestrator": self.orchestrator}
return {
"Name": self.name,
"Metadata": meta,
"Endpoints": self.endpoints
}
return {"Name": self.name, "Metadata": meta, "Endpoints": self.endpoints}
@property
def TLSConfig(self):
@@ -250,16 +274,9 @@ class Context(object):
certs = {}
for endpoint, tls in self.tls_cfg.items():
cert, key = tls.cert
certs[endpoint] = list(
map(os.path.basename, [tls.ca_cert, cert, key]))
return {
"TLSMaterial": certs
}
certs[endpoint] = list(map(os.path.basename, [tls.ca_cert, cert, key]))
return {"TLSMaterial": certs}
@property
def Storage(self):
return {
"Storage": {
"MetadataPath": self.meta_path,
"TLSPath": self.tls_path
}}
return {"Storage": {"MetadataPath": self.meta_path, "TLSPath": self.tls_path}}
@@ -9,7 +9,8 @@
from __future__ import annotations
PROGRAM_PREFIX = 'docker-credential-'
DEFAULT_LINUX_STORE = 'secretservice'
DEFAULT_OSX_STORE = 'osxkeychain'
DEFAULT_WIN32_STORE = 'wincred'
PROGRAM_PREFIX = "docker-credential-"
DEFAULT_LINUX_STORE = "secretservice"
DEFAULT_OSX_STORE = "osxkeychain"
DEFAULT_WIN32_STORE = "wincred"
@@ -23,11 +23,9 @@ class InitializationError(StoreError):
def process_store_error(cpe, program):
message = cpe.output.decode('utf-8')
if 'credentials not found in native keychain' in message:
return CredentialsNotFound(
f'No matching credentials in {program}'
)
message = cpe.output.decode("utf-8")
if "credentials not found in native keychain" in message:
return CredentialsNotFound(f"No matching credentials in {program}")
return StoreError(
f'Credentials store {program} exited with "{cpe.output.decode("utf-8").strip()}".'
)
+30 -33
View File
@@ -13,84 +13,81 @@ import errno
import json
import subprocess
from . import constants
from . import errors
from .utils import create_environment_dict
from .utils import find_executable
from . import constants, errors
from .utils import create_environment_dict, find_executable
class Store(object):
def __init__(self, program, environment=None):
""" Create a store object that acts as an interface to
perform the basic operations for storing, retrieving
and erasing credentials using `program`.
"""Create a store object that acts as an interface to
perform the basic operations for storing, retrieving
and erasing credentials using `program`.
"""
self.program = constants.PROGRAM_PREFIX + program
self.exe = find_executable(self.program)
self.environment = environment
if self.exe is None:
raise errors.InitializationError(
f'{self.program} not installed or not available in PATH'
f"{self.program} not installed or not available in PATH"
)
def get(self, server):
""" Retrieve credentials for `server`. If no credentials are found,
a `StoreError` will be raised.
"""Retrieve credentials for `server`. If no credentials are found,
a `StoreError` will be raised.
"""
if not isinstance(server, bytes):
server = server.encode('utf-8')
data = self._execute('get', server)
result = json.loads(data.decode('utf-8'))
server = server.encode("utf-8")
data = self._execute("get", server)
result = json.loads(data.decode("utf-8"))
# docker-credential-pass will return an object for inexistent servers
# whereas other helpers will exit with returncode != 0. For
# consistency, if no significant data is returned,
# raise CredentialsNotFound
if result['Username'] == '' and result['Secret'] == '':
if result["Username"] == "" and result["Secret"] == "":
raise errors.CredentialsNotFound(
f'No matching credentials in {self.program}'
f"No matching credentials in {self.program}"
)
return result
def store(self, server, username, secret):
""" Store credentials for `server`. Raises a `StoreError` if an error
occurs.
"""Store credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
data_input = json.dumps({
'ServerURL': server,
'Username': username,
'Secret': secret
}).encode('utf-8')
return self._execute('store', data_input)
data_input = json.dumps(
{"ServerURL": server, "Username": username, "Secret": secret}
).encode("utf-8")
return self._execute("store", data_input)
def erase(self, server):
""" Erase credentials for `server`. Raises a `StoreError` if an error
occurs.
"""Erase credentials for `server`. Raises a `StoreError` if an error
occurs.
"""
if not isinstance(server, bytes):
server = server.encode('utf-8')
self._execute('erase', server)
server = server.encode("utf-8")
self._execute("erase", server)
def list(self):
""" List stored credentials. Requires v0.4.0+ of the helper.
"""
data = self._execute('list', None)
return json.loads(data.decode('utf-8'))
"""List stored credentials. Requires v0.4.0+ of the helper."""
data = self._execute("list", None)
return json.loads(data.decode("utf-8"))
def _execute(self, subcmd, data_input):
output = None
env = create_environment_dict(self.environment)
try:
output = subprocess.check_output(
[self.exe, subcmd], input=data_input, env=env,
[self.exe, subcmd],
input=data_input,
env=env,
)
except subprocess.CalledProcessError as e:
raise errors.process_store_error(e, self.program)
except OSError as e:
if e.errno == errno.ENOENT:
raise errors.StoreError(
f'{self.program} not installed or not available in PATH'
f"{self.program} not installed or not available in PATH"
)
else:
raise errors.StoreError(
@@ -10,7 +10,6 @@
from __future__ import annotations
import os
from shutil import which
+23 -18
View File
@@ -9,10 +9,10 @@
from __future__ import annotations
from ._import_helper import HTTPError as _HTTPError
from ansible.module_utils.common.text.converters import to_native
from ._import_helper import HTTPError as _HTTPError
class DockerException(Exception):
"""
@@ -29,15 +29,16 @@ def create_api_error_from_http_exception(e):
"""
response = e.response
try:
explanation = response.json()['message']
explanation = response.json()["message"]
except ValueError:
explanation = to_native((response.content or '').strip())
explanation = to_native((response.content or "").strip())
cls = APIError
if response.status_code == 404:
if explanation and ('No such image' in str(explanation) or
'not found: does not exist or no pull access'
in str(explanation) or
'repository does not exist' in str(explanation)):
if explanation and (
"No such image" in str(explanation)
or "not found: does not exist or no pull access" in str(explanation)
or "repository does not exist" in str(explanation)
):
cls = ImageNotFound
else:
cls = NotFound
@@ -48,6 +49,7 @@ class APIError(_HTTPError, DockerException):
"""
An HTTP error from the API.
"""
def __init__(self, message, response=None, explanation=None):
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 does not
@@ -59,10 +61,10 @@ class APIError(_HTTPError, DockerException):
message = super(APIError, self).__str__()
if self.is_client_error():
message = f'{self.response.status_code} Client Error for {self.response.url}: {self.response.reason}'
message = f"{self.response.status_code} Client Error for {self.response.url}: {self.response.reason}"
elif self.is_server_error():
message = f'{self.response.status_code} Server Error for {self.response.url}: {self.response.reason}'
message = f"{self.response.status_code} Server Error for {self.response.url}: {self.response.reason}"
if self.explanation:
message = f'{message} ("{self.explanation}")'
@@ -121,10 +123,12 @@ class TLSParameterError(DockerException):
self.msg = msg
def __str__(self):
return self.msg + (". TLS configurations should map the Docker CLI "
"client configurations. See "
"https://docs.docker.com/engine/articles/https/ "
"for API details.")
return self.msg + (
". TLS configurations should map the Docker CLI "
"client configurations. See "
"https://docs.docker.com/engine/articles/https/ "
"for API details."
)
class NullResource(DockerException, ValueError):
@@ -135,6 +139,7 @@ class ContainerError(DockerException):
"""
Represents a container that has exited with a non-zero exit code.
"""
def __init__(self, container, exit_status, command, image, stderr):
self.container = container
self.exit_status = exit_status
@@ -171,8 +176,8 @@ def create_unexpected_kwargs_error(name, kwargs):
text.append("got an unexpected keyword argument ")
else:
text.append("got unexpected keyword arguments ")
text.append(', '.join(quoted_kwargs))
return TypeError(''.join(text))
text.append(", ".join(quoted_kwargs))
return TypeError("".join(text))
class MissingContextParameter(DockerException):
@@ -196,7 +201,7 @@ class ContextException(DockerException):
self.msg = msg
def __str__(self):
return (self.msg)
return self.msg
class ContextNotFound(DockerException):
@@ -214,4 +219,4 @@ class MissingRequirementException(DockerException):
self.import_exception = import_exception
def __str__(self):
return (self.msg)
return self.msg
+23 -13
View File
@@ -31,13 +31,20 @@ class TLSConfig(object):
.. _`SSL version`:
https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLSv1
"""
cert = None
ca_cert = None
verify = None
ssl_version = None
def __init__(self, client_cert=None, ca_cert=None, verify=None,
ssl_version=None, assert_hostname=None):
def __init__(
self,
client_cert=None,
ca_cert=None,
verify=None,
ssl_version=None,
assert_hostname=None,
):
# Argument compatibility/mapping with
# https://docs.docker.com/engine/articles/https/
# This diverges from the Docker CLI in that users can specify 'tls'
@@ -61,15 +68,15 @@ class TLSConfig(object):
tls_cert, tls_key = client_cert
except ValueError:
raise errors.TLSParameterError(
'client_cert must be a tuple of'
' (client certificate, key file)'
"client_cert must be a tuple of" " (client certificate, key file)"
)
if not (tls_cert and tls_key) or (not os.path.isfile(tls_cert) or
not os.path.isfile(tls_key)):
if not (tls_cert and tls_key) or (
not os.path.isfile(tls_cert) or not os.path.isfile(tls_key)
):
raise errors.TLSParameterError(
'Path to a certificate and key files must be provided'
' through the client_cert param'
"Path to a certificate and key files must be provided"
" through the client_cert param"
)
self.cert = (tls_cert, tls_key)
@@ -78,7 +85,7 @@ class TLSConfig(object):
self.ca_cert = ca_cert
if self.verify and self.ca_cert and not os.path.isfile(self.ca_cert):
raise errors.TLSParameterError(
'Invalid CA certificate provided for `ca_cert`.'
"Invalid CA certificate provided for `ca_cert`."
)
def configure_client(self, client):
@@ -95,7 +102,10 @@ class TLSConfig(object):
if self.cert:
client.cert = self.cert
client.mount('https://', SSLHTTPAdapter(
ssl_version=self.ssl_version,
assert_hostname=self.assert_hostname,
))
client.mount(
"https://",
SSLHTTPAdapter(
ssl_version=self.ssl_version,
assert_hostname=self.assert_hostname,
),
)
@@ -15,7 +15,7 @@ from .._import_helper import HTTPAdapter as _HTTPAdapter
class BaseHTTPAdapter(_HTTPAdapter):
def close(self):
super(BaseHTTPAdapter, self).close()
if hasattr(self, 'pools'):
if hasattr(self, "pools"):
self.pools.clear()
# Hotfix for requests 2.32.0 and 2.32.1: its commit
@@ -23,7 +23,7 @@ class BaseHTTPAdapter(_HTTPAdapter):
# changes requests.adapters.HTTPAdapter to no longer call get_connection() from
# send(), but instead call _get_connection().
def _get_connection(self, request, *args, **kwargs):
return self.get_connection(request.url, kwargs.get('proxies'))
return self.get_connection(request.url, kwargs.get("proxies"))
# Fix for requests 2.32.2+:
# https://github.com/psf/requests/commit/c98e4d133ef29c46a9b68cd783087218a8075e05
@@ -13,18 +13,16 @@ from queue import Empty
from .. import constants
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
from .npipesocket import NpipeSocket
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class NpipeHTTPConnection(urllib3_connection.HTTPConnection, object):
def __init__(self, npipe_path, timeout=60):
super(NpipeHTTPConnection, self).__init__(
'localhost', timeout=timeout
)
super(NpipeHTTPConnection, self).__init__("localhost", timeout=timeout)
self.npipe_path = npipe_path
self.timeout = timeout
@@ -38,15 +36,13 @@ class NpipeHTTPConnection(urllib3_connection.HTTPConnection, object):
class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, npipe_path, timeout=60, maxsize=10):
super(NpipeHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
"localhost", timeout=timeout, maxsize=maxsize
)
self.npipe_path = npipe_path
self.timeout = timeout
def _new_conn(self):
return NpipeHTTPConnection(
self.npipe_path, self.timeout
)
return NpipeHTTPConnection(self.npipe_path, self.timeout)
# When re-using connections, urllib3 tries to call select() on our
# NpipeSocket instance, causing a crash. To circumvent this, we override
@@ -63,8 +59,7 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
if self.block:
raise urllib3.exceptions.EmptyPoolError(
self,
"Pool reached maximum size and no more "
"connections are allowed."
"Pool reached maximum size and no more " "connections are allowed.",
)
pass # Oh well, we'll create a new connection then
@@ -73,15 +68,21 @@ class NpipeHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
class NpipeHTTPAdapter(BaseHTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + ['npipe_path',
'pools',
'timeout',
'max_pool_size']
__attrs__ = HTTPAdapter.__attrs__ + [
"npipe_path",
"pools",
"timeout",
"max_pool_size",
]
def __init__(self, base_url, timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
self.npipe_path = base_url.replace('npipe://', '')
def __init__(
self,
base_url,
timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
):
self.npipe_path = base_url.replace("npipe://", "")
self.timeout = timeout
self.max_pool_size = max_pool_size
self.pools = RecentlyUsedContainer(
@@ -96,8 +97,7 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
return pool
pool = NpipeHTTPConnectionPool(
self.npipe_path, self.timeout,
maxsize=self.max_pool_size
self.npipe_path, self.timeout, maxsize=self.max_pool_size
)
self.pools[url] = pool
@@ -14,18 +14,19 @@ import io
import time
import traceback
PYWIN32_IMPORT_ERROR = None
try:
import pywintypes
import win32api
import win32event
import win32file
import win32pipe
import pywintypes
import win32event
import win32api
except ImportError:
PYWIN32_IMPORT_ERROR = traceback.format_exc()
cERROR_PIPE_BUSY = 0xe7
cERROR_PIPE_BUSY = 0xE7
cSECURITY_SQOS_PRESENT = 0x100000
cSECURITY_ANONYMOUS = 0
@@ -36,18 +37,17 @@ def check_closed(f):
@functools.wraps(f)
def wrapped(self, *args, **kwargs):
if self._closed:
raise RuntimeError(
'Can not reuse socket after connection was closed.'
)
raise RuntimeError("Can not reuse socket after connection was closed.")
return f(self, *args, **kwargs)
return wrapped
class NpipeSocket(object):
""" Partial implementation of the socket API over windows named pipes.
This implementation is only designed to be used as a client socket,
and server-specific methods (bind, listen, accept...) are not
implemented.
"""Partial implementation of the socket API over windows named pipes.
This implementation is only designed to be used as a client socket,
and server-specific methods (bind, listen, accept...) are not
implemented.
"""
def __init__(self, handle=None):
@@ -74,10 +74,12 @@ class NpipeSocket(object):
0,
None,
win32file.OPEN_EXISTING,
(cSECURITY_ANONYMOUS
(
cSECURITY_ANONYMOUS
| cSECURITY_SQOS_PRESENT
| win32file.FILE_FLAG_OVERLAPPED),
0
| win32file.FILE_FLAG_OVERLAPPED
),
0,
)
except win32pipe.error as e:
# See Remarks:
@@ -87,7 +89,7 @@ class NpipeSocket(object):
# before we got to it. Wait for availability and attempt to
# connect again.
retry_count = retry_count + 1
if (retry_count < MAXIMUM_RETRY_COUNT):
if retry_count < MAXIMUM_RETRY_COUNT:
time.sleep(1)
return self.connect(address, retry_count)
raise e
@@ -126,7 +128,7 @@ class NpipeSocket(object):
raise NotImplementedError()
def makefile(self, mode=None, bufsize=None):
if mode.strip('b') != 'r':
if mode.strip("b") != "r":
raise NotImplementedError()
rawio = NpipeFileIOBase(self)
if bufsize is None or bufsize <= 0:
@@ -158,9 +160,7 @@ class NpipeSocket(object):
overlapped = pywintypes.OVERLAPPED()
overlapped.hEvent = event
err, data = win32file.ReadFile(
self._handle,
readbuf[:nbytes] if nbytes else readbuf,
overlapped
self._handle, readbuf[:nbytes] if nbytes else readbuf, overlapped
)
wait_result = win32event.WaitForSingleObject(event, self._timeout)
if wait_result == win32event.WAIT_TIMEOUT:
@@ -204,7 +204,7 @@ class NpipeSocket(object):
# Blocking mode
self._timeout = win32event.INFINITE
elif not isinstance(value, (float, int)) or value < 0:
raise ValueError('Timeout value out of range')
raise ValueError("Timeout value out of range")
else:
# Timeout mode - Value converted to milliseconds
self._timeout = int(value * 1000)
+56 -47
View File
@@ -18,10 +18,10 @@ import traceback
from queue import Empty
from urllib.parse import urlparse
from .basehttpadapter import BaseHTTPAdapter
from .. import constants
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
PARAMIKO_IMPORT_ERROR = None
try:
@@ -35,51 +35,54 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class SSHSocket(socket.socket):
def __init__(self, host):
super(SSHSocket, self).__init__(
socket.AF_INET, socket.SOCK_STREAM)
super(SSHSocket, self).__init__(socket.AF_INET, socket.SOCK_STREAM)
self.host = host
self.port = None
self.user = None
if ':' in self.host:
self.host, self.port = self.host.split(':')
if '@' in self.host:
self.user, self.host = self.host.split('@')
if ":" in self.host:
self.host, self.port = self.host.split(":")
if "@" in self.host:
self.user, self.host = self.host.split("@")
self.proc = None
def connect(self, **kwargs):
args = ['ssh']
args = ["ssh"]
if self.user:
args = args + ['-l', self.user]
args = args + ["-l", self.user]
if self.port:
args = args + ['-p', self.port]
args = args + ["-p", self.port]
args = args + ['--', self.host, 'docker system dial-stdio']
args = args + ["--", self.host, "docker system dial-stdio"]
preexec_func = None
if not constants.IS_WINDOWS_PLATFORM:
def f():
signal.signal(signal.SIGINT, signal.SIG_IGN)
preexec_func = f
env = dict(os.environ)
# drop LD_LIBRARY_PATH and SSL_CERT_FILE
env.pop('LD_LIBRARY_PATH', None)
env.pop('SSL_CERT_FILE', None)
env.pop("LD_LIBRARY_PATH", None)
env.pop("SSL_CERT_FILE", None)
self.proc = subprocess.Popen(
args,
env=env,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
preexec_fn=preexec_func)
preexec_fn=preexec_func,
)
def _write(self, data):
if not self.proc or self.proc.stdin.closed:
raise Exception('SSH subprocess not initiated.'
'connect() must be called first.')
raise Exception(
"SSH subprocess not initiated." "connect() must be called first."
)
written = self.proc.stdin.write(data)
self.proc.stdin.flush()
return written
@@ -92,8 +95,9 @@ class SSHSocket(socket.socket):
def recv(self, n):
if not self.proc:
raise Exception('SSH subprocess not initiated.'
'connect() must be called first.')
raise Exception(
"SSH subprocess not initiated." "connect() must be called first."
)
return self.proc.stdout.read(n)
def makefile(self, mode):
@@ -106,16 +110,14 @@ class SSHSocket(socket.socket):
def close(self):
if not self.proc or self.proc.stdin.closed:
return
self.proc.stdin.write(b'\n\n')
self.proc.stdin.write(b"\n\n")
self.proc.stdin.flush()
self.proc.terminate()
class SSHConnection(urllib3_connection.HTTPConnection, object):
def __init__(self, ssh_transport=None, timeout=60, host=None):
super(SSHConnection, self).__init__(
'localhost', timeout=timeout
)
super(SSHConnection, self).__init__("localhost", timeout=timeout)
self.ssh_transport = ssh_transport
self.timeout = timeout
self.ssh_host = host
@@ -124,7 +126,7 @@ class SSHConnection(urllib3_connection.HTTPConnection, object):
if self.ssh_transport:
sock = self.ssh_transport.open_session()
sock.settimeout(self.timeout)
sock.exec_command('docker system dial-stdio')
sock.exec_command("docker system dial-stdio")
else:
sock = SSHSocket(self.ssh_host)
sock.settimeout(self.timeout)
@@ -134,11 +136,11 @@ class SSHConnection(urllib3_connection.HTTPConnection, object):
class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
scheme = 'ssh'
scheme = "ssh"
def __init__(self, ssh_client=None, timeout=60, maxsize=10, host=None):
super(SSHConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
"localhost", timeout=timeout, maxsize=maxsize
)
self.ssh_transport = None
self.timeout = timeout
@@ -164,8 +166,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
if self.block:
raise urllib3.exceptions.EmptyPoolError(
self,
"Pool reached maximum size and no more "
"connections are allowed."
"Pool reached maximum size and no more " "connections are allowed.",
)
pass # Oh well, we'll create a new connection then
@@ -175,21 +176,29 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
class SSHHTTPAdapter(BaseHTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + [
'pools', 'timeout', 'ssh_client', 'ssh_params', 'max_pool_size'
"pools",
"timeout",
"ssh_client",
"ssh_params",
"max_pool_size",
]
def __init__(self, base_url, timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
shell_out=False):
def __init__(
self,
base_url,
timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
shell_out=False,
):
self.ssh_client = None
if not shell_out:
self._create_paramiko_client(base_url)
self._connect()
self.ssh_host = base_url
if base_url.startswith('ssh://'):
self.ssh_host = base_url[len('ssh://'):]
if base_url.startswith("ssh://"):
self.ssh_host = base_url[len("ssh://") :]
self.timeout = timeout
self.max_pool_size = max_pool_size
@@ -213,18 +222,18 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
with open(ssh_config_file) as f:
conf.parse(f)
host_config = conf.lookup(base_url.hostname)
if 'proxycommand' in host_config:
if "proxycommand" in host_config:
self.ssh_params["sock"] = paramiko.ProxyCommand(
host_config['proxycommand']
host_config["proxycommand"]
)
if 'hostname' in host_config:
self.ssh_params['hostname'] = host_config['hostname']
if base_url.port is None and 'port' in host_config:
self.ssh_params['port'] = host_config['port']
if base_url.username is None and 'user' in host_config:
self.ssh_params['username'] = host_config['user']
if 'identityfile' in host_config:
self.ssh_params['key_filename'] = host_config['identityfile']
if "hostname" in host_config:
self.ssh_params["hostname"] = host_config["hostname"]
if base_url.port is None and "port" in host_config:
self.ssh_params["port"] = host_config["port"]
if base_url.username is None and "user" in host_config:
self.ssh_params["username"] = host_config["user"]
if "identityfile" in host_config:
self.ssh_params["key_filename"] = host_config["identityfile"]
self.ssh_client.load_system_host_keys()
self.ssh_client.set_missing_host_key_policy(paramiko.RejectPolicy())
@@ -239,7 +248,7 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
ssh_client=self.ssh_client,
timeout=self.timeout,
maxsize=self.max_pool_size,
host=self.ssh_host
host=self.ssh_host,
)
with self.pools.lock:
pool = self.pools.get(url)
@@ -254,7 +263,7 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
ssh_client=self.ssh_client,
timeout=self.timeout,
maxsize=self.max_pool_size,
host=self.ssh_host
host=self.ssh_host,
)
self.pools[url] = pool
@@ -9,12 +9,15 @@
from __future__ import annotations
""" Resolves OpenSSL issues in some servers:
https://lukasa.co.uk/2013/01/Choosing_SSL_Version_In_Requests/
https://github.com/kennethreitz/requests/pull/799
"""
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from .._import_helper import HTTPAdapter, urllib3
from .basehttpadapter import BaseHTTPAdapter
@@ -24,9 +27,9 @@ PoolManager = urllib3.poolmanager.PoolManager
class SSLHTTPAdapter(BaseHTTPAdapter):
'''An HTTPS Transport Adapter that uses an arbitrary SSL version.'''
"""An HTTPS Transport Adapter that uses an arbitrary SSL version."""
__attrs__ = HTTPAdapter.__attrs__ + ['assert_hostname', 'ssl_version']
__attrs__ = HTTPAdapter.__attrs__ + ["assert_hostname", "ssl_version"]
def __init__(self, ssl_version=None, assert_hostname=None, **kwargs):
self.ssl_version = ssl_version
@@ -35,14 +38,14 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
def init_poolmanager(self, connections, maxsize, block=False):
kwargs = {
'num_pools': connections,
'maxsize': maxsize,
'block': block,
"num_pools": connections,
"maxsize": maxsize,
"block": block,
}
if self.assert_hostname is not None:
kwargs['assert_hostname'] = self.assert_hostname
kwargs["assert_hostname"] = self.assert_hostname
if self.ssl_version and self.can_override_ssl_version():
kwargs['ssl_version'] = self.ssl_version
kwargs["ssl_version"] = self.ssl_version
self.poolmanager = PoolManager(**kwargs)
@@ -55,14 +58,17 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
But we still need to take care of when there is a proxy poolmanager
"""
conn = super(SSLHTTPAdapter, self).get_connection(*args, **kwargs)
if self.assert_hostname is not None and conn.assert_hostname != self.assert_hostname:
if (
self.assert_hostname is not None
and conn.assert_hostname != self.assert_hostname
):
conn.assert_hostname = self.assert_hostname
return conn
def can_override_ssl_version(self):
urllib_ver = urllib3.__version__.split('-')[0]
urllib_ver = urllib3.__version__.split("-")[0]
if urllib_ver is None:
return False
if urllib_ver == 'dev':
if urllib_ver == "dev":
return True
return LooseVersion(urllib_ver) > LooseVersion('1.5')
return LooseVersion(urllib_ver) > LooseVersion("1.5")
+22 -22
View File
@@ -11,10 +11,9 @@ from __future__ import annotations
import socket
from .basehttpadapter import BaseHTTPAdapter
from .. import constants
from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@@ -23,9 +22,7 @@ RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
def __init__(self, base_url, unix_socket, timeout=60):
super(UnixHTTPConnection, self).__init__(
'localhost', timeout=timeout
)
super(UnixHTTPConnection, self).__init__("localhost", timeout=timeout)
self.base_url = base_url
self.unix_socket = unix_socket
self.timeout = timeout
@@ -39,7 +36,7 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
def putheader(self, header, *values):
super(UnixHTTPConnection, self).putheader(header, *values)
if header == 'Connection' and 'Upgrade' in values:
if header == "Connection" and "Upgrade" in values:
self.disable_buffering = True
def response_class(self, sock, *args, **kwargs):
@@ -52,31 +49,35 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection, object):
class UnixHTTPConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
def __init__(self, base_url, socket_path, timeout=60, maxsize=10):
super(UnixHTTPConnectionPool, self).__init__(
'localhost', timeout=timeout, maxsize=maxsize
"localhost", timeout=timeout, maxsize=maxsize
)
self.base_url = base_url
self.socket_path = socket_path
self.timeout = timeout
def _new_conn(self):
return UnixHTTPConnection(
self.base_url, self.socket_path, self.timeout
)
return UnixHTTPConnection(self.base_url, self.socket_path, self.timeout)
class UnixHTTPAdapter(BaseHTTPAdapter):
__attrs__ = HTTPAdapter.__attrs__ + ['pools',
'socket_path',
'timeout',
'max_pool_size']
__attrs__ = HTTPAdapter.__attrs__ + [
"pools",
"socket_path",
"timeout",
"max_pool_size",
]
def __init__(self, socket_url, timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE):
socket_path = socket_url.replace('http+unix://', '')
if not socket_path.startswith('/'):
socket_path = '/' + socket_path
def __init__(
self,
socket_url,
timeout=60,
pool_connections=constants.DEFAULT_NUM_POOLS,
max_pool_size=constants.DEFAULT_MAX_POOL_SIZE,
):
socket_path = socket_url.replace("http+unix://", "")
if not socket_path.startswith("/"):
socket_path = "/" + socket_path
self.socket_path = socket_path
self.timeout = timeout
self.max_pool_size = max_pool_size
@@ -92,8 +93,7 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
return pool
pool = UnixHTTPConnectionPool(
url, self.socket_path, self.timeout,
maxsize=self.max_pool_size
url, self.socket_path, self.timeout, maxsize=self.max_pool_size
)
self.pools[url] = pool
+8 -8
View File
@@ -12,7 +12,6 @@ from __future__ import annotations
import socket
from .._import_helper import urllib3
from ..errors import DockerException
@@ -56,26 +55,27 @@ class CancellableStream(object):
sock_fp = self._response.raw._fp.fp
if hasattr(sock_fp, 'raw'):
if hasattr(sock_fp, "raw"):
sock_raw = sock_fp.raw
if hasattr(sock_raw, 'sock'):
if hasattr(sock_raw, "sock"):
sock = sock_raw.sock
elif hasattr(sock_raw, '_sock'):
elif hasattr(sock_raw, "_sock"):
sock = sock_raw._sock
elif hasattr(sock_fp, 'channel'):
elif hasattr(sock_fp, "channel"):
# We are working with a paramiko (SSH) channel, which does not
# support cancelable streams with the current implementation
raise DockerException(
'Cancellable streams not supported for the SSH protocol'
"Cancellable streams not supported for the SSH protocol"
)
else:
sock = sock_fp._sock
if hasattr(urllib3.contrib, 'pyopenssl') and isinstance(
sock, urllib3.contrib.pyopenssl.WrappedSocket):
if hasattr(urllib3.contrib, "pyopenssl") and isinstance(
sock, urllib3.contrib.pyopenssl.WrappedSocket
):
sock = sock.socket
sock.shutdown(socket.SHUT_RDWR)
+39 -49
View File
@@ -16,11 +16,11 @@ import re
import tarfile
import tempfile
from . import fnmatch
from ..constants import IS_WINDOWS_PLATFORM, WINDOWS_LONGPATH_PREFIX
from . import fnmatch
_SEP = re.compile('/|\\\\') if IS_WINDOWS_PLATFORM else re.compile('/')
_SEP = re.compile("/|\\\\") if IS_WINDOWS_PLATFORM else re.compile("/")
def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
@@ -29,16 +29,19 @@ def tar(path, exclude=None, dockerfile=None, fileobj=None, gzip=False):
dockerfile = dockerfile or (None, None)
extra_files = []
if dockerfile[1] is not None:
dockerignore_contents = '\n'.join(
(exclude or ['.dockerignore']) + [dockerfile[0]]
dockerignore_contents = "\n".join(
(exclude or [".dockerignore"]) + [dockerfile[0]]
)
extra_files = [
('.dockerignore', dockerignore_contents),
(".dockerignore", dockerignore_contents),
dockerfile,
]
return create_archive(
files=sorted(exclude_paths(root, exclude, dockerfile=dockerfile[0])),
root=root, fileobj=fileobj, gzip=gzip, extra_files=extra_files
root=root,
fileobj=fileobj,
gzip=gzip,
extra_files=extra_files,
)
@@ -52,9 +55,9 @@ def exclude_paths(root, patterns, dockerfile=None):
"""
if dockerfile is None:
dockerfile = 'Dockerfile'
dockerfile = "Dockerfile"
patterns.append('!' + dockerfile)
patterns.append("!" + dockerfile)
pm = PatternMatcher(patterns)
return set(pm.walk(root))
@@ -64,19 +67,16 @@ def build_file_list(root):
for dirname, dirnames, fnames in os.walk(root):
for filename in fnames + dirnames:
longpath = os.path.join(dirname, filename)
files.append(
longpath.replace(root, '', 1).lstrip('/')
)
files.append(longpath.replace(root, "", 1).lstrip("/"))
return files
def create_archive(root, files=None, fileobj=None, gzip=False,
extra_files=None):
def create_archive(root, files=None, fileobj=None, gzip=False, extra_files=None):
extra_files = extra_files or []
if not fileobj:
fileobj = tempfile.NamedTemporaryFile()
t = tarfile.open(mode='w:gz' if gzip else 'w', fileobj=fileobj)
t = tarfile.open(mode="w:gz" if gzip else "w", fileobj=fileobj)
if files is None:
files = build_file_list(root)
extra_names = set(e[0] for e in extra_files)
@@ -103,19 +103,17 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
if i.isfile():
try:
with open(full_path, 'rb') as f:
with open(full_path, "rb") as f:
t.addfile(i, f)
except IOError:
raise IOError(
f'Can not read file in context: {full_path}'
)
raise IOError(f"Can not read file in context: {full_path}")
else:
# Directories, FIFOs, symlinks... do not need to be read.
t.addfile(i, None)
for name, contents in extra_files:
info = tarfile.TarInfo(name)
contents_encoded = contents.encode('utf-8')
contents_encoded = contents.encode("utf-8")
info.size = len(contents_encoded)
t.addfile(info, io.BytesIO(contents_encoded))
@@ -126,15 +124,15 @@ def create_archive(root, files=None, fileobj=None, gzip=False,
def mkbuildcontext(dockerfile):
f = tempfile.NamedTemporaryFile()
t = tarfile.open(mode='w', fileobj=f)
t = tarfile.open(mode="w", fileobj=f)
if isinstance(dockerfile, io.StringIO):
raise TypeError('Please use io.BytesIO to create in-memory Dockerfiles')
raise TypeError("Please use io.BytesIO to create in-memory Dockerfiles")
elif isinstance(dockerfile, io.BytesIO):
dfinfo = tarfile.TarInfo('Dockerfile')
dfinfo = tarfile.TarInfo("Dockerfile")
dfinfo.size = len(dockerfile.getvalue())
dockerfile.seek(0)
else:
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname='Dockerfile')
dfinfo = t.gettarinfo(fileobj=dockerfile, arcname="Dockerfile")
t.addfile(dfinfo, dockerfile)
t.close()
f.seek(0)
@@ -142,12 +140,12 @@ def mkbuildcontext(dockerfile):
def split_path(p):
return [pt for pt in re.split(_SEP, p) if pt and pt != '.']
return [pt for pt in re.split(_SEP, p) if pt and pt != "."]
def normalize_slashes(p):
if IS_WINDOWS_PLATFORM:
return '/'.join(split_path(p))
return "/".join(split_path(p))
return p
@@ -160,10 +158,8 @@ def walk(root, patterns, default=True):
# https://github.com/moby/moby/blob/master/pkg/fileutils/fileutils.go
class PatternMatcher(object):
def __init__(self, patterns):
self.patterns = list(filter(
lambda p: p.dirs, [Pattern(p) for p in patterns]
))
self.patterns.append(Pattern('!.dockerignore'))
self.patterns = list(filter(lambda p: p.dirs, [Pattern(p) for p in patterns]))
self.patterns.append(Pattern("!.dockerignore"))
def matches(self, filepath):
matched = False
@@ -173,10 +169,10 @@ class PatternMatcher(object):
for pattern in self.patterns:
negative = pattern.exclusion
match = pattern.match(filepath)
if not match and parent_path != '':
if not match and parent_path != "":
if len(pattern.dirs) <= len(parent_path_dirs):
match = pattern.match(
os.path.sep.join(parent_path_dirs[:len(pattern.dirs)])
os.path.sep.join(parent_path_dirs[: len(pattern.dirs)])
)
if match:
@@ -187,10 +183,8 @@ class PatternMatcher(object):
def walk(self, root):
def rec_walk(current_dir):
for f in os.listdir(current_dir):
fpath = os.path.join(
os.path.relpath(current_dir, root), f
)
if fpath.startswith('.' + os.path.sep):
fpath = os.path.join(os.path.relpath(current_dir, root), f)
if fpath.startswith("." + os.path.sep):
fpath = fpath[2:]
match = self.matches(fpath)
if not match:
@@ -210,8 +204,7 @@ class PatternMatcher(object):
for pat in self.patterns:
if not pat.exclusion:
continue
if pat.cleaned_pattern.startswith(
normalize_slashes(fpath)):
if pat.cleaned_pattern.startswith(normalize_slashes(fpath)):
skip = False
break
if skip:
@@ -224,12 +217,12 @@ class PatternMatcher(object):
class Pattern(object):
def __init__(self, pattern_str):
self.exclusion = False
if pattern_str.startswith('!'):
if pattern_str.startswith("!"):
self.exclusion = True
pattern_str = pattern_str[1:]
self.dirs = self.normalize(pattern_str)
self.cleaned_pattern = '/'.join(self.dirs)
self.cleaned_pattern = "/".join(self.dirs)
@classmethod
def normalize(cls, p):
@@ -249,7 +242,7 @@ class Pattern(object):
i = 0
split = split_path(p)
while i < len(split):
if split[i] == '..':
if split[i] == "..":
del split[i]
if i > 0:
del split[i - 1]
@@ -269,17 +262,14 @@ def process_dockerfile(dockerfile, path):
abs_dockerfile = dockerfile
if not os.path.isabs(dockerfile):
abs_dockerfile = os.path.join(path, dockerfile)
if IS_WINDOWS_PLATFORM and path.startswith(
WINDOWS_LONGPATH_PREFIX):
abs_dockerfile = f'{WINDOWS_LONGPATH_PREFIX}{os.path.normpath(abs_dockerfile[len(WINDOWS_LONGPATH_PREFIX):])}'
if (os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[0] or
os.path.relpath(abs_dockerfile, path).startswith('..')):
if IS_WINDOWS_PLATFORM and path.startswith(WINDOWS_LONGPATH_PREFIX):
abs_dockerfile = f"{WINDOWS_LONGPATH_PREFIX}{os.path.normpath(abs_dockerfile[len(WINDOWS_LONGPATH_PREFIX):])}"
if os.path.splitdrive(path)[0] != os.path.splitdrive(abs_dockerfile)[
0
] or os.path.relpath(abs_dockerfile, path).startswith(".."):
# Dockerfile not in context - read data to insert into tar later
with open(abs_dockerfile) as df:
return (
f'.dockerfile.{random.getrandbits(160):x}',
df.read()
)
return (f".dockerfile.{random.getrandbits(160):x}", df.read())
# Dockerfile is inside the context - return path relative to context root
if dockerfile == abs_dockerfile:
+17 -11
View File
@@ -15,8 +15,9 @@ import os
from ..constants import IS_WINDOWS_PLATFORM
DOCKER_CONFIG_FILENAME = os.path.join('.docker', 'config.json')
LEGACY_DOCKER_CONFIG_FILENAME = '.dockercfg'
DOCKER_CONFIG_FILENAME = os.path.join(".docker", "config.json")
LEGACY_DOCKER_CONFIG_FILENAME = ".dockercfg"
log = logging.getLogger(__name__)
@@ -27,12 +28,17 @@ def get_default_config_file():
def find_config_file(config_path=None):
homedir = home_dir()
paths = list(filter(None, [
config_path, # 1
config_path_from_environment(), # 2
os.path.join(homedir, DOCKER_CONFIG_FILENAME), # 3
os.path.join(homedir, LEGACY_DOCKER_CONFIG_FILENAME), # 4
]))
paths = list(
filter(
None,
[
config_path, # 1
config_path_from_environment(), # 2
os.path.join(homedir, DOCKER_CONFIG_FILENAME), # 3
os.path.join(homedir, LEGACY_DOCKER_CONFIG_FILENAME), # 4
],
)
)
log.debug("Trying paths: %s", repr(paths))
@@ -47,7 +53,7 @@ def find_config_file(config_path=None):
def config_path_from_environment():
config_dir = os.environ.get('DOCKER_CONFIG')
config_dir = os.environ.get("DOCKER_CONFIG")
if not config_dir:
return None
return os.path.join(config_dir, os.path.basename(DOCKER_CONFIG_FILENAME))
@@ -59,9 +65,9 @@ def home_dir():
client - use %USERPROFILE% on Windows, $HOME/getuid on POSIX.
"""
if IS_WINDOWS_PLATFORM:
return os.environ.get('USERPROFILE', '')
return os.environ.get("USERPROFILE", "")
else:
return os.path.expanduser('~')
return os.path.expanduser("~")
def load_general_config(config_path=None):
+12 -9
View File
@@ -22,13 +22,13 @@ def check_resource(resource_name):
if resource_id is None and kwargs.get(resource_name):
resource_id = kwargs.pop(resource_name)
if isinstance(resource_id, dict):
resource_id = resource_id.get('Id', resource_id.get('ID'))
resource_id = resource_id.get("Id", resource_id.get("ID"))
if not resource_id:
raise errors.NullResource(
'Resource ID was not provided'
)
raise errors.NullResource("Resource ID was not provided")
return f(self, resource_id, *args, **kwargs)
return wrapped
return decorator
@@ -38,19 +38,22 @@ def minimum_version(version):
def wrapper(self, *args, **kwargs):
if utils.version_lt(self._version, version):
raise errors.InvalidVersion(
f'{f.__name__} is not available for version < {version}'
f"{f.__name__} is not available for version < {version}"
)
return f(self, *args, **kwargs)
return wrapper
return decorator
def update_headers(f):
def inner(self, *args, **kwargs):
if 'HttpHeaders' in self._general_configs:
if not kwargs.get('headers'):
kwargs['headers'] = self._general_configs['HttpHeaders']
if "HttpHeaders" in self._general_configs:
if not kwargs.get("headers"):
kwargs["headers"] = self._general_configs["HttpHeaders"]
else:
kwargs['headers'].update(self._general_configs['HttpHeaders'])
kwargs["headers"].update(self._general_configs["HttpHeaders"])
return f(self, *args, **kwargs)
return inner
+23 -21
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
"""Filename matching with shell patterns.
fnmatch(FILENAME, PATTERN) matches according to the local convention.
@@ -23,6 +24,7 @@ corresponding to PATTERN. (It does not compile it.)
import re
__all__ = ["fnmatch", "fnmatchcase", "translate"]
_cache = {}
@@ -77,50 +79,50 @@ def translate(pat):
There is no way to quote meta-characters.
"""
i, n = 0, len(pat)
res = '^'
res = "^"
while i < n:
c = pat[i]
i = i + 1
if c == '*':
if i < n and pat[i] == '*':
if c == "*":
if i < n and pat[i] == "*":
# is some flavor of "**"
i = i + 1
# Treat **/ as ** so eat the "/"
if i < n and pat[i] == '/':
if i < n and pat[i] == "/":
i = i + 1
if i >= n:
# is "**EOF" - to align with .gitignore just accept all
res = res + '.*'
res = res + ".*"
else:
# is "**"
# Note that this allows for any # of /'s (even 0) because
# the .* will eat everything, even /'s
res = res + '(.*/)?'
res = res + "(.*/)?"
else:
# is "*" so map it to anything but "/"
res = res + '[^/]*'
elif c == '?':
res = res + "[^/]*"
elif c == "?":
# "?" is any char except "/"
res = res + '[^/]'
elif c == '[':
res = res + "[^/]"
elif c == "[":
j = i
if j < n and pat[j] == '!':
if j < n and pat[j] == "!":
j = j + 1
if j < n and pat[j] == ']':
if j < n and pat[j] == "]":
j = j + 1
while j < n and pat[j] != ']':
while j < n and pat[j] != "]":
j = j + 1
if j >= n:
res = res + '\\['
res = res + "\\["
else:
stuff = pat[i:j].replace('\\', '\\\\')
stuff = pat[i:j].replace("\\", "\\\\")
i = j + 1
if stuff[0] == '!':
stuff = '^' + stuff[1:]
elif stuff[0] == '^':
stuff = '\\' + stuff
res = f'{res}[{stuff}]'
if stuff[0] == "!":
stuff = "^" + stuff[1:]
elif stuff[0] == "^":
stuff = "\\" + stuff
res = f"{res}[{stuff}]"
else:
res = res + re.escape(c)
return res + '$'
return res + "$"
@@ -27,7 +27,7 @@ def stream_as_text(stream):
"""
for data in stream:
if not isinstance(data, str):
data = data.decode('utf-8', 'replace')
data = data.decode("utf-8", "replace")
yield data
@@ -38,7 +38,7 @@ def json_splitter(buffer):
buffer = buffer.strip()
try:
obj, index = json_decoder.raw_decode(buffer)
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end():]
rest = buffer[json.decoder.WHITESPACE.match(buffer, index).end() :]
return obj, rest
except ValueError:
return None
@@ -52,11 +52,11 @@ def json_stream(stream):
return split_buffer(stream, json_splitter, json_decoder.decode)
def line_splitter(buffer, separator='\n'):
def line_splitter(buffer, separator="\n"):
index = buffer.find(str(separator))
if index == -1:
return None
return buffer[:index + 1], buffer[index + 1:]
return buffer[: index + 1], buffer[index + 1 :]
def split_buffer(stream, splitter=None, decoder=lambda a: a):
@@ -67,7 +67,7 @@ def split_buffer(stream, splitter=None, decoder=lambda a: a):
of the input.
"""
splitter = splitter or line_splitter
buffered = ''
buffered = ""
for data in stream_as_text(stream):
buffered += data
+16 -14
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import re
PORT_SPEC = re.compile(
"^" # Match full string
"(" # External part
@@ -49,23 +50,25 @@ def build_port_bindings(ports):
def _raise_invalid_port(port):
raise ValueError(f'Invalid port "{port}", should be '
'[[remote_ip:]remote_port[-remote_port]:]'
'port[/protocol]')
raise ValueError(
f'Invalid port "{port}", should be '
"[[remote_ip:]remote_port[-remote_port]:]"
"port[/protocol]"
)
def port_range(start, end, proto, randomly_available_port=False):
if not start:
return start
if not end:
return [f'{start}{proto}']
return [f"{start}{proto}"]
if randomly_available_port:
return [f'{start}-{end}{proto}']
return [f'{port}{proto}' for port in range(int(start), int(end) + 1)]
return [f"{start}-{end}{proto}"]
return [f"{port}{proto}" for port in range(int(start), int(end) + 1)]
def split_port(port):
if hasattr(port, 'legacy_repr'):
if hasattr(port, "legacy_repr"):
# This is the worst hack, but it prevents a bug in Compose 1.14.0
# https://github.com/docker/docker-py/issues/1668
# TODO: remove once fixed in Compose stable
@@ -76,19 +79,18 @@ def split_port(port):
_raise_invalid_port(port)
parts = match.groupdict()
host = parts['host']
proto = parts['proto'] or ''
internal = port_range(parts['int'], parts['int_end'], proto)
external = port_range(
parts['ext'], parts['ext_end'], '', len(internal) == 1)
host = parts["host"]
proto = parts["proto"] or ""
internal = port_range(parts["int"], parts["int_end"], proto)
external = port_range(parts["ext"], parts["ext_end"], "", len(internal) == 1)
if host is None:
if external is not None and len(internal) != len(external):
raise ValueError('Port ranges don\'t match in length')
raise ValueError("Port ranges don't match in length")
return internal, external
else:
if not external:
external = [None] * len(internal)
elif len(internal) != len(external):
raise ValueError('Port ranges don\'t match in length')
raise ValueError("Port ranges don't match in length")
return internal, [(host, ext_port) for ext_port in external]
+22 -21
View File
@@ -13,62 +13,63 @@ from .utils import format_environment
class ProxyConfig(dict):
'''
"""
Hold the client's proxy configuration
'''
"""
@property
def http(self):
return self.get('http')
return self.get("http")
@property
def https(self):
return self.get('https')
return self.get("https")
@property
def ftp(self):
return self.get('ftp')
return self.get("ftp")
@property
def no_proxy(self):
return self.get('no_proxy')
return self.get("no_proxy")
@staticmethod
def from_dict(config):
'''
"""
Instantiate a new ProxyConfig from a dictionary that represents a
client configuration, as described in `the documentation`_.
.. _the documentation:
https://docs.docker.com/network/proxy/#configure-the-docker-client
'''
"""
return ProxyConfig(
http=config.get('httpProxy'),
https=config.get('httpsProxy'),
ftp=config.get('ftpProxy'),
no_proxy=config.get('noProxy'),
http=config.get("httpProxy"),
https=config.get("httpsProxy"),
ftp=config.get("ftpProxy"),
no_proxy=config.get("noProxy"),
)
def get_environment(self):
'''
"""
Return a dictionary representing the environment variables used to
set the proxy settings.
'''
"""
env = {}
if self.http:
env['http_proxy'] = env['HTTP_PROXY'] = self.http
env["http_proxy"] = env["HTTP_PROXY"] = self.http
if self.https:
env['https_proxy'] = env['HTTPS_PROXY'] = self.https
env["https_proxy"] = env["HTTPS_PROXY"] = self.https
if self.ftp:
env['ftp_proxy'] = env['FTP_PROXY'] = self.ftp
env["ftp_proxy"] = env["FTP_PROXY"] = self.ftp
if self.no_proxy:
env['no_proxy'] = env['NO_PROXY'] = self.no_proxy
env["no_proxy"] = env["NO_PROXY"] = self.no_proxy
return env
def inject_proxy_environment(self, environment):
'''
"""
Given a list of strings representing environment variables, prepend the
environment variables corresponding to the proxy settings.
'''
"""
if not self:
return environment
@@ -80,4 +81,4 @@ class ProxyConfig(dict):
return proxy_env + environment
def __str__(self):
return f'ProxyConfig(http={self.http}, https={self.https}, ftp={self.ftp}, no_proxy={self.no_proxy})'
return f"ProxyConfig(http={self.http}, https={self.https}, ftp={self.ftp}, no_proxy={self.no_proxy})"
+13 -11
View File
@@ -48,22 +48,24 @@ def read(socket, n=4096):
poll.poll()
try:
if hasattr(socket, 'recv'):
if hasattr(socket, "recv"):
return socket.recv(n)
if isinstance(socket, getattr(pysocket, 'SocketIO')):
if isinstance(socket, getattr(pysocket, "SocketIO")):
return socket.read(n)
return os.read(socket.fileno(), n)
except EnvironmentError as e:
if e.errno not in recoverable_errors:
raise
except Exception as e:
is_pipe_ended = (isinstance(socket, NpipeSocket) and
len(e.args) > 0 and
e.args[0] == NPIPE_ENDED)
is_pipe_ended = (
isinstance(socket, NpipeSocket)
and len(e.args) > 0
and e.args[0] == NPIPE_ENDED
)
if is_pipe_ended:
# npipes do not support duplex sockets, so we interpret
# a PIPE_ENDED error as a close operation (0-length read).
return ''
return ""
raise
@@ -72,7 +74,7 @@ def read_exactly(socket, n):
Reads exactly n bytes from socket
Raises SocketError if there is not enough data
"""
data = b''
data = b""
while len(data) < n:
next_data = read(socket, n - len(data))
if not next_data:
@@ -93,7 +95,7 @@ def next_frame_header(socket):
except SocketError:
return (-1, -1)
stream, actual = struct.unpack('>BxxxL', data)
stream, actual = struct.unpack(">BxxxL", data)
return (stream, actual)
@@ -160,7 +162,7 @@ def consume_socket_output(frames, demux=False):
if demux is False:
# If the streams are multiplexed, the generator returns strings, that
# we just need to concatenate.
return b''.join(frames)
return b"".join(frames)
# If the streams are demultiplexed, the generator yields tuples
# (stdout, stderr)
@@ -169,7 +171,7 @@ def consume_socket_output(frames, demux=False):
# It is guaranteed that for each frame, one and only one stream
# is not None.
if frame == (None, None):
raise AssertionError(f'frame must be (None, None), but got {frame}')
raise AssertionError(f"frame must be (None, None), but got {frame}")
if frame[0] is not None:
if out[0] is None:
out[0] = frame[0]
@@ -193,4 +195,4 @@ def demux_adaptor(stream_id, data):
elif stream_id == STDERR:
return (None, data)
else:
raise ValueError(f'{stream_id} is not a valid stream')
raise ValueError(f"{stream_id} is not a valid stream")
+147 -150
View File
@@ -16,40 +16,44 @@ import os
import os.path
import shlex
import string
from ansible_collections.community.docker.plugins.module_utils.version import StrictVersion
from urllib.parse import urlparse, urlunparse
from ansible_collections.community.docker.plugins.module_utils.version import (
StrictVersion,
)
from .. import errors
from ..constants import DEFAULT_HTTP_HOST
from ..constants import DEFAULT_UNIX_SOCKET
from ..constants import DEFAULT_NPIPE
from ..constants import BYTE_UNITS
from ..constants import (
BYTE_UNITS,
DEFAULT_HTTP_HOST,
DEFAULT_NPIPE,
DEFAULT_UNIX_SOCKET,
)
from ..tls import TLSConfig
from urllib.parse import urlparse, urlunparse
URLComponents = collections.namedtuple(
'URLComponents',
'scheme netloc url params query fragment',
"URLComponents",
"scheme netloc url params query fragment",
)
def create_ipam_pool(*args, **kwargs):
raise errors.DeprecatedMethod(
'utils.create_ipam_pool has been removed. Please use a '
'docker.types.IPAMPool object instead.'
"utils.create_ipam_pool has been removed. Please use a "
"docker.types.IPAMPool object instead."
)
def create_ipam_config(*args, **kwargs):
raise errors.DeprecatedMethod(
'utils.create_ipam_config has been removed. Please use a '
'docker.types.IPAMConfig object instead.'
"utils.create_ipam_config has been removed. Please use a "
"docker.types.IPAMConfig object instead."
)
def decode_json_header(header):
data = base64.b64decode(header).decode('utf-8')
data = base64.b64decode(header).decode("utf-8")
return json.loads(data)
@@ -84,29 +88,29 @@ def version_gte(v1, v2):
def _convert_port_binding(binding):
result = {'HostIp': '', 'HostPort': ''}
result = {"HostIp": "", "HostPort": ""}
if isinstance(binding, tuple):
if len(binding) == 2:
result['HostPort'] = binding[1]
result['HostIp'] = binding[0]
result["HostPort"] = binding[1]
result["HostIp"] = binding[0]
elif isinstance(binding[0], str):
result['HostIp'] = binding[0]
result["HostIp"] = binding[0]
else:
result['HostPort'] = binding[0]
result["HostPort"] = binding[0]
elif isinstance(binding, dict):
if 'HostPort' in binding:
result['HostPort'] = binding['HostPort']
if 'HostIp' in binding:
result['HostIp'] = binding['HostIp']
if "HostPort" in binding:
result["HostPort"] = binding["HostPort"]
if "HostIp" in binding:
result["HostIp"] = binding["HostIp"]
else:
raise ValueError(binding)
else:
result['HostPort'] = binding
result["HostPort"] = binding
if result['HostPort'] is None:
result['HostPort'] = ''
if result["HostPort"] is None:
result["HostPort"] = ""
else:
result['HostPort'] = str(result['HostPort'])
result["HostPort"] = str(result["HostPort"])
return result
@@ -115,8 +119,8 @@ def convert_port_bindings(port_bindings):
result = {}
for k, v in port_bindings.items():
key = str(k)
if '/' not in key:
key += '/tcp'
if "/" not in key:
key += "/tcp"
if isinstance(v, list):
result[key] = [_convert_port_binding(binding) for binding in v]
else:
@@ -131,46 +135,44 @@ def convert_volume_binds(binds):
result = []
for k, v in binds.items():
if isinstance(k, bytes):
k = k.decode('utf-8')
k = k.decode("utf-8")
if isinstance(v, dict):
if 'ro' in v and 'mode' in v:
raise ValueError(
f'Binding cannot contain both "ro" and "mode": {v!r}'
)
if "ro" in v and "mode" in v:
raise ValueError(f'Binding cannot contain both "ro" and "mode": {v!r}')
bind = v['bind']
bind = v["bind"]
if isinstance(bind, bytes):
bind = bind.decode('utf-8')
bind = bind.decode("utf-8")
if 'ro' in v:
mode = 'ro' if v['ro'] else 'rw'
elif 'mode' in v:
mode = v['mode']
if "ro" in v:
mode = "ro" if v["ro"] else "rw"
elif "mode" in v:
mode = v["mode"]
else:
mode = 'rw'
mode = "rw"
# NOTE: this is only relevant for Linux hosts
# (does not apply in Docker Desktop)
propagation_modes = [
'rshared',
'shared',
'rslave',
'slave',
'rprivate',
'private',
"rshared",
"shared",
"rslave",
"slave",
"rprivate",
"private",
]
if 'propagation' in v and v['propagation'] in propagation_modes:
if "propagation" in v and v["propagation"] in propagation_modes:
if mode:
mode = ','.join([mode, v['propagation']])
mode = ",".join([mode, v["propagation"]])
else:
mode = v['propagation']
mode = v["propagation"]
result.append(f'{k}:{bind}:{mode}')
result.append(f"{k}:{bind}:{mode}")
else:
if isinstance(v, bytes):
v = v.decode('utf-8')
result.append(f'{k}:{v}:rw')
v = v.decode("utf-8")
result.append(f"{k}:{v}:rw")
return result
@@ -180,7 +182,7 @@ def convert_tmpfs_mounts(tmpfs):
if not isinstance(tmpfs, list):
raise ValueError(
f'Expected tmpfs value to be either a list or a dict, found: {type(tmpfs).__name__}'
f"Expected tmpfs value to be either a list or a dict, found: {type(tmpfs).__name__}"
)
result = {}
@@ -205,22 +207,22 @@ def convert_service_networks(networks):
if not networks:
return networks
if not isinstance(networks, list):
raise TypeError('networks parameter must be a list.')
raise TypeError("networks parameter must be a list.")
result = []
for n in networks:
if isinstance(n, str):
n = {'Target': n}
n = {"Target": n}
result.append(n)
return result
def parse_repository_tag(repo_name):
parts = repo_name.rsplit('@', 1)
parts = repo_name.rsplit("@", 1)
if len(parts) == 2:
return tuple(parts)
parts = repo_name.rsplit(':', 1)
if len(parts) == 2 and '/' not in parts[1]:
parts = repo_name.rsplit(":", 1)
if len(parts) == 2 and "/" not in parts[1]:
return tuple(parts)
return repo_name, None
@@ -229,86 +231,81 @@ def parse_host(addr, is_win32=False, tls=False):
# Sensible defaults
if not addr and is_win32:
return DEFAULT_NPIPE
if not addr or addr.strip() == 'unix://':
if not addr or addr.strip() == "unix://":
return DEFAULT_UNIX_SOCKET
addr = addr.strip()
parsed_url = urlparse(addr)
proto = parsed_url.scheme
if not proto or any(x not in string.ascii_letters + '+' for x in proto):
if not proto or any(x not in string.ascii_letters + "+" for x in proto):
# https://bugs.python.org/issue754016
parsed_url = urlparse('//' + addr, 'tcp')
proto = 'tcp'
parsed_url = urlparse("//" + addr, "tcp")
proto = "tcp"
if proto == 'fd':
raise errors.DockerException('fd protocol is not implemented')
if proto == "fd":
raise errors.DockerException("fd protocol is not implemented")
# These protos are valid aliases for our library but not for the
# official spec
if proto == 'http' or proto == 'https':
tls = proto == 'https'
proto = 'tcp'
elif proto == 'http+unix':
proto = 'unix'
if proto == "http" or proto == "https":
tls = proto == "https"
proto = "tcp"
elif proto == "http+unix":
proto = "unix"
if proto not in ('tcp', 'unix', 'npipe', 'ssh'):
raise errors.DockerException(
f"Invalid bind address protocol: {addr}"
)
if proto not in ("tcp", "unix", "npipe", "ssh"):
raise errors.DockerException(f"Invalid bind address protocol: {addr}")
if proto == 'tcp' and not parsed_url.netloc:
if proto == "tcp" and not parsed_url.netloc:
# "tcp://" is exceptionally disallowed by convention;
# omitting a hostname for other protocols is fine
raise errors.DockerException(
f'Invalid bind address format: {addr}'
)
raise errors.DockerException(f"Invalid bind address format: {addr}")
if any([
parsed_url.params, parsed_url.query, parsed_url.fragment,
parsed_url.password
]):
raise errors.DockerException(
f'Invalid bind address format: {addr}'
)
if any(
[parsed_url.params, parsed_url.query, parsed_url.fragment, parsed_url.password]
):
raise errors.DockerException(f"Invalid bind address format: {addr}")
if parsed_url.path and proto == 'ssh':
if parsed_url.path and proto == "ssh":
raise errors.DockerException(
f'Invalid bind address format: no path allowed for this protocol: {addr}'
f"Invalid bind address format: no path allowed for this protocol: {addr}"
)
else:
path = parsed_url.path
if proto == 'unix' and parsed_url.hostname is not None:
if proto == "unix" and parsed_url.hostname is not None:
# For legacy reasons, we consider unix://path
# to be valid and equivalent to unix:///path
path = '/'.join((parsed_url.hostname, path))
path = "/".join((parsed_url.hostname, path))
netloc = parsed_url.netloc
if proto in ('tcp', 'ssh'):
if proto in ("tcp", "ssh"):
port = parsed_url.port or 0
if port <= 0:
port = 22 if proto == 'ssh' else (2375 if tls else 2376)
netloc = f'{parsed_url.netloc}:{port}'
port = 22 if proto == "ssh" else (2375 if tls else 2376)
netloc = f"{parsed_url.netloc}:{port}"
if not parsed_url.hostname:
netloc = f'{DEFAULT_HTTP_HOST}:{port}'
netloc = f"{DEFAULT_HTTP_HOST}:{port}"
# Rewrite schemes to fit library internals (requests adapters)
if proto == 'tcp':
if proto == "tcp":
proto = f"http{'s' if tls else ''}"
elif proto == 'unix':
proto = 'http+unix'
elif proto == "unix":
proto = "http+unix"
if proto in ('http+unix', 'npipe'):
return f"{proto}://{path}".rstrip('/')
return urlunparse(URLComponents(
scheme=proto,
netloc=netloc,
url=path,
params='',
query='',
fragment='',
)).rstrip('/')
if proto in ("http+unix", "npipe"):
return f"{proto}://{path}".rstrip("/")
return urlunparse(
URLComponents(
scheme=proto,
netloc=netloc,
url=path,
params="",
query="",
fragment="",
)
).rstrip("/")
def parse_devices(devices):
@@ -318,10 +315,8 @@ def parse_devices(devices):
device_list.append(device)
continue
if not isinstance(device, str):
raise errors.DockerException(
f'Invalid device type {type(device)}'
)
device_mapping = device.split(':')
raise errors.DockerException(f"Invalid device type {type(device)}")
device_mapping = device.split(":")
if device_mapping:
path_on_host = device_mapping[0]
if len(device_mapping) > 1:
@@ -331,27 +326,29 @@ def parse_devices(devices):
if len(device_mapping) > 2:
permissions = device_mapping[2]
else:
permissions = 'rwm'
device_list.append({
'PathOnHost': path_on_host,
'PathInContainer': path_in_container,
'CgroupPermissions': permissions
})
permissions = "rwm"
device_list.append(
{
"PathOnHost": path_on_host,
"PathInContainer": path_in_container,
"CgroupPermissions": permissions,
}
)
return device_list
def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
if not environment:
environment = os.environ
host = environment.get('DOCKER_HOST')
host = environment.get("DOCKER_HOST")
# empty string for cert path is the same as unset.
cert_path = environment.get('DOCKER_CERT_PATH') or None
cert_path = environment.get("DOCKER_CERT_PATH") or None
# empty string for tls verify counts as "false".
# Any value or 'unset' counts as true.
tls_verify = environment.get('DOCKER_TLS_VERIFY')
if tls_verify == '':
tls_verify = environment.get("DOCKER_TLS_VERIFY")
if tls_verify == "":
tls_verify = False
else:
tls_verify = tls_verify is not None
@@ -360,23 +357,25 @@ def kwargs_from_env(ssl_version=None, assert_hostname=None, environment=None):
params = {}
if host:
params['base_url'] = host
params["base_url"] = host
if not enable_tls:
return params
if not cert_path:
cert_path = os.path.join(os.path.expanduser('~'), '.docker')
cert_path = os.path.join(os.path.expanduser("~"), ".docker")
if not tls_verify and assert_hostname is None:
# assert_hostname is a subset of TLS verification,
# so if it is not set already then set it to false.
assert_hostname = False
params['tls'] = TLSConfig(
client_cert=(os.path.join(cert_path, 'cert.pem'),
os.path.join(cert_path, 'key.pem')),
ca_cert=os.path.join(cert_path, 'ca.pem'),
params["tls"] = TLSConfig(
client_cert=(
os.path.join(cert_path, "cert.pem"),
os.path.join(cert_path, "key.pem"),
),
ca_cert=os.path.join(cert_path, "ca.pem"),
verify=tls_verify,
ssl_version=ssl_version,
assert_hostname=assert_hostname,
@@ -389,13 +388,12 @@ def convert_filters(filters):
result = {}
for k, v in filters.items():
if isinstance(v, bool):
v = 'true' if v else 'false'
v = "true" if v else "false"
if not isinstance(v, list):
v = [v, ]
result[k] = [
str(item) if not isinstance(item, str) else item
for item in v
]
v = [
v,
]
result[k] = [str(item) if not isinstance(item, str) else item for item in v]
return json.dumps(result)
@@ -415,7 +413,7 @@ def parse_bytes(s):
# without a units part. Assuming that the units are bytes.
if suffix.isdigit():
digits_part = s
suffix = 'b'
suffix = "b"
else:
digits_part = s[:-1]
@@ -424,14 +422,14 @@ def parse_bytes(s):
digits = float(digits_part)
except ValueError:
raise errors.DockerException(
f'Failed converting the string value for memory ({digits_part}) to an integer.'
f"Failed converting the string value for memory ({digits_part}) to an integer."
)
# Reconvert to long for the final result
s = int(digits * units[suffix])
else:
raise errors.DockerException(
f'The specified value for memory ({s}) should specify the units. The postfix should be one of the `b` `k` `m` `g` characters'
f"The specified value for memory ({s}) should specify the units. The postfix should be one of the `b` `k` `m` `g` characters"
)
return s
@@ -441,7 +439,7 @@ def normalize_links(links):
if isinstance(links, dict):
links = links.items()
return [f'{k}:{v}' if v else k for k, v in sorted(links)]
return [f"{k}:{v}" if v else k for k, v in sorted(links)]
def parse_env_file(env_file):
@@ -451,22 +449,24 @@ def parse_env_file(env_file):
"""
environment = {}
with open(env_file, 'r') as f:
with open(env_file, "r") as f:
for line in f:
if line[0] == '#':
if line[0] == "#":
continue
line = line.strip()
if not line:
continue
parse_line = line.split('=', 1)
parse_line = line.split("=", 1)
if len(parse_line) == 2:
k, v = parse_line
environment[k] = v
else:
raise errors.DockerException(f'Invalid line in environment file {env_file}:\n{line}')
raise errors.DockerException(
f"Invalid line in environment file {env_file}:\n{line}"
)
return environment
@@ -480,26 +480,23 @@ def format_environment(environment):
if value is None:
return key
if isinstance(value, bytes):
value = value.decode('utf-8')
value = value.decode("utf-8")
return f"{key}={value}"
return f'{key}={value}'
return [format_env(*var) for var in environment.items()]
def format_extra_hosts(extra_hosts, task=False):
# Use format dictated by Swarm API if container is part of a task
if task:
return [
f'{v} {k}' for k, v in sorted(extra_hosts.items())
]
return [f"{v} {k}" for k, v in sorted(extra_hosts.items())]
return [
f'{k}:{v}' for k, v in sorted(extra_hosts.items())
]
return [f"{k}:{v}" for k, v in sorted(extra_hosts.items())]
def create_host_config(self, *args, **kwargs):
raise errors.DeprecatedMethod(
'utils.create_host_config has been removed. Please use a '
'docker.types.HostConfig object instead.'
"utils.create_host_config has been removed. Please use a "
"docker.types.HostConfig object instead."
)