Add typing information, 2/n (#1178)

* Add typing to Docker Stack modules. Clean modules up.

* Add typing to Docker Swarm modules.

* Add typing to unit tests.

* Add more typing.

* Add ignore.txt entries.
This commit is contained in:
Felix Fontein
2025-10-25 01:16:04 +02:00
committed by GitHub
parent 3350283bcc
commit 6ad4bfcd40
84 changed files with 1496 additions and 1161 deletions
+2 -2
View File
@@ -141,7 +141,7 @@ class Connection(ConnectionBase):
transport = "community.docker.docker"
has_pipelining = True
def __init__(self, *args, **kwargs) -> None:
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
super().__init__(*args, **kwargs)
# Note: docker supports running as non-root in some configurations.
@@ -476,7 +476,7 @@ class Connection(ConnectionBase):
display.debug("done with docker.exec_command()")
return (p.returncode, stdout, stderr)
def _prefix_login_path(self, remote_path):
def _prefix_login_path(self, remote_path: str) -> str:
"""Make sure that we put files into a standard path
If a path is relative, then we need to choose where to put it.
+2 -2
View File
@@ -192,7 +192,7 @@ class Connection(ConnectionBase):
f'An unexpected requests error occurred for container "{remote_addr}" when trying to talk to the Docker daemon: {e}'
)
def __init__(self, *args, **kwargs) -> None:
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
super().__init__(*args, **kwargs)
self.client: AnsibleDockerClient | None = None
@@ -319,7 +319,7 @@ class Connection(ConnectionBase):
become_output = [b""]
def append_become_output(stream_id, data):
def append_become_output(stream_id: int, data: bytes) -> None:
become_output[0] += data
exec_socket_handler.set_block_done_callback(
+1 -1
View File
@@ -65,7 +65,7 @@ class Connection(ConnectionBase):
transport = "community.docker.nsenter"
has_pipelining = False
def __init__(self, *args, **kwargs) -> None:
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
super().__init__(*args, **kwargs)
self.cwd = None
self._nsenter_pid = None
+4 -1
View File
@@ -221,7 +221,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
return ip_addr
def _should_skip_host(
self, machine_name: str, env_var_tuples, daemon_env: DaemonEnv
self,
machine_name: str,
env_var_tuples: list[tuple[str, str]],
daemon_env: DaemonEnv,
) -> bool:
if not env_var_tuples:
warning_prefix = f"Unable to fetch Docker daemon env vars from Docker Machine for host {machine_name}"
+3 -3
View File
@@ -67,7 +67,7 @@ except ImportError:
pass
class FakeURLLIB3:
def __init__(self):
def __init__(self) -> None:
self._collections = self
self.poolmanager = self
self.connection = self
@@ -81,14 +81,14 @@ except ImportError:
)
class FakeURLLIB3Connection:
def __init__(self):
def __init__(self) -> None:
self.HTTPConnection = _HTTPConnection # pylint: disable=invalid-name
urllib3 = FakeURLLIB3()
urllib3_connection = FakeURLLIB3Connection()
def fail_on_missing_imports():
def fail_on_missing_imports() -> None:
if REQUESTS_IMPORT_ERROR is not None:
from .errors import MissingRequirementException # pylint: disable=cyclic-import
+50 -39
View File
@@ -55,6 +55,7 @@ from ..utils.socket import consume_socket_output, demux_adaptor, frames_iter
if t.TYPE_CHECKING:
from requests import Response
from requests.adapters import BaseAdapter
from ..._socket_helper import SocketLike
@@ -258,23 +259,23 @@ class APIClient(_Session):
return kwargs
@update_headers
def _post(self, url: str, **kwargs):
def _post(self, url: str, **kwargs: t.Any) -> Response:
return self.post(url, **self._set_request_timeout(kwargs))
@update_headers
def _get(self, url: str, **kwargs):
def _get(self, url: str, **kwargs: t.Any) -> Response:
return self.get(url, **self._set_request_timeout(kwargs))
@update_headers
def _head(self, url: str, **kwargs):
def _head(self, url: str, **kwargs: t.Any) -> Response:
return self.head(url, **self._set_request_timeout(kwargs))
@update_headers
def _put(self, url: str, **kwargs):
def _put(self, url: str, **kwargs: t.Any) -> Response:
return self.put(url, **self._set_request_timeout(kwargs))
@update_headers
def _delete(self, url: str, **kwargs):
def _delete(self, url: str, **kwargs: t.Any) -> Response:
return self.delete(url, **self._set_request_timeout(kwargs))
def _url(self, pathfmt: str, *args: str, versioned_api: bool = True) -> str:
@@ -343,7 +344,7 @@ class APIClient(_Session):
return response.text
def _post_json(
self, url: str, data: dict[str, str | None] | t.Any, **kwargs
self, url: str, data: dict[str, str | None] | t.Any, **kwargs: t.Any
) -> Response:
# Go <1.1 cannot unserialize null to a string
# so we do this disgusting thing here.
@@ -556,22 +557,30 @@ class APIClient(_Session):
"""
socket = self._get_raw_response_socket(response)
gen: t.Generator = frames_iter(socket, tty)
gen = frames_iter(socket, tty)
if demux:
# The generator will output tuples (stdout, stderr)
gen = (demux_adaptor(*frame) for frame in gen)
demux_gen: t.Generator[tuple[bytes | None, bytes | None]] = (
demux_adaptor(*frame) for frame in gen
)
if stream:
return demux_gen
try:
# Wait for all the frames, concatenate them, and return the result
return consume_socket_output(demux_gen, demux=True)
finally:
response.close()
else:
# The generator will output strings
gen = (data for (dummy, data) in gen)
if stream:
return gen
try:
# Wait for all the frames, concatenate them, and return the result
return consume_socket_output(gen, demux=demux)
finally:
response.close()
mux_gen: t.Generator[bytes] = (data for (dummy, data) in gen)
if stream:
return mux_gen
try:
# Wait for all the frames, concatenate them, and return the result
return consume_socket_output(mux_gen, demux=False)
finally:
response.close()
def _disable_socket_timeout(self, socket: SocketLike) -> None:
"""Depending on the combination of python version and whether we are
@@ -637,11 +646,11 @@ class APIClient(_Session):
return self._multiplexed_response_stream_helper(res)
return sep.join(list(self._multiplexed_buffer_helper(res)))
def _unmount(self, *args) -> None:
def _unmount(self, *args: t.Any) -> None:
for proto in args:
self.adapters.pop(proto)
def get_adapter(self, url: str):
def get_adapter(self, url: str) -> BaseAdapter:
try:
return super().get_adapter(url)
except _InvalidSchema as e:
@@ -696,19 +705,19 @@ class APIClient(_Session):
else:
log.debug("No auth config found")
def get_binary(self, pathfmt: str, *args: str, **kwargs) -> bytes:
def get_binary(self, pathfmt: str, *args: str, **kwargs: t.Any) -> bytes:
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
get_binary=True,
)
def get_json(self, pathfmt: str, *args: str, **kwargs) -> t.Any:
def get_json(self, pathfmt: str, *args: str, **kwargs: t.Any) -> t.Any:
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs),
get_json=True,
)
def get_text(self, pathfmt: str, *args: str, **kwargs) -> str:
def get_text(self, pathfmt: str, *args: str, **kwargs: t.Any) -> str:
return self._result(
self._get(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
@@ -718,7 +727,7 @@ class APIClient(_Session):
pathfmt: str,
*args: str,
chunk_size: int = DEFAULT_DATA_CHUNK_SIZE,
**kwargs,
**kwargs: t.Any,
) -> t.Generator[bytes]:
res = self._get(
self._url(pathfmt, *args, versioned_api=True), stream=True, **kwargs
@@ -726,23 +735,25 @@ class APIClient(_Session):
self._raise_for_status(res)
return self._stream_raw_result(res, chunk_size=chunk_size, decode=False)
def delete_call(self, pathfmt: str, *args: str, **kwargs) -> None:
def delete_call(self, pathfmt: str, *args: str, **kwargs: t.Any) -> None:
self._raise_for_status(
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def delete_json(self, pathfmt: str, *args: str, **kwargs) -> t.Any:
def delete_json(self, pathfmt: str, *args: str, **kwargs: t.Any) -> t.Any:
return self._result(
self._delete(self._url(pathfmt, *args, versioned_api=True), **kwargs),
get_json=True,
)
def post_call(self, pathfmt: str, *args: str, **kwargs) -> None:
def post_call(self, pathfmt: str, *args: str, **kwargs: t.Any) -> None:
self._raise_for_status(
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs)
)
def post_json(self, pathfmt: str, *args: str, data: t.Any = None, **kwargs) -> None:
def post_json(
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs: t.Any
) -> None:
self._raise_for_status(
self._post_json(
self._url(pathfmt, *args, versioned_api=True), data, **kwargs
@@ -750,7 +761,7 @@ class APIClient(_Session):
)
def post_json_to_binary(
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs: t.Any
) -> bytes:
return self._result(
self._post_json(
@@ -760,7 +771,7 @@ class APIClient(_Session):
)
def post_json_to_json(
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs: t.Any
) -> t.Any:
return self._result(
self._post_json(
@@ -770,7 +781,7 @@ class APIClient(_Session):
)
def post_json_to_text(
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs
self, pathfmt: str, *args: str, data: t.Any = None, **kwargs: t.Any
) -> str:
return self._result(
self._post_json(
@@ -784,7 +795,7 @@ class APIClient(_Session):
*args: str,
data: t.Any = None,
headers: dict[str, str] | None = None,
**kwargs,
**kwargs: t.Any,
) -> SocketLike:
headers = headers.copy() if headers else {}
headers.update(
@@ -813,7 +824,7 @@ class APIClient(_Session):
stream: t.Literal[True],
tty: bool = True,
demux: t.Literal[False] = False,
**kwargs,
**kwargs: t.Any,
) -> t.Generator[bytes]: ...
@t.overload
@@ -826,7 +837,7 @@ class APIClient(_Session):
stream: t.Literal[True],
tty: t.Literal[True] = True,
demux: t.Literal[True],
**kwargs,
**kwargs: t.Any,
) -> t.Generator[tuple[bytes, None]]: ...
@t.overload
@@ -839,7 +850,7 @@ class APIClient(_Session):
stream: t.Literal[True],
tty: t.Literal[False],
demux: t.Literal[True],
**kwargs,
**kwargs: t.Any,
) -> t.Generator[tuple[bytes, None] | tuple[None, bytes]]: ...
@t.overload
@@ -852,7 +863,7 @@ class APIClient(_Session):
stream: t.Literal[False],
tty: bool = True,
demux: t.Literal[False] = False,
**kwargs,
**kwargs: t.Any,
) -> bytes: ...
@t.overload
@@ -865,7 +876,7 @@ class APIClient(_Session):
stream: t.Literal[False],
tty: t.Literal[True] = True,
demux: t.Literal[True],
**kwargs,
**kwargs: t.Any,
) -> tuple[bytes, None]: ...
@t.overload
@@ -878,7 +889,7 @@ class APIClient(_Session):
stream: t.Literal[False],
tty: t.Literal[False],
demux: t.Literal[True],
**kwargs,
**kwargs: t.Any,
) -> tuple[bytes, bytes]: ...
def post_json_to_stream(
@@ -890,7 +901,7 @@ class APIClient(_Session):
stream: bool = False,
demux: bool = False,
tty: bool = False,
**kwargs,
**kwargs: t.Any,
) -> t.Any:
headers = headers.copy() if headers else {}
headers.update(
@@ -912,7 +923,7 @@ class APIClient(_Session):
demux=demux,
)
def post_to_json(self, pathfmt: str, *args: str, **kwargs) -> t.Any:
def post_to_json(self, pathfmt: str, *args: str, **kwargs: t.Any) -> t.Any:
return self._result(
self._post(self._url(pathfmt, *args, versioned_api=True), **kwargs),
get_json=True,
+6 -4
View File
@@ -106,7 +106,7 @@ class AuthConfig(dict):
@classmethod
def parse_auth(
cls, entries: dict[str, dict[str, t.Any]], raise_on_error=False
cls, entries: dict[str, dict[str, t.Any]], raise_on_error: bool = False
) -> dict[str, dict[str, t.Any]]:
"""
Parses authentication entries
@@ -294,7 +294,7 @@ class AuthConfig(dict):
except StoreError as e:
raise errors.DockerException(f"Credentials store error: {e}")
def _get_store_instance(self, name: str):
def _get_store_instance(self, name: str) -> Store:
if name not in self._stores:
self._stores[name] = Store(name, environment=self._credstore_env)
return self._stores[name]
@@ -326,8 +326,10 @@ class AuthConfig(dict):
def resolve_authconfig(
authconfig, registry: str | None = None, credstore_env: dict[str, str] | None = None
):
authconfig: AuthConfig | dict[str, t.Any],
registry: str | None = None,
credstore_env: dict[str, str] | None = None,
) -> dict[str, t.Any] | None:
if not isinstance(authconfig, AuthConfig):
authconfig = AuthConfig(authconfig, credstore_env)
return authconfig.resolve_authconfig(registry)
+1 -1
View File
@@ -89,7 +89,7 @@ def get_meta_dir(name: str | None = None) -> str:
return meta_dir
def get_meta_file(name) -> str:
def get_meta_file(name: str) -> str:
return os.path.join(get_meta_dir(name), METAFILE)
+8 -1
View File
@@ -18,6 +18,10 @@ from ansible.module_utils.common.text.converters import to_native
from ._import_helper import HTTPError as _HTTPError
if t.TYPE_CHECKING:
from requests import Response
class DockerException(Exception):
"""
A base class from which all other exceptions inherit.
@@ -55,7 +59,10 @@ class APIError(_HTTPError, DockerException):
"""
def __init__(
self, message: str | Exception, response=None, explanation: str | None = None
self,
message: str | Exception,
response: Response | None = None,
explanation: str | None = None,
) -> None:
# requests 1.2 supports response as a keyword argument, but
# requests 1.1 does not
@@ -11,6 +11,7 @@
from __future__ import annotations
import typing as t
from queue import Empty
from .. import constants
@@ -19,6 +20,12 @@ from .basehttpadapter import BaseHTTPAdapter
from .npipesocket import NpipeSocket
if t.TYPE_CHECKING:
from collections.abc import Mapping
from requests import PreparedRequest
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@@ -91,7 +98,9 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
)
super().__init__()
def get_connection(self, url: str | bytes, proxies=None) -> NpipeHTTPConnectionPool:
def get_connection(
self, url: str | bytes, proxies: Mapping[str, str] | None = None
) -> NpipeHTTPConnectionPool:
with self.pools.lock:
pool = self.pools.get(url)
if pool:
@@ -104,7 +113,9 @@ class NpipeHTTPAdapter(BaseHTTPAdapter):
return pool
def request_url(self, request, proxies) -> str:
def request_url(
self, request: PreparedRequest, proxies: Mapping[str, str] | None
) -> str:
# The select_proxy utility in requests errors out when the provided URL
# does not have a hostname, like is the case when using a UNIX socket.
# Since proxies are an irrelevant notion in the case of UNIX sockets
@@ -64,7 +64,7 @@ class NpipeSocket:
implemented.
"""
def __init__(self, handle=None) -> None:
def __init__(self, handle: t.Any | None = None) -> None:
self._timeout = win32pipe.NMPWAIT_USE_DEFAULT_WAIT
self._handle = handle
self._address: str | None = None
@@ -74,15 +74,17 @@ class NpipeSocket:
def accept(self) -> t.NoReturn:
raise NotImplementedError()
def bind(self, address) -> t.NoReturn:
def bind(self, address: t.Any) -> t.NoReturn:
raise NotImplementedError()
def close(self) -> None:
if self._handle is None:
raise ValueError("Handle not present")
self._handle.Close()
self._closed = True
@check_closed
def connect(self, address, retry_count: int = 0) -> None:
def connect(self, address: str, retry_count: int = 0) -> None:
try:
handle = win32file.CreateFile(
address,
@@ -116,11 +118,11 @@ class NpipeSocket:
self._address = address
@check_closed
def connect_ex(self, address) -> None:
def connect_ex(self, address: str) -> None:
self.connect(address)
@check_closed
def detach(self):
def detach(self) -> t.Any:
self._closed = True
return self._handle
@@ -134,16 +136,18 @@ class NpipeSocket:
def getsockname(self) -> str | None:
return self._address
def getsockopt(self, level, optname, buflen=None) -> t.NoReturn:
def getsockopt(
self, level: t.Any, optname: t.Any, buflen: t.Any = None
) -> t.NoReturn:
raise NotImplementedError()
def ioctl(self, control, option) -> t.NoReturn:
def ioctl(self, control: t.Any, option: t.Any) -> t.NoReturn:
raise NotImplementedError()
def listen(self, backlog) -> t.NoReturn:
def listen(self, backlog: t.Any) -> t.NoReturn:
raise NotImplementedError()
def makefile(self, mode: str, bufsize: int | None = None):
def makefile(self, mode: str, bufsize: int | None = None) -> t.IO[bytes]:
if mode.strip("b") != "r":
raise NotImplementedError()
rawio = NpipeFileIOBase(self)
@@ -153,6 +157,8 @@ class NpipeSocket:
@check_closed
def recv(self, bufsize: int, flags: int = 0) -> str:
if self._handle is None:
raise ValueError("Handle not present")
dummy_err, data = win32file.ReadFile(self._handle, bufsize)
return data
@@ -169,6 +175,8 @@ class NpipeSocket:
@check_closed
def recv_into(self, buf: Buffer, nbytes: int = 0) -> int:
if self._handle is None:
raise ValueError("Handle not present")
readbuf = buf if isinstance(buf, memoryview) else memoryview(buf)
event = win32event.CreateEvent(None, True, True, None)
@@ -188,6 +196,8 @@ class NpipeSocket:
@check_closed
def send(self, string: Buffer, flags: int = 0) -> int:
if self._handle is None:
raise ValueError("Handle not present")
event = win32event.CreateEvent(None, True, True, None)
try:
overlapped = pywintypes.OVERLAPPED()
@@ -210,7 +220,7 @@ class NpipeSocket:
self.connect(address)
return self.send(string)
def setblocking(self, flag: bool):
def setblocking(self, flag: bool) -> None:
if flag:
return self.settimeout(None)
return self.settimeout(0)
@@ -228,16 +238,16 @@ class NpipeSocket:
def gettimeout(self) -> int | float | None:
return self._timeout
def setsockopt(self, level, optname, value) -> t.NoReturn:
def setsockopt(self, level: t.Any, optname: t.Any, value: t.Any) -> t.NoReturn:
raise NotImplementedError()
@check_closed
def shutdown(self, how) -> None:
def shutdown(self, how: t.Any) -> None:
return self.close()
class NpipeFileIOBase(io.RawIOBase):
def __init__(self, npipe_socket) -> None:
def __init__(self, npipe_socket: NpipeSocket | None) -> None:
self.sock = npipe_socket
def close(self) -> None:
@@ -245,7 +255,10 @@ class NpipeFileIOBase(io.RawIOBase):
self.sock = None
def fileno(self) -> int:
return self.sock.fileno()
if self.sock is None:
raise RuntimeError("socket is closed")
# TODO: This is definitely a bug, NpipeSocket.fileno() does not exist!
return self.sock.fileno() # type: ignore
def isatty(self) -> bool:
return False
@@ -254,6 +267,8 @@ class NpipeFileIOBase(io.RawIOBase):
return True
def readinto(self, buf: Buffer) -> int:
if self.sock is None:
raise RuntimeError("socket is closed")
return self.sock.recv_into(buf)
def seekable(self) -> bool:
+17 -14
View File
@@ -35,7 +35,7 @@ else:
PARAMIKO_IMPORT_ERROR = None # pylint: disable=invalid-name
if t.TYPE_CHECKING:
from collections.abc import Buffer
from collections.abc import Buffer, Mapping
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
@@ -67,7 +67,7 @@ class SSHSocket(socket.socket):
preexec_func = None
if not constants.IS_WINDOWS_PLATFORM:
def f():
def f() -> None:
signal.signal(signal.SIGINT, signal.SIG_IGN)
preexec_func = f
@@ -100,13 +100,13 @@ class SSHSocket(socket.socket):
self.proc.stdin.flush()
return written
def sendall(self, data: Buffer, *args, **kwargs) -> None:
def sendall(self, data: Buffer, *args: t.Any, **kwargs: t.Any) -> None:
self._write(data)
def send(self, data: Buffer, *args, **kwargs) -> int:
def send(self, data: Buffer, *args: t.Any, **kwargs: t.Any) -> int:
return self._write(data)
def recv(self, n: int, *args, **kwargs) -> bytes:
def recv(self, n: int, *args: t.Any, **kwargs: t.Any) -> bytes:
if not self.proc:
raise RuntimeError(
"SSH subprocess not initiated. connect() must be called first."
@@ -114,7 +114,7 @@ class SSHSocket(socket.socket):
assert self.proc.stdout is not None
return self.proc.stdout.read(n)
def makefile(self, mode: str, *args, **kwargs) -> t.IO: # type: ignore
def makefile(self, mode: str, *args: t.Any, **kwargs: t.Any) -> t.IO: # type: ignore
if not self.proc:
self.connect()
assert self.proc is not None
@@ -138,7 +138,7 @@ class SSHConnection(urllib3_connection.HTTPConnection):
def __init__(
self,
*,
ssh_transport=None,
ssh_transport: paramiko.Transport | None = None,
timeout: int | float = 60,
host: str,
) -> None:
@@ -146,18 +146,19 @@ class SSHConnection(urllib3_connection.HTTPConnection):
self.ssh_transport = ssh_transport
self.timeout = timeout
self.ssh_host = host
self.sock: paramiko.Channel | SSHSocket | None = None
def connect(self) -> None:
if self.ssh_transport:
sock = self.ssh_transport.open_session()
sock.settimeout(self.timeout)
sock.exec_command("docker system dial-stdio")
channel = self.ssh_transport.open_session()
channel.settimeout(self.timeout)
channel.exec_command("docker system dial-stdio")
self.sock = channel
else:
sock = SSHSocket(self.ssh_host)
sock.settimeout(self.timeout)
sock.connect()
self.sock = sock
self.sock = sock
class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
@@ -172,7 +173,7 @@ class SSHConnectionPool(urllib3.connectionpool.HTTPConnectionPool):
host: str,
) -> None:
super().__init__("localhost", timeout=timeout, maxsize=maxsize)
self.ssh_transport = None
self.ssh_transport: paramiko.Transport | None = None
self.timeout = timeout
if ssh_client:
self.ssh_transport = ssh_client.get_transport()
@@ -276,7 +277,9 @@ class SSHHTTPAdapter(BaseHTTPAdapter):
if self.ssh_client:
self.ssh_client.connect(**self.ssh_params)
def get_connection(self, url: str | bytes, proxies=None) -> SSHConnectionPool:
def get_connection(
self, url: str | bytes, proxies: Mapping[str, str] | None = None
) -> SSHConnectionPool:
if not self.ssh_client:
return SSHConnectionPool(
ssh_client=self.ssh_client,
@@ -33,7 +33,7 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
def __init__(
self,
assert_hostname: bool | None = None,
**kwargs,
**kwargs: t.Any,
) -> None:
self.assert_hostname = assert_hostname
super().__init__(**kwargs)
@@ -51,7 +51,7 @@ class SSLHTTPAdapter(BaseHTTPAdapter):
self.poolmanager = PoolManager(**kwargs)
def get_connection(self, *args, **kwargs) -> urllib3.ConnectionPool:
def get_connection(self, *args: t.Any, **kwargs: t.Any) -> urllib3.ConnectionPool:
"""
Ensure assert_hostname is set correctly on our pool
@@ -19,12 +19,20 @@ from .._import_helper import HTTPAdapter, urllib3, urllib3_connection
from .basehttpadapter import BaseHTTPAdapter
if t.TYPE_CHECKING:
from collections.abc import Mapping
from requests import PreparedRequest
from ..._socket_helper import SocketLike
RecentlyUsedContainer = urllib3._collections.RecentlyUsedContainer
class UnixHTTPConnection(urllib3_connection.HTTPConnection):
def __init__(
self, base_url: str | bytes, unix_socket, timeout: int | float = 60
self, base_url: str | bytes, unix_socket: str, timeout: int | float = 60
) -> None:
super().__init__("localhost", timeout=timeout)
self.base_url = base_url
@@ -43,7 +51,7 @@ class UnixHTTPConnection(urllib3_connection.HTTPConnection):
if header == "Connection" and "Upgrade" in values:
self.disable_buffering = True
def response_class(self, sock, *args, **kwargs) -> t.Any:
def response_class(self, sock: SocketLike, *args: t.Any, **kwargs: t.Any) -> t.Any:
# FIXME: We may need to disable buffering on Py3,
# but there's no clear way to do it at the moment. See:
# https://github.com/docker/docker-py/issues/1799
@@ -88,12 +96,16 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
self.socket_path = socket_path
self.timeout = timeout
self.max_pool_size = max_pool_size
self.pools = RecentlyUsedContainer(
pool_connections, dispose_func=lambda p: p.close()
)
def f(p: t.Any) -> None:
p.close()
self.pools = RecentlyUsedContainer(pool_connections, dispose_func=f)
super().__init__()
def get_connection(self, url: str | bytes, proxies=None) -> UnixHTTPConnectionPool:
def get_connection(
self, url: str | bytes, proxies: Mapping[str, str] | None = None
) -> UnixHTTPConnectionPool:
with self.pools.lock:
pool = self.pools.get(url)
if pool:
@@ -106,7 +118,7 @@ class UnixHTTPAdapter(BaseHTTPAdapter):
return pool
def request_url(self, request, proxies) -> str:
def request_url(self, request: PreparedRequest, proxies: Mapping[str, str]) -> str:
# The select_proxy utility in requests errors out when the provided URL
# does not have a hostname, like is the case when using a UNIX socket.
# Since proxies are an irrelevant notion in the case of UNIX sockets
+11 -5
View File
@@ -18,7 +18,13 @@ from .._import_helper import urllib3
from ..errors import DockerException
class CancellableStream:
if t.TYPE_CHECKING:
from requests import Response
_T = t.TypeVar("_T")
class CancellableStream(t.Generic[_T]):
"""
Stream wrapper for real-time events, logs, etc. from the server.
@@ -30,14 +36,14 @@ class CancellableStream:
>>> events.close()
"""
def __init__(self, stream, response) -> None:
def __init__(self, stream: t.Generator[_T], response: Response) -> None:
self._stream = stream
self._response = response
def __iter__(self) -> t.Self:
return self
def __next__(self):
def __next__(self) -> _T:
try:
return next(self._stream)
except urllib3.exceptions.ProtocolError as exc:
@@ -56,7 +62,7 @@ class CancellableStream:
# find the underlying socket object
# based on api.client._get_raw_response_socket
sock_fp = self._response.raw._fp.fp
sock_fp = self._response.raw._fp.fp # type: ignore
if hasattr(sock_fp, "raw"):
sock_raw = sock_fp.raw
@@ -74,7 +80,7 @@ class CancellableStream:
"Cancellable streams not supported for the SSH protocol"
)
else:
sock = sock_fp._sock
sock = sock_fp._sock # type: ignore
if hasattr(urllib3.contrib, "pyopenssl") and isinstance(
sock, urllib3.contrib.pyopenssl.WrappedSocket
+1 -1
View File
@@ -37,7 +37,7 @@ def _purge() -> None:
_cache.clear()
def fnmatch(name: str, pat: str):
def fnmatch(name: str, pat: str) -> bool:
"""Test whether FILENAME matches PATTERN.
Patterns are Unix shell style:
+1 -1
View File
@@ -108,7 +108,7 @@ def port_range(
def split_port(
port: str,
port: str | int,
) -> tuple[list[str], list[str] | list[tuple[str, str | None]] | None]:
port = str(port)
match = PORT_SPEC.match(port)
+13 -1
View File
@@ -11,6 +11,8 @@
from __future__ import annotations
import typing as t
from .utils import format_environment
@@ -67,7 +69,17 @@ class ProxyConfig(dict):
env["no_proxy"] = env["NO_PROXY"] = self.no_proxy
return env
def inject_proxy_environment(self, environment: list[str]) -> list[str]:
@t.overload
def inject_proxy_environment(self, environment: list[str]) -> list[str]: ...
@t.overload
def inject_proxy_environment(
self, environment: list[str] | None
) -> list[str] | None: ...
def inject_proxy_environment(
self, environment: list[str] | None
) -> list[str] | None:
"""
Given a list of strings representing environment variables, prepend the
environment variables corresponding to the proxy settings.
+44 -19
View File
@@ -22,7 +22,9 @@ from ..transport.npipesocket import NpipeSocket
if t.TYPE_CHECKING:
from collections.abc import Iterable
from collections.abc import Iterable, Sequence
from ..._socket_helper import SocketLike
STDOUT = 1
@@ -38,14 +40,14 @@ class SocketError(Exception):
NPIPE_ENDED = 109
def read(socket, n: int = 4096) -> bytes | None:
def read(socket: SocketLike, n: int = 4096) -> bytes | None:
"""
Reads at most n bytes from socket
"""
recoverable_errors = (errno.EINTR, errno.EDEADLK, errno.EWOULDBLOCK)
if not isinstance(socket, NpipeSocket):
if not isinstance(socket, NpipeSocket): # type: ignore[unreachable]
if not hasattr(select, "poll"):
# Limited to 1024
select.select([socket], [], [])
@@ -66,7 +68,7 @@ def read(socket, n: int = 4096) -> bytes | None:
return None # TODO ???
except Exception as e:
is_pipe_ended = (
isinstance(socket, NpipeSocket)
isinstance(socket, NpipeSocket) # type: ignore[unreachable]
and len(e.args) > 0
and e.args[0] == NPIPE_ENDED
)
@@ -77,7 +79,7 @@ def read(socket, n: int = 4096) -> bytes | None:
raise
def read_exactly(socket, n: int) -> bytes:
def read_exactly(socket: SocketLike, n: int) -> bytes:
"""
Reads exactly n bytes from socket
Raises SocketError if there is not enough data
@@ -91,7 +93,7 @@ def read_exactly(socket, n: int) -> bytes:
return data
def next_frame_header(socket) -> tuple[int, int]:
def next_frame_header(socket: SocketLike) -> tuple[int, int]:
"""
Returns the stream and size of the next frame of data waiting to be read
from socket, according to the protocol defined here:
@@ -107,7 +109,7 @@ def next_frame_header(socket) -> tuple[int, int]:
return (stream, actual)
def frames_iter(socket, tty: bool) -> t.Generator[tuple[int, bytes]]:
def frames_iter(socket: SocketLike, tty: bool) -> t.Generator[tuple[int, bytes]]:
"""
Return a generator of frames read from socket. A frame is a tuple where
the first item is the stream number and the second item is a chunk of data.
@@ -120,7 +122,7 @@ def frames_iter(socket, tty: bool) -> t.Generator[tuple[int, bytes]]:
return frames_iter_no_tty(socket)
def frames_iter_no_tty(socket) -> t.Generator[tuple[int, bytes]]:
def frames_iter_no_tty(socket: SocketLike) -> t.Generator[tuple[int, bytes]]:
"""
Returns a generator of data read from the socket when the tty setting is
not enabled.
@@ -141,7 +143,7 @@ def frames_iter_no_tty(socket) -> t.Generator[tuple[int, bytes]]:
yield (stream, result)
def frames_iter_tty(socket) -> t.Generator[bytes]:
def frames_iter_tty(socket: SocketLike) -> t.Generator[bytes]:
"""
Return a generator of data read from the socket when the tty setting is
enabled.
@@ -155,20 +157,42 @@ def frames_iter_tty(socket) -> t.Generator[bytes]:
@t.overload
def consume_socket_output(frames, demux: t.Literal[False] = False) -> bytes: ...
@t.overload
def consume_socket_output(frames, demux: t.Literal[True]) -> tuple[bytes, bytes]: ...
def consume_socket_output(
frames: Sequence[bytes] | t.Generator[bytes], demux: t.Literal[False] = False
) -> bytes: ...
@t.overload
def consume_socket_output(
frames, demux: bool = False
frames: (
Sequence[tuple[bytes | None, bytes | None]]
| t.Generator[tuple[bytes | None, bytes | None]]
),
demux: t.Literal[True],
) -> tuple[bytes, bytes]: ...
@t.overload
def consume_socket_output(
frames: (
Sequence[bytes]
| Sequence[tuple[bytes | None, bytes | None]]
| t.Generator[bytes]
| t.Generator[tuple[bytes | None, bytes | None]]
),
demux: bool = False,
) -> bytes | tuple[bytes, bytes]: ...
def consume_socket_output(frames, demux: bool = False) -> bytes | tuple[bytes, bytes]:
def consume_socket_output(
frames: (
Sequence[bytes]
| Sequence[tuple[bytes | None, bytes | None]]
| t.Generator[bytes]
| t.Generator[tuple[bytes | None, bytes | None]]
),
demux: bool = False,
) -> bytes | tuple[bytes, bytes]:
"""
Iterate through frames read from the socket and return the result.
@@ -183,12 +207,13 @@ def consume_socket_output(frames, demux: bool = False) -> bytes | tuple[bytes, b
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) # type: ignore
# If the streams are demultiplexed, the generator yields tuples
# (stdout, stderr)
out: list[bytes | None] = [None, None]
for frame in frames:
frame: tuple[bytes | None, bytes | None]
for frame in frames: # type: ignore
# It is guaranteed that for each frame, one and only one stream
# is not None.
if frame == (None, None):
@@ -202,7 +227,7 @@ def consume_socket_output(frames, demux: bool = False) -> bytes | tuple[bytes, b
if out[1] is None:
out[1] = frame[1]
else:
out[1] += frame[1]
out[1] += frame[1] # type: ignore[operator]
return tuple(out) # type: ignore
+12 -5
View File
@@ -46,7 +46,7 @@ URLComponents = collections.namedtuple(
)
def decode_json_header(header: str) -> dict[str, t.Any]:
def decode_json_header(header: str | bytes) -> dict[str, t.Any]:
data = base64.b64decode(header).decode("utf-8")
return json.loads(data)
@@ -143,7 +143,12 @@ def convert_port_bindings(
def convert_volume_binds(
binds: list[str] | Mapping[str | bytes, dict[str, str | bytes] | bytes | str | int],
binds: (
list[str]
| Mapping[
str | bytes, dict[str, str | bytes] | dict[str, str] | bytes | str | int
]
),
) -> list[str]:
if isinstance(binds, list):
return binds # type: ignore
@@ -403,7 +408,9 @@ def kwargs_from_env(
return params
def convert_filters(filters: Mapping[str, bool | str | list[str]]) -> str:
def convert_filters(
filters: Mapping[str, bool | str | int | list[int] | list[str] | list[str | int]],
) -> str:
result = {}
for k, v in filters.items():
if isinstance(v, bool):
@@ -495,8 +502,8 @@ def split_command(command: str) -> list[str]:
return shlex.split(command)
def format_environment(environment: Mapping[str, str | bytes]) -> list[str]:
def format_env(key, value):
def format_environment(environment: Mapping[str, str | bytes | None]) -> list[str]:
def format_env(key: str, value: str | bytes | None) -> str:
if value is None:
return key
if isinstance(value, bytes):
+3 -5
View File
@@ -91,7 +91,7 @@ if not HAS_DOCKER_PY:
# No Docker SDK for Python. Create a place holder client to allow
# instantiation of AnsibleModule and proper error handing
class Client: # type: ignore # noqa: F811, pylint: disable=function-redefined
def __init__(self, **kwargs):
def __init__(self, **kwargs: t.Any) -> None:
pass
class APIError(Exception): # type: ignore # noqa: F811, pylint: disable=function-redefined
@@ -226,7 +226,7 @@ class AnsibleDockerClientBase(Client):
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
def log(self, msg: t.Any, pretty_print: bool = False):
def log(self, msg: t.Any, pretty_print: bool = False) -> None:
pass
# if self.debug:
# from .util import log_debug
@@ -609,7 +609,7 @@ class AnsibleDockerClientBase(Client):
return new_tag, old_tag == new_tag
def inspect_distribution(self, image: str, **kwargs) -> dict[str, t.Any]:
def inspect_distribution(self, image: str, **kwargs: t.Any) -> dict[str, t.Any]:
"""
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.
@@ -629,7 +629,6 @@ class AnsibleDockerClientBase(Client):
class AnsibleDockerClient(AnsibleDockerClientBase):
def __init__(
self,
argument_spec: dict[str, t.Any] | None = None,
@@ -651,7 +650,6 @@ class AnsibleDockerClient(AnsibleDockerClientBase):
option_minimal_versions_ignore_params: Sequence[str] | None = None,
fail_results: dict[str, t.Any] | None = None,
):
# Modules can put information in here which will always be returned
# in case client.fail() is called.
self.fail_results = fail_results or {}
+2 -2
View File
@@ -146,7 +146,7 @@ class AnsibleDockerClientBase(Client):
f"Docker API version is {self.docker_api_version_str}. Minimum version required is {min_docker_api_version}."
)
def log(self, msg: t.Any, pretty_print: bool = False):
def log(self, msg: t.Any, pretty_print: bool = False) -> None:
pass
# if self.debug:
# from .util import log_debug
@@ -295,7 +295,7 @@ class AnsibleDockerClientBase(Client):
),
}
def depr(*args, **kwargs):
def depr(*args: t.Any, **kwargs: t.Any) -> None:
self.deprecate(*args, **kwargs)
update_tls_hostname(
+8 -9
View File
@@ -82,7 +82,7 @@ class AnsibleDockerClientBase:
def __init__(
self,
common_args,
common_args: dict[str, t.Any],
min_docker_api_version: str | None = None,
needs_api_version: bool = True,
) -> None:
@@ -91,15 +91,15 @@ class AnsibleDockerClientBase:
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:
cli = common_args.get("docker_cli")
if cli is None:
try:
self._cli = get_bin_path("docker")
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._cli = cli
self._cli_base = [self._cli]
docker_host = common_args["docker_host"]
if not docker_host and not common_args["cli_context"]:
@@ -149,7 +149,7 @@ class AnsibleDockerClientBase:
"Internal error: cannot have needs_api_version=False with min_docker_api_version not None"
)
def log(self, msg: str, pretty_print: bool = False):
def log(self, msg: str, pretty_print: bool = False) -> None:
pass
# if self.debug:
# from .util import log_debug
@@ -227,7 +227,7 @@ class AnsibleDockerClientBase:
return rc, result, stderr
@abc.abstractmethod
def fail(self, msg: str, **kwargs) -> t.NoReturn:
def fail(self, msg: str, **kwargs: t.Any) -> t.NoReturn:
pass
@abc.abstractmethod
@@ -395,7 +395,6 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
fail_results: dict[str, t.Any] | None = None,
needs_api_version: bool = True,
) -> None:
# Modules can put information in here which will always be returned
# in case client.fail() is called.
self.fail_results = fail_results or {}
@@ -463,7 +462,7 @@ class AnsibleModuleDockerClient(AnsibleDockerClientBase):
)
return rc, stdout, stderr
def fail(self, msg: str, **kwargs) -> t.NoReturn:
def fail(self, msg: str, **kwargs: t.Any) -> t.NoReturn:
self.fail_results.update(kwargs)
self.module.fail_json(msg=msg, **sanitize_result(self.fail_results))
+2 -2
View File
@@ -971,7 +971,7 @@ class BaseComposeManager(DockerBaseClass):
stderr: str | bytes,
ignore_service_pull_events: bool = False,
ignore_build_events: bool = False,
):
) -> None:
result["changed"] = result.get("changed", False) or has_changes(
events,
ignore_service_pull_events=ignore_service_pull_events,
@@ -989,7 +989,7 @@ class BaseComposeManager(DockerBaseClass):
stdout: str | bytes,
stderr: bytes,
rc: int,
):
) -> bool:
return update_failed(
result,
events,
+4 -2
View File
@@ -330,6 +330,8 @@ def stat_file(
client._raise_for_status(response)
header = response.headers.get("x-docker-container-path-stat")
try:
if header is None:
raise ValueError("x-docker-container-path-stat header not present")
stat_data = json.loads(base64.b64decode(header))
except Exception as exc:
raise DockerUnexpectedError(
@@ -482,14 +484,14 @@ def fetch_file(
shutil.copyfileobj(in_f, out_f)
return in_path
def process_symlink(in_path, member) -> str:
def process_symlink(in_path: str, member: tarfile.TarInfo) -> str:
if os.path.exists(b_out_path):
os.unlink(b_out_path)
os.symlink(member.linkname, b_out_path)
return in_path
def process_other(in_path, member) -> str:
def process_other(in_path: str, member: tarfile.TarInfo) -> str:
raise DockerFileCopyError(
f'Remote file "{in_path}" is not a regular file or a symbolic link'
)
@@ -193,7 +193,9 @@ class OptionGroup:
) -> None:
if preprocess is None:
def preprocess(module, values):
def preprocess(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
return values
self.preprocess = preprocess
@@ -207,8 +209,8 @@ class OptionGroup:
self.ansible_required_by = ansible_required_by or {}
self.argument_spec: dict[str, t.Any] = {}
def add_option(self, *args, **kwargs) -> OptionGroup:
option = Option(*args, owner=self, **kwargs)
def add_option(self, name: str, **kwargs: t.Any) -> OptionGroup:
option = Option(name, owner=self, **kwargs)
if not option.not_a_container_option:
self.options.append(option)
self.all_options.append(option)
@@ -788,7 +790,7 @@ def _preprocess_mounts(
) -> dict[str, t.Any]:
last: dict[str, str] = {}
def check_collision(t, name):
def check_collision(t: str, name: str) -> None:
if t in last:
if name == last[t]:
module.fail_json(
@@ -1069,7 +1071,9 @@ def _preprocess_ports(
return values
def _compare_platform(option: Option, param_value: t.Any, container_value: t.Any):
def _compare_platform(
option: Option, param_value: t.Any, container_value: t.Any
) -> bool:
if option.comparison == "ignore":
return True
try:
@@ -872,7 +872,7 @@ class DockerAPIEngine(Engine[AnsibleDockerClient]):
image: dict[str, t.Any] | None,
values: dict[str, t.Any],
host_info: dict[str, t.Any] | None,
):
) -> dict[str, t.Any]:
if len(options) != 1:
raise AssertionError(
"host_config_value can only be used for a single option"
@@ -1961,7 +1961,14 @@ def _update_value_restart(
}
def _get_values_ports(module, container, api_version, options, image, host_info):
def _get_values_ports(
module: AnsibleModule,
container: dict[str, t.Any],
api_version: LooseVersion,
options: list[Option],
image: dict[str, t.Any] | None,
host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
host_config = container["HostConfig"]
config = container["Config"]
@@ -292,7 +292,7 @@ class ContainerManager(DockerBaseClass, t.Generic[Client]):
if self.module.params[param] is None:
self.module.params[param] = value
def fail(self, *args, **kwargs) -> t.NoReturn:
def fail(self, *args: str, **kwargs: t.Any) -> t.NoReturn:
# mypy doesn't know that Client has fail() method
raise self.client.fail(*args, **kwargs) # type: ignore
@@ -714,7 +714,7 @@ class ContainerManager(DockerBaseClass, t.Generic[Client]):
container_image: dict[str, t.Any] | None,
image: dict[str, t.Any] | None,
host_info: dict[str, t.Any] | None,
):
) -> None:
assert container.raw is not None
container_values = engine.get_value(
self.module,
@@ -767,12 +767,12 @@ class ContainerManager(DockerBaseClass, t.Generic[Client]):
# Since the order does not matter, sort so that the diff output is better.
if option.name == "expected_mounts":
# For selected values, use one entry as key
def sort_key_fn(x):
def sort_key_fn(x: dict[str, t.Any]) -> t.Any:
return x["target"]
else:
# We sort the list of dictionaries by using the sorted items of a dict as its key.
def sort_key_fn(x):
def sort_key_fn(x: dict[str, t.Any]) -> t.Any:
return sorted(
(a, to_text(b, errors="surrogate_or_strict"))
for a, b in x.items()
+8 -2
View File
@@ -26,6 +26,7 @@ from ansible_collections.community.docker.plugins.module_utils._socket_helper im
if t.TYPE_CHECKING:
from collections.abc import Callable
from types import TracebackType
from ansible.module_utils.basic import AnsibleModule
@@ -70,7 +71,12 @@ class DockerSocketHandlerBase:
def __enter__(self) -> t.Self:
return self
def __exit__(self, type_, value, tb) -> None:
def __exit__(
self,
type_: t.Type[BaseException] | None,
value: BaseException | None,
tb: TracebackType | None,
) -> None:
self._selector.close()
def set_block_done_callback(
@@ -210,7 +216,7 @@ class DockerSocketHandlerBase:
stdout = []
stderr = []
def append_block(stream_id, data):
def append_block(stream_id: int, data: bytes) -> None:
if stream_id == docker_socket.STDOUT:
stdout.append(data)
elif stream_id == docker_socket.STDERR:
+1 -2
View File
@@ -28,7 +28,6 @@ from ansible_collections.community.docker.plugins.module_utils._version import (
class AnsibleDockerSwarmClient(AnsibleDockerClient):
def get_swarm_node_id(self) -> str | None:
"""
Get the 'NodeID' of the Swarm node or 'None' if host is not in Swarm. It returns the NodeID
@@ -281,7 +280,7 @@ class AnsibleDockerSwarmClient(AnsibleDockerClient):
def get_node_name_by_id(self, nodeid: str) -> str:
return self.get_node_inspect(nodeid)["Description"]["Hostname"]
def get_unlock_key(self) -> str | None:
def get_unlock_key(self) -> dict[str, t.Any] | None:
if self.docker_py_version < LooseVersion("2.7.0"):
return None
return super().get_unlock_key()
+9 -3
View File
@@ -23,6 +23,12 @@ if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
from ._common import AnsibleDockerClientBase as CADCB
from ._common_api import AnsibleDockerClientBase as CAPIADCB
from ._common_cli import AnsibleDockerClientBase as CCLIADCB
Client = t.Union[CADCB, CAPIADCB, CCLIADCB]
DEFAULT_DOCKER_HOST = "unix:///var/run/docker.sock"
DEFAULT_TLS = False
@@ -119,7 +125,7 @@ def sanitize_result(data: t.Any) -> t.Any:
return data
def log_debug(msg: t.Any, pretty_print: bool = False):
def log_debug(msg: t.Any, pretty_print: bool = False) -> None:
"""Write a log message to docker.log.
If ``pretty_print=True``, the message will be pretty-printed as JSON.
@@ -325,7 +331,7 @@ class DifferenceTracker:
def sanitize_labels(
labels: dict[str, t.Any] | None,
labels_field: str,
client=None,
client: Client | None = None,
module: AnsibleModule | None = None,
) -> None:
def fail(msg: str) -> t.NoReturn:
@@ -371,7 +377,7 @@ def clean_dict_booleans_for_docker_api(
which is the expected format of filters which accept lists such as labels.
"""
def sanitize(value):
def sanitize(value: t.Any) -> str:
if value is True:
return "true"
if value is False:
+1 -1
View File
@@ -147,7 +147,7 @@ class PullManager(BaseComposeManager):
f"--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}"
)
def get_pull_cmd(self, dry_run: bool):
def get_pull_cmd(self, dry_run: bool) -> list[str]:
args = self.get_base_args() + ["pull"]
if self.policy != "always":
args.extend(["--policy", self.policy])
+15 -16
View File
@@ -198,6 +198,7 @@ config_name:
import base64
import hashlib
import traceback
import typing as t
try:
@@ -220,9 +221,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ConfigManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
super().__init__()
self.client = client
@@ -253,10 +252,10 @@ class ConfigManager(DockerBaseClass):
if self.rolling_versions:
self.version = 0
self.data_key = None
self.configs = []
self.data_key: str | None = None
self.configs: list[dict[str, t.Any]] = []
def __call__(self):
def __call__(self) -> None:
self.get_config()
if self.state == "present":
self.data_key = hashlib.sha224(self.data).hexdigest()
@@ -265,7 +264,7 @@ class ConfigManager(DockerBaseClass):
elif self.state == "absent":
self.absent()
def get_version(self, config):
def get_version(self, config: dict[str, t.Any]) -> int:
try:
return int(
config.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
@@ -273,14 +272,14 @@ class ConfigManager(DockerBaseClass):
except ValueError:
return 0
def remove_old_versions(self):
def remove_old_versions(self) -> None:
if not self.rolling_versions or self.versions_to_keep < 0:
return
if not self.check_mode:
while len(self.configs) > max(self.versions_to_keep, 1):
self.remove_config(self.configs.pop(0))
def get_config(self):
def get_config(self) -> None:
"""Find an existing config."""
try:
configs = self.client.configs(filters={"name": self.name})
@@ -299,9 +298,9 @@ class ConfigManager(DockerBaseClass):
config for config in configs if config["Spec"]["Name"] == self.name
]
def create_config(self):
def create_config(self) -> str | None:
"""Create a new config"""
config_id = None
config_id: str | dict[str, t.Any] | None = None
# We ca not see the data after creation, so adding a label we can use for idempotency check
labels = {"ansible_key": self.data_key}
if self.rolling_versions:
@@ -325,18 +324,18 @@ class ConfigManager(DockerBaseClass):
self.client.fail(f"Error creating config: {exc}")
if isinstance(config_id, dict):
config_id = config_id["ID"]
return config_id["ID"]
return config_id
def remove_config(self, config):
def remove_config(self, config: dict[str, t.Any]) -> None:
try:
if not self.check_mode:
self.client.remove_config(config["ID"])
except APIError as exc:
self.client.fail(f"Error removing config {config['Spec']['Name']}: {exc}")
def present(self):
def present(self) -> None:
"""Handles state == 'present', creating or updating the config"""
if self.configs:
config = self.configs[-1]
@@ -378,7 +377,7 @@ class ConfigManager(DockerBaseClass):
self.results["config_id"] = self.create_config()
self.results["config_name"] = self.name
def absent(self):
def absent(self) -> None:
"""Handles state == 'absent', removing the config"""
if self.configs:
for config in self.configs:
@@ -386,7 +385,7 @@ class ConfigManager(DockerBaseClass):
self.results["changed"] = True
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"state": {
+14 -13
View File
@@ -347,7 +347,7 @@ def retrieve_diff(
max_file_size_for_diff: int,
regular_stat: dict[str, t.Any] | None = None,
link_target: str | None = None,
):
) -> None:
if diff is None:
return
if regular_stat is not None:
@@ -497,9 +497,9 @@ def is_file_idempotent(
container_path: str,
follow_links: bool,
local_follow_links: bool,
owner_id,
group_id,
mode,
owner_id: int,
group_id: int,
mode: int | None,
force: bool | None = False,
diff: dict[str, t.Any] | None = None,
max_file_size_for_diff: int = 1,
@@ -744,9 +744,9 @@ def copy_file_into_container(
container_path: str,
follow_links: bool,
local_follow_links: bool,
owner_id,
group_id,
mode,
owner_id: int,
group_id: int,
mode: int | None,
force: bool | None = False,
do_diff: bool = False,
max_file_size_for_diff: int = 1,
@@ -797,9 +797,9 @@ def is_content_idempotent(
content: bytes,
container_path: str,
follow_links: bool,
owner_id,
group_id,
mode,
owner_id: int,
group_id: int,
mode: int,
force: bool | None = False,
diff: dict[str, t.Any] | None = None,
max_file_size_for_diff: int = 1,
@@ -989,9 +989,9 @@ def copy_content_into_container(
content: bytes,
container_path: str,
follow_links: bool,
owner_id,
group_id,
mode,
owner_id: int,
group_id: int,
mode: int,
force: bool | None = False,
do_diff: bool = False,
max_file_size_for_diff: int = 1,
@@ -1133,6 +1133,7 @@ def main() -> None:
owner_id, group_id = determine_user_group(client, container)
if content is not None:
assert mode is not None # see required_by above
copy_content_into_container(
client,
container,
+5 -5
View File
@@ -667,7 +667,7 @@ class ImageManager(DockerBaseClass):
:rtype: str
"""
def build_msg(reason):
def build_msg(reason: str) -> str:
return f"Archived image {current_image_name} to {archive_path}, {reason}"
try:
@@ -877,7 +877,7 @@ class ImageManager(DockerBaseClass):
self.push_image(repo, repo_tag)
@staticmethod
def _extract_output_line(line: dict[str, t.Any], output: list[str]):
def _extract_output_line(line: dict[str, t.Any], output: list[str]) -> None:
"""
Extract text line from stream output and, if found, adds it to output.
"""
@@ -1165,18 +1165,18 @@ def main() -> None:
("source", "load", ["load_path"]),
]
def detect_etc_hosts(client):
def detect_etc_hosts(client: AnsibleDockerClient) -> bool:
return client.module.params["build"] and bool(
client.module.params["build"].get("etc_hosts")
)
def detect_build_platform(client):
def detect_build_platform(client: AnsibleDockerClient) -> bool:
return (
client.module.params["build"]
and client.module.params["build"].get("platform") is not None
)
def detect_pull_platform(client):
def detect_pull_platform(client: AnsibleDockerClient) -> bool:
return (
client.module.params["pull"]
and client.module.params["pull"].get("platform") is not None
+1 -1
View File
@@ -379,7 +379,7 @@ def normalize_ipam_config_key(key: str) -> str:
return special_cases.get(key, key.lower())
def dicts_are_essentially_equal(a: dict[str, t.Any], b: dict[str, t.Any]):
def dicts_are_essentially_equal(a: dict[str, t.Any], b: dict[str, t.Any]) -> bool:
"""Make sure that a is a subset of b, where None entries of a are ignored."""
for k, v in a.items():
if v is None:
+15 -14
View File
@@ -134,6 +134,7 @@ node:
"""
import traceback
import typing as t
try:
@@ -157,18 +158,19 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass):
def __init__(self, client):
hostname: str
def __init__(self, client: AnsibleDockerSwarmClient) -> None:
super().__init__()
# Spec
self.name = None
self.labels = None
self.labels_state = None
self.labels_to_remove = None
self.labels: dict[str, t.Any] | None = None
self.labels_state: t.Literal["merge", "replace"] = "merge"
self.labels_to_remove: list[str] | None = None
# Node
self.availability = None
self.role = None
self.availability: t.Literal["active", "pause", "drain"] | None = None
self.role: t.Literal["worker", "manager"] | None = None
for key, value in client.module.params.items():
setattr(self, key, value)
@@ -177,9 +179,9 @@ class TaskParameters(DockerBaseClass):
class SwarmNodeManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(
self, client: AnsibleDockerSwarmClient, results: dict[str, t.Any]
) -> None:
super().__init__()
self.client = client
@@ -192,10 +194,9 @@ class SwarmNodeManager(DockerBaseClass):
self.node_update()
def node_update(self):
def node_update(self) -> None:
if not (self.client.check_if_swarm_node(node_id=self.parameters.hostname)):
self.client.fail("This node is not part of a swarm.")
return
if self.client.check_if_swarm_node_is_down():
self.client.fail("Can not update the node. The node is down.")
@@ -206,7 +207,7 @@ class SwarmNodeManager(DockerBaseClass):
self.client.fail(f"Failed to get node information for {exc}")
changed = False
node_spec = {
node_spec: dict[str, t.Any] = {
"Availability": self.parameters.availability,
"Role": self.parameters.role,
"Labels": self.parameters.labels,
@@ -277,7 +278,7 @@ class SwarmNodeManager(DockerBaseClass):
self.results["changed"] = changed
def main():
def main() -> None:
argument_spec = {
"hostname": {"type": "str", "required": True},
"labels": {"type": "dict"},
+6 -6
View File
@@ -87,6 +87,7 @@ nodes:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._common import (
RequestException,
@@ -103,9 +104,8 @@ except ImportError:
pass
def get_node_facts(client):
results = []
def get_node_facts(client: AnsibleDockerSwarmClient) -> list[dict[str, t.Any]]:
results: list[dict[str, t.Any]] = []
if client.module.params["self"] is True:
self_node_id = client.get_swarm_node_id()
@@ -114,8 +114,8 @@ def get_node_facts(client):
return results
if client.module.params["name"] is None:
node_info = client.get_all_nodes_inspect()
return node_info
node_info_list = client.get_all_nodes_inspect()
return node_info_list
nodes = client.module.params["name"]
if not isinstance(nodes, list):
@@ -130,7 +130,7 @@ def get_node_facts(client):
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "list", "elements": "str"},
"self": {"type": "bool", "default": False},
+5 -7
View File
@@ -204,12 +204,10 @@ class DockerPluginManager:
elif state == "disable":
self.disable()
if self.diff or self.check_mode or self.parameters.debug:
if self.diff:
self.diff_result["before"], self.diff_result["after"] = (
self.diff_tracker.get_before_after()
)
self.diff = self.diff_result
if self.diff:
self.diff_result["before"], self.diff_result["after"] = (
self.diff_tracker.get_before_after()
)
def get_existing_plugin(self) -> dict[str, t.Any] | None:
try:
@@ -409,7 +407,7 @@ class DockerPluginManager:
result: dict[str, t.Any] = {
"actions": self.actions,
"changed": self.changed,
"diff": self.diff,
"diff": self.diff_result,
"plugin": plugin_data,
}
if (
+15 -16
View File
@@ -190,6 +190,7 @@ secret_name:
import base64
import hashlib
import traceback
import typing as t
try:
@@ -212,9 +213,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class SecretManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
super().__init__()
self.client = client
@@ -244,10 +243,10 @@ class SecretManager(DockerBaseClass):
if self.rolling_versions:
self.version = 0
self.data_key = None
self.secrets = []
self.data_key: str | None = None
self.secrets: list[dict[str, t.Any]] = []
def __call__(self):
def __call__(self) -> None:
self.get_secret()
if self.state == "present":
self.data_key = hashlib.sha224(self.data).hexdigest()
@@ -256,7 +255,7 @@ class SecretManager(DockerBaseClass):
elif self.state == "absent":
self.absent()
def get_version(self, secret):
def get_version(self, secret: dict[str, t.Any]) -> int:
try:
return int(
secret.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
@@ -264,14 +263,14 @@ class SecretManager(DockerBaseClass):
except ValueError:
return 0
def remove_old_versions(self):
def remove_old_versions(self) -> None:
if not self.rolling_versions or self.versions_to_keep < 0:
return
if not self.check_mode:
while len(self.secrets) > max(self.versions_to_keep, 1):
self.remove_secret(self.secrets.pop(0))
def get_secret(self):
def get_secret(self) -> None:
"""Find an existing secret."""
try:
secrets = self.client.secrets(filters={"name": self.name})
@@ -290,9 +289,9 @@ class SecretManager(DockerBaseClass):
secret for secret in secrets if secret["Spec"]["Name"] == self.name
]
def create_secret(self):
def create_secret(self) -> str | None:
"""Create a new secret"""
secret_id = None
secret_id: str | dict[str, t.Any] | None = None
# We cannot see the data after creation, so adding a label we can use for idempotency check
labels = {"ansible_key": self.data_key}
if self.rolling_versions:
@@ -312,18 +311,18 @@ class SecretManager(DockerBaseClass):
self.client.fail(f"Error creating secret: {exc}")
if isinstance(secret_id, dict):
secret_id = secret_id["ID"]
return secret_id["ID"]
return secret_id
def remove_secret(self, secret):
def remove_secret(self, secret: dict[str, t.Any]) -> None:
try:
if not self.check_mode:
self.client.remove_secret(secret["ID"])
except APIError as exc:
self.client.fail(f"Error removing secret {secret['Spec']['Name']}: {exc}")
def present(self):
def present(self) -> None:
"""Handles state == 'present', creating or updating the secret"""
if self.secrets:
secret = self.secrets[-1]
@@ -357,7 +356,7 @@ class SecretManager(DockerBaseClass):
self.results["secret_id"] = self.create_secret()
self.results["secret_name"] = self.name
def absent(self):
def absent(self) -> None:
"""Handles state == 'absent', removing the secret"""
if self.secrets:
for secret in self.secrets:
@@ -365,7 +364,7 @@ class SecretManager(DockerBaseClass):
self.results["changed"] = True
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"state": {
+23 -9
View File
@@ -158,6 +158,7 @@ import json
import os
import tempfile
import traceback
import typing as t
from time import sleep
from ansible.module_utils.common.text.converters import to_native
@@ -183,7 +184,9 @@ except ImportError:
HAS_YAML = False
def docker_stack_services(client, stack_name):
def docker_stack_services(
client: AnsibleModuleDockerClient, stack_name: str
) -> list[str]:
dummy_rc, out, err = client.call_cli(
"stack", "services", stack_name, "--format", "{{.Name}}"
)
@@ -192,7 +195,9 @@ def docker_stack_services(client, stack_name):
return to_native(out).strip().split("\n")
def docker_service_inspect(client, service_name):
def docker_service_inspect(
client: AnsibleModuleDockerClient, service_name: str
) -> dict[str, t.Any] | None:
rc, out, dummy_err = client.call_cli("service", "inspect", service_name)
if rc != 0:
return None
@@ -200,7 +205,9 @@ def docker_service_inspect(client, service_name):
return ret
def docker_stack_deploy(client, stack_name, compose_files):
def docker_stack_deploy(
client: AnsibleModuleDockerClient, stack_name: str, compose_files: list[str]
) -> tuple[int, str, str]:
command = ["stack", "deploy"]
if client.module.params["prune"]:
command += ["--prune"]
@@ -217,14 +224,21 @@ def docker_stack_deploy(client, stack_name, compose_files):
return rc, to_native(out), to_native(err)
def docker_stack_inspect(client, stack_name):
ret = {}
def docker_stack_inspect(
client: AnsibleModuleDockerClient, stack_name: str
) -> dict[str, dict[str, t.Any] | None]:
ret: dict[str, dict[str, t.Any] | None] = {}
for service_name in docker_stack_services(client, stack_name):
ret[service_name] = docker_service_inspect(client, service_name)
return ret
def docker_stack_rm(client, stack_name, retries, interval):
def docker_stack_rm(
client: AnsibleModuleDockerClient,
stack_name: str,
retries: int,
interval: int | float,
) -> tuple[int, str, str]:
command = ["stack", "rm", stack_name]
if not client.module.params["detach"]:
command += ["--detach=false"]
@@ -237,7 +251,7 @@ def docker_stack_rm(client, stack_name, retries, interval):
return rc, to_native(out), to_native(err)
def main():
def main() -> None:
client = AnsibleModuleDockerClient(
argument_spec={
"name": {"type": "str", "required": True},
@@ -258,10 +272,10 @@ def main():
)
if not HAS_JSONDIFF:
return client.fail("jsondiff is not installed, try 'pip install jsondiff'")
client.fail("jsondiff is not installed, try 'pip install jsondiff'")
if not HAS_YAML:
return client.fail("yaml is not installed, try 'pip install pyyaml'")
client.fail("yaml is not installed, try 'pip install pyyaml'")
try:
state = client.module.params["state"]
+1 -10
View File
@@ -85,16 +85,7 @@ from ansible_collections.community.docker.plugins.module_utils._common_cli impor
)
def docker_stack_list(module):
docker_bin = module.get_bin_path("docker", required=True)
rc, out, err = module.run_command(
[docker_bin, "stack", "ls", "--format={{json .}}"]
)
return rc, out.strip(), err.strip()
def main():
def main() -> None:
client = AnsibleModuleDockerClient(
argument_spec={},
supports_check_mode=True,
+1 -10
View File
@@ -93,16 +93,7 @@ from ansible_collections.community.docker.plugins.module_utils._common_cli impor
)
def docker_stack_task(module, stack_name):
docker_bin = module.get_bin_path("docker", required=True)
rc, out, err = module.run_command(
[docker_bin, "stack", "ps", stack_name, "--format={{json .}}"]
)
return rc, out.strip(), err.strip()
def main():
def main() -> None:
client = AnsibleModuleDockerClient(
argument_spec={"name": {"type": "str", "required": True}},
supports_check_mode=True,
+59 -51
View File
@@ -292,6 +292,7 @@ actions:
import json
import traceback
import typing as t
try:
@@ -314,40 +315,40 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass):
def __init__(self):
def __init__(self) -> None:
super().__init__()
self.advertise_addr = None
self.listen_addr = None
self.remote_addrs = None
self.join_token = None
self.data_path_addr = None
self.data_path_port = None
self.advertise_addr: str | None = None
self.listen_addr: str | None = None
self.remote_addrs: list[str] | None = None
self.join_token: str | None = None
self.data_path_addr: str | None = None
self.data_path_port: int | None = None
self.spec = None
# Spec
self.snapshot_interval = None
self.task_history_retention_limit = None
self.keep_old_snapshots = None
self.log_entries_for_slow_followers = None
self.heartbeat_tick = None
self.election_tick = None
self.dispatcher_heartbeat_period = None
self.node_cert_expiry = None
self.name = None
self.labels = None
self.snapshot_interval: int | None = None
self.task_history_retention_limit: int | None = None
self.keep_old_snapshots: int | None = None
self.log_entries_for_slow_followers: int | None = None
self.heartbeat_tick: int | None = None
self.election_tick: int | None = None
self.dispatcher_heartbeat_period: int | None = None
self.node_cert_expiry: int | None = None
self.name: str | None = None
self.labels: dict[str, t.Any] | None = None
self.log_driver = None
self.signing_ca_cert = None
self.signing_ca_key = None
self.ca_force_rotate = None
self.autolock_managers = None
self.rotate_worker_token = None
self.rotate_manager_token = None
self.default_addr_pool = None
self.subnet_size = None
self.signing_ca_cert: str | None = None
self.signing_ca_key: str | None = None
self.ca_force_rotate: int | None = None
self.autolock_managers: bool | None = None
self.rotate_worker_token: bool | None = None
self.rotate_manager_token: bool | None = None
self.default_addr_pool: list[str] | None = None
self.subnet_size: int | None = None
@staticmethod
def from_ansible_params(client):
def from_ansible_params(client: AnsibleDockerSwarmClient) -> TaskParameters:
result = TaskParameters()
for key, value in client.module.params.items():
if key in result.__dict__:
@@ -356,7 +357,7 @@ class TaskParameters(DockerBaseClass):
result.update_parameters(client)
return result
def update_from_swarm_info(self, swarm_info):
def update_from_swarm_info(self, swarm_info: dict[str, t.Any]) -> None:
spec = swarm_info["Spec"]
ca_config = spec.get("CAConfig") or {}
@@ -400,7 +401,7 @@ class TaskParameters(DockerBaseClass):
if "LogDriver" in spec["TaskDefaults"]:
self.log_driver = spec["TaskDefaults"]["LogDriver"]
def update_parameters(self, client):
def update_parameters(self, client: AnsibleDockerSwarmClient) -> None:
assign = {
"snapshot_interval": "snapshot_interval",
"task_history_retention_limit": "task_history_retention_limit",
@@ -427,7 +428,12 @@ class TaskParameters(DockerBaseClass):
params[dest] = value
self.spec = client.create_swarm_spec(**params)
def compare_to_active(self, other, client, differences):
def compare_to_active(
self,
other: TaskParameters,
client: AnsibleDockerSwarmClient,
differences: DifferenceTracker,
) -> DifferenceTracker:
for k in self.__dict__:
if k in (
"advertise_addr",
@@ -459,26 +465,28 @@ class TaskParameters(DockerBaseClass):
class SwarmManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(
self, client: AnsibleDockerSwarmClient, results: dict[str, t.Any]
) -> None:
super().__init__()
self.client = client
self.results = results
self.check_mode = self.client.check_mode
self.swarm_info = {}
self.swarm_info: dict[str, t.Any] = {}
self.state = client.module.params["state"]
self.force = client.module.params["force"]
self.node_id = client.module.params["node_id"]
self.state: t.Literal["present", "join", "absent", "remove"] = (
client.module.params["state"]
)
self.force: bool = client.module.params["force"]
self.node_id: str | None = client.module.params["node_id"]
self.differences = DifferenceTracker()
self.parameters = TaskParameters.from_ansible_params(client)
self.created = False
def __call__(self):
def __call__(self) -> None:
choice_map = {
"present": self.init_swarm,
"join": self.join,
@@ -486,14 +494,14 @@ class SwarmManager(DockerBaseClass):
"remove": self.remove,
}
choice_map.get(self.state)()
choice_map[self.state]()
if self.client.module._diff or self.parameters.debug:
diff = {}
diff["before"], diff["after"] = self.differences.get_before_after()
self.results["diff"] = diff
def inspect_swarm(self):
def inspect_swarm(self) -> None:
try:
data = self.client.inspect_swarm()
json_str = json.dumps(data, ensure_ascii=False)
@@ -507,7 +515,7 @@ class SwarmManager(DockerBaseClass):
except APIError:
pass
def get_unlock_key(self):
def get_unlock_key(self) -> dict[str, t.Any]:
default = {"UnlockKey": None}
if not self.has_swarm_lock_changed():
return default
@@ -516,18 +524,18 @@ class SwarmManager(DockerBaseClass):
except APIError:
return default
def has_swarm_lock_changed(self):
return self.parameters.autolock_managers and (
def has_swarm_lock_changed(self) -> bool:
return bool(self.parameters.autolock_managers) and (
self.created or self.differences.has_difference_for("autolock_managers")
)
def init_swarm(self):
def init_swarm(self) -> None:
if not self.force and self.client.check_if_swarm_manager():
self.__update_swarm()
return
if not self.check_mode:
init_arguments = {
init_arguments: dict[str, t.Any] = {
"advertise_addr": self.parameters.advertise_addr,
"listen_addr": self.parameters.listen_addr,
"force_new_cluster": self.force,
@@ -562,7 +570,7 @@ class SwarmManager(DockerBaseClass):
"UnlockKey": self.swarm_info.get("UnlockKey"),
}
def __update_swarm(self):
def __update_swarm(self) -> None:
try:
self.inspect_swarm()
version = self.swarm_info["Version"]["Index"]
@@ -587,13 +595,12 @@ class SwarmManager(DockerBaseClass):
)
except APIError as exc:
self.client.fail(f"Can not update a Swarm Cluster: {exc}")
return
self.inspect_swarm()
self.results["actions"].append("Swarm cluster updated")
self.results["changed"] = True
def join(self):
def join(self) -> None:
if self.client.check_if_swarm_node():
self.results["actions"].append("This node is already part of a swarm.")
return
@@ -614,7 +621,7 @@ class SwarmManager(DockerBaseClass):
self.differences.add("joined", parameter=True, active=False)
self.results["changed"] = True
def leave(self):
def leave(self) -> None:
if not self.client.check_if_swarm_node():
self.results["actions"].append("This node is not part of a swarm.")
return
@@ -627,7 +634,7 @@ class SwarmManager(DockerBaseClass):
self.differences.add("joined", parameter="absent", active="present")
self.results["changed"] = True
def remove(self):
def remove(self) -> None:
if not self.client.check_if_swarm_manager():
self.client.fail("This node is not a manager.")
@@ -655,11 +662,12 @@ class SwarmManager(DockerBaseClass):
self.results["changed"] = True
def _detect_remove_operation(client):
def _detect_remove_operation(client: AnsibleDockerSwarmClient) -> bool:
return client.module.params["state"] == "remove"
def main():
def main() -> None:
# TODO: missing option log_driver?
argument_spec = {
"advertise_addr": {"type": "str"},
"data_path_addr": {"type": "str"},
+23 -13
View File
@@ -186,6 +186,7 @@ tasks:
"""
import traceback
import typing as t
try:
@@ -207,16 +208,20 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class DockerSwarmManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(
self, client: AnsibleDockerSwarmClient, results: dict[str, t.Any]
) -> None:
super().__init__()
self.client = client
self.results = results
self.verbose_output = self.client.module.params["verbose_output"]
listed_objects = ["tasks", "services", "nodes"]
listed_objects: list[t.Literal["nodes", "tasks", "services"]] = [
"tasks",
"services",
"nodes",
]
self.client.fail_task_if_not_swarm_manager()
@@ -235,15 +240,18 @@ class DockerSwarmManager(DockerBaseClass):
if self.client.module.params["unlock_key"]:
self.results["swarm_unlock_key"] = self.get_docker_swarm_unlock_key()
def get_docker_swarm_facts(self):
def get_docker_swarm_facts(self) -> dict[str, t.Any]:
try:
return self.client.inspect_swarm()
except APIError as exc:
self.client.fail(f"Error inspecting docker swarm: {exc}")
def get_docker_items_list(self, docker_object=None, filters=None):
items = None
items_list = []
def get_docker_items_list(
self,
docker_object: t.Literal["nodes", "tasks", "services"],
filters: dict[str, str],
) -> list[dict[str, t.Any]]:
items_list: list[dict[str, t.Any]] = []
try:
if docker_object == "nodes":
@@ -252,6 +260,8 @@ class DockerSwarmManager(DockerBaseClass):
items = self.client.tasks(filters=filters)
elif docker_object == "services":
items = self.client.services(filters=filters)
else:
raise ValueError(f"Invalid docker_object {docker_object}")
except APIError as exc:
self.client.fail(
f"Error inspecting docker swarm for object '{docker_object}': {exc}"
@@ -276,7 +286,7 @@ class DockerSwarmManager(DockerBaseClass):
return items_list
@staticmethod
def get_essential_facts_nodes(item):
def get_essential_facts_nodes(item: dict[str, t.Any]) -> dict[str, t.Any]:
object_essentials = {}
object_essentials["ID"] = item.get("ID")
@@ -298,7 +308,7 @@ class DockerSwarmManager(DockerBaseClass):
return object_essentials
def get_essential_facts_tasks(self, item):
def get_essential_facts_tasks(self, item: dict[str, t.Any]) -> dict[str, t.Any]:
object_essentials = {}
object_essentials["ID"] = item["ID"]
@@ -319,7 +329,7 @@ class DockerSwarmManager(DockerBaseClass):
return object_essentials
@staticmethod
def get_essential_facts_services(item):
def get_essential_facts_services(item: dict[str, t.Any]) -> dict[str, t.Any]:
object_essentials = {}
object_essentials["ID"] = item["ID"]
@@ -343,12 +353,12 @@ class DockerSwarmManager(DockerBaseClass):
return object_essentials
def get_docker_swarm_unlock_key(self):
def get_docker_swarm_unlock_key(self) -> str | None:
unlock_key = self.client.get_unlock_key() or {}
return unlock_key.get("UnlockKey") or None
def main():
def main() -> None:
argument_spec = {
"nodes": {"type": "bool", "default": False},
"nodes_filters": {"type": "dict"},
+180 -127
View File
@@ -853,6 +853,7 @@ EXAMPLES = r"""
import shlex
import time
import traceback
import typing as t
from ansible.module_utils.basic import human_to_bytes
from ansible.module_utils.common.text.converters import to_text
@@ -891,7 +892,9 @@ except ImportError:
pass
def get_docker_environment(env, env_files):
def get_docker_environment(
env: str | dict[str, t.Any] | list[t.Any] | None, env_files: list[str] | None
) -> list[str] | None:
"""
Will return a list of "KEY=VALUE" items. Supplied env variable can
be either a list or a dictionary.
@@ -899,7 +902,7 @@ def get_docker_environment(env, env_files):
If environment files are combined with explicit environment variables,
the explicit environment variables take precedence.
"""
env_dict = {}
env_dict: dict[str, str] = {}
if env_files:
for env_file in env_files:
parsed_env_file = parse_env_file(env_file)
@@ -936,7 +939,21 @@ def get_docker_environment(env, env_files):
return sorted(env_list)
def get_docker_networks(networks, network_ids):
@t.overload
def get_docker_networks(
networks: list[str | dict[str, t.Any]], network_ids: dict[str, str]
) -> list[dict[str, t.Any]]: ...
@t.overload
def get_docker_networks(
networks: list[str | dict[str, t.Any]] | None, network_ids: dict[str, str]
) -> list[dict[str, t.Any]] | None: ...
def get_docker_networks(
networks: list[str | dict[str, t.Any]] | None, network_ids: dict[str, str]
) -> list[dict[str, t.Any]] | None:
"""
Validate a list of network names or a list of network dictionaries.
Network names will be resolved to ids by using the network_ids mapping.
@@ -945,6 +962,7 @@ def get_docker_networks(networks, network_ids):
return None
parsed_networks = []
for network in networks:
parsed_network: dict[str, t.Any]
if isinstance(network, str):
parsed_network = {"name": network}
elif isinstance(network, dict):
@@ -988,7 +1006,7 @@ def get_docker_networks(networks, network_ids):
return parsed_networks or []
def get_nanoseconds_from_raw_option(name, value):
def get_nanoseconds_from_raw_option(name: str, value: t.Any) -> int | None:
if value is None:
return None
if isinstance(value, int):
@@ -1003,12 +1021,14 @@ def get_nanoseconds_from_raw_option(name, value):
)
def get_value(key, values, default=None):
def get_value(key: str, values: dict[str, t.Any], default: t.Any = None) -> t.Any:
value = values.get(key)
return value if value is not None else default
def has_dict_changed(new_dict, old_dict):
def has_dict_changed(
new_dict: dict[str, t.Any] | None, old_dict: dict[str, t.Any] | None
) -> bool:
"""
Check if new_dict has differences compared to old_dict while
ignoring keys in old_dict which are None in new_dict.
@@ -1019,6 +1039,9 @@ def has_dict_changed(new_dict, old_dict):
return True
if not old_dict and new_dict:
return True
if old_dict is None:
# in this case new_dict is empty, only the type checker didn't notice
return False
defined_options = {
option: value for option, value in new_dict.items() if value is not None
}
@@ -1031,12 +1054,17 @@ def has_dict_changed(new_dict, old_dict):
return False
def has_list_changed(new_list, old_list, sort_lists=True, sort_key=None):
def has_list_changed(
new_list: list[t.Any] | None,
old_list: list[t.Any] | None,
sort_lists: bool = True,
sort_key: str | None = None,
) -> bool:
"""
Check two lists have differences. Sort lists by default.
"""
def sort_list(unsorted_list):
def sort_list(unsorted_list: list[t.Any]) -> list[t.Any]:
"""
Sort a given list.
The list may contain dictionaries, so use the sort key to handle them.
@@ -1093,7 +1121,10 @@ def has_list_changed(new_list, old_list, sort_lists=True, sort_key=None):
return False
def have_networks_changed(new_networks, old_networks):
def have_networks_changed(
new_networks: list[dict[str, t.Any]] | None,
old_networks: list[dict[str, t.Any]] | None,
) -> bool:
"""Special case list checking for networks to sort aliases"""
if new_networks is None:
@@ -1123,68 +1154,72 @@ def have_networks_changed(new_networks, old_networks):
class DockerService(DockerBaseClass):
def __init__(self, docker_api_version, docker_py_version):
def __init__(
self, docker_api_version: LooseVersion, docker_py_version: LooseVersion
) -> None:
super().__init__()
self.image = ""
self.command = None
self.args = None
self.endpoint_mode = None
self.dns = None
self.healthcheck = None
self.healthcheck_disabled = None
self.hostname = None
self.hosts = None
self.tty = None
self.dns_search = None
self.dns_options = None
self.env = None
self.force_update = None
self.groups = None
self.log_driver = None
self.log_driver_options = None
self.labels = None
self.container_labels = None
self.sysctls = None
self.limit_cpu = None
self.limit_memory = None
self.reserve_cpu = None
self.reserve_memory = None
self.mode = "replicated"
self.user = None
self.mounts = None
self.configs = None
self.secrets = None
self.constraints = None
self.replicas_max_per_node = None
self.networks = None
self.stop_grace_period = None
self.stop_signal = None
self.publish = None
self.placement_preferences = None
self.replicas = -1
self.image: str | None = ""
self.command: t.Any = None
self.args: list[str] | None = None
self.endpoint_mode: t.Literal["vip", "dnsrr"] | None = None
self.dns: list[str] | None = None
self.healthcheck: dict[str, t.Any] | None = None
self.healthcheck_disabled: bool | None = None
self.hostname: str | None = None
self.hosts: dict[str, t.Any] | None = None
self.tty: bool | None = None
self.dns_search: list[str] | None = None
self.dns_options: list[str] | None = None
self.env: t.Any = None
self.force_update: int | None = None
self.groups: list[str] | None = None
self.log_driver: str | None = None
self.log_driver_options: dict[str, t.Any] | None = None
self.labels: dict[str, t.Any] | None = None
self.container_labels: dict[str, t.Any] | None = None
self.sysctls: dict[str, t.Any] | None = None
self.limit_cpu: float | None = None
self.limit_memory: int | None = None
self.reserve_cpu: float | None = None
self.reserve_memory: int | None = None
self.mode: t.Literal["replicated", "global", "replicated-job"] = "replicated"
self.user: str | None = None
self.mounts: list[dict[str, t.Any]] | None = None
self.configs: list[dict[str, t.Any]] | None = None
self.secrets: list[dict[str, t.Any]] | None = None
self.constraints: list[str] | None = None
self.replicas_max_per_node: int | None = None
self.networks: list[t.Any] | None = None
self.stop_grace_period: int | None = None
self.stop_signal: str | None = None
self.publish: list[dict[str, t.Any]] | None = None
self.placement_preferences: list[dict[str, t.Any]] | None = None
self.replicas: int | None = -1
self.service_id = False
self.service_version = False
self.read_only = None
self.restart_policy = None
self.restart_policy_attempts = None
self.restart_policy_delay = None
self.restart_policy_window = None
self.rollback_config = None
self.update_delay = None
self.update_parallelism = None
self.update_failure_action = None
self.update_monitor = None
self.update_max_failure_ratio = None
self.update_order = None
self.working_dir = None
self.init = None
self.cap_add = None
self.cap_drop = None
self.read_only: bool | None = None
self.restart_policy: t.Literal["none", "on-failure", "any"] | None = None
self.restart_policy_attempts: int | None = None
self.restart_policy_delay: str | None = None
self.restart_policy_window: str | None = None
self.rollback_config: dict[str, t.Any] | None = None
self.update_delay: str | None = None
self.update_parallelism: int | None = None
self.update_failure_action: (
t.Literal["continue", "pause", "rollback"] | None
) = None
self.update_monitor: str | None = None
self.update_max_failure_ratio: float | None = None
self.update_order: str | None = None
self.working_dir: str | None = None
self.init: bool | None = None
self.cap_add: list[str] | None = None
self.cap_drop: list[str] | None = None
self.docker_api_version = docker_api_version
self.docker_py_version = docker_py_version
def get_facts(self):
def get_facts(self) -> dict[str, t.Any]:
return {
"image": self.image,
"mounts": self.mounts,
@@ -1242,19 +1277,21 @@ class DockerService(DockerBaseClass):
}
@property
def can_update_networks(self):
def can_update_networks(self) -> bool:
# Before Docker API 1.29 adding/removing networks was not supported
return self.docker_api_version >= LooseVersion(
"1.29"
) and self.docker_py_version >= LooseVersion("2.7")
@property
def can_use_task_template_networks(self):
def can_use_task_template_networks(self) -> bool:
# In Docker API 1.25 attaching networks to TaskTemplate is preferred over Spec
return self.docker_py_version >= LooseVersion("2.7")
@staticmethod
def get_restart_config_from_ansible_params(params):
def get_restart_config_from_ansible_params(
params: dict[str, t.Any],
) -> dict[str, t.Any]:
restart_config = params["restart_config"] or {}
condition = get_value(
"condition",
@@ -1282,7 +1319,9 @@ class DockerService(DockerBaseClass):
}
@staticmethod
def get_update_config_from_ansible_params(params):
def get_update_config_from_ansible_params(
params: dict[str, t.Any],
) -> dict[str, t.Any]:
update_config = params["update_config"] or {}
parallelism = get_value(
"parallelism",
@@ -1320,7 +1359,9 @@ class DockerService(DockerBaseClass):
}
@staticmethod
def get_rollback_config_from_ansible_params(params):
def get_rollback_config_from_ansible_params(
params: dict[str, t.Any],
) -> dict[str, t.Any] | None:
if params["rollback_config"] is None:
return None
rollback_config = params["rollback_config"] or {}
@@ -1340,7 +1381,7 @@ class DockerService(DockerBaseClass):
}
@staticmethod
def get_logging_from_ansible_params(params):
def get_logging_from_ansible_params(params: dict[str, t.Any]) -> dict[str, t.Any]:
logging_config = params["logging"] or {}
driver = get_value(
"driver",
@@ -1356,7 +1397,7 @@ class DockerService(DockerBaseClass):
}
@staticmethod
def get_limits_from_ansible_params(params):
def get_limits_from_ansible_params(params: dict[str, t.Any]) -> dict[str, t.Any]:
limits = params["limits"] or {}
cpus = get_value(
"cpus",
@@ -1379,7 +1420,9 @@ class DockerService(DockerBaseClass):
}
@staticmethod
def get_reservations_from_ansible_params(params):
def get_reservations_from_ansible_params(
params: dict[str, t.Any],
) -> dict[str, t.Any]:
reservations = params["reservations"] or {}
cpus = get_value(
"cpus",
@@ -1403,7 +1446,7 @@ class DockerService(DockerBaseClass):
}
@staticmethod
def get_placement_from_ansible_params(params):
def get_placement_from_ansible_params(params: dict[str, t.Any]) -> dict[str, t.Any]:
placement = params["placement"] or {}
constraints = get_value("constraints", placement)
@@ -1419,14 +1462,14 @@ class DockerService(DockerBaseClass):
@classmethod
def from_ansible_params(
cls,
ap,
old_service,
image_digest,
secret_ids,
config_ids,
network_ids,
client,
):
ap: dict[str, t.Any],
old_service: DockerService | None,
image_digest: str,
secret_ids: dict[str, str],
config_ids: dict[str, str],
network_ids: dict[str, str],
client: AnsibleDockerClient,
) -> DockerService:
s = DockerService(client.docker_api_version, client.docker_py_version)
s.image = image_digest
s.args = ap["args"]
@@ -1596,7 +1639,7 @@ class DockerService(DockerBaseClass):
return s
def compare(self, os):
def compare(self, os: DockerService) -> tuple[bool, DifferenceTracker, bool, bool]:
differences = DifferenceTracker()
needs_rebuild = False
force_update = False
@@ -1784,7 +1827,7 @@ class DockerService(DockerBaseClass):
differences.add(
"update_order", parameter=self.update_order, active=os.update_order
)
has_image_changed, change = self.has_image_changed(os.image)
has_image_changed, change = self.has_image_changed(os.image or "")
if has_image_changed:
differences.add("image", parameter=self.image, active=change)
if self.user and self.user != os.user:
@@ -1828,7 +1871,7 @@ class DockerService(DockerBaseClass):
force_update,
)
def has_healthcheck_changed(self, old_publish):
def has_healthcheck_changed(self, old_publish: DockerService) -> bool:
if self.healthcheck_disabled is False and self.healthcheck is None:
return False
if self.healthcheck_disabled:
@@ -1838,14 +1881,14 @@ class DockerService(DockerBaseClass):
return False
return self.healthcheck != old_publish.healthcheck
def has_publish_changed(self, old_publish):
def has_publish_changed(self, old_publish: list[dict[str, t.Any]] | None) -> bool:
if self.publish is None:
return False
old_publish = old_publish or []
if len(self.publish) != len(old_publish):
return True
def publish_sorter(item):
def publish_sorter(item: dict[str, t.Any]) -> tuple[int, int, str]:
return (
item.get("published_port") or 0,
item.get("target_port") or 0,
@@ -1869,12 +1912,13 @@ class DockerService(DockerBaseClass):
return True
return False
def has_image_changed(self, old_image):
def has_image_changed(self, old_image: str) -> tuple[bool, str]:
assert self.image is not None
if "@" not in self.image:
old_image = old_image.split("@")[0]
return self.image != old_image, old_image
def build_container_spec(self):
def build_container_spec(self) -> types.ContainerSpec:
mounts = None
if self.mounts is not None:
mounts = []
@@ -1945,7 +1989,7 @@ class DockerService(DockerBaseClass):
secrets.append(types.SecretReference(**secret_args))
dns_config_args = {}
dns_config_args: dict[str, t.Any] = {}
if self.dns is not None:
dns_config_args["nameservers"] = self.dns
if self.dns_search is not None:
@@ -1954,7 +1998,7 @@ class DockerService(DockerBaseClass):
dns_config_args["options"] = self.dns_options
dns_config = types.DNSConfig(**dns_config_args) if dns_config_args else None
container_spec_args = {}
container_spec_args: dict[str, t.Any] = {}
if self.command is not None:
container_spec_args["command"] = self.command
if self.args is not None:
@@ -2004,8 +2048,8 @@ class DockerService(DockerBaseClass):
return types.ContainerSpec(self.image, **container_spec_args)
def build_placement(self):
placement_args = {}
def build_placement(self) -> types.Placement | None:
placement_args: dict[str, t.Any] = {}
if self.constraints is not None:
placement_args["constraints"] = self.constraints
if self.replicas_max_per_node is not None:
@@ -2018,8 +2062,8 @@ class DockerService(DockerBaseClass):
]
return types.Placement(**placement_args) if placement_args else None
def build_update_config(self):
update_config_args = {}
def build_update_config(self) -> types.UpdateConfig | None:
update_config_args: dict[str, t.Any] = {}
if self.update_parallelism is not None:
update_config_args["parallelism"] = self.update_parallelism
if self.update_delay is not None:
@@ -2034,16 +2078,16 @@ class DockerService(DockerBaseClass):
update_config_args["order"] = self.update_order
return types.UpdateConfig(**update_config_args) if update_config_args else None
def build_log_driver(self):
log_driver_args = {}
def build_log_driver(self) -> types.DriverConfig | None:
log_driver_args: dict[str, t.Any] = {}
if self.log_driver is not None:
log_driver_args["name"] = self.log_driver
if self.log_driver_options is not None:
log_driver_args["options"] = self.log_driver_options
return types.DriverConfig(**log_driver_args) if log_driver_args else None
def build_restart_policy(self):
restart_policy_args = {}
def build_restart_policy(self) -> types.RestartPolicy | None:
restart_policy_args: dict[str, t.Any] = {}
if self.restart_policy is not None:
restart_policy_args["condition"] = self.restart_policy
if self.restart_policy_delay is not None:
@@ -2056,7 +2100,7 @@ class DockerService(DockerBaseClass):
types.RestartPolicy(**restart_policy_args) if restart_policy_args else None
)
def build_rollback_config(self):
def build_rollback_config(self) -> types.RollbackConfig | None:
if self.rollback_config is None:
return None
rollback_config_options = [
@@ -2078,8 +2122,8 @@ class DockerService(DockerBaseClass):
else None
)
def build_resources(self):
resources_args = {}
def build_resources(self) -> types.Resources | None:
resources_args: dict[str, t.Any] = {}
if self.limit_cpu is not None:
resources_args["cpu_limit"] = int(self.limit_cpu * 1000000000.0)
if self.limit_memory is not None:
@@ -2090,12 +2134,16 @@ class DockerService(DockerBaseClass):
resources_args["mem_reservation"] = self.reserve_memory
return types.Resources(**resources_args) if resources_args else None
def build_task_template(self, container_spec, placement=None):
def build_task_template(
self,
container_spec: types.ContainerSpec,
placement: types.Placement | None = None,
) -> types.TaskTemplate:
log_driver = self.build_log_driver()
restart_policy = self.build_restart_policy()
resources = self.build_resources()
task_template_args = {}
task_template_args: dict[str, t.Any] = {}
if placement is not None:
task_template_args["placement"] = placement
if log_driver is not None:
@@ -2112,12 +2160,12 @@ class DockerService(DockerBaseClass):
task_template_args["networks"] = networks
return types.TaskTemplate(container_spec=container_spec, **task_template_args)
def build_service_mode(self):
def build_service_mode(self) -> types.ServiceMode:
if self.mode == "global":
self.replicas = None
return types.ServiceMode(self.mode, replicas=self.replicas)
def build_networks(self):
def build_networks(self) -> list[dict[str, t.Any]] | None:
networks = None
if self.networks is not None:
networks = []
@@ -2130,8 +2178,8 @@ class DockerService(DockerBaseClass):
networks.append(docker_network)
return networks
def build_endpoint_spec(self):
endpoint_spec_args = {}
def build_endpoint_spec(self) -> types.EndpointSpec | None:
endpoint_spec_args: dict[str, t.Any] = {}
if self.publish is not None:
ports = []
for port in self.publish:
@@ -2149,7 +2197,7 @@ class DockerService(DockerBaseClass):
endpoint_spec_args["mode"] = self.endpoint_mode
return types.EndpointSpec(**endpoint_spec_args) if endpoint_spec_args else None
def build_docker_service(self):
def build_docker_service(self) -> dict[str, t.Any]:
container_spec = self.build_container_spec()
placement = self.build_placement()
task_template = self.build_task_template(container_spec, placement)
@@ -2159,7 +2207,10 @@ class DockerService(DockerBaseClass):
service_mode = self.build_service_mode()
endpoint_spec = self.build_endpoint_spec()
service = {"task_template": task_template, "mode": service_mode}
service: dict[str, t.Any] = {
"task_template": task_template,
"mode": service_mode,
}
if update_config:
service["update_config"] = update_config
if rollback_config:
@@ -2176,13 +2227,12 @@ class DockerService(DockerBaseClass):
class DockerServiceManager:
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient):
self.client = client
self.retries = 2
self.diff_tracker = None
self.diff_tracker: DifferenceTracker | None = None
def get_service(self, name):
def get_service(self, name: str) -> DockerService | None:
try:
raw_data = self.client.inspect_service(name)
except NotFound:
@@ -2415,7 +2465,9 @@ class DockerServiceManager:
ds.init = task_template_data["ContainerSpec"].get("Init", False)
return ds
def update_service(self, name, old_service, new_service):
def update_service(
self, name: str, old_service: DockerService, new_service: DockerService
) -> None:
service_data = new_service.build_docker_service()
result = self.client.update_service(
old_service.service_id,
@@ -2427,15 +2479,15 @@ class DockerServiceManager:
# (see https://github.com/docker/docker-py/pull/2272)
self.client.report_warnings(result, ["Warning"])
def create_service(self, name, service):
def create_service(self, name: str, service: DockerService) -> None:
service_data = service.build_docker_service()
result = self.client.create_service(name=name, **service_data)
self.client.report_warnings(result, ["Warning"])
def remove_service(self, name):
def remove_service(self, name: str) -> None:
self.client.remove_service(name)
def get_image_digest(self, name, resolve=False):
def get_image_digest(self, name: str, resolve: bool = False) -> str:
if not name or not resolve:
return name
repo, tag = parse_repository_tag(name)
@@ -2446,10 +2498,10 @@ class DockerServiceManager:
digest = distribution_data["Descriptor"]["digest"]
return f"{name}@{digest}"
def get_networks_names_ids(self):
def get_networks_names_ids(self) -> dict[str, str]:
return {network["Name"]: network["Id"] for network in self.client.networks()}
def get_missing_secret_ids(self):
def get_missing_secret_ids(self) -> dict[str, str]:
"""
Resolve missing secret ids by looking them up by name
"""
@@ -2471,7 +2523,7 @@ class DockerServiceManager:
self.client.fail(f'Could not find a secret named "{secret_name}"')
return secrets
def get_missing_config_ids(self):
def get_missing_config_ids(self) -> dict[str, str]:
"""
Resolve missing config ids by looking them up by name
"""
@@ -2493,7 +2545,7 @@ class DockerServiceManager:
self.client.fail(f'Could not find a config named "{config_name}"')
return configs
def run(self):
def run(self) -> tuple[str, bool, bool, list[str], dict[str, t.Any]]:
self.diff_tracker = DifferenceTracker()
module = self.client.module
@@ -2582,7 +2634,7 @@ class DockerServiceManager:
return msg, changed, rebuilt, differences.get_legacy_docker_diffs(), facts
def run_safe(self):
def run_safe(self) -> tuple[str, bool, bool, list[str], dict[str, t.Any]]:
while True:
try:
return self.run()
@@ -2596,20 +2648,20 @@ class DockerServiceManager:
raise
def _detect_publish_mode_usage(client):
def _detect_publish_mode_usage(client: AnsibleDockerClient) -> bool:
for publish_def in client.module.params["publish"] or []:
if publish_def.get("mode"):
return True
return False
def _detect_healthcheck_start_period(client):
def _detect_healthcheck_start_period(client: AnsibleDockerClient) -> bool:
if client.module.params["healthcheck"]:
return client.module.params["healthcheck"]["start_period"] is not None
return False
def _detect_mount_tmpfs_usage(client):
def _detect_mount_tmpfs_usage(client: AnsibleDockerClient) -> bool:
for mount in client.module.params["mounts"] or []:
if mount.get("type") == "tmpfs":
return True
@@ -2620,14 +2672,14 @@ def _detect_mount_tmpfs_usage(client):
return False
def _detect_update_config_failure_action_rollback(client):
def _detect_update_config_failure_action_rollback(client: AnsibleDockerClient) -> bool:
rollback_config_failure_action = (client.module.params["update_config"] or {}).get(
"failure_action"
)
return rollback_config_failure_action == "rollback"
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"image": {"type": "str"},
@@ -2948,6 +3000,7 @@ def main():
"swarm_service": facts,
}
if client.module._diff:
assert dsm.diff_tracker is not None
before, after = dsm.diff_tracker.get_before_after()
results["diff"] = {"before": before, "after": after}
+3 -2
View File
@@ -63,6 +63,7 @@ service:
"""
import traceback
import typing as t
try:
@@ -79,12 +80,12 @@ from ansible_collections.community.docker.plugins.module_utils._swarm import (
)
def get_service_info(client):
def get_service_info(client: AnsibleDockerSwarmClient) -> dict[str, t.Any] | None:
service = client.module.params["name"]
return client.get_service_inspect(service_id=service, skip_missing=True)
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
}