Add typing to Docker Swarm modules.

This commit is contained in:
Felix Fontein 2025-10-24 08:59:10 +02:00
parent 86031cd7f6
commit 13d39a36eb
15 changed files with 306 additions and 246 deletions

View File

@ -28,7 +28,6 @@ from ansible_collections.community.docker.plugins.module_utils._version import (
class AnsibleDockerSwarmClient(AnsibleDockerClient): class AnsibleDockerSwarmClient(AnsibleDockerClient):
def get_swarm_node_id(self) -> str | None: 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 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: def get_node_name_by_id(self, nodeid: str) -> str:
return self.get_node_inspect(nodeid)["Description"]["Hostname"] 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"): if self.docker_py_version < LooseVersion("2.7.0"):
return None return None
return super().get_unlock_key() return super().get_unlock_key()

View File

@ -198,6 +198,7 @@ config_name:
import base64 import base64
import hashlib import hashlib
import traceback import traceback
import typing as t
try: try:
@ -220,9 +221,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ConfigManager(DockerBaseClass): class ConfigManager(DockerBaseClass):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
def __init__(self, client, results):
super().__init__() super().__init__()
self.client = client self.client = client
@ -253,10 +252,10 @@ class ConfigManager(DockerBaseClass):
if self.rolling_versions: if self.rolling_versions:
self.version = 0 self.version = 0
self.data_key = None self.data_key: str | None = None
self.configs = [] self.configs: list[dict[str, t.Any]] = []
def __call__(self): def __call__(self) -> None:
self.get_config() self.get_config()
if self.state == "present": if self.state == "present":
self.data_key = hashlib.sha224(self.data).hexdigest() self.data_key = hashlib.sha224(self.data).hexdigest()
@ -265,7 +264,7 @@ class ConfigManager(DockerBaseClass):
elif self.state == "absent": elif self.state == "absent":
self.absent() self.absent()
def get_version(self, config): def get_version(self, config: dict[str, t.Any]) -> int:
try: try:
return int( return int(
config.get("Spec", {}).get("Labels", {}).get("ansible_version", 0) config.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
@ -273,14 +272,14 @@ class ConfigManager(DockerBaseClass):
except ValueError: except ValueError:
return 0 return 0
def remove_old_versions(self): def remove_old_versions(self) -> None:
if not self.rolling_versions or self.versions_to_keep < 0: if not self.rolling_versions or self.versions_to_keep < 0:
return return
if not self.check_mode: if not self.check_mode:
while len(self.configs) > max(self.versions_to_keep, 1): while len(self.configs) > max(self.versions_to_keep, 1):
self.remove_config(self.configs.pop(0)) self.remove_config(self.configs.pop(0))
def get_config(self): def get_config(self) -> None:
"""Find an existing config.""" """Find an existing config."""
try: try:
configs = self.client.configs(filters={"name": self.name}) 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 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""" """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 # We ca not see the data after creation, so adding a label we can use for idempotency check
labels = {"ansible_key": self.data_key} labels = {"ansible_key": self.data_key}
if self.rolling_versions: if self.rolling_versions:
@ -325,18 +324,18 @@ class ConfigManager(DockerBaseClass):
self.client.fail(f"Error creating config: {exc}") self.client.fail(f"Error creating config: {exc}")
if isinstance(config_id, dict): if isinstance(config_id, dict):
config_id = config_id["ID"] return config_id["ID"]
return config_id return config_id
def remove_config(self, config): def remove_config(self, config: dict[str, t.Any]) -> None:
try: try:
if not self.check_mode: if not self.check_mode:
self.client.remove_config(config["ID"]) self.client.remove_config(config["ID"])
except APIError as exc: except APIError as exc:
self.client.fail(f"Error removing config {config['Spec']['Name']}: {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""" """Handles state == 'present', creating or updating the config"""
if self.configs: if self.configs:
config = self.configs[-1] config = self.configs[-1]
@ -378,7 +377,7 @@ class ConfigManager(DockerBaseClass):
self.results["config_id"] = self.create_config() self.results["config_id"] = self.create_config()
self.results["config_name"] = self.name self.results["config_name"] = self.name
def absent(self): def absent(self) -> None:
"""Handles state == 'absent', removing the config""" """Handles state == 'absent', removing the config"""
if self.configs: if self.configs:
for config in self.configs: for config in self.configs:
@ -386,7 +385,7 @@ class ConfigManager(DockerBaseClass):
self.results["changed"] = True self.results["changed"] = True
def main(): def main() -> None:
argument_spec = { argument_spec = {
"name": {"type": "str", "required": True}, "name": {"type": "str", "required": True},
"state": { "state": {

View File

@ -134,6 +134,7 @@ node:
""" """
import traceback import traceback
import typing as t
try: try:
@ -157,18 +158,19 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass): class TaskParameters(DockerBaseClass):
def __init__(self, client): hostname: str
def __init__(self, client: AnsibleDockerSwarmClient) -> None:
super().__init__() super().__init__()
# Spec # Spec
self.name = None self.labels: dict[str, t.Any] | None = None
self.labels = None self.labels_state: t.Literal["merge", "replace"] = "merge"
self.labels_state = None self.labels_to_remove: list[str] | None = None
self.labels_to_remove = None
# Node # Node
self.availability = None self.availability: t.Literal["active", "pause", "drain"] | None = None
self.role = None self.role: t.Literal["worker", "manager"] | None = None
for key, value in client.module.params.items(): for key, value in client.module.params.items():
setattr(self, key, value) setattr(self, key, value)
@ -177,9 +179,9 @@ class TaskParameters(DockerBaseClass):
class SwarmNodeManager(DockerBaseClass): class SwarmNodeManager(DockerBaseClass):
def __init__(
def __init__(self, client, results): self, client: AnsibleDockerSwarmClient, results: dict[str, t.Any]
) -> None:
super().__init__() super().__init__()
self.client = client self.client = client
@ -192,10 +194,9 @@ class SwarmNodeManager(DockerBaseClass):
self.node_update() 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)): if not (self.client.check_if_swarm_node(node_id=self.parameters.hostname)):
self.client.fail("This node is not part of a swarm.") self.client.fail("This node is not part of a swarm.")
return
if self.client.check_if_swarm_node_is_down(): if self.client.check_if_swarm_node_is_down():
self.client.fail("Can not update the node. The 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}") self.client.fail(f"Failed to get node information for {exc}")
changed = False changed = False
node_spec = { node_spec: dict[str, t.Any] = {
"Availability": self.parameters.availability, "Availability": self.parameters.availability,
"Role": self.parameters.role, "Role": self.parameters.role,
"Labels": self.parameters.labels, "Labels": self.parameters.labels,
@ -277,7 +278,7 @@ class SwarmNodeManager(DockerBaseClass):
self.results["changed"] = changed self.results["changed"] = changed
def main(): def main() -> None:
argument_spec = { argument_spec = {
"hostname": {"type": "str", "required": True}, "hostname": {"type": "str", "required": True},
"labels": {"type": "dict"}, "labels": {"type": "dict"},

View File

@ -87,6 +87,7 @@ nodes:
""" """
import traceback import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._common import ( from ansible_collections.community.docker.plugins.module_utils._common import (
RequestException, RequestException,
@ -103,9 +104,8 @@ except ImportError:
pass pass
def get_node_facts(client): def get_node_facts(client: AnsibleDockerSwarmClient) -> list[dict[str, t.Any]]:
results: list[dict[str, t.Any]] = []
results = []
if client.module.params["self"] is True: if client.module.params["self"] is True:
self_node_id = client.get_swarm_node_id() self_node_id = client.get_swarm_node_id()
@ -114,8 +114,8 @@ def get_node_facts(client):
return results return results
if client.module.params["name"] is None: if client.module.params["name"] is None:
node_info = client.get_all_nodes_inspect() node_info_list = client.get_all_nodes_inspect()
return node_info return node_info_list
nodes = client.module.params["name"] nodes = client.module.params["name"]
if not isinstance(nodes, list): if not isinstance(nodes, list):
@ -130,7 +130,7 @@ def get_node_facts(client):
return results return results
def main(): def main() -> None:
argument_spec = { argument_spec = {
"name": {"type": "list", "elements": "str"}, "name": {"type": "list", "elements": "str"},
"self": {"type": "bool", "default": False}, "self": {"type": "bool", "default": False},

View File

@ -190,6 +190,7 @@ secret_name:
import base64 import base64
import hashlib import hashlib
import traceback import traceback
import typing as t
try: try:
@ -212,9 +213,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class SecretManager(DockerBaseClass): class SecretManager(DockerBaseClass):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
def __init__(self, client, results):
super().__init__() super().__init__()
self.client = client self.client = client
@ -244,10 +243,10 @@ class SecretManager(DockerBaseClass):
if self.rolling_versions: if self.rolling_versions:
self.version = 0 self.version = 0
self.data_key = None self.data_key: str | None = None
self.secrets = [] self.secrets: list[dict[str, t.Any]] = []
def __call__(self): def __call__(self) -> None:
self.get_secret() self.get_secret()
if self.state == "present": if self.state == "present":
self.data_key = hashlib.sha224(self.data).hexdigest() self.data_key = hashlib.sha224(self.data).hexdigest()
@ -256,7 +255,7 @@ class SecretManager(DockerBaseClass):
elif self.state == "absent": elif self.state == "absent":
self.absent() self.absent()
def get_version(self, secret): def get_version(self, secret: dict[str, t.Any]) -> int:
try: try:
return int( return int(
secret.get("Spec", {}).get("Labels", {}).get("ansible_version", 0) secret.get("Spec", {}).get("Labels", {}).get("ansible_version", 0)
@ -264,14 +263,14 @@ class SecretManager(DockerBaseClass):
except ValueError: except ValueError:
return 0 return 0
def remove_old_versions(self): def remove_old_versions(self) -> None:
if not self.rolling_versions or self.versions_to_keep < 0: if not self.rolling_versions or self.versions_to_keep < 0:
return return
if not self.check_mode: if not self.check_mode:
while len(self.secrets) > max(self.versions_to_keep, 1): while len(self.secrets) > max(self.versions_to_keep, 1):
self.remove_secret(self.secrets.pop(0)) self.remove_secret(self.secrets.pop(0))
def get_secret(self): def get_secret(self) -> None:
"""Find an existing secret.""" """Find an existing secret."""
try: try:
secrets = self.client.secrets(filters={"name": self.name}) 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 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""" """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 # We cannot see the data after creation, so adding a label we can use for idempotency check
labels = {"ansible_key": self.data_key} labels = {"ansible_key": self.data_key}
if self.rolling_versions: if self.rolling_versions:
@ -312,18 +311,18 @@ class SecretManager(DockerBaseClass):
self.client.fail(f"Error creating secret: {exc}") self.client.fail(f"Error creating secret: {exc}")
if isinstance(secret_id, dict): if isinstance(secret_id, dict):
secret_id = secret_id["ID"] return secret_id["ID"]
return secret_id return secret_id
def remove_secret(self, secret): def remove_secret(self, secret: dict[str, t.Any]) -> None:
try: try:
if not self.check_mode: if not self.check_mode:
self.client.remove_secret(secret["ID"]) self.client.remove_secret(secret["ID"])
except APIError as exc: except APIError as exc:
self.client.fail(f"Error removing secret {secret['Spec']['Name']}: {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""" """Handles state == 'present', creating or updating the secret"""
if self.secrets: if self.secrets:
secret = self.secrets[-1] secret = self.secrets[-1]
@ -357,7 +356,7 @@ class SecretManager(DockerBaseClass):
self.results["secret_id"] = self.create_secret() self.results["secret_id"] = self.create_secret()
self.results["secret_name"] = self.name self.results["secret_name"] = self.name
def absent(self): def absent(self) -> None:
"""Handles state == 'absent', removing the secret""" """Handles state == 'absent', removing the secret"""
if self.secrets: if self.secrets:
for secret in self.secrets: for secret in self.secrets:
@ -365,7 +364,7 @@ class SecretManager(DockerBaseClass):
self.results["changed"] = True self.results["changed"] = True
def main(): def main() -> None:
argument_spec = { argument_spec = {
"name": {"type": "str", "required": True}, "name": {"type": "str", "required": True},
"state": { "state": {

View File

@ -84,7 +84,6 @@ EXAMPLES = r"""
import json import json
import traceback import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native from ansible.module_utils.common.text.converters import to_native

View File

@ -292,6 +292,7 @@ actions:
import json import json
import traceback import traceback
import typing as t
try: try:
@ -314,40 +315,40 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass): class TaskParameters(DockerBaseClass):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self.advertise_addr = None self.advertise_addr: str | None = None
self.listen_addr = None self.listen_addr: str | None = None
self.remote_addrs = None self.remote_addrs: list[str] | None = None
self.join_token = None self.join_token: str | None = None
self.data_path_addr = None self.data_path_addr: str | None = None
self.data_path_port = None self.data_path_port: int | None = None
self.spec = None self.spec = None
# Spec # Spec
self.snapshot_interval = None self.snapshot_interval: int | None = None
self.task_history_retention_limit = None self.task_history_retention_limit: int | None = None
self.keep_old_snapshots = None self.keep_old_snapshots: int | None = None
self.log_entries_for_slow_followers = None self.log_entries_for_slow_followers: int | None = None
self.heartbeat_tick = None self.heartbeat_tick: int | None = None
self.election_tick = None self.election_tick: int | None = None
self.dispatcher_heartbeat_period = None self.dispatcher_heartbeat_period: int | None = None
self.node_cert_expiry = None self.node_cert_expiry: int | None = None
self.name = None self.name: str | None = None
self.labels = None self.labels: dict[str, t.Any] | None = None
self.log_driver = None self.log_driver = None
self.signing_ca_cert = None self.signing_ca_cert: str | None = None
self.signing_ca_key = None self.signing_ca_key: str | None = None
self.ca_force_rotate = None self.ca_force_rotate: int | None = None
self.autolock_managers = None self.autolock_managers: bool | None = None
self.rotate_worker_token = None self.rotate_worker_token: bool | None = None
self.rotate_manager_token = None self.rotate_manager_token: bool | None = None
self.default_addr_pool = None self.default_addr_pool: list[str] | None = None
self.subnet_size = None self.subnet_size: int | None = None
@staticmethod @staticmethod
def from_ansible_params(client): def from_ansible_params(client: AnsibleDockerSwarmClient) -> TaskParameters:
result = TaskParameters() result = TaskParameters()
for key, value in client.module.params.items(): for key, value in client.module.params.items():
if key in result.__dict__: if key in result.__dict__:
@ -356,7 +357,7 @@ class TaskParameters(DockerBaseClass):
result.update_parameters(client) result.update_parameters(client)
return result 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"] spec = swarm_info["Spec"]
ca_config = spec.get("CAConfig") or {} ca_config = spec.get("CAConfig") or {}
@ -400,7 +401,7 @@ class TaskParameters(DockerBaseClass):
if "LogDriver" in spec["TaskDefaults"]: if "LogDriver" in spec["TaskDefaults"]:
self.log_driver = spec["TaskDefaults"]["LogDriver"] self.log_driver = spec["TaskDefaults"]["LogDriver"]
def update_parameters(self, client): def update_parameters(self, client: AnsibleDockerSwarmClient) -> None:
assign = { assign = {
"snapshot_interval": "snapshot_interval", "snapshot_interval": "snapshot_interval",
"task_history_retention_limit": "task_history_retention_limit", "task_history_retention_limit": "task_history_retention_limit",
@ -427,7 +428,12 @@ class TaskParameters(DockerBaseClass):
params[dest] = value params[dest] = value
self.spec = client.create_swarm_spec(**params) 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__: for k in self.__dict__:
if k in ( if k in (
"advertise_addr", "advertise_addr",
@ -459,26 +465,28 @@ class TaskParameters(DockerBaseClass):
class SwarmManager(DockerBaseClass): class SwarmManager(DockerBaseClass):
def __init__(
def __init__(self, client, results): self, client: AnsibleDockerSwarmClient, results: dict[str, t.Any]
) -> None:
super().__init__() super().__init__()
self.client = client self.client = client
self.results = results self.results = results
self.check_mode = self.client.check_mode self.check_mode = self.client.check_mode
self.swarm_info = {} self.swarm_info: dict[str, t.Any] = {}
self.state = client.module.params["state"] self.state: t.Literal["present", "join", "absent", "remove"] = (
self.force = client.module.params["force"] client.module.params["state"]
self.node_id = client.module.params["node_id"] )
self.force: bool = client.module.params["force"]
self.node_id: str | None = client.module.params["node_id"]
self.differences = DifferenceTracker() self.differences = DifferenceTracker()
self.parameters = TaskParameters.from_ansible_params(client) self.parameters = TaskParameters.from_ansible_params(client)
self.created = False self.created = False
def __call__(self): def __call__(self) -> None:
choice_map = { choice_map = {
"present": self.init_swarm, "present": self.init_swarm,
"join": self.join, "join": self.join,
@ -486,14 +494,14 @@ class SwarmManager(DockerBaseClass):
"remove": self.remove, "remove": self.remove,
} }
choice_map.get(self.state)() choice_map[self.state]()
if self.client.module._diff or self.parameters.debug: if self.client.module._diff or self.parameters.debug:
diff = {} diff = {}
diff["before"], diff["after"] = self.differences.get_before_after() diff["before"], diff["after"] = self.differences.get_before_after()
self.results["diff"] = diff self.results["diff"] = diff
def inspect_swarm(self): def inspect_swarm(self) -> None:
try: try:
data = self.client.inspect_swarm() data = self.client.inspect_swarm()
json_str = json.dumps(data, ensure_ascii=False) json_str = json.dumps(data, ensure_ascii=False)
@ -507,7 +515,7 @@ class SwarmManager(DockerBaseClass):
except APIError: except APIError:
pass pass
def get_unlock_key(self): def get_unlock_key(self) -> dict[str, t.Any]:
default = {"UnlockKey": None} default = {"UnlockKey": None}
if not self.has_swarm_lock_changed(): if not self.has_swarm_lock_changed():
return default return default
@ -516,18 +524,18 @@ class SwarmManager(DockerBaseClass):
except APIError: except APIError:
return default return default
def has_swarm_lock_changed(self): def has_swarm_lock_changed(self) -> bool:
return self.parameters.autolock_managers and ( return bool(self.parameters.autolock_managers) and (
self.created or self.differences.has_difference_for("autolock_managers") 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(): if not self.force and self.client.check_if_swarm_manager():
self.__update_swarm() self.__update_swarm()
return return
if not self.check_mode: if not self.check_mode:
init_arguments = { init_arguments: dict[str, t.Any] = {
"advertise_addr": self.parameters.advertise_addr, "advertise_addr": self.parameters.advertise_addr,
"listen_addr": self.parameters.listen_addr, "listen_addr": self.parameters.listen_addr,
"force_new_cluster": self.force, "force_new_cluster": self.force,
@ -562,7 +570,7 @@ class SwarmManager(DockerBaseClass):
"UnlockKey": self.swarm_info.get("UnlockKey"), "UnlockKey": self.swarm_info.get("UnlockKey"),
} }
def __update_swarm(self): def __update_swarm(self) -> None:
try: try:
self.inspect_swarm() self.inspect_swarm()
version = self.swarm_info["Version"]["Index"] version = self.swarm_info["Version"]["Index"]
@ -587,13 +595,12 @@ class SwarmManager(DockerBaseClass):
) )
except APIError as exc: except APIError as exc:
self.client.fail(f"Can not update a Swarm Cluster: {exc}") self.client.fail(f"Can not update a Swarm Cluster: {exc}")
return
self.inspect_swarm() self.inspect_swarm()
self.results["actions"].append("Swarm cluster updated") self.results["actions"].append("Swarm cluster updated")
self.results["changed"] = True self.results["changed"] = True
def join(self): def join(self) -> None:
if self.client.check_if_swarm_node(): if self.client.check_if_swarm_node():
self.results["actions"].append("This node is already part of a swarm.") self.results["actions"].append("This node is already part of a swarm.")
return return
@ -614,7 +621,7 @@ class SwarmManager(DockerBaseClass):
self.differences.add("joined", parameter=True, active=False) self.differences.add("joined", parameter=True, active=False)
self.results["changed"] = True self.results["changed"] = True
def leave(self): def leave(self) -> None:
if not self.client.check_if_swarm_node(): if not self.client.check_if_swarm_node():
self.results["actions"].append("This node is not part of a swarm.") self.results["actions"].append("This node is not part of a swarm.")
return return
@ -627,7 +634,7 @@ class SwarmManager(DockerBaseClass):
self.differences.add("joined", parameter="absent", active="present") self.differences.add("joined", parameter="absent", active="present")
self.results["changed"] = True self.results["changed"] = True
def remove(self): def remove(self) -> None:
if not self.client.check_if_swarm_manager(): if not self.client.check_if_swarm_manager():
self.client.fail("This node is not a manager.") self.client.fail("This node is not a manager.")
@ -655,11 +662,12 @@ class SwarmManager(DockerBaseClass):
self.results["changed"] = True self.results["changed"] = True
def _detect_remove_operation(client): def _detect_remove_operation(client: AnsibleDockerSwarmClient) -> bool:
return client.module.params["state"] == "remove" return client.module.params["state"] == "remove"
def main(): def main() -> None:
# TODO: missing option log_driver?
argument_spec = { argument_spec = {
"advertise_addr": {"type": "str"}, "advertise_addr": {"type": "str"},
"data_path_addr": {"type": "str"}, "data_path_addr": {"type": "str"},

View File

@ -186,6 +186,7 @@ tasks:
""" """
import traceback import traceback
import typing as t
try: try:
@ -207,16 +208,20 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class DockerSwarmManager(DockerBaseClass): class DockerSwarmManager(DockerBaseClass):
def __init__(
def __init__(self, client, results): self, client: AnsibleDockerSwarmClient, results: dict[str, t.Any]
) -> None:
super().__init__() super().__init__()
self.client = client self.client = client
self.results = results self.results = results
self.verbose_output = self.client.module.params["verbose_output"] 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() self.client.fail_task_if_not_swarm_manager()
@ -235,15 +240,16 @@ class DockerSwarmManager(DockerBaseClass):
if self.client.module.params["unlock_key"]: if self.client.module.params["unlock_key"]:
self.results["swarm_unlock_key"] = self.get_docker_swarm_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: try:
return self.client.inspect_swarm() return self.client.inspect_swarm()
except APIError as exc: except APIError as exc:
self.client.fail(f"Error inspecting docker swarm: {exc}") self.client.fail(f"Error inspecting docker swarm: {exc}")
def get_docker_items_list(self, docker_object=None, filters=None): def get_docker_items_list(
items = None self, docker_object: t.Literal["nodes", "tasks", "services"], filters=None
items_list = [] ) -> list[dict[str, t.Any]]:
items_list: list[dict[str, t.Any]] = []
try: try:
if docker_object == "nodes": if docker_object == "nodes":
@ -252,6 +258,8 @@ class DockerSwarmManager(DockerBaseClass):
items = self.client.tasks(filters=filters) items = self.client.tasks(filters=filters)
elif docker_object == "services": elif docker_object == "services":
items = self.client.services(filters=filters) items = self.client.services(filters=filters)
else:
raise ValueError(f"Invalid docker_object {docker_object}")
except APIError as exc: except APIError as exc:
self.client.fail( self.client.fail(
f"Error inspecting docker swarm for object '{docker_object}': {exc}" f"Error inspecting docker swarm for object '{docker_object}': {exc}"
@ -276,7 +284,7 @@ class DockerSwarmManager(DockerBaseClass):
return items_list return items_list
@staticmethod @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 = {}
object_essentials["ID"] = item.get("ID") object_essentials["ID"] = item.get("ID")
@ -298,7 +306,7 @@ class DockerSwarmManager(DockerBaseClass):
return object_essentials 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 = {}
object_essentials["ID"] = item["ID"] object_essentials["ID"] = item["ID"]
@ -319,7 +327,7 @@ class DockerSwarmManager(DockerBaseClass):
return object_essentials return object_essentials
@staticmethod @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 = {}
object_essentials["ID"] = item["ID"] object_essentials["ID"] = item["ID"]
@ -343,12 +351,12 @@ class DockerSwarmManager(DockerBaseClass):
return object_essentials 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 {} unlock_key = self.client.get_unlock_key() or {}
return unlock_key.get("UnlockKey") or None return unlock_key.get("UnlockKey") or None
def main(): def main() -> None:
argument_spec = { argument_spec = {
"nodes": {"type": "bool", "default": False}, "nodes": {"type": "bool", "default": False},
"nodes_filters": {"type": "dict"}, "nodes_filters": {"type": "dict"},

View File

@ -853,6 +853,7 @@ EXAMPLES = r"""
import shlex import shlex
import time import time
import traceback import traceback
import typing as t
from ansible.module_utils.basic import human_to_bytes from ansible.module_utils.basic import human_to_bytes
from ansible.module_utils.common.text.converters import to_text from ansible.module_utils.common.text.converters import to_text
@ -891,7 +892,9 @@ except ImportError:
pass 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 Will return a list of "KEY=VALUE" items. Supplied env variable can
be either a list or a dictionary. 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, If environment files are combined with explicit environment variables,
the explicit environment variables take precedence. the explicit environment variables take precedence.
""" """
env_dict = {} env_dict: dict[str, str] = {}
if env_files: if env_files:
for env_file in env_files: for env_file in env_files:
parsed_env_file = parse_env_file(env_file) parsed_env_file = parse_env_file(env_file)
@ -936,7 +939,9 @@ def get_docker_environment(env, env_files):
return sorted(env_list) return sorted(env_list)
def get_docker_networks(networks, network_ids): 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. 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. Network names will be resolved to ids by using the network_ids mapping.
@ -945,6 +950,7 @@ def get_docker_networks(networks, network_ids):
return None return None
parsed_networks = [] parsed_networks = []
for network in networks: for network in networks:
parsed_network: dict[str, t.Any]
if isinstance(network, str): if isinstance(network, str):
parsed_network = {"name": network} parsed_network = {"name": network}
elif isinstance(network, dict): elif isinstance(network, dict):
@ -988,7 +994,7 @@ def get_docker_networks(networks, network_ids):
return parsed_networks or [] 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: if value is None:
return None return None
if isinstance(value, int): if isinstance(value, int):
@ -1003,12 +1009,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) value = values.get(key)
return value if value is not None else default 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 Check if new_dict has differences compared to old_dict while
ignoring keys in old_dict which are None in new_dict. ignoring keys in old_dict which are None in new_dict.
@ -1019,6 +1027,9 @@ def has_dict_changed(new_dict, old_dict):
return True return True
if not old_dict and new_dict: if not old_dict and new_dict:
return True 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 = { defined_options = {
option: value for option, value in new_dict.items() if value is not None option: value for option, value in new_dict.items() if value is not None
} }
@ -1031,12 +1042,17 @@ def has_dict_changed(new_dict, old_dict):
return False 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. 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. Sort a given list.
The list may contain dictionaries, so use the sort key to handle them. The list may contain dictionaries, so use the sort key to handle them.
@ -1093,7 +1109,10 @@ def has_list_changed(new_list, old_list, sort_lists=True, sort_key=None):
return False 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""" """Special case list checking for networks to sort aliases"""
if new_networks is None: if new_networks is None:
@ -1123,68 +1142,72 @@ def have_networks_changed(new_networks, old_networks):
class DockerService(DockerBaseClass): 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__() super().__init__()
self.image = "" self.image: str | None = ""
self.command = None self.command: t.Any = None
self.args = None self.args: list[str] | None = None
self.endpoint_mode = None self.endpoint_mode: t.Literal["vip", "dnsrr"] | None = None
self.dns = None self.dns: list[str] | None = None
self.healthcheck = None self.healthcheck: dict[str, t.Any] | None = None
self.healthcheck_disabled = None self.healthcheck_disabled: bool | None = None
self.hostname = None self.hostname: str | None = None
self.hosts = None self.hosts: dict[str, t.Any] | None = None
self.tty = None self.tty: bool | None = None
self.dns_search = None self.dns_search: list[str] | None = None
self.dns_options = None self.dns_options: list[str] | None = None
self.env = None self.env: t.Any = None
self.force_update = None self.force_update: int | None = None
self.groups = None self.groups: list[str] | None = None
self.log_driver = None self.log_driver: str | None = None
self.log_driver_options = None self.log_driver_options: dict[str, t.Any] | None = None
self.labels = None self.labels: dict[str, t.Any] | None = None
self.container_labels = None self.container_labels: dict[str, t.Any] | None = None
self.sysctls = None self.sysctls: dict[str, t.Any] | None = None
self.limit_cpu = None self.limit_cpu: float | None = None
self.limit_memory = None self.limit_memory: int | None = None
self.reserve_cpu = None self.reserve_cpu: float | None = None
self.reserve_memory = None self.reserve_memory: int | None = None
self.mode = "replicated" self.mode: t.Literal["replicated", "global", "replicated-job"] = "replicated"
self.user = None self.user: str | None = None
self.mounts = None self.mounts: list[dict[str, t.Any]] | None = None
self.configs = None self.configs: list[dict[str, t.Any]] | None = None
self.secrets = None self.secrets: list[dict[str, t.Any]] | None = None
self.constraints = None self.constraints: list[str] | None = None
self.replicas_max_per_node = None self.replicas_max_per_node: int | None = None
self.networks = None self.networks: list[t.Any] | None = None
self.stop_grace_period = None self.stop_grace_period: int | None = None
self.stop_signal = None self.stop_signal: str | None = None
self.publish = None self.publish: list[dict[str, t.Any]] | None = None
self.placement_preferences = None self.placement_preferences: list[dict[str, t.Any]] | None = None
self.replicas = -1 self.replicas: int | None = -1
self.service_id = False self.service_id = False
self.service_version = False self.service_version = False
self.read_only = None self.read_only: bool | None = None
self.restart_policy = None self.restart_policy: t.Literal["none", "on-failure", "any"] | None = None
self.restart_policy_attempts = None self.restart_policy_attempts: int | None = None
self.restart_policy_delay = None self.restart_policy_delay: str | None = None
self.restart_policy_window = None self.restart_policy_window: str | None = None
self.rollback_config = None self.rollback_config: dict[str, t.Any] | None = None
self.update_delay = None self.update_delay: str | None = None
self.update_parallelism = None self.update_parallelism: int | None = None
self.update_failure_action = None self.update_failure_action: (
self.update_monitor = None t.Literal["continue", "pause", "rollback"] | None
self.update_max_failure_ratio = None ) = None
self.update_order = None self.update_monitor: str | None = None
self.working_dir = None self.update_max_failure_ratio: float | None = None
self.init = None self.update_order: str | None = None
self.cap_add = None self.working_dir: str | None = None
self.cap_drop = 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_api_version = docker_api_version
self.docker_py_version = docker_py_version self.docker_py_version = docker_py_version
def get_facts(self): def get_facts(self) -> dict[str, t.Any]:
return { return {
"image": self.image, "image": self.image,
"mounts": self.mounts, "mounts": self.mounts,
@ -1242,19 +1265,21 @@ class DockerService(DockerBaseClass):
} }
@property @property
def can_update_networks(self): def can_update_networks(self) -> bool:
# Before Docker API 1.29 adding/removing networks was not supported # Before Docker API 1.29 adding/removing networks was not supported
return self.docker_api_version >= LooseVersion( return self.docker_api_version >= LooseVersion(
"1.29" "1.29"
) and self.docker_py_version >= LooseVersion("2.7") ) and self.docker_py_version >= LooseVersion("2.7")
@property @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 # In Docker API 1.25 attaching networks to TaskTemplate is preferred over Spec
return self.docker_py_version >= LooseVersion("2.7") return self.docker_py_version >= LooseVersion("2.7")
@staticmethod @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 {} restart_config = params["restart_config"] or {}
condition = get_value( condition = get_value(
"condition", "condition",
@ -1282,7 +1307,9 @@ class DockerService(DockerBaseClass):
} }
@staticmethod @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 {} update_config = params["update_config"] or {}
parallelism = get_value( parallelism = get_value(
"parallelism", "parallelism",
@ -1320,7 +1347,9 @@ class DockerService(DockerBaseClass):
} }
@staticmethod @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: if params["rollback_config"] is None:
return None return None
rollback_config = params["rollback_config"] or {} rollback_config = params["rollback_config"] or {}
@ -1340,7 +1369,7 @@ class DockerService(DockerBaseClass):
} }
@staticmethod @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 {} logging_config = params["logging"] or {}
driver = get_value( driver = get_value(
"driver", "driver",
@ -1356,7 +1385,7 @@ class DockerService(DockerBaseClass):
} }
@staticmethod @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 {} limits = params["limits"] or {}
cpus = get_value( cpus = get_value(
"cpus", "cpus",
@ -1379,7 +1408,9 @@ class DockerService(DockerBaseClass):
} }
@staticmethod @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 {} reservations = params["reservations"] or {}
cpus = get_value( cpus = get_value(
"cpus", "cpus",
@ -1403,7 +1434,7 @@ class DockerService(DockerBaseClass):
} }
@staticmethod @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 {} placement = params["placement"] or {}
constraints = get_value("constraints", placement) constraints = get_value("constraints", placement)
@ -1419,14 +1450,14 @@ class DockerService(DockerBaseClass):
@classmethod @classmethod
def from_ansible_params( def from_ansible_params(
cls, cls,
ap, ap: dict[str, t.Any],
old_service, old_service,
image_digest, image_digest,
secret_ids, secret_ids: dict[str, str],
config_ids, config_ids: dict[str, str],
network_ids, network_ids: dict[str, str],
client, client: AnsibleDockerClient,
): ) -> DockerService:
s = DockerService(client.docker_api_version, client.docker_py_version) s = DockerService(client.docker_api_version, client.docker_py_version)
s.image = image_digest s.image = image_digest
s.args = ap["args"] s.args = ap["args"]
@ -1596,7 +1627,7 @@ class DockerService(DockerBaseClass):
return s return s
def compare(self, os): def compare(self, os: DockerService) -> tuple[bool, DifferenceTracker, bool, bool]:
differences = DifferenceTracker() differences = DifferenceTracker()
needs_rebuild = False needs_rebuild = False
force_update = False force_update = False
@ -1784,7 +1815,7 @@ class DockerService(DockerBaseClass):
differences.add( differences.add(
"update_order", parameter=self.update_order, active=os.update_order "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: if has_image_changed:
differences.add("image", parameter=self.image, active=change) differences.add("image", parameter=self.image, active=change)
if self.user and self.user != os.user: if self.user and self.user != os.user:
@ -1828,7 +1859,7 @@ class DockerService(DockerBaseClass):
force_update, 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: if self.healthcheck_disabled is False and self.healthcheck is None:
return False return False
if self.healthcheck_disabled: if self.healthcheck_disabled:
@ -1838,14 +1869,14 @@ class DockerService(DockerBaseClass):
return False return False
return self.healthcheck != old_publish.healthcheck 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: if self.publish is None:
return False return False
old_publish = old_publish or [] old_publish = old_publish or []
if len(self.publish) != len(old_publish): if len(self.publish) != len(old_publish):
return True return True
def publish_sorter(item): def publish_sorter(item: dict[str, t.Any]) -> tuple[int, int, str]:
return ( return (
item.get("published_port") or 0, item.get("published_port") or 0,
item.get("target_port") or 0, item.get("target_port") or 0,
@ -1869,12 +1900,13 @@ class DockerService(DockerBaseClass):
return True return True
return False 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: if "@" not in self.image:
old_image = old_image.split("@")[0] old_image = old_image.split("@")[0]
return self.image != old_image, old_image return self.image != old_image, old_image
def build_container_spec(self): def build_container_spec(self) -> types.ContainerSpec:
mounts = None mounts = None
if self.mounts is not None: if self.mounts is not None:
mounts = [] mounts = []
@ -1945,7 +1977,7 @@ class DockerService(DockerBaseClass):
secrets.append(types.SecretReference(**secret_args)) secrets.append(types.SecretReference(**secret_args))
dns_config_args = {} dns_config_args: dict[str, t.Any] = {}
if self.dns is not None: if self.dns is not None:
dns_config_args["nameservers"] = self.dns dns_config_args["nameservers"] = self.dns
if self.dns_search is not None: if self.dns_search is not None:
@ -1954,7 +1986,7 @@ class DockerService(DockerBaseClass):
dns_config_args["options"] = self.dns_options dns_config_args["options"] = self.dns_options
dns_config = types.DNSConfig(**dns_config_args) if dns_config_args else None 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: if self.command is not None:
container_spec_args["command"] = self.command container_spec_args["command"] = self.command
if self.args is not None: if self.args is not None:
@ -2004,8 +2036,8 @@ class DockerService(DockerBaseClass):
return types.ContainerSpec(self.image, **container_spec_args) return types.ContainerSpec(self.image, **container_spec_args)
def build_placement(self): def build_placement(self) -> types.Placement | None:
placement_args = {} placement_args: dict[str, t.Any] = {}
if self.constraints is not None: if self.constraints is not None:
placement_args["constraints"] = self.constraints placement_args["constraints"] = self.constraints
if self.replicas_max_per_node is not None: if self.replicas_max_per_node is not None:
@ -2018,8 +2050,8 @@ class DockerService(DockerBaseClass):
] ]
return types.Placement(**placement_args) if placement_args else None return types.Placement(**placement_args) if placement_args else None
def build_update_config(self): def build_update_config(self) -> types.UpdateConfig | None:
update_config_args = {} update_config_args: dict[str, t.Any] = {}
if self.update_parallelism is not None: if self.update_parallelism is not None:
update_config_args["parallelism"] = self.update_parallelism update_config_args["parallelism"] = self.update_parallelism
if self.update_delay is not None: if self.update_delay is not None:
@ -2034,16 +2066,16 @@ class DockerService(DockerBaseClass):
update_config_args["order"] = self.update_order update_config_args["order"] = self.update_order
return types.UpdateConfig(**update_config_args) if update_config_args else None return types.UpdateConfig(**update_config_args) if update_config_args else None
def build_log_driver(self): def build_log_driver(self) -> types.DriverConfig | None:
log_driver_args = {} log_driver_args: dict[str, t.Any] = {}
if self.log_driver is not None: if self.log_driver is not None:
log_driver_args["name"] = self.log_driver log_driver_args["name"] = self.log_driver
if self.log_driver_options is not None: if self.log_driver_options is not None:
log_driver_args["options"] = self.log_driver_options log_driver_args["options"] = self.log_driver_options
return types.DriverConfig(**log_driver_args) if log_driver_args else None return types.DriverConfig(**log_driver_args) if log_driver_args else None
def build_restart_policy(self): def build_restart_policy(self) -> types.RestartPolicy | None:
restart_policy_args = {} restart_policy_args: dict[str, t.Any] = {}
if self.restart_policy is not None: if self.restart_policy is not None:
restart_policy_args["condition"] = self.restart_policy restart_policy_args["condition"] = self.restart_policy
if self.restart_policy_delay is not None: if self.restart_policy_delay is not None:
@ -2056,7 +2088,7 @@ class DockerService(DockerBaseClass):
types.RestartPolicy(**restart_policy_args) if restart_policy_args else None 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: if self.rollback_config is None:
return None return None
rollback_config_options = [ rollback_config_options = [
@ -2078,8 +2110,8 @@ class DockerService(DockerBaseClass):
else None else None
) )
def build_resources(self): def build_resources(self) -> types.Resources | None:
resources_args = {} resources_args: dict[str, t.Any] = {}
if self.limit_cpu is not None: if self.limit_cpu is not None:
resources_args["cpu_limit"] = int(self.limit_cpu * 1000000000.0) resources_args["cpu_limit"] = int(self.limit_cpu * 1000000000.0)
if self.limit_memory is not None: if self.limit_memory is not None:
@ -2090,12 +2122,16 @@ class DockerService(DockerBaseClass):
resources_args["mem_reservation"] = self.reserve_memory resources_args["mem_reservation"] = self.reserve_memory
return types.Resources(**resources_args) if resources_args else None 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() log_driver = self.build_log_driver()
restart_policy = self.build_restart_policy() restart_policy = self.build_restart_policy()
resources = self.build_resources() resources = self.build_resources()
task_template_args = {} task_template_args: dict[str, t.Any] = {}
if placement is not None: if placement is not None:
task_template_args["placement"] = placement task_template_args["placement"] = placement
if log_driver is not None: if log_driver is not None:
@ -2112,12 +2148,12 @@ class DockerService(DockerBaseClass):
task_template_args["networks"] = networks task_template_args["networks"] = networks
return types.TaskTemplate(container_spec=container_spec, **task_template_args) 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": if self.mode == "global":
self.replicas = None self.replicas = None
return types.ServiceMode(self.mode, replicas=self.replicas) return types.ServiceMode(self.mode, replicas=self.replicas)
def build_networks(self): def build_networks(self) -> list[dict[str, t.Any]] | None:
networks = None networks = None
if self.networks is not None: if self.networks is not None:
networks = [] networks = []
@ -2130,8 +2166,8 @@ class DockerService(DockerBaseClass):
networks.append(docker_network) networks.append(docker_network)
return networks return networks
def build_endpoint_spec(self): def build_endpoint_spec(self) -> types.EndpointSpec | None:
endpoint_spec_args = {} endpoint_spec_args: dict[str, t.Any] = {}
if self.publish is not None: if self.publish is not None:
ports = [] ports = []
for port in self.publish: for port in self.publish:
@ -2149,7 +2185,7 @@ class DockerService(DockerBaseClass):
endpoint_spec_args["mode"] = self.endpoint_mode endpoint_spec_args["mode"] = self.endpoint_mode
return types.EndpointSpec(**endpoint_spec_args) if endpoint_spec_args else None 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() container_spec = self.build_container_spec()
placement = self.build_placement() placement = self.build_placement()
task_template = self.build_task_template(container_spec, placement) task_template = self.build_task_template(container_spec, placement)
@ -2159,7 +2195,10 @@ class DockerService(DockerBaseClass):
service_mode = self.build_service_mode() service_mode = self.build_service_mode()
endpoint_spec = self.build_endpoint_spec() 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: if update_config:
service["update_config"] = update_config service["update_config"] = update_config
if rollback_config: if rollback_config:
@ -2176,13 +2215,12 @@ class DockerService(DockerBaseClass):
class DockerServiceManager: class DockerServiceManager:
def __init__(self, client: AnsibleDockerClient):
def __init__(self, client):
self.client = client self.client = client
self.retries = 2 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: try:
raw_data = self.client.inspect_service(name) raw_data = self.client.inspect_service(name)
except NotFound: except NotFound:
@ -2415,7 +2453,9 @@ class DockerServiceManager:
ds.init = task_template_data["ContainerSpec"].get("Init", False) ds.init = task_template_data["ContainerSpec"].get("Init", False)
return ds 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() service_data = new_service.build_docker_service()
result = self.client.update_service( result = self.client.update_service(
old_service.service_id, old_service.service_id,
@ -2427,15 +2467,15 @@ class DockerServiceManager:
# (see https://github.com/docker/docker-py/pull/2272) # (see https://github.com/docker/docker-py/pull/2272)
self.client.report_warnings(result, ["Warning"]) 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() service_data = service.build_docker_service()
result = self.client.create_service(name=name, **service_data) result = self.client.create_service(name=name, **service_data)
self.client.report_warnings(result, ["Warning"]) self.client.report_warnings(result, ["Warning"])
def remove_service(self, name): def remove_service(self, name: str) -> None:
self.client.remove_service(name) 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: if not name or not resolve:
return name return name
repo, tag = parse_repository_tag(name) repo, tag = parse_repository_tag(name)
@ -2446,10 +2486,10 @@ class DockerServiceManager:
digest = distribution_data["Descriptor"]["digest"] digest = distribution_data["Descriptor"]["digest"]
return f"{name}@{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()} 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 Resolve missing secret ids by looking them up by name
""" """
@ -2471,7 +2511,7 @@ class DockerServiceManager:
self.client.fail(f'Could not find a secret named "{secret_name}"') self.client.fail(f'Could not find a secret named "{secret_name}"')
return secrets 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 Resolve missing config ids by looking them up by name
""" """
@ -2493,7 +2533,7 @@ class DockerServiceManager:
self.client.fail(f'Could not find a config named "{config_name}"') self.client.fail(f'Could not find a config named "{config_name}"')
return configs return configs
def run(self): def run(self) -> tuple[str, bool, bool, list[str], dict[str, t.Any]]:
self.diff_tracker = DifferenceTracker() self.diff_tracker = DifferenceTracker()
module = self.client.module module = self.client.module
@ -2582,7 +2622,7 @@ class DockerServiceManager:
return msg, changed, rebuilt, differences.get_legacy_docker_diffs(), facts 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: while True:
try: try:
return self.run() return self.run()
@ -2596,20 +2636,20 @@ class DockerServiceManager:
raise raise
def _detect_publish_mode_usage(client): def _detect_publish_mode_usage(client: AnsibleDockerClient) -> bool:
for publish_def in client.module.params["publish"] or []: for publish_def in client.module.params["publish"] or []:
if publish_def.get("mode"): if publish_def.get("mode"):
return True return True
return False return False
def _detect_healthcheck_start_period(client): def _detect_healthcheck_start_period(client: AnsibleDockerClient) -> bool:
if client.module.params["healthcheck"]: if client.module.params["healthcheck"]:
return client.module.params["healthcheck"]["start_period"] is not None return client.module.params["healthcheck"]["start_period"] is not None
return False return False
def _detect_mount_tmpfs_usage(client): def _detect_mount_tmpfs_usage(client: AnsibleDockerClient) -> bool:
for mount in client.module.params["mounts"] or []: for mount in client.module.params["mounts"] or []:
if mount.get("type") == "tmpfs": if mount.get("type") == "tmpfs":
return True return True
@ -2620,14 +2660,14 @@ def _detect_mount_tmpfs_usage(client):
return False 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( rollback_config_failure_action = (client.module.params["update_config"] or {}).get(
"failure_action" "failure_action"
) )
return rollback_config_failure_action == "rollback" return rollback_config_failure_action == "rollback"
def main(): def main() -> None:
argument_spec = { argument_spec = {
"name": {"type": "str", "required": True}, "name": {"type": "str", "required": True},
"image": {"type": "str"}, "image": {"type": "str"},
@ -2948,6 +2988,7 @@ def main():
"swarm_service": facts, "swarm_service": facts,
} }
if client.module._diff: if client.module._diff:
assert dsm.diff_tracker is not None
before, after = dsm.diff_tracker.get_before_after() before, after = dsm.diff_tracker.get_before_after()
results["diff"] = {"before": before, "after": after} results["diff"] = {"before": before, "after": after}

View File

@ -63,6 +63,7 @@ service:
""" """
import traceback import traceback
import typing as t
try: 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"] service = client.module.params["name"]
return client.get_service_inspect(service_id=service, skip_missing=True) return client.get_service_inspect(service_id=service, skip_missing=True)
def main(): def main() -> None:
argument_spec = { argument_spec = {
"name": {"type": "str", "required": True}, "name": {"type": "str", "required": True},
} }

View File

@ -19,4 +19,5 @@ plugins/modules/docker_image.py no-assert
plugins/modules/docker_image_tag.py no-assert plugins/modules/docker_image_tag.py no-assert
plugins/modules/docker_login.py no-assert plugins/modules/docker_login.py no-assert
plugins/modules/docker_plugin.py no-assert plugins/modules/docker_plugin.py no-assert
plugins/modules/docker_swarm_service.py no-assert
plugins/modules/docker_volume.py no-assert plugins/modules/docker_volume.py no-assert

View File

@ -18,4 +18,5 @@ plugins/modules/docker_image.py no-assert
plugins/modules/docker_image_tag.py no-assert plugins/modules/docker_image_tag.py no-assert
plugins/modules/docker_login.py no-assert plugins/modules/docker_login.py no-assert
plugins/modules/docker_plugin.py no-assert plugins/modules/docker_plugin.py no-assert
plugins/modules/docker_swarm_service.py no-assert
plugins/modules/docker_volume.py no-assert plugins/modules/docker_volume.py no-assert

View File

@ -12,4 +12,5 @@ plugins/modules/docker_image.py no-assert
plugins/modules/docker_image_tag.py no-assert plugins/modules/docker_image_tag.py no-assert
plugins/modules/docker_login.py no-assert plugins/modules/docker_login.py no-assert
plugins/modules/docker_plugin.py no-assert plugins/modules/docker_plugin.py no-assert
plugins/modules/docker_swarm_service.py no-assert
plugins/modules/docker_volume.py no-assert plugins/modules/docker_volume.py no-assert

View File

@ -12,4 +12,5 @@ plugins/modules/docker_image.py no-assert
plugins/modules/docker_image_tag.py no-assert plugins/modules/docker_image_tag.py no-assert
plugins/modules/docker_login.py no-assert plugins/modules/docker_login.py no-assert
plugins/modules/docker_plugin.py no-assert plugins/modules/docker_plugin.py no-assert
plugins/modules/docker_swarm_service.py no-assert
plugins/modules/docker_volume.py no-assert plugins/modules/docker_volume.py no-assert

View File

@ -12,4 +12,5 @@ plugins/modules/docker_image.py no-assert
plugins/modules/docker_image_tag.py no-assert plugins/modules/docker_image_tag.py no-assert
plugins/modules/docker_login.py no-assert plugins/modules/docker_login.py no-assert
plugins/modules/docker_plugin.py no-assert plugins/modules/docker_plugin.py no-assert
plugins/modules/docker_swarm_service.py no-assert
plugins/modules/docker_volume.py no-assert plugins/modules/docker_volume.py no-assert