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."
)
+46 -44
View File
@@ -29,44 +29,44 @@ class _Mode(object):
_ESCAPE_DICT = {
'"': '"',
'\\': '\\',
"\\": "\\",
"'": "'",
'/': '/',
'b': '\b',
'f': '\f',
'n': '\n',
'r': '\r',
't': '\t',
"/": "/",
"b": "\b",
"f": "\f",
"n": "\n",
"r": "\r",
"t": "\t",
}
_HEX_DICT = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
'a': 0xA,
'b': 0xB,
'c': 0xC,
'd': 0xD,
'e': 0xE,
'f': 0xF,
'A': 0xA,
'B': 0xB,
'C': 0xC,
'D': 0xD,
'E': 0xE,
'F': 0xF,
"0": 0,
"1": 1,
"2": 2,
"3": 3,
"4": 4,
"5": 5,
"6": 6,
"7": 7,
"8": 8,
"9": 9,
"a": 0xA,
"b": 0xB,
"c": 0xC,
"d": 0xD,
"e": 0xE,
"f": 0xF,
"A": 0xA,
"B": 0xB,
"C": 0xC,
"D": 0xD,
"E": 0xE,
"F": 0xF,
}
def _is_ident(cur):
return cur > ' ' and cur not in ('"', '=')
return cur > " " and cur not in ('"', "=")
class _Parser(object):
@@ -89,16 +89,18 @@ class _Parser(object):
def parse_unicode_sequence(self):
if self.index + 6 > self.length:
raise InvalidLogFmt('Not enough space for unicode escape')
if self.line[self.index:self.index + 2] != '\\u':
raise InvalidLogFmt('Invalid unicode escape start')
raise InvalidLogFmt("Not enough space for unicode escape")
if self.line[self.index : self.index + 2] != "\\u":
raise InvalidLogFmt("Invalid unicode escape start")
v = 0
for i in range(self.index + 2, self.index + 6):
v <<= 4
try:
v += _HEX_DICT[self.line[self.index]]
except KeyError:
raise InvalidLogFmt(f'Invalid unicode escape digit {self.line[self.index]!r}')
raise InvalidLogFmt(
f"Invalid unicode escape digit {self.line[self.index]!r}"
)
self.index += 6
return chr(v)
@@ -111,8 +113,8 @@ def parse_line(line, logrus_mode=False):
mode = _Mode.GARBAGE
def handle_kv(has_no_value=False):
k = ''.join(key)
v = None if has_no_value else ''.join(value)
k = "".join(key)
v = None if has_no_value else "".join(value)
result[k] = v
del key[:]
del value[:]
@@ -128,7 +130,7 @@ def parse_line(line, logrus_mode=False):
key.append(cur)
parser.next()
return _Mode.KEY
elif cur == '=':
elif cur == "=":
parser.next()
return _Mode.EQUAL
else:
@@ -162,16 +164,16 @@ def parse_line(line, logrus_mode=False):
return _Mode.GARBAGE
def parse_quoted_value(cur):
if cur == '\\':
if cur == "\\":
parser.next()
if parser.done():
raise InvalidLogFmt('Unterminated escape sequence in quoted string')
raise InvalidLogFmt("Unterminated escape sequence in quoted string")
cur = parser.cur()
if cur in _ESCAPE_DICT:
value.append(_ESCAPE_DICT[cur])
elif cur != 'u':
elif cur != "u":
es = f"\\{cur}"
raise InvalidLogFmt(f'Unknown escape sequence {es!r}')
raise InvalidLogFmt(f"Unknown escape sequence {es!r}")
else:
parser.prev()
value.append(parser.parse_unicode_sequence())
@@ -181,8 +183,8 @@ def parse_line(line, logrus_mode=False):
handle_kv()
parser.next()
return _Mode.GARBAGE
elif cur < ' ':
raise InvalidLogFmt('Control characters in quoted string are not allowed')
elif cur < " ":
raise InvalidLogFmt("Control characters in quoted string are not allowed")
else:
value.append(cur)
parser.next()
@@ -204,5 +206,5 @@ def parse_line(line, logrus_mode=False):
elif mode == _Mode.IDENT_VALUE:
handle_kv()
elif mode == _Mode.QUOTED_VALUE:
raise InvalidLogFmt('Unterminated quoted string')
raise InvalidLogFmt("Unterminated quoted string")
return result
+92 -32
View File
@@ -13,36 +13,74 @@ from __future__ import annotations
import re
_VALID_STR = re.compile('^[A-Za-z0-9_-]+$')
_VALID_STR = re.compile("^[A-Za-z0-9_-]+$")
def _validate_part(string, part, part_name):
if not part:
raise ValueError(f'Invalid platform string "{string}": {part_name} is empty')
if not _VALID_STR.match(part):
raise ValueError(f'Invalid platform string "{string}": {part_name} has invalid characters')
raise ValueError(
f'Invalid platform string "{string}": {part_name} has invalid characters'
)
return part
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L32-L38
_KNOWN_OS = (
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js",
"linux", "nacl", "netbsd", "openbsd", "plan9", "solaris", "windows", "zos",
"aix",
"android",
"darwin",
"dragonfly",
"freebsd",
"hurd",
"illumos",
"ios",
"js",
"linux",
"nacl",
"netbsd",
"openbsd",
"plan9",
"solaris",
"windows",
"zos",
)
# See https://github.com/containerd/containerd/blob/main/platforms/database.go#L54-L60
_KNOWN_ARCH = (
"386", "amd64", "amd64p32", "arm", "armbe", "arm64", "arm64be", "ppc64", "ppc64le",
"loong64", "mips", "mipsle", "mips64", "mips64le", "mips64p32", "mips64p32le",
"ppc", "riscv", "riscv64", "s390", "s390x", "sparc", "sparc64", "wasm",
"386",
"amd64",
"amd64p32",
"arm",
"armbe",
"arm64",
"arm64be",
"ppc64",
"ppc64le",
"loong64",
"mips",
"mipsle",
"mips64",
"mips64le",
"mips64p32",
"mips64p32le",
"ppc",
"riscv",
"riscv64",
"s390",
"s390x",
"sparc",
"sparc64",
"wasm",
)
def _normalize_os(os_str):
# See normalizeOS() in https://github.com/containerd/containerd/blob/main/platforms/database.go
os_str = os_str.lower()
if os_str == 'macos':
os_str = 'darwin'
if os_str == "macos":
os_str = "darwin"
return os_str
@@ -94,9 +132,9 @@ class _Platform(object):
self.variant = variant
if variant is not None:
if arch is None:
raise ValueError('If variant is given, architecture must be given too')
raise ValueError("If variant is given, architecture must be given too")
if os is None:
raise ValueError('If variant is given, os must be given too')
raise ValueError("If variant is given, os must be given too")
@classmethod
def parse_platform_string(cls, string, daemon_os=None, daemon_arch=None):
@@ -104,40 +142,48 @@ class _Platform(object):
if string is None:
return cls()
if not string:
raise ValueError('Platform string must be non-empty')
parts = string.split('/', 2)
raise ValueError("Platform string must be non-empty")
parts = string.split("/", 2)
arch = None
variant = None
if len(parts) == 1:
_validate_part(string, string, 'OS/architecture')
_validate_part(string, string, "OS/architecture")
# The part is either OS or architecture
os = _normalize_os(string)
if os in _KNOWN_OS:
if daemon_arch is not None:
arch, variant = _normalize_arch(daemon_arch, '')
arch, variant = _normalize_arch(daemon_arch, "")
return cls(os=os, arch=arch, variant=variant)
arch, variant = _normalize_arch(os, '')
arch, variant = _normalize_arch(os, "")
if arch in _KNOWN_ARCH:
return cls(
os=_normalize_os(daemon_os) if daemon_os else None,
arch=arch or None,
variant=variant or None,
)
raise ValueError(f'Invalid platform string "{string}": unknown OS or architecture')
os = _validate_part(string, parts[0], 'OS')
raise ValueError(
f'Invalid platform string "{string}": unknown OS or architecture'
)
os = _validate_part(string, parts[0], "OS")
if not os:
raise ValueError(f'Invalid platform string "{string}": OS is empty')
arch = _validate_part(string, parts[1], 'architecture') if len(parts) > 1 else None
arch = (
_validate_part(string, parts[1], "architecture") if len(parts) > 1 else None
)
if arch is not None and not arch:
raise ValueError(f'Invalid platform string "{string}": architecture is empty')
variant = _validate_part(string, parts[2], 'variant') if len(parts) > 2 else None
raise ValueError(
f'Invalid platform string "{string}": architecture is empty'
)
variant = (
_validate_part(string, parts[2], "variant") if len(parts) > 2 else None
)
if variant is not None and not variant:
raise ValueError(f'Invalid platform string "{string}": variant is empty')
arch, variant = _normalize_arch(arch, variant or '')
if len(parts) == 2 and arch == 'arm' and variant == 'v7':
arch, variant = _normalize_arch(arch, variant or "")
if len(parts) == 2 and arch == "arm" and variant == "v7":
variant = None
if len(parts) == 3 and arch == 'arm64' and variant == '':
variant = 'v8'
if len(parts) == 3 and arch == "arm64" and variant == "":
variant = "v8"
return cls(os=_normalize_os(os), arch=arch, variant=variant or None)
def __str__(self):
@@ -152,27 +198,41 @@ class _Platform(object):
parts = [self.arch]
else:
parts = []
return '/'.join(parts)
return "/".join(parts)
def __repr__(self):
return f'_Platform(os={self.os!r}, arch={self.arch!r}, variant={self.variant!r})'
return (
f"_Platform(os={self.os!r}, arch={self.arch!r}, variant={self.variant!r})"
)
def __eq__(self, other):
return self.os == other.os and self.arch == other.arch and self.variant == other.variant
return (
self.os == other.os
and self.arch == other.arch
and self.variant == other.variant
)
def normalize_platform_string(string, daemon_os=None, daemon_arch=None):
return str(_Platform.parse_platform_string(string, daemon_os=daemon_os, daemon_arch=daemon_arch))
return str(
_Platform.parse_platform_string(
string, daemon_os=daemon_os, daemon_arch=daemon_arch
)
)
def compose_platform_string(os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None):
def compose_platform_string(
os=None, arch=None, variant=None, daemon_os=None, daemon_arch=None
):
if os is None and daemon_os is not None:
os = _normalize_os(daemon_os)
if arch is None and daemon_arch is not None:
arch, variant = _normalize_arch(daemon_arch, variant or '')
arch, variant = _normalize_arch(daemon_arch, variant or "")
variant = variant or None
return str(_Platform(os=os, arch=arch, variant=variant or None))
def compare_platform_strings(string1, string2):
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(string2)
return _Platform.parse_platform_string(string1) == _Platform.parse_platform_string(
string2
)
+9 -9
View File
@@ -11,32 +11,32 @@ from ansible.module_utils.common.text.converters import to_bytes, to_native, to_
def generate_insecure_key():
'''Do NOT use this for cryptographic purposes!'''
"""Do NOT use this for cryptographic purposes!"""
while True:
# Generate a one-byte key. Right now the functions below do not use more
# than one byte, so this is sufficient.
key = bytes([random.randint(0, 255)])
# Return anything that is not zero
if key != b'\x00':
if key != b"\x00":
return key
def scramble(value, key):
'''Do NOT use this for cryptographic purposes!'''
"""Do NOT use this for cryptographic purposes!"""
if len(key) < 1:
raise ValueError('Key must be at least one byte')
raise ValueError("Key must be at least one byte")
value = to_bytes(value)
k = key[0]
value = bytes([k ^ b for b in value])
return '=S=' + to_native(base64.b64encode(value))
return "=S=" + to_native(base64.b64encode(value))
def unscramble(value, key):
'''Do NOT use this for cryptographic purposes!'''
"""Do NOT use this for cryptographic purposes!"""
if len(key) < 1:
raise ValueError('Key must be at least one byte')
if not value.startswith('=S='):
raise ValueError('Value does not start with indicator')
raise ValueError("Key must be at least one byte")
if not value.startswith("=S="):
raise ValueError("Value does not start with indicator")
value = base64.b64decode(value[3:])
k = key[0]
value = bytes([k ^ b for b in value])
+277 -158
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import abc
import os
import platform
@@ -14,9 +13,11 @@ import traceback
from collections.abc import Mapping, Sequence
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
HAS_DOCKER_PY = True
HAS_DOCKER_PY_2 = False
@@ -26,15 +27,16 @@ HAS_DOCKER_TRACEBACK = None
try:
from requests.exceptions import SSLError
from docker import __version__ as docker_version
from docker import auth
from docker.errors import APIError, NotFound, TLSParameterError
from docker.tls import TLSConfig
from docker import auth
if LooseVersion(docker_version) >= LooseVersion('3.0.0'):
if LooseVersion(docker_version) >= LooseVersion("3.0.0"):
HAS_DOCKER_PY_3 = True
from docker import APIClient as Client
elif LooseVersion(docker_version) >= LooseVersion('2.0.0'):
elif LooseVersion(docker_version) >= LooseVersion("2.0.0"):
HAS_DOCKER_PY_2 = True
from docker import APIClient as Client
else:
@@ -52,6 +54,7 @@ except ImportError as exc:
try:
# docker (Docker SDK for Python >= 2.0.0)
import docker.models # noqa: F401, pylint: disable=unused-import
HAS_DOCKER_MODELS = True
except ImportError:
HAS_DOCKER_MODELS = False
@@ -59,13 +62,16 @@ except ImportError:
try:
# docker-py (Docker SDK for Python < 2.0.0)
import docker.ssladapter # noqa: F401, pylint: disable=unused-import
HAS_DOCKER_SSLADAPTER = True
except ImportError:
HAS_DOCKER_SSLADAPTER = False
try:
from requests.exceptions import RequestException # noqa: F401, pylint: disable=unused-import
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
RequestException,
)
except ImportError:
# Either Docker SDK for Python is no longer using requests, or Docker SDK for Python is not around either,
# or Docker SDK for Python's dependency requests is missing. In any case, define an exception
@@ -73,30 +79,59 @@ except ImportError:
class RequestException(Exception):
pass
from ansible_collections.community.docker.plugins.module_utils.util import (
BYTE_SUFFIXES, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_DOCKER_REGISTRY, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_TLS_HOSTNAME, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DOCKER_COMMON_ARGS_VARS, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DifferenceTracker, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DockerBaseClass, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
clean_dict_booleans_for_docker_api, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
compare_dict_allow_more_present, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
compare_generic, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
convert_duration_to_nanosecond, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_image_name_id, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_valid_tag, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
omit_none_from_dict, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
parse_healthcheck, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
DEFAULT_DOCKER_HOST,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_TLS,
DEFAULT_TLS_VERIFY,
DEFAULT_TLS_HOSTNAME, # TODO: remove
DEFAULT_TIMEOUT_SECONDS,
DOCKER_COMMON_ARGS,
DOCKER_COMMON_ARGS_VARS, # TODO: remove
DOCKER_MUTUALLY_EXCLUSIVE,
DOCKER_REQUIRED_TOGETHER,
DEFAULT_DOCKER_REGISTRY, # TODO: remove
BYTE_SUFFIXES, # TODO: remove
is_image_name_id, # TODO: remove
is_valid_tag, # TODO: remove
sanitize_result,
DockerBaseClass, # TODO: remove
update_tls_hostname,
compare_dict_allow_more_present, # TODO: remove
compare_generic, # TODO: remove
DifferenceTracker, # TODO: remove
clean_dict_booleans_for_docker_api, # TODO: remove
convert_duration_to_nanosecond, # TODO: remove
parse_healthcheck, # TODO: remove
omit_none_from_dict, # TODO: remove
)
@@ -120,8 +155,10 @@ if not HAS_DOCKER_PY:
def _get_tls_config(fail_function, **kwargs):
if 'assert_hostname' in kwargs and LooseVersion(docker_version) >= LooseVersion('7.0.0b1'):
assert_hostname = kwargs.pop('assert_hostname')
if "assert_hostname" in kwargs and LooseVersion(docker_version) >= LooseVersion(
"7.0.0b1"
):
assert_hostname = kwargs.pop("assert_hostname")
if assert_hostname is not None:
fail_function(
"tls_hostname is not compatible with Docker SDK for Python 7.0.0+. You are using"
@@ -139,51 +176,55 @@ def _get_tls_config(fail_function, **kwargs):
def is_using_tls(auth):
return auth['tls_verify'] or auth['tls']
return auth["tls_verify"] or auth["tls"]
def get_connect_params(auth, fail_function):
if is_using_tls(auth):
auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://')
auth["docker_host"] = auth["docker_host"].replace("tcp://", "https://")
result = dict(
base_url=auth['docker_host'],
version=auth['api_version'],
timeout=auth['timeout'],
base_url=auth["docker_host"],
version=auth["api_version"],
timeout=auth["timeout"],
)
if auth['tls_verify']:
if auth["tls_verify"]:
# TLS with verification
tls_config = dict(
verify=True,
assert_hostname=auth['tls_hostname'],
assert_hostname=auth["tls_hostname"],
fail_function=fail_function,
)
if auth['cert_path'] and auth['key_path']:
tls_config['client_cert'] = (auth['cert_path'], auth['key_path'])
if auth['cacert_path']:
tls_config['ca_cert'] = auth['cacert_path']
result['tls'] = _get_tls_config(**tls_config)
elif auth['tls']:
if auth["cert_path"] and auth["key_path"]:
tls_config["client_cert"] = (auth["cert_path"], auth["key_path"])
if auth["cacert_path"]:
tls_config["ca_cert"] = auth["cacert_path"]
result["tls"] = _get_tls_config(**tls_config)
elif auth["tls"]:
# TLS without verification
tls_config = dict(
verify=False,
fail_function=fail_function,
)
if auth['cert_path'] and auth['key_path']:
tls_config['client_cert'] = (auth['cert_path'], auth['key_path'])
result['tls'] = _get_tls_config(**tls_config)
if auth["cert_path"] and auth["key_path"]:
tls_config["client_cert"] = (auth["cert_path"], auth["key_path"])
result["tls"] = _get_tls_config(**tls_config)
if auth.get('use_ssh_client'):
if LooseVersion(docker_version) < LooseVersion('4.4.0'):
fail_function("use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer")
result['use_ssh_client'] = True
if auth.get("use_ssh_client"):
if LooseVersion(docker_version) < LooseVersion("4.4.0"):
fail_function(
"use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer"
)
result["use_ssh_client"] = True
# No TLS
return result
DOCKERPYUPGRADE_SWITCH_TO_DOCKER = "Try `pip uninstall docker-py` followed by `pip install docker`."
DOCKERPYUPGRADE_SWITCH_TO_DOCKER = (
"Try `pip uninstall docker-py` followed by `pip install docker`."
)
DOCKERPYUPGRADE_UPGRADE_DOCKER = "Use `pip install --upgrade docker` to upgrade."
DOCKERPYUPGRADE_RECOMMEND_DOCKER = "Use `pip install --upgrade docker-py` to upgrade."
@@ -192,17 +233,19 @@ class AnsibleDockerClientBase(Client):
def __init__(self, min_docker_version=None, min_docker_api_version=None):
if min_docker_version is None:
min_docker_version = MIN_DOCKER_VERSION
NEEDS_DOCKER_PY2 = (LooseVersion(min_docker_version) >= LooseVersion('2.0.0'))
NEEDS_DOCKER_PY2 = LooseVersion(min_docker_version) >= LooseVersion("2.0.0")
self.docker_py_version = LooseVersion(docker_version)
if HAS_DOCKER_MODELS and HAS_DOCKER_SSLADAPTER:
self.fail("Cannot have both the docker-py and docker python modules (old and new version of Docker "
"SDK for Python) installed together as they use the same namespace and cause a corrupt "
"installation. Please uninstall both packages, and re-install only the docker-py or docker "
f"python module (for {platform.node()}'s Python {sys.executable}). It is recommended to install the docker module. Please "
"note that simply uninstalling one of the modules can leave the other module in a broken "
"state.")
self.fail(
"Cannot have both the docker-py and docker python modules (old and new version of Docker "
"SDK for Python) installed together as they use the same namespace and cause a corrupt "
"installation. Please uninstall both packages, and re-install only the docker-py or docker "
f"python module (for {platform.node()}'s Python {sys.executable}). It is recommended to install the docker module. Please "
"note that simply uninstalling one of the modules can leave the other module in a broken "
"state."
)
if not HAS_DOCKER_PY:
msg = missing_required_lib("Docker SDK for Python: docker>=5.0.0")
@@ -218,13 +261,15 @@ class AnsibleDockerClientBase(Client):
# The minimal required version is < 2.0 (and the current version as well).
# Advertise docker (instead of docker-py).
msg += DOCKERPYUPGRADE_RECOMMEND_DOCKER
elif docker_version < LooseVersion('2.0'):
elif docker_version < LooseVersion("2.0"):
msg += DOCKERPYUPGRADE_SWITCH_TO_DOCKER
else:
msg += DOCKERPYUPGRADE_UPGRADE_DOCKER
self.fail(msg)
self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
self._connect_params = get_connect_params(
self.auth_params, fail_function=self.fail
)
try:
super(AnsibleDockerClientBase, self).__init__(**self._connect_params)
@@ -235,9 +280,11 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error connecting: {exc}")
self.docker_api_version = LooseVersion(self.docker_api_version_str)
min_docker_api_version = min_docker_api_version or '1.25'
min_docker_api_version = min_docker_api_version or "1.25"
if self.docker_api_version < LooseVersion(min_docker_api_version):
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
self.fail(
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
def log(self, msg, pretty_print=False):
pass
@@ -253,16 +300,16 @@ class AnsibleDockerClientBase(Client):
pass
@staticmethod
def _get_value(param_name, param_value, env_variable, default_value, type='str'):
def _get_value(param_name, param_value, env_variable, default_value, type="str"):
if param_value is not None:
# take module parameter value
if type == 'bool':
if type == "bool":
if param_value in BOOLEANS_TRUE:
return True
if param_value in BOOLEANS_FALSE:
return False
return bool(param_value)
if type == 'int':
if type == "int":
return int(param_value)
return param_value
@@ -270,19 +317,19 @@ class AnsibleDockerClientBase(Client):
env_value = os.environ.get(env_variable)
if env_value is not None:
# take the env variable value
if param_name == 'cert_path':
return os.path.join(env_value, 'cert.pem')
if param_name == 'cacert_path':
return os.path.join(env_value, 'ca.pem')
if param_name == 'key_path':
return os.path.join(env_value, 'key.pem')
if type == 'bool':
if param_name == "cert_path":
return os.path.join(env_value, "cert.pem")
if param_name == "cacert_path":
return os.path.join(env_value, "ca.pem")
if param_name == "key_path":
return os.path.join(env_value, "key.pem")
if type == "bool":
if env_value in BOOLEANS_TRUE:
return True
if env_value in BOOLEANS_FALSE:
return False
return bool(env_value)
if type == 'int':
if type == "int":
return int(env_value)
return env_value
@@ -298,7 +345,7 @@ class AnsibleDockerClientBase(Client):
# Get authentication credentials.
# Precedence: module parameters-> environment variables-> defaults.
self.log('Getting credentials')
self.log("Getting credentials")
client_params = self._get_params()
@@ -307,21 +354,56 @@ class AnsibleDockerClientBase(Client):
params[key] = client_params.get(key)
result = dict(
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
DEFAULT_DOCKER_HOST, type='str'),
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
'DOCKER_TLS_HOSTNAME', None, type='str'),
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
'auto', type='str'),
cacert_path=self._get_value('cacert_path', params['ca_path'], 'DOCKER_CERT_PATH', None, type='str'),
cert_path=self._get_value('cert_path', params['client_cert'], 'DOCKER_CERT_PATH', None, type='str'),
key_path=self._get_value('key_path', params['client_key'], 'DOCKER_CERT_PATH', None, type='str'),
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS, type='bool'),
tls_verify=self._get_value('validate_certs', params['validate_certs'], 'DOCKER_TLS_VERIFY',
DEFAULT_TLS_VERIFY, type='bool'),
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
DEFAULT_TIMEOUT_SECONDS, type='int'),
use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False, type='bool'),
docker_host=self._get_value(
"docker_host",
params["docker_host"],
"DOCKER_HOST",
DEFAULT_DOCKER_HOST,
type="str",
),
tls_hostname=self._get_value(
"tls_hostname",
params["tls_hostname"],
"DOCKER_TLS_HOSTNAME",
None,
type="str",
),
api_version=self._get_value(
"api_version",
params["api_version"],
"DOCKER_API_VERSION",
"auto",
type="str",
),
cacert_path=self._get_value(
"cacert_path", params["ca_path"], "DOCKER_CERT_PATH", None, type="str"
),
cert_path=self._get_value(
"cert_path", params["client_cert"], "DOCKER_CERT_PATH", None, type="str"
),
key_path=self._get_value(
"key_path", params["client_key"], "DOCKER_CERT_PATH", None, type="str"
),
tls=self._get_value(
"tls", params["tls"], "DOCKER_TLS", DEFAULT_TLS, type="bool"
),
tls_verify=self._get_value(
"validate_certs",
params["validate_certs"],
"DOCKER_TLS_VERIFY",
DEFAULT_TLS_VERIFY,
type="bool",
),
timeout=self._get_value(
"timeout",
params["timeout"],
"DOCKER_TIMEOUT",
DEFAULT_TIMEOUT_SECONDS,
type="int",
),
use_ssh_client=self._get_value(
"use_ssh_client", params["use_ssh_client"], None, False, type="bool"
),
)
update_tls_hostname(result)
@@ -331,11 +413,13 @@ class AnsibleDockerClientBase(Client):
def _handle_ssl_error(self, error):
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
if match:
hostname = self.auth_params['tls_hostname']
self.fail(f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true.")
hostname = self.auth_params["tls_hostname"]
self.fail(
f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true."
)
self.fail(f"SSL Exception: {error}")
def get_container_by_id(self, container_id):
@@ -350,27 +434,30 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error inspecting container: {exc}")
def get_container(self, name=None):
'''
"""
Lookup a container and return the inspection results.
'''
"""
if name is None:
return None
search_name = name
if not name.startswith('/'):
search_name = '/' + name
if not name.startswith("/"):
search_name = "/" + name
result = None
try:
for container in self.containers(all=True):
self.log(f"testing container: {container['Names']}")
if isinstance(container['Names'], list) and search_name in container['Names']:
if (
isinstance(container["Names"], list)
and search_name in container["Names"]
):
result = container
break
if container['Id'].startswith(name):
if container["Id"].startswith(name):
result = container
break
if container['Id'] == name:
if container["Id"] == name:
result = container
break
except SSLError as exc:
@@ -381,12 +468,12 @@ class AnsibleDockerClientBase(Client):
if result is None:
return None
return self.get_container_by_id(result['Id'])
return self.get_container_by_id(result["Id"])
def get_network(self, name=None, network_id=None):
'''
"""
Lookup a network and return the inspection results.
'''
"""
if name is None and network_id is None:
return None
@@ -396,10 +483,10 @@ class AnsibleDockerClientBase(Client):
try:
for network in self.networks():
self.log(f"testing network: {network['Name']}")
if name == network['Name']:
if name == network["Name"]:
result = network
break
if network['Id'].startswith(name):
if network["Id"].startswith(name):
result = network
break
except SSLError as exc:
@@ -408,7 +495,7 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error retrieving network list: {exc}")
if result is not None:
network_id = result['Id']
network_id = result["Id"]
if network_id is not None:
try:
@@ -423,9 +510,9 @@ class AnsibleDockerClientBase(Client):
return result
def find_image(self, name, tag):
'''
"""
Lookup an image (by name and tag) and return the inspection results.
'''
"""
if not name:
return None
@@ -434,14 +521,14 @@ class AnsibleDockerClientBase(Client):
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = auth.resolve_repository_name(name)
if registry == 'docker.io':
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith('library/'):
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len('library/'):]
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
@@ -451,7 +538,7 @@ class AnsibleDockerClientBase(Client):
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and '/' not in repo_name:
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
@@ -463,7 +550,7 @@ class AnsibleDockerClientBase(Client):
if len(images) == 1:
try:
inspection = self.inspect_image(images[0]['Id'])
inspection = self.inspect_image(images[0]["Id"])
except NotFound:
self.log(f"Image {name}:{tag} not found.")
return None
@@ -475,9 +562,9 @@ class AnsibleDockerClientBase(Client):
return None
def find_image_by_id(self, image_id, accept_missing_image=False):
'''
"""
Lookup an image (by ID) and return the inspection results.
'''
"""
if not image_id:
return None
@@ -494,11 +581,11 @@ class AnsibleDockerClientBase(Client):
return inspection
def _image_lookup(self, name, tag):
'''
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
'''
"""
try:
response = self.images(name=name)
except Exception as exc:
@@ -509,33 +596,35 @@ class AnsibleDockerClientBase(Client):
lookup_digest = f"{name}@{tag}"
images = []
for image in response:
tags = image.get('RepoTags')
digests = image.get('RepoDigests')
tags = image.get("RepoTags")
digests = image.get("RepoDigests")
if (tags and lookup in tags) or (digests and lookup_digest in digests):
images = [image]
break
return images
def pull_image(self, name, tag="latest", platform=None):
'''
"""
Pull an image
'''
"""
kwargs = dict(
tag=tag,
stream=True,
decode=True,
)
if platform is not None:
kwargs['platform'] = platform
kwargs["platform"] = platform
self.log(f"Pulling image {name}:{tag}")
old_tag = self.find_image(name, tag)
try:
for line in self.pull(name, **kwargs):
self.log(line, pretty_print=True)
if line.get('error'):
if line.get('errorDetail'):
error_detail = line.get('errorDetail')
self.fail(f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}")
if line.get("error"):
if line.get("errorDetail"):
error_detail = line.get("errorDetail")
self.fail(
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
)
else:
self.fail(f"Error pulling {name} - {line.get('error')}")
except Exception as exc:
@@ -546,27 +635,43 @@ class AnsibleDockerClientBase(Client):
return new_tag, old_tag == new_tag
def inspect_distribution(self, image, **kwargs):
'''
"""
Get image digest by directly calling the Docker API when running Docker SDK < 4.0.0
since prior versions did not support accessing private repositories.
'''
if self.docker_py_version < LooseVersion('4.0.0'):
"""
if self.docker_py_version < LooseVersion("4.0.0"):
registry = auth.resolve_repository_name(image)[0]
header = auth.get_config_header(self, registry)
if header:
return self._result(self._get(
self._url('/distribution/{0}/json', image),
headers={'X-Registry-Auth': header}
), json=True)
return super(AnsibleDockerClientBase, self).inspect_distribution(image, **kwargs)
return self._result(
self._get(
self._url("/distribution/{0}/json", image),
headers={"X-Registry-Auth": header},
),
json=True,
)
return super(AnsibleDockerClientBase, self).inspect_distribution(
image, **kwargs
)
class AnsibleDockerClient(AnsibleDockerClientBase):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None, required_one_of=None, required_by=None,
min_docker_version=None, min_docker_api_version=None, option_minimal_versions=None,
option_minimal_versions_ignore_params=None, fail_results=None):
def __init__(
self,
argument_spec=None,
supports_check_mode=False,
mutually_exclusive=None,
required_together=None,
required_if=None,
required_one_of=None,
required_by=None,
min_docker_version=None,
min_docker_api_version=None,
option_minimal_versions=None,
option_minimal_versions_ignore_params=None,
fail_results=None,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
@@ -598,22 +703,27 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
required_by=required_by or {},
)
self.debug = self.module.params.get('debug')
self.debug = self.module.params.get("debug")
self.check_mode = self.module.check_mode
super(AnsibleDockerClient, self).__init__(
min_docker_version=min_docker_version,
min_docker_api_version=min_docker_api_version)
min_docker_api_version=min_docker_api_version,
)
if option_minimal_versions is not None:
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
self._get_minimal_versions(
option_minimal_versions, option_minimal_versions_ignore_params
)
def fail(self, msg, **kwargs):
self.fail_results.update(kwargs)
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
self.module.deprecate(
msg, version=version, date=date, collection_name=collection_name
)
def _get_params(self):
return self.module.params
@@ -631,26 +741,33 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
# Test whether option is supported, and store result
support_docker_py = True
support_docker_api = True
if 'docker_py_version' in data:
support_docker_py = self.docker_py_version >= LooseVersion(data['docker_py_version'])
if 'docker_api_version' in data:
support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version'])
data['supported'] = support_docker_py and support_docker_api
if "docker_py_version" in data:
support_docker_py = self.docker_py_version >= LooseVersion(
data["docker_py_version"]
)
if "docker_api_version" in data:
support_docker_api = self.docker_api_version >= LooseVersion(
data["docker_api_version"]
)
data["supported"] = support_docker_py and support_docker_api
# Fail if option is not supported but used
if not data['supported']:
if not data["supported"]:
# Test whether option is specified
if 'detect_usage' in data:
used = data['detect_usage'](self)
if "detect_usage" in data:
used = data["detect_usage"](self)
else:
used = self.module.params.get(option) is not None
if used and 'default' in self.module.argument_spec[option]:
used = self.module.params[option] != self.module.argument_spec[option]['default']
if used and "default" in self.module.argument_spec[option]:
used = (
self.module.params[option]
!= self.module.argument_spec[option]["default"]
)
if used:
# If the option is used, compose error message.
if 'usage_msg' in data:
usg = data['usage_msg']
if "usage_msg" in data:
usg = data["usage_msg"]
else:
usg = f'set {option} option'
usg = f"set {option} option"
if not support_docker_api:
msg = f"Docker API version is {self.docker_api_version_str}. Minimum version required is {data['docker_api_version']} to {usg}."
elif not support_docker_py:
@@ -658,19 +775,21 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
f"Docker SDK for Python version is {docker_version} ({platform.node()}'s Python {sys.executable})."
f" Minimum version required is {data['docker_py_version']} to {usg}. "
)
if LooseVersion(data['docker_py_version']) < LooseVersion('2.0.0'):
if LooseVersion(data["docker_py_version"]) < LooseVersion(
"2.0.0"
):
msg += DOCKERPYUPGRADE_RECOMMEND_DOCKER
elif self.docker_py_version < LooseVersion('2.0.0'):
elif self.docker_py_version < LooseVersion("2.0.0"):
msg += DOCKERPYUPGRADE_SWITCH_TO_DOCKER
else:
msg += DOCKERPYUPGRADE_UPGRADE_DOCKER
else:
# should not happen
msg = f'Cannot {usg} with your configuration.'
msg = f"Cannot {usg} with your configuration."
self.fail(msg)
def report_warnings(self, result, warnings_key=None):
'''
"""
Checks result of client operation for warnings, and if present, outputs them.
warnings_key should be a list of keys used to crawl the result dictionary.
@@ -681,15 +800,15 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
In most cases (if warnings are returned at all), warnings_key should be
['Warnings'] or ['Warning']. The default value (if not specified) is ['Warnings'].
'''
"""
if warnings_key is None:
warnings_key = ['Warnings']
warnings_key = ["Warnings"]
for key in warnings_key:
if not isinstance(result, Mapping):
return
result = result.get(key)
if isinstance(result, Sequence):
for warning in result:
self.module.warn(f'Docker warning: {warning}')
self.module.warn(f"Docker warning: {warning}")
elif isinstance(result, str) and result:
self.module.warn(f'Docker warning: {result}')
self.module.warn(f"Docker warning: {result}")
+235 -142
View File
@@ -5,30 +5,37 @@
from __future__ import annotations
import abc
import os
import re
from collections.abc import Mapping, Sequence
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.parsing.convert_bool import BOOLEANS_TRUE, BOOLEANS_FALSE
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
try:
from requests.exceptions import RequestException, SSLError # noqa: F401, pylint: disable=unused-import
from requests.exceptions import ( # noqa: F401, pylint: disable=unused-import
RequestException,
SSLError,
)
except ImportError:
# Define an exception class RequestException so that our code does not break.
class RequestException(Exception):
pass
from ansible_collections.community.docker.plugins.module_utils._api import auth
from ansible_collections.community.docker.plugins.module_utils._api.api.client import APIClient as Client
from ansible_collections.community.docker.plugins.module_utils._api.api.client import (
APIClient as Client,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
NotFound,
MissingRequirementException,
NotFound,
TLSParameterError,
)
from ansible_collections.community.docker.plugins.module_utils._api.tls import TLSConfig
@@ -36,19 +43,26 @@ from ansible_collections.community.docker.plugins.module_utils._api.utils.utils
convert_filters,
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_DOCKER_REGISTRY, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
DEFAULT_TLS_HOSTNAME, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_image_name_id, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import (
is_valid_tag, # TODO: remove
)
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
DEFAULT_DOCKER_HOST,
DEFAULT_TIMEOUT_SECONDS,
DEFAULT_TLS,
DEFAULT_TLS_VERIFY,
DEFAULT_TLS_HOSTNAME, # TODO: remove
DEFAULT_TIMEOUT_SECONDS,
DOCKER_COMMON_ARGS,
DOCKER_MUTUALLY_EXCLUSIVE,
DOCKER_REQUIRED_TOGETHER,
DEFAULT_DOCKER_REGISTRY, # TODO: remove
is_image_name_id, # TODO: remove
is_valid_tag, # TODO: remove
sanitize_result,
update_tls_hostname,
)
@@ -63,43 +77,45 @@ def _get_tls_config(fail_function, **kwargs):
def is_using_tls(auth_data):
return auth_data['tls_verify'] or auth_data['tls']
return auth_data["tls_verify"] or auth_data["tls"]
def get_connect_params(auth_data, fail_function):
if is_using_tls(auth_data):
auth_data['docker_host'] = auth_data['docker_host'].replace('tcp://', 'https://')
auth_data["docker_host"] = auth_data["docker_host"].replace(
"tcp://", "https://"
)
result = dict(
base_url=auth_data['docker_host'],
version=auth_data['api_version'],
timeout=auth_data['timeout'],
base_url=auth_data["docker_host"],
version=auth_data["api_version"],
timeout=auth_data["timeout"],
)
if auth_data['tls_verify']:
if auth_data["tls_verify"]:
# TLS with verification
tls_config = dict(
verify=True,
assert_hostname=auth_data['tls_hostname'],
assert_hostname=auth_data["tls_hostname"],
fail_function=fail_function,
)
if auth_data['cert_path'] and auth_data['key_path']:
tls_config['client_cert'] = (auth_data['cert_path'], auth_data['key_path'])
if auth_data['cacert_path']:
tls_config['ca_cert'] = auth_data['cacert_path']
result['tls'] = _get_tls_config(**tls_config)
elif auth_data['tls']:
if auth_data["cert_path"] and auth_data["key_path"]:
tls_config["client_cert"] = (auth_data["cert_path"], auth_data["key_path"])
if auth_data["cacert_path"]:
tls_config["ca_cert"] = auth_data["cacert_path"]
result["tls"] = _get_tls_config(**tls_config)
elif auth_data["tls"]:
# TLS without verification
tls_config = dict(
verify=False,
fail_function=fail_function,
)
if auth_data['cert_path'] and auth_data['key_path']:
tls_config['client_cert'] = (auth_data['cert_path'], auth_data['key_path'])
result['tls'] = _get_tls_config(**tls_config)
if auth_data["cert_path"] and auth_data["key_path"]:
tls_config["client_cert"] = (auth_data["cert_path"], auth_data["key_path"])
result["tls"] = _get_tls_config(**tls_config)
if auth_data.get('use_ssh_client'):
result['use_ssh_client'] = True
if auth_data.get("use_ssh_client"):
result["use_ssh_client"] = True
# No TLS
return result
@@ -107,22 +123,28 @@ def get_connect_params(auth_data, fail_function):
class AnsibleDockerClientBase(Client):
def __init__(self, min_docker_api_version=None):
self._connect_params = get_connect_params(self.auth_params, fail_function=self.fail)
self._connect_params = get_connect_params(
self.auth_params, fail_function=self.fail
)
try:
super(AnsibleDockerClientBase, self).__init__(**self._connect_params)
self.docker_api_version_str = self.api_version
except MissingRequirementException as exc:
self.fail(missing_required_lib(exc.requirement), exception=exc.import_exception)
self.fail(
missing_required_lib(exc.requirement), exception=exc.import_exception
)
except APIError as exc:
self.fail(f"Docker API error: {exc}")
except Exception as exc:
self.fail(f"Error connecting: {exc}")
self.docker_api_version = LooseVersion(self.docker_api_version_str)
min_docker_api_version = min_docker_api_version or '1.25'
min_docker_api_version = min_docker_api_version or "1.25"
if self.docker_api_version < LooseVersion(min_docker_api_version):
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
self.fail(
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
def log(self, msg, pretty_print=False):
pass
@@ -138,16 +160,16 @@ class AnsibleDockerClientBase(Client):
pass
@staticmethod
def _get_value(param_name, param_value, env_variable, default_value, type='str'):
def _get_value(param_name, param_value, env_variable, default_value, type="str"):
if param_value is not None:
# take module parameter value
if type == 'bool':
if type == "bool":
if param_value in BOOLEANS_TRUE:
return True
if param_value in BOOLEANS_FALSE:
return False
return bool(param_value)
if type == 'int':
if type == "int":
return int(param_value)
return param_value
@@ -155,19 +177,19 @@ class AnsibleDockerClientBase(Client):
env_value = os.environ.get(env_variable)
if env_value is not None:
# take the env variable value
if param_name == 'cert_path':
return os.path.join(env_value, 'cert.pem')
if param_name == 'cacert_path':
return os.path.join(env_value, 'ca.pem')
if param_name == 'key_path':
return os.path.join(env_value, 'key.pem')
if type == 'bool':
if param_name == "cert_path":
return os.path.join(env_value, "cert.pem")
if param_name == "cacert_path":
return os.path.join(env_value, "ca.pem")
if param_name == "key_path":
return os.path.join(env_value, "key.pem")
if type == "bool":
if env_value in BOOLEANS_TRUE:
return True
if env_value in BOOLEANS_FALSE:
return False
return bool(env_value)
if type == 'int':
if type == "int":
return int(env_value)
return env_value
@@ -183,7 +205,7 @@ class AnsibleDockerClientBase(Client):
# Get authentication credentials.
# Precedence: module parameters-> environment variables-> defaults.
self.log('Getting credentials')
self.log("Getting credentials")
client_params = self._get_params()
@@ -192,44 +214,86 @@ class AnsibleDockerClientBase(Client):
params[key] = client_params.get(key)
result = dict(
docker_host=self._get_value('docker_host', params['docker_host'], 'DOCKER_HOST',
DEFAULT_DOCKER_HOST, type='str'),
tls_hostname=self._get_value('tls_hostname', params['tls_hostname'],
'DOCKER_TLS_HOSTNAME', None, type='str'),
api_version=self._get_value('api_version', params['api_version'], 'DOCKER_API_VERSION',
'auto', type='str'),
cacert_path=self._get_value('cacert_path', params['ca_path'], 'DOCKER_CERT_PATH', None, type='str'),
cert_path=self._get_value('cert_path', params['client_cert'], 'DOCKER_CERT_PATH', None, type='str'),
key_path=self._get_value('key_path', params['client_key'], 'DOCKER_CERT_PATH', None, type='str'),
tls=self._get_value('tls', params['tls'], 'DOCKER_TLS', DEFAULT_TLS, type='bool'),
tls_verify=self._get_value('validate_certs', params['validate_certs'], 'DOCKER_TLS_VERIFY',
DEFAULT_TLS_VERIFY, type='bool'),
timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT',
DEFAULT_TIMEOUT_SECONDS, type='int'),
use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False, type='bool'),
docker_host=self._get_value(
"docker_host",
params["docker_host"],
"DOCKER_HOST",
DEFAULT_DOCKER_HOST,
type="str",
),
tls_hostname=self._get_value(
"tls_hostname",
params["tls_hostname"],
"DOCKER_TLS_HOSTNAME",
None,
type="str",
),
api_version=self._get_value(
"api_version",
params["api_version"],
"DOCKER_API_VERSION",
"auto",
type="str",
),
cacert_path=self._get_value(
"cacert_path", params["ca_path"], "DOCKER_CERT_PATH", None, type="str"
),
cert_path=self._get_value(
"cert_path", params["client_cert"], "DOCKER_CERT_PATH", None, type="str"
),
key_path=self._get_value(
"key_path", params["client_key"], "DOCKER_CERT_PATH", None, type="str"
),
tls=self._get_value(
"tls", params["tls"], "DOCKER_TLS", DEFAULT_TLS, type="bool"
),
tls_verify=self._get_value(
"validate_certs",
params["validate_certs"],
"DOCKER_TLS_VERIFY",
DEFAULT_TLS_VERIFY,
type="bool",
),
timeout=self._get_value(
"timeout",
params["timeout"],
"DOCKER_TIMEOUT",
DEFAULT_TIMEOUT_SECONDS,
type="int",
),
use_ssh_client=self._get_value(
"use_ssh_client", params["use_ssh_client"], None, False, type="bool"
),
)
def depr(*args, **kwargs):
self.deprecate(*args, **kwargs)
update_tls_hostname(result, old_behavior=True, deprecate_function=depr, uses_tls=is_using_tls(result))
update_tls_hostname(
result,
old_behavior=True,
deprecate_function=depr,
uses_tls=is_using_tls(result),
)
return result
def _handle_ssl_error(self, error):
match = re.match(r"hostname.*doesn\'t match (\'.*\')", str(error))
if match:
hostname = self.auth_params['tls_hostname']
self.fail(f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true.")
hostname = self.auth_params["tls_hostname"]
self.fail(
f"You asked for verification that Docker daemons certificate's hostname matches {hostname}. "
f"The actual certificate's hostname is {match.group(1)}. Most likely you need to set DOCKER_TLS_HOSTNAME "
f"or pass `tls_hostname` with a value of {match.group(1)}. You may also use TLS without verification by "
"setting the `tls` parameter to true."
)
self.fail(f"SSL Exception: {error}")
def get_container_by_id(self, container_id):
try:
self.log(f"Inspecting container Id {container_id}")
result = self.get_json('/containers/{0}/json', container_id)
result = self.get_json("/containers/{0}/json", container_id)
self.log("Completed container inspection")
return result
except NotFound as dummy:
@@ -238,34 +302,37 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error inspecting container: {exc}")
def get_container(self, name=None):
'''
"""
Lookup a container and return the inspection results.
'''
"""
if name is None:
return None
search_name = name
if not name.startswith('/'):
search_name = '/' + name
if not name.startswith("/"):
search_name = "/" + name
result = None
try:
params = {
'limit': -1,
'all': 1,
'size': 0,
'trunc_cmd': 0,
"limit": -1,
"all": 1,
"size": 0,
"trunc_cmd": 0,
}
containers = self.get_json("/containers/json", params=params)
for container in containers:
self.log(f"testing container: {container['Names']}")
if isinstance(container['Names'], list) and search_name in container['Names']:
if (
isinstance(container["Names"], list)
and search_name in container["Names"]
):
result = container
break
if container['Id'].startswith(name):
if container["Id"].startswith(name):
result = container
break
if container['Id'] == name:
if container["Id"] == name:
result = container
break
except SSLError as exc:
@@ -276,12 +343,12 @@ class AnsibleDockerClientBase(Client):
if result is None:
return None
return self.get_container_by_id(result['Id'])
return self.get_container_by_id(result["Id"])
def get_network(self, name=None, network_id=None):
'''
"""
Lookup a network and return the inspection results.
'''
"""
if name is None and network_id is None:
return None
@@ -292,10 +359,10 @@ class AnsibleDockerClientBase(Client):
networks = self.get_json("/networks")
for network in networks:
self.log(f"testing network: {network['Name']}")
if name == network['Name']:
if name == network["Name"]:
result = network
break
if network['Id'].startswith(name):
if network["Id"].startswith(name):
result = network
break
except SSLError as exc:
@@ -304,12 +371,12 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error retrieving network list: {exc}")
if result is not None:
network_id = result['Id']
network_id = result["Id"]
if network_id is not None:
try:
self.log(f"Inspecting network Id {network_id}")
result = self.get_json('/networks/{0}', network_id)
result = self.get_json("/networks/{0}", network_id)
self.log("Completed network inspection")
except NotFound as dummy:
return None
@@ -319,21 +386,21 @@ class AnsibleDockerClientBase(Client):
return result
def _image_lookup(self, name, tag):
'''
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
'''
"""
try:
params = {
'only_ids': 0,
'all': 0,
"only_ids": 0,
"all": 0,
}
if LooseVersion(self.api_version) < LooseVersion('1.25'):
if LooseVersion(self.api_version) < LooseVersion("1.25"):
# only use "filter" on API 1.24 and under, as it is deprecated
params['filter'] = name
params["filter"] = name
else:
params['filters'] = convert_filters({'reference': name})
params["filters"] = convert_filters({"reference": name})
images = self.get_json("/images/json", params=params)
except Exception as exc:
self.fail(f"Error searching for image {name} - {exc}")
@@ -343,17 +410,17 @@ class AnsibleDockerClientBase(Client):
response = images
images = []
for image in response:
tags = image.get('RepoTags')
digests = image.get('RepoDigests')
tags = image.get("RepoTags")
digests = image.get("RepoDigests")
if (tags and lookup in tags) or (digests and lookup_digest in digests):
images = [image]
break
return images
def find_image(self, name, tag):
'''
"""
Lookup an image (by name and tag) and return the inspection results.
'''
"""
if not name:
return None
@@ -362,14 +429,14 @@ class AnsibleDockerClientBase(Client):
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = auth.resolve_repository_name(name)
if registry == 'docker.io':
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith('library/'):
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len('library/'):]
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
@@ -379,7 +446,7 @@ class AnsibleDockerClientBase(Client):
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and '/' not in repo_name:
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
@@ -391,7 +458,7 @@ class AnsibleDockerClientBase(Client):
if len(images) == 1:
try:
return self.get_json('/images/{0}/json', images[0]['Id'])
return self.get_json("/images/{0}/json", images[0]["Id"])
except NotFound:
self.log(f"Image {name}:{tag} not found.")
return None
@@ -402,15 +469,15 @@ class AnsibleDockerClientBase(Client):
return None
def find_image_by_id(self, image_id, accept_missing_image=False):
'''
"""
Lookup an image (by ID) and return the inspection results.
'''
"""
if not image_id:
return None
self.log(f"Find image {image_id} (by ID)")
try:
return self.get_json('/images/{0}/json', image_id)
return self.get_json("/images/{0}/json", image_id)
except NotFound as exc:
if not accept_missing_image:
self.fail(f"Error inspecting image ID {image_id} - {exc}")
@@ -420,37 +487,42 @@ class AnsibleDockerClientBase(Client):
self.fail(f"Error inspecting image ID {image_id} - {exc}")
def pull_image(self, name, tag="latest", platform=None):
'''
"""
Pull an image
'''
"""
self.log(f"Pulling image {name}:{tag}")
old_tag = self.find_image(name, tag)
try:
repository, image_tag = parse_repository_tag(name)
registry, repo_name = auth.resolve_repository_name(repository)
params = {
'tag': tag or image_tag or 'latest',
'fromImage': repository,
"tag": tag or image_tag or "latest",
"fromImage": repository,
}
if platform is not None:
params['platform'] = platform
params["platform"] = platform
headers = {}
header = auth.get_config_header(self, registry)
if header:
headers['X-Registry-Auth'] = header
headers["X-Registry-Auth"] = header
response = self._post(
self._url('/images/create'), params=params, headers=headers,
stream=True, timeout=None
self._url("/images/create"),
params=params,
headers=headers,
stream=True,
timeout=None,
)
self._raise_for_status(response)
for line in self._stream_helper(response, decode=True):
self.log(line, pretty_print=True)
if line.get('error'):
if line.get('errorDetail'):
error_detail = line.get('errorDetail')
self.fail(f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}")
if line.get("error"):
if line.get("errorDetail"):
error_detail = line.get("errorDetail")
self.fail(
f"Error pulling {name} - code: {error_detail.get('code')} message: {error_detail.get('message')}"
)
else:
self.fail(f"Error pulling {name} - {line.get('error')}")
except Exception as exc:
@@ -463,10 +535,20 @@ class AnsibleDockerClientBase(Client):
class AnsibleDockerClient(AnsibleDockerClientBase):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None, required_one_of=None, required_by=None,
min_docker_api_version=None, option_minimal_versions=None,
option_minimal_versions_ignore_params=None, fail_results=None):
def __init__(
self,
argument_spec=None,
supports_check_mode=False,
mutually_exclusive=None,
required_together=None,
required_if=None,
required_one_of=None,
required_by=None,
min_docker_api_version=None,
option_minimal_versions=None,
option_minimal_versions_ignore_params=None,
fail_results=None,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
@@ -498,20 +580,26 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
required_by=required_by or {},
)
self.debug = self.module.params.get('debug')
self.debug = self.module.params.get("debug")
self.check_mode = self.module.check_mode
super(AnsibleDockerClient, self).__init__(min_docker_api_version=min_docker_api_version)
super(AnsibleDockerClient, self).__init__(
min_docker_api_version=min_docker_api_version
)
if option_minimal_versions is not None:
self._get_minimal_versions(option_minimal_versions, option_minimal_versions_ignore_params)
self._get_minimal_versions(
option_minimal_versions, option_minimal_versions_ignore_params
)
def fail(self, msg, **kwargs):
self.fail_results.update(kwargs)
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
self.module.deprecate(
msg, version=version, date=date, collection_name=collection_name
)
def _get_params(self):
return self.module.params
@@ -528,33 +616,38 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
for option, data in self.option_minimal_versions.items():
# Test whether option is supported, and store result
support_docker_api = True
if 'docker_api_version' in data:
support_docker_api = self.docker_api_version >= LooseVersion(data['docker_api_version'])
data['supported'] = support_docker_api
if "docker_api_version" in data:
support_docker_api = self.docker_api_version >= LooseVersion(
data["docker_api_version"]
)
data["supported"] = support_docker_api
# Fail if option is not supported but used
if not data['supported']:
if not data["supported"]:
# Test whether option is specified
if 'detect_usage' in data:
used = data['detect_usage'](self)
if "detect_usage" in data:
used = data["detect_usage"](self)
else:
used = self.module.params.get(option) is not None
if used and 'default' in self.module.argument_spec[option]:
used = self.module.params[option] != self.module.argument_spec[option]['default']
if used and "default" in self.module.argument_spec[option]:
used = (
self.module.params[option]
!= self.module.argument_spec[option]["default"]
)
if used:
# If the option is used, compose error message.
if 'usage_msg' in data:
usg = data['usage_msg']
if "usage_msg" in data:
usg = data["usage_msg"]
else:
usg = f'set {option} option'
usg = f"set {option} option"
if not support_docker_api:
msg = f"Docker API version is {self.docker_api_version_str}. Minimum version required is {data['docker_api_version']} to {usg}."
else:
# should not happen
msg = f'Cannot {usg} with your configuration.'
msg = f"Cannot {usg} with your configuration."
self.fail(msg)
def report_warnings(self, result, warnings_key=None):
'''
"""
Checks result of client operation for warnings, and if present, outputs them.
warnings_key should be a list of keys used to crawl the result dictionary.
@@ -565,15 +658,15 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
In most cases (if warnings are returned at all), warnings_key should be
['Warnings'] or ['Warning']. The default value (if not specified) is ['Warnings'].
'''
"""
if warnings_key is None:
warnings_key = ['Warnings']
warnings_key = ["Warnings"]
for key in warnings_key:
if not isinstance(result, Mapping):
return
result = result.get(key)
if isinstance(result, Sequence):
for warning in result:
self.module.warn(f'Docker warning: {warning}')
self.module.warn(f"Docker warning: {warning}")
elif isinstance(result, str) and result:
self.module.warn(f'Docker warning: {result}')
self.module.warn(f"Docker warning: {result}")
+133 -77
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import abc
import json
import shlex
@@ -12,11 +11,9 @@ import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils._api.auth import resolve_repository_name
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
resolve_repository_name,
)
from ansible_collections.community.docker.plugins.module_utils.util import ( # noqa: F401, pylint: disable=unused-import
DEFAULT_DOCKER_HOST,
DEFAULT_TLS,
@@ -25,20 +22,35 @@ from ansible_collections.community.docker.plugins.module_utils.util import ( #
DOCKER_REQUIRED_TOGETHER,
sanitize_result,
)
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
DOCKER_COMMON_ARGS = dict(
docker_cli=dict(type='path'),
docker_host=dict(type='str', fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']),
client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
docker_cli=dict(type="path"),
docker_host=dict(
type="str", fallback=(env_fallback, ["DOCKER_HOST"]), aliases=["docker_url"]
),
tls_hostname=dict(type="str", fallback=(env_fallback, ["DOCKER_TLS_HOSTNAME"])),
api_version=dict(
type="str",
default="auto",
fallback=(env_fallback, ["DOCKER_API_VERSION"]),
aliases=["docker_api_version"],
),
ca_path=dict(type="path", aliases=["ca_cert", "tls_ca_cert", "cacert_path"]),
client_cert=dict(type="path", aliases=["tls_client_cert", "cert_path"]),
client_key=dict(type="path", aliases=["tls_client_key", "key_path"]),
tls=dict(type="bool", default=DEFAULT_TLS, fallback=(env_fallback, ["DOCKER_TLS"])),
validate_certs=dict(
type="bool",
default=DEFAULT_TLS_VERIFY,
fallback=(env_fallback, ["DOCKER_TLS_VERIFY"]),
aliases=["tls_verify"],
),
# debug=dict(type='bool', default=False),
cli_context=dict(type='str'),
cli_context=dict(type="str"),
)
@@ -47,55 +59,71 @@ class DockerException(Exception):
class AnsibleDockerClientBase(object):
def __init__(self, common_args, min_docker_api_version=None, needs_api_version=True):
def __init__(
self, common_args, min_docker_api_version=None, needs_api_version=True
):
self._environment = {}
if common_args['tls_hostname']:
self._environment['DOCKER_TLS_HOSTNAME'] = common_args['tls_hostname']
if common_args['api_version'] and common_args['api_version'] != 'auto':
self._environment['DOCKER_API_VERSION'] = common_args['api_version']
self._cli = common_args.get('docker_cli')
if common_args["tls_hostname"]:
self._environment["DOCKER_TLS_HOSTNAME"] = common_args["tls_hostname"]
if common_args["api_version"] and common_args["api_version"] != "auto":
self._environment["DOCKER_API_VERSION"] = common_args["api_version"]
self._cli = common_args.get("docker_cli")
if self._cli is None:
try:
self._cli = get_bin_path('docker')
self._cli = get_bin_path("docker")
except ValueError:
self.fail('Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter')
self.fail(
"Cannot find docker CLI in path. Please provide it explicitly with the docker_cli parameter"
)
self._cli_base = [self._cli]
docker_host = common_args['docker_host']
if not docker_host and not common_args['cli_context']:
docker_host = common_args["docker_host"]
if not docker_host and not common_args["cli_context"]:
docker_host = DEFAULT_DOCKER_HOST
if docker_host:
self._cli_base.extend(['--host', docker_host])
if common_args['validate_certs']:
self._cli_base.append('--tlsverify')
elif common_args['tls']:
self._cli_base.append('--tls')
if common_args['ca_path']:
self._cli_base.extend(['--tlscacert', common_args['ca_path']])
if common_args['client_cert']:
self._cli_base.extend(['--tlscert', common_args['client_cert']])
if common_args['client_key']:
self._cli_base.extend(['--tlskey', common_args['client_key']])
if common_args['cli_context']:
self._cli_base.extend(['--context', common_args['cli_context']])
self._cli_base.extend(["--host", docker_host])
if common_args["validate_certs"]:
self._cli_base.append("--tlsverify")
elif common_args["tls"]:
self._cli_base.append("--tls")
if common_args["ca_path"]:
self._cli_base.extend(["--tlscacert", common_args["ca_path"]])
if common_args["client_cert"]:
self._cli_base.extend(["--tlscert", common_args["client_cert"]])
if common_args["client_key"]:
self._cli_base.extend(["--tlskey", common_args["client_key"]])
if common_args["cli_context"]:
self._cli_base.extend(["--context", common_args["cli_context"]])
# `--format json` was only added as a shorthand for `--format {{ json . }}` in Docker 23.0
dummy, self._version, dummy = self.call_cli_json('version', '--format', '{{ json . }}', check_rc=True)
dummy, self._version, dummy = self.call_cli_json(
"version", "--format", "{{ json . }}", check_rc=True
)
self._info = None
if needs_api_version:
if not isinstance(self._version.get('Server'), dict) or not isinstance(self._version['Server'].get('ApiVersion'), str):
self.fail('Cannot determine Docker Daemon information. Are you maybe using podman instead of docker?')
self.docker_api_version_str = to_native(self._version['Server']['ApiVersion'])
if not isinstance(self._version.get("Server"), dict) or not isinstance(
self._version["Server"].get("ApiVersion"), str
):
self.fail(
"Cannot determine Docker Daemon information. Are you maybe using podman instead of docker?"
)
self.docker_api_version_str = to_native(
self._version["Server"]["ApiVersion"]
)
self.docker_api_version = LooseVersion(self.docker_api_version_str)
min_docker_api_version = min_docker_api_version or '1.25'
min_docker_api_version = min_docker_api_version or "1.25"
if self.docker_api_version < LooseVersion(min_docker_api_version):
self.fail(f'Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}.')
self.fail(
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
else:
self.docker_api_version_str = None
self.docker_api_version = None
if min_docker_api_version is not None:
self.fail('Internal error: cannot have needs_api_version=False with min_docker_api_version not None')
self.fail(
"Internal error: cannot have needs_api_version=False with min_docker_api_version not None"
)
def log(self, msg, pretty_print=False):
pass
@@ -113,7 +141,7 @@ class AnsibleDockerClientBase(object):
return self._cli_base + list(args)
def _compose_cmd_str(self, args):
return ' '.join(shlex.quote(a) for a in self._compose_cmd(args))
return " ".join(shlex.quote(a) for a in self._compose_cmd(args))
@abc.abstractmethod
def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None):
@@ -121,19 +149,21 @@ class AnsibleDockerClientBase(object):
# def call_cli_json(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False):
def call_cli_json(self, *args, **kwargs):
warn_on_stderr = kwargs.pop('warn_on_stderr', False)
warn_on_stderr = kwargs.pop("warn_on_stderr", False)
rc, stdout, stderr = self.call_cli(*args, **kwargs)
if warn_on_stderr and stderr:
self.warn(to_native(stderr))
try:
data = json.loads(stdout)
except Exception as exc:
self.fail(f'Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}')
self.fail(
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}"
)
return rc, data, stderr
# def call_cli_json_stream(self, *args, check_rc=False, data=None, cwd=None, environ_update=None, warn_on_stderr=False):
def call_cli_json_stream(self, *args, **kwargs):
warn_on_stderr = kwargs.pop('warn_on_stderr', False)
warn_on_stderr = kwargs.pop("warn_on_stderr", False)
rc, stdout, stderr = self.call_cli(*args, **kwargs)
if warn_on_stderr and stderr:
self.warn(to_native(stderr))
@@ -141,10 +171,12 @@ class AnsibleDockerClientBase(object):
try:
for line in stdout.splitlines():
line = line.strip()
if line.startswith(b'{'):
if line.startswith(b"{"):
result.append(json.loads(line))
except Exception as exc:
self.fail(f'Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}')
self.fail(
f"Error while parsing JSON output of {self._compose_cmd_str(args)}: {exc}\nJSON output: {to_native(stdout)}"
)
return rc, result, stderr
@abc.abstractmethod
@@ -161,26 +193,36 @@ class AnsibleDockerClientBase(object):
def get_cli_info(self):
if self._info is None:
dummy, self._info, dummy = self.call_cli_json('info', '--format', '{{ json . }}', check_rc=True)
dummy, self._info, dummy = self.call_cli_json(
"info", "--format", "{{ json . }}", check_rc=True
)
return self._info
def get_client_plugin_info(self, component):
cli_info = self.get_cli_info()
if not isinstance(cli_info.get('ClientInfo'), dict):
self.fail('Cannot determine Docker client information. Are you maybe using podman instead of docker?')
for plugin in cli_info['ClientInfo'].get('Plugins') or []:
if plugin.get('Name') == component:
if not isinstance(cli_info.get("ClientInfo"), dict):
self.fail(
"Cannot determine Docker client information. Are you maybe using podman instead of docker?"
)
for plugin in cli_info["ClientInfo"].get("Plugins") or []:
if plugin.get("Name") == component:
return plugin
return None
def _image_lookup(self, name, tag):
'''
"""
Including a tag in the name parameter sent to the Docker SDK for Python images method
does not work consistently. Instead, get the result set for name and manually check
if the tag exists.
'''
"""
dummy, images, dummy = self.call_cli_json_stream(
'image', 'ls', '--format', '{{ json . }}', '--no-trunc', '--filter', f'reference={name}',
"image",
"ls",
"--format",
"{{ json . }}",
"--no-trunc",
"--filter",
f"reference={name}",
check_rc=True,
)
if tag:
@@ -189,15 +231,15 @@ class AnsibleDockerClientBase(object):
response = images
images = []
for image in response:
if image.get('Tag') == tag or image.get('Digest') == tag:
if image.get("Tag") == tag or image.get("Digest") == tag:
images = [image]
break
return images
def find_image(self, name, tag):
'''
"""
Lookup an image (by name and tag) and return the inspection results.
'''
"""
if not name:
return None
@@ -206,14 +248,14 @@ class AnsibleDockerClientBase(object):
if not images:
# In API <= 1.20 seeing 'docker.io/<name>' as the name of images pulled from docker hub
registry, repo_name = resolve_repository_name(name)
if registry == 'docker.io':
if registry == "docker.io":
# If docker.io is explicitly there in name, the image
# is not found in some cases (#41509)
self.log(f"Check for docker.io image: {repo_name}")
images = self._image_lookup(repo_name, tag)
if not images and repo_name.startswith('library/'):
if not images and repo_name.startswith("library/"):
# Sometimes library/xxx images are not found
lookup = repo_name[len('library/'):]
lookup = repo_name[len("library/") :]
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images:
@@ -223,7 +265,7 @@ class AnsibleDockerClientBase(object):
lookup = f"{registry}/{repo_name}"
self.log(f"Check for docker.io image: {lookup}")
images = self._image_lookup(lookup, tag)
if not images and '/' not in repo_name:
if not images and "/" not in repo_name:
# This seems to be happening with podman-docker
# (https://github.com/ansible-collections/community.docker/issues/291)
lookup = f"{registry}/library/{repo_name}"
@@ -234,7 +276,7 @@ class AnsibleDockerClientBase(object):
self.fail(f"Daemon returned more than one result for {name}:{tag}")
if len(images) == 1:
rc, image, stderr = self.call_cli_json('image', 'inspect', images[0]['ID'])
rc, image, stderr = self.call_cli_json("image", "inspect", images[0]["ID"])
if not image:
self.log(f"Image {name}:{tag} not found.")
return None
@@ -246,14 +288,14 @@ class AnsibleDockerClientBase(object):
return None
def find_image_by_id(self, image_id, accept_missing_image=False):
'''
"""
Lookup an image (by ID) and return the inspection results.
'''
"""
if not image_id:
return None
self.log(f"Find image {image_id} (by ID)")
rc, image, stderr = self.call_cli_json('image', 'inspect', image_id)
rc, image, stderr = self.call_cli_json("image", "inspect", image_id)
if not image:
if not accept_missing_image:
self.fail(f"Error inspecting image ID {image_id} - {to_native(stderr)}")
@@ -265,9 +307,19 @@ class AnsibleDockerClientBase(object):
class AnsibleModuleDockerClient(AnsibleDockerClientBase):
def __init__(self, argument_spec=None, supports_check_mode=False, mutually_exclusive=None,
required_together=None, required_if=None, required_one_of=None, required_by=None,
min_docker_api_version=None, fail_results=None, needs_api_version=True):
def __init__(
self,
argument_spec=None,
supports_check_mode=False,
mutually_exclusive=None,
required_together=None,
required_if=None,
required_one_of=None,
required_by=None,
min_docker_api_version=None,
fail_results=None,
needs_api_version=True,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
@@ -279,7 +331,7 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
merged_arg_spec.update(argument_spec)
self.arg_spec = merged_arg_spec
mutually_exclusive_params = [('docker_host', 'cli_context')]
mutually_exclusive_params = [("docker_host", "cli_context")]
mutually_exclusive_params += DOCKER_MUTUALLY_EXCLUSIVE
if mutually_exclusive:
mutually_exclusive_params += mutually_exclusive
@@ -305,7 +357,9 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
common_args = dict((k, self.module.params[k]) for k in DOCKER_COMMON_ARGS)
super(AnsibleModuleDockerClient, self).__init__(
common_args, min_docker_api_version=min_docker_api_version, needs_api_version=needs_api_version,
common_args,
min_docker_api_version=min_docker_api_version,
needs_api_version=needs_api_version,
)
def call_cli(self, *args, check_rc=False, data=None, cwd=None, environ_update=None):
@@ -333,4 +387,6 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
self.module.warn(msg)
def deprecate(self, msg, version=None, date=None, collection_name=None):
self.module.deprecate(msg, version=version, date=date, collection_name=collection_name)
self.module.deprecate(
msg, version=version, date=date, collection_name=collection_name
)
File diff suppressed because it is too large Load Diff
+174 -84
View File
@@ -15,8 +15,10 @@ import stat
import tarfile
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.docker.plugins.module_utils._api.errors import APIError, NotFound
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
NotFound,
)
class DockerFileCopyError(Exception):
@@ -34,52 +36,64 @@ class DockerFileNotFound(DockerFileCopyError):
def _put_archive(client, container, path, data):
# data can also be file object for streaming. This is because _put uses requests's put().
# See https://requests.readthedocs.io/en/latest/user/advanced/#streaming-uploads
url = client._url('/containers/{0}/archive', container)
res = client._put(url, params={'path': path}, data=data)
url = client._url("/containers/{0}/archive", container)
res = client._put(url, params={"path": path}, data=data)
client._raise_for_status(res)
return res.status_code == 200
def _symlink_tar_creator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
def _symlink_tar_creator(
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
):
if not stat.S_ISLNK(file_stat.st_mode):
raise DockerUnexpectedError('stat information is not for a symlink')
raise DockerUnexpectedError("stat information is not for a symlink")
bio = io.BytesIO()
with tarfile.open(fileobj=bio, mode='w|', dereference=False, encoding='utf-8') as tar:
with tarfile.open(
fileobj=bio, mode="w|", dereference=False, encoding="utf-8"
) as tar:
# Note that without both name (bytes) and arcname (unicode), this either fails for
# Python 2.7, Python 3.5/3.6, or Python 3.7+. Only when passing both (in this
# form) it works with Python 2.7, 3.5, 3.6, and 3.7 up to 3.11
tarinfo = tar.gettarinfo(b_in_path, arcname=to_text(out_file))
tarinfo.uid = user_id
tarinfo.uname = ''
tarinfo.uname = ""
if user_name:
tarinfo.uname = user_name
tarinfo.gid = group_id
tarinfo.gname = ''
tarinfo.gname = ""
tarinfo.mode &= 0o700
if mode is not None:
tarinfo.mode = mode
if not tarinfo.issym():
raise DockerUnexpectedError('stat information is not for a symlink')
raise DockerUnexpectedError("stat information is not for a symlink")
tar.addfile(tarinfo)
return bio.getvalue()
def _symlink_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
yield _symlink_tar_creator(b_in_path, file_stat, out_file, user_id, group_id, mode, user_name)
def _symlink_tar_generator(
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
):
yield _symlink_tar_creator(
b_in_path, file_stat, out_file, user_id, group_id, mode, user_name
)
def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None):
def _regular_file_tar_generator(
b_in_path, file_stat, out_file, user_id, group_id, mode=None, user_name=None
):
if not stat.S_ISREG(file_stat.st_mode):
raise DockerUnexpectedError('stat information is not for a regular file')
raise DockerUnexpectedError("stat information is not for a regular file")
tarinfo = tarfile.TarInfo()
tarinfo.name = os.path.splitdrive(to_text(out_file))[1].replace(os.sep, '/').lstrip('/')
tarinfo.name = (
os.path.splitdrive(to_text(out_file))[1].replace(os.sep, "/").lstrip("/")
)
tarinfo.mode = (file_stat.st_mode & 0o700) if mode is None else mode
tarinfo.uid = user_id
tarinfo.gid = group_id
tarinfo.size = file_stat.st_size
tarinfo.mtime = file_stat.st_mtime
tarinfo.type = tarfile.REGTYPE
tarinfo.linkname = ''
tarinfo.linkname = ""
if user_name:
tarinfo.uname = user_name
@@ -89,7 +103,7 @@ def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_i
size = tarinfo.size
total_size += size
with open(b_in_path, 'rb') as f:
with open(b_in_path, "rb") as f:
while size > 0:
to_read = min(size, 65536)
buf = f.read(to_read)
@@ -117,16 +131,20 @@ def _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_i
yield tarfile.NUL * (tarfile.RECORDSIZE - remainder)
def _regular_content_tar_generator(content, out_file, user_id, group_id, mode, user_name=None):
def _regular_content_tar_generator(
content, out_file, user_id, group_id, mode, user_name=None
):
tarinfo = tarfile.TarInfo()
tarinfo.name = os.path.splitdrive(to_text(out_file))[1].replace(os.sep, '/').lstrip('/')
tarinfo.name = (
os.path.splitdrive(to_text(out_file))[1].replace(os.sep, "/").lstrip("/")
)
tarinfo.mode = mode
tarinfo.uid = user_id
tarinfo.gid = group_id
tarinfo.size = len(content)
tarinfo.mtime = int(datetime.datetime.now().timestamp())
tarinfo.type = tarfile.REGTYPE
tarinfo.linkname = ''
tarinfo.linkname = ""
if user_name:
tarinfo.uname = user_name
@@ -152,13 +170,22 @@ def _regular_content_tar_generator(content, out_file, user_id, group_id, mode, u
yield tarfile.NUL * (tarfile.RECORDSIZE - remainder)
def put_file(client, container, in_path, out_path, user_id, group_id, mode=None, user_name=None, follow_links=False):
def put_file(
client,
container,
in_path,
out_path,
user_id,
group_id,
mode=None,
user_name=None,
follow_links=False,
):
"""Transfer a file from local to Docker container."""
if not os.path.exists(to_bytes(in_path, errors='surrogate_or_strict')):
raise DockerFileNotFound(
f"file or module does not exist: {to_native(in_path)}")
if not os.path.exists(to_bytes(in_path, errors="surrogate_or_strict")):
raise DockerFileNotFound(f"file or module does not exist: {to_native(in_path)}")
b_in_path = to_bytes(in_path, errors='surrogate_or_strict')
b_in_path = to_bytes(in_path, errors="surrogate_or_strict")
out_dir, out_file = os.path.split(out_path)
@@ -168,28 +195,53 @@ def put_file(client, container, in_path, out_path, user_id, group_id, mode=None,
file_stat = os.lstat(b_in_path)
if stat.S_ISREG(file_stat.st_mode):
stream = _regular_file_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=mode, user_name=user_name)
stream = _regular_file_tar_generator(
b_in_path,
file_stat,
out_file,
user_id,
group_id,
mode=mode,
user_name=user_name,
)
elif stat.S_ISLNK(file_stat.st_mode):
stream = _symlink_tar_generator(b_in_path, file_stat, out_file, user_id, group_id, mode=mode, user_name=user_name)
stream = _symlink_tar_generator(
b_in_path,
file_stat,
out_file,
user_id,
group_id,
mode=mode,
user_name=user_name,
)
else:
file_part = ' referenced by' if follow_links else ''
file_part = " referenced by" if follow_links else ""
raise DockerFileCopyError(
f'File{file_part} {in_path} is neither a regular file nor a symlink (stat mode {oct(file_stat.st_mode)}).')
f"File{file_part} {in_path} is neither a regular file nor a symlink (stat mode {oct(file_stat.st_mode)})."
)
ok = _put_archive(client, container, out_dir, stream)
if not ok:
raise DockerUnexpectedError(f'Unknown error while creating file "{out_path}" in container "{container}".')
raise DockerUnexpectedError(
f'Unknown error while creating file "{out_path}" in container "{container}".'
)
def put_file_content(client, container, content, out_path, user_id, group_id, mode, user_name=None):
def put_file_content(
client, container, content, out_path, user_id, group_id, mode, user_name=None
):
"""Transfer a file from local to Docker container."""
out_dir, out_file = os.path.split(out_path)
stream = _regular_content_tar_generator(content, out_file, user_id, group_id, mode, user_name=user_name)
stream = _regular_content_tar_generator(
content, out_file, user_id, group_id, mode, user_name=user_name
)
ok = _put_archive(client, container, out_dir, stream)
if not ok:
raise DockerUnexpectedError(f'Unknown error while creating file "{out_path}" in container "{container}".')
raise DockerUnexpectedError(
f'Unknown error while creating file "{out_path}" in container "{container}".'
)
def stat_file(client, container, in_path, follow_links=False, log=None):
@@ -208,30 +260,32 @@ def stat_file(client, container, in_path, follow_links=False, log=None):
while True:
if in_path in considered_in_paths:
raise DockerFileCopyError(f'Found infinite symbolic link loop when trying to stating "{in_path}"')
raise DockerFileCopyError(
f'Found infinite symbolic link loop when trying to stating "{in_path}"'
)
considered_in_paths.add(in_path)
if log:
log(f'FETCH: Stating "{in_path}"')
response = client._head(
client._url('/containers/{0}/archive', container),
params={'path': in_path},
client._url("/containers/{0}/archive", container),
params={"path": in_path},
)
if response.status_code == 404:
return in_path, None, None
client._raise_for_status(response)
header = response.headers.get('x-docker-container-path-stat')
header = response.headers.get("x-docker-container-path-stat")
try:
stat_data = json.loads(base64.b64decode(header))
except Exception as exc:
raise DockerUnexpectedError(
f'When retrieving information for {in_path} from {container}, obtained header {header!r} that cannot be loaded as JSON: {exc}'
f"When retrieving information for {in_path} from {container}, obtained header {header!r} that cannot be loaded as JSON: {exc}"
)
# https://pkg.go.dev/io/fs#FileMode: bit 32 - 5 means ModeSymlink
if stat_data['mode'] & (1 << (32 - 5)) != 0:
link_target = stat_data['linkTarget']
if stat_data["mode"] & (1 << (32 - 5)) != 0:
link_target = stat_data["linkTarget"]
if not follow_links:
return in_path, stat_data, link_target
in_path = os.path.join(os.path.split(in_path)[0], link_target)
@@ -243,7 +297,7 @@ def stat_file(client, container, in_path, follow_links=False, log=None):
class _RawGeneratorFileobj(io.RawIOBase):
def __init__(self, stream):
self._stream = stream
self._buf = b''
self._buf = b""
def readable(self):
return True
@@ -251,7 +305,7 @@ class _RawGeneratorFileobj(io.RawIOBase):
def _readinto_from_buf(self, b, index, length):
cpy = min(length - index, len(self._buf))
if cpy:
b[index:index + cpy] = self._buf[:cpy]
b[index : index + cpy] = self._buf[:cpy]
self._buf = self._buf[cpy:]
index += cpy
return index
@@ -273,38 +327,55 @@ class _RawGeneratorFileobj(io.RawIOBase):
def _stream_generator_to_fileobj(stream):
'''Given a generator that generates chunks of bytes, create a readable buffered stream.'''
"""Given a generator that generates chunks of bytes, create a readable buffered stream."""
raw = _RawGeneratorFileobj(stream)
return io.BufferedReader(raw)
def fetch_file_ex(client, container, in_path, process_none, process_regular, process_symlink, process_other, follow_links=False, log=None):
def fetch_file_ex(
client,
container,
in_path,
process_none,
process_regular,
process_symlink,
process_other,
follow_links=False,
log=None,
):
"""Fetch a file (as a tar file entry) from a Docker container to local."""
considered_in_paths = set()
while True:
if in_path in considered_in_paths:
raise DockerFileCopyError(f'Found infinite symbolic link loop when trying to fetch "{in_path}"')
raise DockerFileCopyError(
f'Found infinite symbolic link loop when trying to fetch "{in_path}"'
)
considered_in_paths.add(in_path)
if log:
log(f'FETCH: Fetching "{in_path}"')
try:
stream = client.get_raw_stream(
'/containers/{0}/archive', container,
params={'path': in_path},
headers={'Accept-Encoding': 'identity'},
"/containers/{0}/archive",
container,
params={"path": in_path},
headers={"Accept-Encoding": "identity"},
)
except NotFound:
return process_none(in_path)
with tarfile.open(fileobj=_stream_generator_to_fileobj(stream), mode='r|') as tar:
with tarfile.open(
fileobj=_stream_generator_to_fileobj(stream), mode="r|"
) as tar:
symlink_member = None
result = None
found = False
for member in tar:
if found:
raise DockerUnexpectedError('Received tarfile contains more than one file!')
raise DockerUnexpectedError(
"Received tarfile contains more than one file!"
)
found = True
if member.issym():
symlink_member = member
@@ -316,21 +387,23 @@ def fetch_file_ex(client, container, in_path, process_none, process_regular, pro
if symlink_member:
if not follow_links:
return process_symlink(in_path, symlink_member)
in_path = os.path.join(os.path.split(in_path)[0], symlink_member.linkname)
in_path = os.path.join(
os.path.split(in_path)[0], symlink_member.linkname
)
if log:
log(f'FETCH: Following symbolic link to "{in_path}"')
continue
if found:
return result
raise DockerUnexpectedError('Received tarfile is empty!')
raise DockerUnexpectedError("Received tarfile is empty!")
def fetch_file(client, container, in_path, out_path, follow_links=False, log=None):
b_out_path = to_bytes(out_path, errors='surrogate_or_strict')
b_out_path = to_bytes(out_path, errors="surrogate_or_strict")
def process_none(in_path):
raise DockerFileNotFound(
f'File {in_path} does not exist in container {container}'
f"File {in_path} does not exist in container {container}"
)
def process_regular(in_path, tar, member):
@@ -338,7 +411,7 @@ def fetch_file(client, container, in_path, out_path, follow_links=False, log=Non
os.unlink(b_out_path)
with tar.extractfile(member) as in_f:
with open(b_out_path, 'wb') as out_f:
with open(b_out_path, "wb") as out_f:
shutil.copyfileobj(in_f, out_f)
return in_path
@@ -350,56 +423,71 @@ def fetch_file(client, container, in_path, out_path, follow_links=False, log=Non
return in_path
def process_other(in_path, member):
raise DockerFileCopyError(f'Remote file "{in_path}" is not a regular file or a symbolic link')
raise DockerFileCopyError(
f'Remote file "{in_path}" is not a regular file or a symbolic link'
)
return fetch_file_ex(client, container, in_path, process_none, process_regular, process_symlink, process_other, follow_links=follow_links, log=log)
return fetch_file_ex(
client,
container,
in_path,
process_none,
process_regular,
process_symlink,
process_other,
follow_links=follow_links,
log=log,
)
def _execute_command(client, container, command, log=None, check_rc=False):
if log:
log(f'Executing {command} in {container}')
log(f"Executing {command} in {container}")
data = {
'Container': container,
'User': '',
'Privileged': False,
'Tty': False,
'AttachStdin': False,
'AttachStdout': True,
'AttachStderr': True,
'Cmd': command,
"Container": container,
"User": "",
"Privileged": False,
"Tty": False,
"AttachStdin": False,
"AttachStdout": True,
"AttachStderr": True,
"Cmd": command,
}
if 'detachKeys' in client._general_configs:
data['detachKeys'] = client._general_configs['detachKeys']
if "detachKeys" in client._general_configs:
data["detachKeys"] = client._general_configs["detachKeys"]
try:
exec_data = client.post_json_to_json('/containers/{0}/exec', container, data=data)
exec_data = client.post_json_to_json(
"/containers/{0}/exec", container, data=data
)
except NotFound as e:
raise DockerFileCopyError(f'Could not find container "{container}"') from e
except APIError as e:
if e.response is not None and e.response.status_code == 409:
raise DockerFileCopyError(f'Cannot execute command in paused container "{container}"') from e
raise DockerFileCopyError(
f'Cannot execute command in paused container "{container}"'
) from e
raise
exec_id = exec_data['Id']
exec_id = exec_data["Id"]
data = {
'Tty': False,
'Detach': False
}
stdout, stderr = client.post_json_to_stream('/exec/{0}/start', exec_id, stream=False, demux=True, tty=False)
data = {"Tty": False, "Detach": False}
stdout, stderr = client.post_json_to_stream(
"/exec/{0}/start", exec_id, stream=False, demux=True, tty=False
)
result = client.get_json('/exec/{0}/json', exec_id)
result = client.get_json("/exec/{0}/json", exec_id)
rc = result.get('ExitCode') or 0
stdout = stdout or b''
stderr = stderr or b''
rc = result.get("ExitCode") or 0
stdout = stdout or b""
stderr = stderr or b""
if log:
log(f'Exit code {rc}, stdout {stdout!r}, stderr {stderr!r}')
log(f"Exit code {rc}, stdout {stdout!r}, stderr {stderr!r}")
if check_rc and rc != 0:
command_str = ' '.join(command)
command_str = " ".join(command)
raise DockerUnexpectedError(
f'Obtained unexpected exit code {rc} when running "{command_str}" in {container}.\nSTDOUT: {stdout}\nSTDERR: {stderr}'
)
@@ -408,12 +496,14 @@ def _execute_command(client, container, command, log=None, check_rc=False):
def determine_user_group(client, container, log=None):
dummy, stdout, stderr = _execute_command(client, container, ['/bin/sh', '-c', 'id -u && id -g'], check_rc=True, log=log)
dummy, stdout, stderr = _execute_command(
client, container, ["/bin/sh", "-c", "id -u && id -g"], check_rc=True, log=log
)
stdout_lines = stdout.splitlines()
if len(stdout_lines) != 2:
raise DockerUnexpectedError(
f'Expected two-line output to obtain user and group ID for container {container}, but got {len(stdout_lines)} lines:\n{stdout}'
f"Expected two-line output to obtain user and group ID for container {container}, but got {len(stdout_lines)} lines:\n{stdout}"
)
user_id, group_id = stdout_lines
+25 -24
View File
@@ -10,18 +10,18 @@ import tarfile
class ImageArchiveManifestSummary(object):
'''
"""
Represents data extracted from a manifest.json found in the tar archive output of the
"docker image save some:tag > some.tar" command.
'''
"""
def __init__(self, image_id, repo_tags):
'''
"""
:param image_id: File name portion of Config entry, e.g. abcde12345 from abcde12345.json
:type image_id: str
:param repo_tags Docker image names, e.g. ["hello-world:latest"]
:type repo_tags: list[str]
'''
"""
self.image_id = image_id
self.repo_tags = repo_tags
@@ -32,7 +32,7 @@ class ImageArchiveInvalidException(Exception):
def api_image_id(archive_image_id):
'''
"""
Accepts an image hash in the format stored in manifest.json, and returns an equivalent identifier
that represents the same image hash, but in the format presented by the Docker Engine API.
@@ -41,13 +41,13 @@ def api_image_id(archive_image_id):
:returns: Prefixed hash used by REST api
:rtype: str
'''
"""
return f'sha256:{archive_image_id}'
return f"sha256:{archive_image_id}"
def load_archived_image_manifest(archive_path):
'''
"""
Attempts to get image IDs and image names from metadata stored in the image
archive tar file.
@@ -63,17 +63,17 @@ def load_archived_image_manifest(archive_path):
:return: None, if no file at archive_path, or a list of ImageArchiveManifestSummary objects.
:rtype: ImageArchiveManifestSummary
'''
"""
try:
# FileNotFoundError does not exist in Python 2
if not os.path.isfile(archive_path):
return None
with tarfile.open(archive_path, 'r') as tf:
with tarfile.open(archive_path, "r") as tf:
try:
try:
with tf.extractfile('manifest.json') as ef:
with tf.extractfile("manifest.json") as ef:
manifest = json.load(ef)
except Exception as exc:
raise ImageArchiveInvalidException(
@@ -88,7 +88,7 @@ def load_archived_image_manifest(archive_path):
result = []
for index, meta in enumerate(manifest):
try:
config_file = meta['Config']
config_file = meta["Config"]
except KeyError as exc:
raise ImageArchiveInvalidException(
f"Failed to get Config entry from {index + 1}th manifest in manifest.json: {exc}"
@@ -103,23 +103,22 @@ def load_archived_image_manifest(archive_path):
f"Failed to extract image id from config file name {config_file}: {exc}"
) from exc
for prefix in (
'blobs/sha256/', # Moby 25.0.0, Docker API 1.44
):
for prefix in ("blobs/sha256/",): # Moby 25.0.0, Docker API 1.44
if image_id.startswith(prefix):
image_id = image_id[len(prefix):]
image_id = image_id[len(prefix) :]
try:
repo_tags = meta['RepoTags']
repo_tags = meta["RepoTags"]
except KeyError as exc:
raise ImageArchiveInvalidException(
f"Failed to get RepoTags entry from {index + 1}th manifest in manifest.json: {exc}"
) from exc
result.append(ImageArchiveManifestSummary(
image_id=image_id,
repo_tags=repo_tags
))
result.append(
ImageArchiveManifestSummary(
image_id=image_id, repo_tags=repo_tags
)
)
return result
except ImageArchiveInvalidException:
@@ -132,11 +131,13 @@ def load_archived_image_manifest(archive_path):
except ImageArchiveInvalidException:
raise
except Exception as exc:
raise ImageArchiveInvalidException(f"Failed to open tar file {archive_path}: {exc}") from exc
raise ImageArchiveInvalidException(
f"Failed to open tar file {archive_path}: {exc}"
) from exc
def archived_image_manifest(archive_path):
'''
"""
Attempts to get Image.Id and image name from metadata stored in the image
archive tar file.
@@ -152,7 +153,7 @@ def archived_image_manifest(archive_path):
:return: None, if no file at archive_path, or the extracted image ID, which will not have a sha256: prefix.
:rtype: ImageArchiveManifestSummary
'''
"""
results = load_archived_image_manifest(archive_path)
if results is None:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -8,5 +8,4 @@
from __future__ import annotations
import selectors # noqa: F401, pylint: disable=unused-import
+31 -21
View File
@@ -4,14 +4,14 @@
from __future__ import annotations
import os
import os.path
import socket as pysocket
import struct
from ansible_collections.community.docker.plugins.module_utils._api.utils import socket as docker_socket
from ansible_collections.community.docker.plugins.module_utils._api.utils import (
socket as docker_socket,
)
from ansible_collections.community.docker.plugins.module_utils.socket_helper import (
make_unblocking,
shutdown_writing,
@@ -31,19 +31,21 @@ class DockerSocketHandlerBase(object):
self._log = log
else:
self._log = lambda msg: True
self._paramiko_read_workaround = hasattr(sock, 'send_ready') and 'paramiko' in str(type(sock))
self._paramiko_read_workaround = hasattr(
sock, "send_ready"
) and "paramiko" in str(type(sock))
self._sock = sock
self._block_done_callback = None
self._block_buffer = []
self._eof = False
self._read_buffer = b''
self._write_buffer = b''
self._read_buffer = b""
self._write_buffer = b""
self._end_of_writing = False
self._current_stream = None
self._current_missing = 0
self._current_buffer = b''
self._current_buffer = b""
self._selector = self._selectors.DefaultSelector()
self._selector.register(self._sock, self._selectors.EVENT_READ)
@@ -70,26 +72,26 @@ class DockerSocketHandlerBase(object):
def _read(self):
if self._eof:
return
if hasattr(self._sock, 'recv'):
if hasattr(self._sock, "recv"):
try:
data = self._sock.recv(262144)
except Exception as e:
# After calling self._sock.shutdown(), OpenSSL's/urllib3's
# WrappedSocket seems to eventually raise ZeroReturnError in
# case of EOF
if 'OpenSSL.SSL.ZeroReturnError' in str(type(e)):
if "OpenSSL.SSL.ZeroReturnError" in str(type(e)):
self._eof = True
return
else:
raise
elif isinstance(self._sock, getattr(pysocket, 'SocketIO')):
elif isinstance(self._sock, getattr(pysocket, "SocketIO")):
data = self._sock.read()
else:
data = os.read(self._sock.fileno())
if data is None:
# no data available
return
self._log(f'read {len(data)} bytes')
self._log(f"read {len(data)} bytes")
if len(data) == 0:
# Stream EOF
self._eof = True
@@ -103,10 +105,12 @@ class DockerSocketHandlerBase(object):
self._current_missing -= n
if self._current_missing == 0:
self._add_block(self._current_stream, self._current_buffer)
self._current_buffer = b''
self._current_buffer = b""
if len(self._read_buffer) < 8:
break
self._current_stream, self._current_missing = struct.unpack('>BxxxL', self._read_buffer[:8])
self._current_stream, self._current_missing = struct.unpack(
">BxxxL", self._read_buffer[:8]
)
self._read_buffer = self._read_buffer[8:]
if self._current_missing < 0:
# Stream EOF (as reported by docker daemon)
@@ -116,22 +120,28 @@ class DockerSocketHandlerBase(object):
def _handle_end_of_writing(self):
if self._end_of_writing and len(self._write_buffer) == 0:
self._end_of_writing = False
self._log('Shutting socket down for writing')
self._log("Shutting socket down for writing")
shutdown_writing(self._sock, self._log)
def _write(self):
if len(self._write_buffer) > 0:
written = write_to_socket(self._sock, self._write_buffer)
self._write_buffer = self._write_buffer[written:]
self._log(f'wrote {written} bytes, {len(self._write_buffer)} are left')
self._log(f"wrote {written} bytes, {len(self._write_buffer)} are left")
if len(self._write_buffer) > 0:
self._selector.modify(self._sock, self._selectors.EVENT_READ | self._selectors.EVENT_WRITE)
self._selector.modify(
self._sock, self._selectors.EVENT_READ | self._selectors.EVENT_WRITE
)
else:
self._selector.modify(self._sock, self._selectors.EVENT_READ)
self._handle_end_of_writing()
def select(self, timeout=None, _internal_recursion=False):
if not _internal_recursion and self._paramiko_read_workaround and len(self._write_buffer) > 0:
if (
not _internal_recursion
and self._paramiko_read_workaround
and len(self._write_buffer) > 0
):
# When the SSH transport is used, Docker SDK for Python internally uses Paramiko, whose
# Channel object supports select(), but only for reading
# (https://github.com/paramiko/paramiko/issues/695).
@@ -147,13 +157,13 @@ class DockerSocketHandlerBase(object):
return True
if timeout is not None:
timeout -= PARAMIKO_POLL_TIMEOUT
self._log(f'select... ({timeout})')
self._log(f"select... ({timeout})")
events = self._selector.select(timeout)
for key, event in events:
if key.fileobj == self._sock:
ev_read = event & self._selectors.EVENT_READ != 0
ev_write = event & self._selectors.EVENT_WRITE != 0
self._log(f'select event read:{ev_read} write:{ev_write}')
self._log(f"select event read:{ev_read} write:{ev_write}")
if event & self._selectors.EVENT_READ != 0:
self._read()
if event & self._selectors.EVENT_WRITE != 0:
@@ -182,14 +192,14 @@ class DockerSocketHandlerBase(object):
elif stream_id == docker_socket.STDERR:
stderr.append(data)
else:
raise ValueError(f'{stream_id} is not a valid stream ID')
raise ValueError(f"{stream_id} is not a valid stream ID")
self.end_of_writing()
self.set_block_done_callback(append_block)
while not self._eof:
self.select()
return b''.join(stdout), b''.join(stderr)
return b"".join(stdout), b"".join(stderr)
def write(self, str):
self._write_buffer += str
+19 -12
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import fcntl
import os
import os.path
@@ -12,17 +11,25 @@ import socket as pysocket
def make_file_unblocking(file):
fcntl.fcntl(file.fileno(), fcntl.F_SETFL, fcntl.fcntl(file.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK)
fcntl.fcntl(
file.fileno(),
fcntl.F_SETFL,
fcntl.fcntl(file.fileno(), fcntl.F_GETFL) | os.O_NONBLOCK,
)
def make_file_blocking(file):
fcntl.fcntl(file.fileno(), fcntl.F_SETFL, fcntl.fcntl(file.fileno(), fcntl.F_GETFL) & ~os.O_NONBLOCK)
fcntl.fcntl(
file.fileno(),
fcntl.F_SETFL,
fcntl.fcntl(file.fileno(), fcntl.F_GETFL) & ~os.O_NONBLOCK,
)
def make_unblocking(sock):
if hasattr(sock, '_sock'):
if hasattr(sock, "_sock"):
sock._sock.setblocking(0)
elif hasattr(sock, 'setblocking'):
elif hasattr(sock, "setblocking"):
sock.setblocking(0)
else:
make_file_unblocking(sock)
@@ -37,27 +44,27 @@ def shutdown_writing(sock, log=_empty_writer):
# a close_notify TLS alert without completely shutting down the connection.
# Calling sock.shutdown(pysocket.SHUT_WR) simply turns of TLS encryption and from that
# point on the raw encrypted data is returned when sock.recv() is called. :-(
if hasattr(sock, 'shutdown_write'):
if hasattr(sock, "shutdown_write"):
sock.shutdown_write()
elif hasattr(sock, 'shutdown'):
elif hasattr(sock, "shutdown"):
try:
sock.shutdown(pysocket.SHUT_WR)
except TypeError as e:
# probably: "TypeError: shutdown() takes 1 positional argument but 2 were given"
log(f'Shutting down for writing not possible; trying shutdown instead: {e}')
log(f"Shutting down for writing not possible; trying shutdown instead: {e}")
sock.shutdown()
elif isinstance(sock, getattr(pysocket, 'SocketIO')):
elif isinstance(sock, getattr(pysocket, "SocketIO")):
sock._sock.shutdown(pysocket.SHUT_WR)
else:
log('No idea how to signal end of writing')
log("No idea how to signal end of writing")
def write_to_socket(sock, data):
if hasattr(sock, '_send_until_done'):
if hasattr(sock, "_send_until_done"):
# WrappedSocket (urllib3/contrib/pyopenssl) does not have `send`, but
# only `sendall`, which uses `_send_until_done` under the hood.
return sock._send_until_done(data)
elif hasattr(sock, 'send'):
elif hasattr(sock, "send"):
return sock.send(data)
else:
return os.write(sock.fileno(), data)
+57 -35
View File
@@ -5,19 +5,22 @@
from __future__ import annotations
import json
from time import sleep
try:
from docker.errors import APIError, NotFound
except ImportError:
# missing Docker SDK for Python handled in ansible.module_utils.docker.common
pass
from ansible_collections.community.docker.plugins.module_utils.version import LooseVersion
from ansible_collections.community.docker.plugins.module_utils.common import AnsibleDockerClient
from ansible_collections.community.docker.plugins.module_utils.common import (
AnsibleDockerClient,
)
from ansible_collections.community.docker.plugins.module_utils.version import (
LooseVersion,
)
class AnsibleDockerSwarmClient(AnsibleDockerClient):
@@ -41,8 +44,8 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if info:
json_str = json.dumps(info, ensure_ascii=False)
swarm_info = json.loads(json_str)
if swarm_info['Swarm']['NodeID']:
return swarm_info['Swarm']['NodeID']
if swarm_info["Swarm"]["NodeID"]:
return swarm_info["Swarm"]["NodeID"]
return None
def check_if_swarm_node(self, node_id=None):
@@ -66,9 +69,13 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if info:
json_str = json.dumps(info, ensure_ascii=False)
swarm_info = json.loads(json_str)
if swarm_info['Swarm']['NodeID']:
if swarm_info["Swarm"]["NodeID"]:
return True
if swarm_info['Swarm']['LocalNodeState'] in ('active', 'pending', 'locked'):
if swarm_info["Swarm"]["LocalNodeState"] in (
"active",
"pending",
"locked",
):
return True
return False
else:
@@ -77,7 +84,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
except APIError:
return
if node_info['ID'] is not None:
if node_info["ID"] is not None:
return True
return False
@@ -100,7 +107,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
If host is not a swarm manager then Ansible task on this host should end with 'failed' state
"""
if not self.check_if_swarm_manager():
self.fail("Error running docker swarm module: must run on swarm manager node")
self.fail(
"Error running docker swarm module: must run on swarm manager node"
)
def check_if_swarm_worker(self):
"""
@@ -136,7 +145,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if retry > 0:
sleep(5)
node_info = self.get_node_inspect(node_id=node_id)
if node_info['Status']['State'] == 'down':
if node_info["Status"]["State"] == "down":
return True
return False
@@ -160,7 +169,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
node_info = self.inspect_node(node_id=node_id)
except APIError as exc:
if exc.status_code == 503:
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
self.fail(
"Cannot inspect node: To inspect node execute module on Swarm Manager"
)
if exc.status_code == 404:
if skip_missing:
return None
@@ -171,16 +182,19 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
json_str = json.dumps(node_info, ensure_ascii=False)
node_info = json.loads(json_str)
if 'ManagerStatus' in node_info:
if node_info['ManagerStatus'].get('Leader'):
if "ManagerStatus" in node_info:
if node_info["ManagerStatus"].get("Leader"):
# This is workaround of bug in Docker when in some cases the Leader IP is 0.0.0.0
# Check moby/moby#35437 for details
count_colons = node_info['ManagerStatus']['Addr'].count(":")
count_colons = node_info["ManagerStatus"]["Addr"].count(":")
if count_colons == 1:
swarm_leader_ip = node_info['ManagerStatus']['Addr'].split(":", 1)[0] or node_info['Status']['Addr']
swarm_leader_ip = (
node_info["ManagerStatus"]["Addr"].split(":", 1)[0]
or node_info["Status"]["Addr"]
)
else:
swarm_leader_ip = node_info['Status']['Addr']
node_info['Status']['Addr'] = swarm_leader_ip
swarm_leader_ip = node_info["Status"]["Addr"]
node_info["Status"]["Addr"] = swarm_leader_ip
return node_info
def get_all_nodes_inspect(self):
@@ -194,7 +208,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
node_info = self.nodes()
except APIError as exc:
if exc.status_code == 503:
self.fail("Cannot inspect node: To inspect node execute module on Swarm Manager")
self.fail(
"Cannot inspect node: To inspect node execute module on Swarm Manager"
)
self.fail(f"Error while reading from Swarm manager: {exc}")
except Exception as exc:
self.fail(f"Error inspecting swarm node: {exc}")
@@ -203,7 +219,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
node_info = json.loads(json_str)
return node_info
def get_all_nodes_list(self, output='short'):
def get_all_nodes_list(self, output="short"):
"""
Returns list of nodes registered in Swarm
@@ -219,22 +235,26 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
if nodes_inspect is None:
return None
if output == 'short':
if output == "short":
for node in nodes_inspect:
nodes_list.append(node['Description']['Hostname'])
elif output == 'long':
nodes_list.append(node["Description"]["Hostname"])
elif output == "long":
for node in nodes_inspect:
node_property = {}
node_property.update({'ID': node['ID']})
node_property.update({'Hostname': node['Description']['Hostname']})
node_property.update({'Status': node['Status']['State']})
node_property.update({'Availability': node['Spec']['Availability']})
if 'ManagerStatus' in node:
if node['ManagerStatus']['Leader'] is True:
node_property.update({'Leader': True})
node_property.update({'ManagerStatus': node['ManagerStatus']['Reachability']})
node_property.update({'EngineVersion': node['Description']['Engine']['EngineVersion']})
node_property.update({"ID": node["ID"]})
node_property.update({"Hostname": node["Description"]["Hostname"]})
node_property.update({"Status": node["Status"]["State"]})
node_property.update({"Availability": node["Spec"]["Availability"]})
if "ManagerStatus" in node:
if node["ManagerStatus"]["Leader"] is True:
node_property.update({"Leader": True})
node_property.update(
{"ManagerStatus": node["ManagerStatus"]["Reachability"]}
)
node_property.update(
{"EngineVersion": node["Description"]["Engine"]["EngineVersion"]}
)
nodes_list.append(node_property)
else:
@@ -243,10 +263,10 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
return nodes_list
def get_node_name_by_id(self, nodeid):
return self.get_node_inspect(nodeid)['Description']['Hostname']
return self.get_node_inspect(nodeid)["Description"]["Hostname"]
def get_unlock_key(self):
if self.docker_py_version < LooseVersion('2.7.0'):
if self.docker_py_version < LooseVersion("2.7.0"):
return None
return super(AnsibleDockerSwarmClient, self).get_unlock_key()
@@ -268,7 +288,9 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
return None
except APIError as exc:
if exc.status_code == 503:
self.fail("Cannot inspect service: To inspect service execute module on Swarm Manager")
self.fail(
"Cannot inspect service: To inspect service execute module on Swarm Manager"
)
self.fail(f"Error inspecting swarm service: {exc}")
except Exception as exc:
self.fail(f"Error inspecting swarm service: {exc}")
+134 -93
View File
@@ -4,7 +4,6 @@
from __future__ import annotations
import json
import re
from datetime import timedelta
@@ -15,45 +14,64 @@ from ansible.module_utils.common.collections import is_sequence
from ansible.module_utils.common.text.converters import to_text
DEFAULT_DOCKER_HOST = 'unix:///var/run/docker.sock'
DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock"
DEFAULT_TLS = False
DEFAULT_TLS_VERIFY = False
DEFAULT_TLS_HOSTNAME = 'localhost' # deprecated
DEFAULT_TLS_HOSTNAME = "localhost" # deprecated
DEFAULT_TIMEOUT_SECONDS = 60
DOCKER_COMMON_ARGS = dict(
docker_host=dict(type='str', default=DEFAULT_DOCKER_HOST, fallback=(env_fallback, ['DOCKER_HOST']), aliases=['docker_url']),
tls_hostname=dict(type='str', fallback=(env_fallback, ['DOCKER_TLS_HOSTNAME'])),
api_version=dict(type='str', default='auto', fallback=(env_fallback, ['DOCKER_API_VERSION']), aliases=['docker_api_version']),
timeout=dict(type='int', default=DEFAULT_TIMEOUT_SECONDS, fallback=(env_fallback, ['DOCKER_TIMEOUT'])),
ca_path=dict(type='path', aliases=['ca_cert', 'tls_ca_cert', 'cacert_path']),
client_cert=dict(type='path', aliases=['tls_client_cert', 'cert_path']),
client_key=dict(type='path', aliases=['tls_client_key', 'key_path']),
tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])),
use_ssh_client=dict(type='bool', default=False),
validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']),
debug=dict(type='bool', default=False)
docker_host=dict(
type="str",
default=DEFAULT_DOCKER_HOST,
fallback=(env_fallback, ["DOCKER_HOST"]),
aliases=["docker_url"],
),
tls_hostname=dict(type="str", fallback=(env_fallback, ["DOCKER_TLS_HOSTNAME"])),
api_version=dict(
type="str",
default="auto",
fallback=(env_fallback, ["DOCKER_API_VERSION"]),
aliases=["docker_api_version"],
),
timeout=dict(
type="int",
default=DEFAULT_TIMEOUT_SECONDS,
fallback=(env_fallback, ["DOCKER_TIMEOUT"]),
),
ca_path=dict(type="path", aliases=["ca_cert", "tls_ca_cert", "cacert_path"]),
client_cert=dict(type="path", aliases=["tls_client_cert", "cert_path"]),
client_key=dict(type="path", aliases=["tls_client_key", "key_path"]),
tls=dict(type="bool", default=DEFAULT_TLS, fallback=(env_fallback, ["DOCKER_TLS"])),
use_ssh_client=dict(type="bool", default=False),
validate_certs=dict(
type="bool",
default=DEFAULT_TLS_VERIFY,
fallback=(env_fallback, ["DOCKER_TLS_VERIFY"]),
aliases=["tls_verify"],
),
debug=dict(type="bool", default=False),
)
DOCKER_COMMON_ARGS_VARS = dict([
[option_name, f'ansible_docker_{option_name}']
for option_name in DOCKER_COMMON_ARGS
if option_name != 'debug'
])
DOCKER_COMMON_ARGS_VARS = dict(
[
[option_name, f"ansible_docker_{option_name}"]
for option_name in DOCKER_COMMON_ARGS
if option_name != "debug"
]
)
DOCKER_MUTUALLY_EXCLUSIVE = []
DOCKER_REQUIRED_TOGETHER = [
['client_cert', 'client_key']
]
DOCKER_REQUIRED_TOGETHER = [["client_cert", "client_key"]]
DEFAULT_DOCKER_REGISTRY = 'https://index.docker.io/v1/'
BYTE_SUFFIXES = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
DEFAULT_DOCKER_REGISTRY = "https://index.docker.io/v1/"
BYTE_SUFFIXES = ["B", "KB", "MB", "GB", "TB", "PB"]
def is_image_name_id(name):
"""Check whether the given image name is in fact an image ID (hash)."""
if re.match('^sha256:[0-9a-fA-F]{64}$', name):
if re.match("^sha256:[0-9a-fA-F]{64}$", name):
return True
return False
@@ -64,7 +82,7 @@ def is_valid_tag(tag, allow_empty=False):
return allow_empty
# See here ("Extended description") for a definition what tags can be:
# https://docs.docker.com/engine/reference/commandline/tag/
return bool(re.match('^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$', tag))
return bool(re.match("^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$", tag))
def sanitize_result(data):
@@ -90,10 +108,12 @@ def log_debug(msg, pretty_print=False):
If ``pretty_print=True``, the message will be pretty-printed as JSON.
"""
with open('docker.log', 'a') as log_file:
with open("docker.log", "a") as log_file:
if pretty_print:
log_file.write(json.dumps(msg, sort_keys=True, indent=4, separators=(',', ': ')))
log_file.write('\n')
log_file.write(
json.dumps(msg, sort_keys=True, indent=4, separators=(",", ": "))
)
log_file.write("\n")
else:
log_file.write(f"{msg}\n")
@@ -108,17 +128,19 @@ class DockerBaseClass(object):
# log_debug(msg, pretty_print=pretty_print)
def update_tls_hostname(result, old_behavior=False, deprecate_function=None, uses_tls=True):
if result['tls_hostname'] is None:
def update_tls_hostname(
result, old_behavior=False, deprecate_function=None, uses_tls=True
):
if result["tls_hostname"] is None:
# get default machine name from the url
parsed_url = urlparse(result['docker_host'])
result['tls_hostname'] = parsed_url.netloc.rsplit(':', 1)[0]
parsed_url = urlparse(result["docker_host"])
result["tls_hostname"] = parsed_url.netloc.rsplit(":", 1)[0]
def compare_dict_allow_more_present(av, bv):
'''
"""
Compare two dictionaries for whether every entry of the first is in the second.
'''
"""
for key, value in av.items():
if key not in bv:
return False
@@ -128,7 +150,7 @@ def compare_dict_allow_more_present(av, bv):
def compare_generic(a, b, method, datatype):
'''
"""
Compare values a and b as described by method and datatype.
Returns ``True`` if the values compare equal, and ``False`` if not.
@@ -151,8 +173,8 @@ def compare_generic(a, b, method, datatype):
not matter and which contain ``dict``s; ``allow_more_present`` is used
for the ``dict``s, and these are assumed to be dictionaries of values;
- ``dict``: for dictionaries of values.
'''
if method == 'ignore':
"""
if method == "ignore":
return True
# If a or b is None:
if a is None or b is None:
@@ -161,18 +183,18 @@ def compare_generic(a, b, method, datatype):
return True
# Otherwise, not equal for values, and equal
# if the other is empty for set/list/dict
if datatype == 'value':
if datatype == "value":
return False
# For allow_more_present, allow a to be None
if method == 'allow_more_present' and a is None:
if method == "allow_more_present" and a is None:
return True
# Otherwise, the iterable object which is not None must have length 0
return len(b if a is None else a) == 0
# Do proper comparison (both objects not None)
if datatype == 'value':
if datatype == "value":
return a == b
elif datatype == 'list':
if method == 'strict':
elif datatype == "list":
if method == "strict":
return a == b
else:
i = 0
@@ -183,19 +205,19 @@ def compare_generic(a, b, method, datatype):
return False
i += 1
return True
elif datatype == 'dict':
if method == 'strict':
elif datatype == "dict":
if method == "strict":
return a == b
else:
return compare_dict_allow_more_present(a, b)
elif datatype == 'set':
elif datatype == "set":
set_a = set(a)
set_b = set(b)
if method == 'strict':
if method == "strict":
return set_a == set_b
else:
return set_b >= set_a
elif datatype == 'set(dict)':
elif datatype == "set(dict)":
for av in a:
found = False
for bv in b:
@@ -204,7 +226,7 @@ def compare_generic(a, b, method, datatype):
break
if not found:
return False
if method == 'strict':
if method == "strict":
# If we would know that both a and b do not contain duplicates,
# we could simply compare len(a) to len(b) to finish this test.
# We can assume that b has no duplicates (as it is returned by
@@ -225,11 +247,13 @@ class DifferenceTracker(object):
self._diff = []
def add(self, name, parameter=None, active=None):
self._diff.append(dict(
name=name,
parameter=parameter,
active=active,
))
self._diff.append(
dict(
name=name,
parameter=parameter,
active=active,
)
)
def merge(self, other_tracker):
self._diff.extend(other_tracker._diff)
@@ -239,41 +263,41 @@ class DifferenceTracker(object):
return len(self._diff) == 0
def get_before_after(self):
'''
"""
Return texts ``before`` and ``after``.
'''
"""
before = dict()
after = dict()
for item in self._diff:
before[item['name']] = item['active']
after[item['name']] = item['parameter']
before[item["name"]] = item["active"]
after[item["name"]] = item["parameter"]
return before, after
def has_difference_for(self, name):
'''
"""
Returns a boolean if a difference exists for name
'''
return any(diff for diff in self._diff if diff['name'] == name)
"""
return any(diff for diff in self._diff if diff["name"] == name)
def get_legacy_docker_container_diffs(self):
'''
"""
Return differences in the docker_container legacy format.
'''
"""
result = []
for entry in self._diff:
item = dict()
item[entry['name']] = dict(
parameter=entry['parameter'],
container=entry['active'],
item[entry["name"]] = dict(
parameter=entry["parameter"],
container=entry["active"],
)
result.append(item)
return result
def get_legacy_docker_diffs(self):
'''
"""
Return differences in the docker_container legacy format.
'''
result = [entry['name'] for entry in self._diff]
"""
result = [entry["name"] for entry in self._diff]
return result
@@ -291,31 +315,38 @@ def sanitize_labels(labels, labels_field, client=None, module=None):
if not isinstance(k, str):
fail(f"The key {k!r} of {labels_field} is not a string!")
if isinstance(v, (bool, float)):
fail(f"The value {v!r} for {k!r} of {labels_field} is not a string or something than can be safely converted to a string!")
fail(
f"The value {v!r} for {k!r} of {labels_field} is not a string or something than can be safely converted to a string!"
)
labels[k] = to_text(v)
def clean_dict_booleans_for_docker_api(data, allow_sequences=False):
'''
"""
Go does not like Python booleans 'True' or 'False', while Ansible is just
fine with them in YAML. As such, they need to be converted in cases where
we pass dictionaries to the Docker API (e.g. docker_network's
driver_options and docker_prune's filters). When `allow_sequences=True`
YAML sequences (lists, tuples) are converted to [str] instead of str([...])
which is the expected format of filters which accept lists such as labels.
'''
"""
def sanitize(value):
if value is True:
return 'true'
return "true"
elif value is False:
return 'false'
return "false"
else:
return str(value)
result = dict()
if data is not None:
for k, v in data.items():
result[str(k)] = [sanitize(e) for e in v] if allow_sequences and is_sequence(v) else sanitize(v)
result[str(k)] = (
[sanitize(e) for e in v]
if allow_sequences and is_sequence(v)
else sanitize(v)
)
return result
@@ -324,30 +355,30 @@ def convert_duration_to_nanosecond(time_str):
Return time duration in nanosecond.
"""
if not isinstance(time_str, str):
raise ValueError(f'Missing unit in duration - {time_str}')
raise ValueError(f"Missing unit in duration - {time_str}")
regex = re.compile(
r'^(((?P<hours>\d+)h)?'
r'((?P<minutes>\d+)m(?!s))?'
r'((?P<seconds>\d+)s)?'
r'((?P<milliseconds>\d+)ms)?'
r'((?P<microseconds>\d+)us)?)$'
r"^(((?P<hours>\d+)h)?"
r"((?P<minutes>\d+)m(?!s))?"
r"((?P<seconds>\d+)s)?"
r"((?P<milliseconds>\d+)ms)?"
r"((?P<microseconds>\d+)us)?)$"
)
parts = regex.match(time_str)
if not parts:
raise ValueError(f'Invalid time duration - {time_str}')
raise ValueError(f"Invalid time duration - {time_str}")
parts = parts.groupdict()
time_params = {}
for (name, value) in parts.items():
for name, value in parts.items():
if value:
time_params[name] = int(value)
delta = timedelta(**time_params)
time_in_nanoseconds = (
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10 ** 6
) * 10 ** 3
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
) * 10**3
return time_in_nanoseconds
@@ -355,7 +386,7 @@ def convert_duration_to_nanosecond(time_str):
def normalize_healthcheck_test(test):
if isinstance(test, (tuple, list)):
return [str(e) for e in test]
return ['CMD-SHELL', str(test)]
return ["CMD-SHELL", str(test)]
def normalize_healthcheck(healthcheck, normalize_test=False):
@@ -365,9 +396,17 @@ def normalize_healthcheck(healthcheck, normalize_test=False):
result = dict()
# All supported healthcheck parameters
options = ('test', 'test_cli_compatible', 'interval', 'timeout', 'start_period', 'start_interval', 'retries')
options = (
"test",
"test_cli_compatible",
"interval",
"timeout",
"start_period",
"start_interval",
"retries",
)
duration_options = ('interval', 'timeout', 'start_period', 'start_interval')
duration_options = ("interval", "timeout", "start_period", "start_interval")
for key in options:
if key in healthcheck:
@@ -378,16 +417,18 @@ def normalize_healthcheck(healthcheck, normalize_test=False):
continue
if key in duration_options:
value = convert_duration_to_nanosecond(value)
if not value and not (healthcheck.get('test_cli_compatible') and key == 'test'):
if not value and not (
healthcheck.get("test_cli_compatible") and key == "test"
):
continue
if key == 'retries':
if key == "retries":
try:
value = int(value)
except ValueError:
raise ValueError(
f'Cannot parse number of retries for healthcheck. Expected an integer, got "{value}".'
)
if key == 'test' and value and normalize_test:
if key == "test" and value and normalize_test:
value = normalize_healthcheck_test(value)
result[key] = value
@@ -399,12 +440,12 @@ def parse_healthcheck(healthcheck):
Return dictionary of healthcheck parameters and boolean if
healthcheck defined in image was requested to be disabled.
"""
if (not healthcheck) or (not healthcheck.get('test')):
if (not healthcheck) or (not healthcheck.get("test")):
return None, None
result = normalize_healthcheck(healthcheck, normalize_test=True)
if result['test'] == ['NONE']:
if result["test"] == ["NONE"]:
# If the user explicitly disables the healthcheck, return None
# as the healthcheck object, and set disable_healthcheck to True
return None, True
+4 -2
View File
@@ -8,5 +8,7 @@
from __future__ import annotations
from ansible.module_utils.compat.version import LooseVersion, StrictVersion # noqa: F401, pylint: disable=unused-import
from ansible.module_utils.compat.version import ( # noqa: F401, pylint: disable=unused-import
LooseVersion,
StrictVersion,
)