Add typing information, 1/2 (#1176)

* Re-enable typing and improve config.

* Make mypy pass.

* Improve settings.

* First batch of types.

* Add more type hints.

* Fixes.

* Format.

* Fix split_port() without returning to previous type chaos.

* Continue with type hints (and ignores).
This commit is contained in:
Felix Fontein
2025-10-23 07:05:42 +02:00
committed by GitHub
parent 24f35644e3
commit 3350283bcc
92 changed files with 4366 additions and 2272 deletions
+286 -129
View File
@@ -12,6 +12,7 @@ import abc
import os
import re
import shlex
import typing as t
from functools import partial
from ansible.module_utils.common.text.converters import to_text
@@ -32,6 +33,23 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
)
if t.TYPE_CHECKING:
from collections.abc import Callable, Sequence
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.docker.plugins.module_utils._version import (
LooseVersion,
)
ValueType = t.Literal["set", "list", "dict", "bool", "int", "float", "str"]
AnsibleType = t.Literal["list", "dict", "bool", "int", "float", "str"]
ComparisonMode = t.Literal["ignore", "strict", "allow_more_present"]
ComparisonType = t.Literal["set", "set(dict)", "list", "dict", "value"]
Client = t.TypeVar("Client")
_DEFAULT_IP_REPLACEMENT_STRING = (
"[[DEFAULT_IP:iewahhaeB4Sae6Aen8IeShairoh4zeph7xaekoh8Geingunaesaeweiy3ooleiwi]]"
)
@@ -54,7 +72,9 @@ _MOUNT_OPTION_TYPES = {
}
def _get_ansible_type(value_type):
def _get_ansible_type(
value_type: ValueType,
) -> AnsibleType:
if value_type == "set":
return "list"
if value_type not in ("list", "dict", "bool", "int", "float", "str"):
@@ -65,21 +85,22 @@ def _get_ansible_type(value_type):
class Option:
def __init__(
self,
name,
value_type,
owner,
ansible_type=None,
elements=None,
ansible_elements=None,
ansible_suboptions=None,
ansible_aliases=None,
ansible_choices=None,
needs_no_suboptions=False,
default_comparison=None,
not_a_container_option=False,
not_an_ansible_option=False,
copy_comparison_from=None,
compare=None,
name: str,
*,
value_type: ValueType,
owner: OptionGroup,
ansible_type: AnsibleType | None = None,
elements: ValueType | None = None,
ansible_elements: AnsibleType | None = None,
ansible_suboptions: dict[str, t.Any] | None = None,
ansible_aliases: Sequence[str] | None = None,
ansible_choices: Sequence[str] | None = None,
needs_no_suboptions: bool = False,
default_comparison: ComparisonMode | None = None,
not_a_container_option: bool = False,
not_an_ansible_option: bool = False,
copy_comparison_from: str | None = None,
compare: Callable[[Option, t.Any, t.Any], bool] | None = None,
):
self.name = name
self.value_type = value_type
@@ -95,8 +116,8 @@ class Option:
if (elements is None and ansible_elements is None) and needs_ansible_elements:
raise ValueError("Ansible elements required for Ansible lists")
self.elements = elements if needs_elements else None
self.ansible_elements = (
(ansible_elements or _get_ansible_type(elements))
self.ansible_elements: AnsibleType | None = (
(ansible_elements or _get_ansible_type(elements or "str"))
if needs_ansible_elements
else None
)
@@ -119,10 +140,12 @@ class Option:
self.ansible_suboptions = ansible_suboptions if needs_suboptions else None
self.ansible_aliases = ansible_aliases or []
self.ansible_choices = ansible_choices
comparison_type = self.value_type
if comparison_type == "set" and self.elements == "dict":
comparison_type: ComparisonType
if self.value_type == "set" and self.elements == "dict":
comparison_type = "set(dict)"
elif comparison_type not in ("set", "list", "dict"):
elif self.value_type in ("set", "list", "dict"):
comparison_type = self.value_type # type: ignore
else:
comparison_type = "value"
self.comparison_type = comparison_type
if default_comparison is not None:
@@ -152,36 +175,45 @@ class Option:
class OptionGroup:
def __init__(
self,
preprocess=None,
ansible_mutually_exclusive=None,
ansible_required_together=None,
ansible_required_one_of=None,
ansible_required_if=None,
ansible_required_by=None,
):
*,
preprocess: (
Callable[[AnsibleModule, dict[str, t.Any]], dict[str, t.Any]] | None
) = None,
ansible_mutually_exclusive: Sequence[Sequence[str]] | None = None,
ansible_required_together: Sequence[Sequence[str]] | None = None,
ansible_required_one_of: Sequence[Sequence[str]] | None = None,
ansible_required_if: (
Sequence[
tuple[str, t.Any, Sequence[str]]
| tuple[str, t.Any, Sequence[str], bool]
]
| None
) = None,
ansible_required_by: dict[str, Sequence[str]] | None = None,
) -> None:
if preprocess is None:
def preprocess(module, values):
return values
self.preprocess = preprocess
self.options = []
self.all_options = []
self.engines = {}
self.options: list[Option] = []
self.all_options: list[Option] = []
self.engines: dict[str, Engine] = {}
self.ansible_mutually_exclusive = ansible_mutually_exclusive or []
self.ansible_required_together = ansible_required_together or []
self.ansible_required_one_of = ansible_required_one_of or []
self.ansible_required_if = ansible_required_if or []
self.ansible_required_by = ansible_required_by or {}
self.argument_spec = {}
self.argument_spec: dict[str, t.Any] = {}
def add_option(self, *args, **kwargs):
def add_option(self, *args, **kwargs) -> OptionGroup:
option = Option(*args, owner=self, **kwargs)
if not option.not_a_container_option:
self.options.append(option)
self.all_options.append(option)
if not option.not_an_ansible_option:
ansible_option = {
ansible_option: dict[str, t.Any] = {
"type": option.ansible_type,
}
if option.ansible_elements is not None:
@@ -195,213 +227,297 @@ class OptionGroup:
self.argument_spec[option.name] = ansible_option
return self
def supports_engine(self, engine_name):
def supports_engine(self, engine_name: str) -> bool:
return engine_name in self.engines
def get_engine(self, engine_name):
def get_engine(self, engine_name: str) -> Engine:
return self.engines[engine_name]
def add_engine(self, engine_name, engine):
def add_engine(self, engine_name: str, engine: Engine) -> OptionGroup:
self.engines[engine_name] = engine
return self
class Engine:
min_api_version = None # string or None
min_api_version_obj = None # LooseVersion object or None
extra_option_minimal_versions = None # dict[str, dict[str, Any]] or None
class Engine(t.Generic[Client]):
min_api_version: str | None = None
min_api_version_obj: LooseVersion | None = None
extra_option_minimal_versions: dict[str, dict[str, t.Any]] | None = None
@abc.abstractmethod
def get_value(self, module, container, api_version, options, image, host_info):
def get_value(
self,
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]:
pass
def compare_value(self, option, param_value, container_value):
def compare_value(
self, option: Option, param_value: t.Any, container_value: t.Any
) -> bool:
return option.compare(param_value, container_value)
@abc.abstractmethod
def set_value(self, module, data, api_version, options, values):
def set_value(
self,
module: AnsibleModule,
data: dict[str, t.Any],
api_version: LooseVersion,
options: list[Option],
values: dict[str, t.Any],
) -> None:
pass
@abc.abstractmethod
def get_expected_values(
self, module, client, api_version, options, image, values, host_info
):
self,
module: AnsibleModule,
client: Client,
api_version: LooseVersion,
options: list[Option],
image: dict[str, t.Any] | None,
values: dict[str, t.Any],
host_info: dict[str, t.Any] | None,
) -> dict[str, t.Any]:
pass
@abc.abstractmethod
def ignore_mismatching_result(
self,
module,
client,
api_version,
option,
image,
container_value,
expected_value,
host_info,
):
module: AnsibleModule,
client: Client,
api_version: LooseVersion,
option: Option,
image: dict[str, t.Any] | None,
container_value: t.Any,
expected_value: t.Any,
host_info: dict[str, t.Any] | None,
) -> bool:
pass
@abc.abstractmethod
def preprocess_value(self, module, client, api_version, options, values):
def preprocess_value(
self,
module: AnsibleModule,
client: Client,
api_version: LooseVersion,
options: list[Option],
values: dict[str, t.Any],
) -> dict[str, t.Any]:
pass
@abc.abstractmethod
def update_value(self, module, data, api_version, options, values):
def update_value(
self,
module: AnsibleModule,
data: dict[str, t.Any],
api_version: LooseVersion,
options: list[Option],
values: dict[str, t.Any],
) -> None:
pass
@abc.abstractmethod
def can_set_value(self, api_version):
def can_set_value(self, api_version: LooseVersion) -> bool:
pass
@abc.abstractmethod
def can_update_value(self, api_version):
def can_update_value(self, api_version: LooseVersion) -> bool:
pass
@abc.abstractmethod
def needs_container_image(self, values):
def needs_container_image(self, values: dict[str, t.Any]) -> bool:
pass
@abc.abstractmethod
def needs_host_info(self, values):
def needs_host_info(self, values: dict[str, t.Any]) -> bool:
pass
class EngineDriver:
name = None # string
class EngineDriver(t.Generic[Client]):
name: str
@abc.abstractmethod
def setup(
self,
argument_spec,
mutually_exclusive=None,
required_together=None,
required_one_of=None,
required_if=None,
required_by=None,
):
# Return (module, active_options, client)
argument_spec: dict[str, t.Any],
mutually_exclusive: Sequence[Sequence[str]] | None = None,
required_together: Sequence[Sequence[str]] | None = None,
required_one_of: Sequence[Sequence[str]] | None = None,
required_if: (
Sequence[
tuple[str, t.Any, Sequence[str]]
| tuple[str, t.Any, Sequence[str], bool]
]
| None
) = None,
required_by: dict[str, Sequence[str]] | None = None,
) -> tuple[AnsibleModule, list[OptionGroup], Client]:
pass
@abc.abstractmethod
def get_host_info(self, client):
def get_host_info(self, client: Client) -> dict[str, t.Any]:
pass
@abc.abstractmethod
def get_api_version(self, client):
def get_api_version(self, client: Client) -> LooseVersion:
pass
@abc.abstractmethod
def get_container_id(self, container):
def get_container_id(self, container: dict[str, t.Any]) -> str:
pass
@abc.abstractmethod
def get_image_from_container(self, container):
def get_image_from_container(self, container: dict[str, t.Any]) -> str:
pass
@abc.abstractmethod
def get_image_name_from_container(self, container):
def get_image_name_from_container(self, container: dict[str, t.Any]) -> str | None:
pass
@abc.abstractmethod
def is_container_removing(self, container):
def is_container_removing(self, container: dict[str, t.Any]) -> bool:
pass
@abc.abstractmethod
def is_container_running(self, container):
def is_container_running(self, container: dict[str, t.Any]) -> bool:
pass
@abc.abstractmethod
def is_container_paused(self, container):
def is_container_paused(self, container: dict[str, t.Any]) -> bool:
pass
@abc.abstractmethod
def inspect_container_by_name(self, client, container_name):
def inspect_container_by_name(
self, client: Client, container_name: str
) -> dict[str, t.Any] | None:
pass
@abc.abstractmethod
def inspect_container_by_id(self, client, container_id):
def inspect_container_by_id(
self, client: Client, container_id: str
) -> dict[str, t.Any] | None:
pass
@abc.abstractmethod
def inspect_image_by_id(self, client, image_id):
def inspect_image_by_id(
self, client: Client, image_id: str
) -> dict[str, t.Any] | None:
pass
@abc.abstractmethod
def inspect_image_by_name(self, client, repository, tag):
def inspect_image_by_name(
self, client: Client, repository: str, tag: str
) -> dict[str, t.Any] | None:
pass
@abc.abstractmethod
def pull_image(self, client, repository, tag, image_platform=None):
def pull_image(
self,
client: Client,
repository: str,
tag: str,
image_platform: str | None = None,
) -> tuple[dict[str, t.Any] | None, bool]:
pass
@abc.abstractmethod
def pause_container(self, client, container_id):
def pause_container(self, client: Client, container_id: str) -> None:
pass
@abc.abstractmethod
def unpause_container(self, client, container_id):
def unpause_container(self, client: Client, container_id: str) -> None:
pass
@abc.abstractmethod
def disconnect_container_from_network(self, client, container_id, network_id):
def disconnect_container_from_network(
self, client: Client, container_id: str, network_id: str
) -> None:
pass
@abc.abstractmethod
def connect_container_to_network(
self, client, container_id, network_id, parameters=None
):
self,
client: Client,
container_id: str,
network_id: str,
parameters: dict[str, t.Any] | None = None,
) -> None:
pass
def create_container_supports_more_than_one_network(self, client):
def create_container_supports_more_than_one_network(self, client: Client) -> bool:
return False
@abc.abstractmethod
def create_container(
self, client, container_name, create_parameters, networks=None
):
self,
client: Client,
container_name: str,
create_parameters: dict[str, t.Any],
networks: dict[str, dict[str, t.Any]] | None = None,
) -> str:
pass
@abc.abstractmethod
def start_container(self, client, container_id):
def start_container(self, client: Client, container_id: str) -> None:
pass
@abc.abstractmethod
def wait_for_container(self, client, container_id, timeout=None):
def wait_for_container(
self, client: Client, container_id: str, timeout: int | float | None = None
) -> int | None:
pass
@abc.abstractmethod
def get_container_output(self, client, container_id):
def get_container_output(
self, client: Client, container_id: str
) -> tuple[bytes, t.Literal[True]] | tuple[str, t.Literal[False]]:
pass
@abc.abstractmethod
def update_container(self, client, container_id, update_parameters):
def update_container(
self, client: Client, container_id: str, update_parameters: dict[str, t.Any]
) -> None:
pass
@abc.abstractmethod
def restart_container(self, client, container_id, timeout=None):
def restart_container(
self, client: Client, container_id: str, timeout: int | float | None = None
) -> None:
pass
@abc.abstractmethod
def kill_container(self, client, container_id, kill_signal=None):
def kill_container(
self, client: Client, container_id: str, kill_signal: str | None = None
) -> None:
pass
@abc.abstractmethod
def stop_container(self, client, container_id, timeout=None):
def stop_container(
self, client: Client, container_id: str, timeout: int | float | None = None
) -> None:
pass
@abc.abstractmethod
def remove_container(
self, client, container_id, remove_volumes=False, link=False, force=False
):
self,
client: Client,
container_id: str,
remove_volumes: bool = False,
link: bool = False,
force: bool = False,
) -> None:
pass
@abc.abstractmethod
def run(self, runner, client):
def run(self, runner: Callable[[], None], client: Client) -> None:
pass
def _is_volume_permissions(mode):
def _is_volume_permissions(mode: str) -> bool:
for part in mode.split(","):
if part not in (
"rw",
@@ -423,7 +539,7 @@ def _is_volume_permissions(mode):
return True
def _parse_port_range(range_or_port, module):
def _parse_port_range(range_or_port: str, module: AnsibleModule) -> list[int]:
"""
Parses a string containing either a single port or a range of ports.
@@ -443,7 +559,7 @@ def _parse_port_range(range_or_port, module):
module.fail_json(msg=f'Invalid port: "{range_or_port}"')
def _split_colon_ipv6(text, module):
def _split_colon_ipv6(text: str, module: AnsibleModule) -> list[str]:
"""
Split string by ':', while keeping IPv6 addresses in square brackets in one component.
"""
@@ -475,7 +591,9 @@ def _split_colon_ipv6(text, module):
return result
def _preprocess_command(module, values):
def _preprocess_command(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "command" not in values:
return values
value = values["command"]
@@ -502,7 +620,9 @@ def _preprocess_command(module, values):
}
def _preprocess_entrypoint(module, values):
def _preprocess_entrypoint(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "entrypoint" not in values:
return values
value = values["entrypoint"]
@@ -522,7 +642,9 @@ def _preprocess_entrypoint(module, values):
}
def _preprocess_env(module, values):
def _preprocess_env(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if not values:
return {}
final_env = {}
@@ -546,7 +668,9 @@ def _preprocess_env(module, values):
}
def _preprocess_healthcheck(module, values):
def _preprocess_healthcheck(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if not values:
return {}
return {
@@ -556,7 +680,12 @@ def _preprocess_healthcheck(module, values):
}
def _preprocess_convert_to_bytes(module, values, name, unlimited_value=None):
def _preprocess_convert_to_bytes(
module: AnsibleModule,
values: dict[str, t.Any],
name: str,
unlimited_value: int | None = None,
) -> dict[str, t.Any]:
if name not in values:
return values
try:
@@ -571,7 +700,9 @@ def _preprocess_convert_to_bytes(module, values, name, unlimited_value=None):
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
def _preprocess_mac_address(module, values):
def _preprocess_mac_address(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "mac_address" not in values:
return values
return {
@@ -579,7 +710,9 @@ def _preprocess_mac_address(module, values):
}
def _preprocess_networks(module, values):
def _preprocess_networks(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if (
module.params["networks_cli_compatible"] is True
and values.get("networks")
@@ -605,14 +738,18 @@ def _preprocess_networks(module, values):
return values
def _preprocess_sysctls(module, values):
def _preprocess_sysctls(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "sysctls" in values:
for key, value in values["sysctls"].items():
values["sysctls"][key] = to_text(value, errors="surrogate_or_strict")
return values
def _preprocess_tmpfs(module, values):
def _preprocess_tmpfs(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "tmpfs" not in values:
return values
result = {}
@@ -625,7 +762,9 @@ def _preprocess_tmpfs(module, values):
return {"tmpfs": result}
def _preprocess_ulimits(module, values):
def _preprocess_ulimits(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "ulimits" not in values:
return values
result = []
@@ -644,8 +783,10 @@ def _preprocess_ulimits(module, values):
}
def _preprocess_mounts(module, values):
last = {}
def _preprocess_mounts(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
last: dict[str, str] = {}
def check_collision(t, name):
if t in last:
@@ -776,7 +917,9 @@ def _preprocess_mounts(module, values):
return values
def _preprocess_labels(module, values):
def _preprocess_labels(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
result = {}
if "labels" in values:
labels = values["labels"]
@@ -787,13 +930,15 @@ def _preprocess_labels(module, values):
return result
def _preprocess_log(module, values):
result = {}
def _preprocess_log(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
if "log_driver" not in values:
return result
result["log_driver"] = values["log_driver"]
if "log_options" in values:
options = {}
options: dict[str, str] = {}
for k, v in values["log_options"].items():
if not isinstance(v, str):
value = to_text(v, errors="surrogate_or_strict")
@@ -807,7 +952,9 @@ def _preprocess_log(module, values):
return result
def _preprocess_ports(module, values):
def _preprocess_ports(
module: AnsibleModule, values: dict[str, t.Any]
) -> dict[str, t.Any]:
if "published_ports" in values:
if "all" in values["published_ports"]:
module.fail_json(
@@ -815,7 +962,12 @@ def _preprocess_ports(module, values):
"to randomly assign port mappings for those not specified by published_ports."
)
binds = {}
binds: dict[
str | int,
tuple[str]
| tuple[str, str | int]
| list[tuple[str] | tuple[str, str | int]],
] = {}
for port in values["published_ports"]:
parts = _split_colon_ipv6(
to_text(port, errors="surrogate_or_strict"), module
@@ -827,6 +979,7 @@ def _preprocess_ports(module, values):
container_ports = _parse_port_range(container_port, module)
p_len = len(parts)
port_binds: Sequence[tuple[str] | tuple[str, str | int]]
if p_len == 1:
port_binds = len(container_ports) * [(_DEFAULT_IP_REPLACEMENT_STRING,)]
elif p_len == 2:
@@ -865,8 +1018,12 @@ def _preprocess_ports(module, values):
"Maybe you forgot to use square brackets ([...]) around an IPv6 address?"
)
for bind, container_port in zip(port_binds, container_ports):
idx = f"{container_port}/{protocol}" if protocol else container_port
for bind, container_port_val in zip(port_binds, container_ports):
idx = (
f"{container_port_val}/{protocol}"
if protocol
else container_port_val
)
if idx in binds:
old_bind = binds[idx]
if isinstance(old_bind, list):
@@ -882,9 +1039,9 @@ def _preprocess_ports(module, values):
for port in values["exposed_ports"]:
port = to_text(port, errors="surrogate_or_strict").strip()
protocol = "tcp"
match = re.search(r"(/.+$)", port)
if match:
protocol = match.group(1).replace("/", "")
matcher = re.search(r"(/.+$)", port)
if matcher:
protocol = matcher.group(1).replace("/", "")
port = re.sub(r"/.+$", "", port)
exposed.append((port, protocol))
if "published_ports" in values:
@@ -912,7 +1069,7 @@ def _preprocess_ports(module, values):
return values
def _compare_platform(option, param_value, container_value):
def _compare_platform(option: Option, param_value: t.Any, container_value: t.Any):
if option.comparison == "ignore":
return True
try:
File diff suppressed because it is too large Load Diff
+246 -136
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
import re
import typing as t
from time import sleep
from ansible.module_utils.common.text.converters import to_text
@@ -16,6 +17,9 @@ from ansible.module_utils.common.text.converters import to_text
from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import (
parse_repository_tag,
)
from ansible_collections.community.docker.plugins.module_utils._module_container.base import (
Client,
)
from ansible_collections.community.docker.plugins.module_utils._util import (
DifferenceTracker,
DockerBaseClass,
@@ -25,13 +29,23 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
)
if t.TYPE_CHECKING:
from collections.abc import Sequence
from ansible.module_utils.basic import AnsibleModule
from .base import Engine, EngineDriver, Option, OptionGroup
class Container(DockerBaseClass):
def __init__(self, container, engine_driver):
def __init__(
self, container: dict[str, t.Any] | None, engine_driver: EngineDriver
) -> None:
super().__init__()
self.raw = container
self.id = None
self.image = None
self.image_name = None
self.id: str | None = None
self.image: str | None = None
self.image_name: str | None = None
self.container = container
self.engine_driver = engine_driver
if container:
@@ -41,11 +55,11 @@ class Container(DockerBaseClass):
self.log(self.container, pretty_print=True)
@property
def exists(self):
def exists(self) -> bool:
return bool(self.container)
@property
def removing(self):
def removing(self) -> bool:
return (
self.engine_driver.is_container_removing(self.container)
if self.container
@@ -53,7 +67,7 @@ class Container(DockerBaseClass):
)
@property
def running(self):
def running(self) -> bool:
return (
self.engine_driver.is_container_running(self.container)
if self.container
@@ -61,7 +75,7 @@ class Container(DockerBaseClass):
)
@property
def paused(self):
def paused(self) -> bool:
return (
self.engine_driver.is_container_paused(self.container)
if self.container
@@ -69,8 +83,14 @@ class Container(DockerBaseClass):
)
class ContainerManager(DockerBaseClass):
def __init__(self, module, engine_driver, client, active_options):
class ContainerManager(DockerBaseClass, t.Generic[Client]):
def __init__(
self,
module: AnsibleModule,
engine_driver: EngineDriver,
client: Client,
active_options: list[OptionGroup],
) -> None:
super().__init__()
self.module = module
self.engine_driver = engine_driver
@@ -78,46 +98,64 @@ class ContainerManager(DockerBaseClass):
self.options = active_options
self.all_options = self._collect_all_options(active_options)
self.check_mode = self.module.check_mode
self.param_cleanup = self.module.params["cleanup"]
self.param_container_default_behavior = self.module.params[
"container_default_behavior"
]
self.param_default_host_ip = self.module.params["default_host_ip"]
self.param_debug = self.module.params["debug"]
self.param_force_kill = self.module.params["force_kill"]
self.param_image = self.module.params["image"]
self.param_image_comparison = self.module.params["image_comparison"]
self.param_image_label_mismatch = self.module.params["image_label_mismatch"]
self.param_image_name_mismatch = self.module.params["image_name_mismatch"]
self.param_keep_volumes = self.module.params["keep_volumes"]
self.param_kill_signal = self.module.params["kill_signal"]
self.param_name = self.module.params["name"]
self.param_networks_cli_compatible = self.module.params[
self.param_cleanup: bool = self.module.params["cleanup"]
self.param_container_default_behavior: t.Literal[
"compatibility", "no_defaults"
] = self.module.params["container_default_behavior"]
self.param_default_host_ip: str | None = self.module.params["default_host_ip"]
self.param_debug: bool = self.module.params["debug"]
self.param_force_kill: bool = self.module.params["force_kill"]
self.param_image: str | None = self.module.params["image"]
self.param_image_comparison: t.Literal["desired-image", "current-image"] = (
self.module.params["image_comparison"]
)
self.param_image_label_mismatch: t.Literal["ignore", "fail"] = (
self.module.params["image_label_mismatch"]
)
self.param_image_name_mismatch: t.Literal["ignore", "recreate"] = (
self.module.params["image_name_mismatch"]
)
self.param_keep_volumes: bool = self.module.params["keep_volumes"]
self.param_kill_signal: str | None = self.module.params["kill_signal"]
self.param_name: str = self.module.params["name"]
self.param_networks_cli_compatible: bool = self.module.params[
"networks_cli_compatible"
]
self.param_output_logs = self.module.params["output_logs"]
self.param_paused = self.module.params["paused"]
self.param_pull = self.module.params["pull"]
if self.param_pull is True:
self.param_pull = "always"
if self.param_pull is False:
self.param_pull = "missing"
self.param_pull_check_mode_behavior = self.module.params[
"pull_check_mode_behavior"
self.param_output_logs: bool = self.module.params["output_logs"]
self.param_paused: bool | None = self.module.params["paused"]
param_pull: t.Literal["never", "missing", "always", True, False] = (
self.module.params["pull"]
)
if param_pull is True:
param_pull = "always"
if param_pull is False:
param_pull = "missing"
self.param_pull: t.Literal["never", "missing", "always"] = param_pull
self.param_pull_check_mode_behavior: t.Literal[
"image_not_present", "always"
] = self.module.params["pull_check_mode_behavior"]
self.param_recreate: bool = self.module.params["recreate"]
self.param_removal_wait_timeout: int | float | None = self.module.params[
"removal_wait_timeout"
]
self.param_recreate = self.module.params["recreate"]
self.param_removal_wait_timeout = self.module.params["removal_wait_timeout"]
self.param_healthy_wait_timeout = self.module.params["healthy_wait_timeout"]
if self.param_healthy_wait_timeout <= 0:
self.param_healthy_wait_timeout: int | float | None = self.module.params[
"healthy_wait_timeout"
]
if (
self.param_healthy_wait_timeout is not None
and self.param_healthy_wait_timeout <= 0
):
self.param_healthy_wait_timeout = None
self.param_restart = self.module.params["restart"]
self.param_state = self.module.params["state"]
self.param_restart: bool = self.module.params["restart"]
self.param_state: t.Literal[
"absent", "present", "healthy", "started", "stopped"
] = self.module.params["state"]
self._parse_comparisons()
self._update_params()
self.results = {"changed": False, "actions": []}
self.diff = {}
self.diff: dict[str, t.Any] = {}
self.diff_tracker = DifferenceTracker()
self.facts = {}
self.facts: dict[str, t.Any] | None = {}
if self.param_default_host_ip:
valid_ip = False
if re.match(
@@ -134,16 +172,22 @@ class ContainerManager(DockerBaseClass):
"The value of default_host_ip must be an empty string, an IPv4 address, "
f'or an IPv6 address. Got "{self.param_default_host_ip}" instead.'
)
self.parameters = None
self.parameters: list[tuple[OptionGroup, dict[str, t.Any]]] | None = None
def _collect_all_options(self, active_options):
def _add_action(self, action: dict[str, t.Any]) -> None:
actions: list[dict[str, t.Any]] = self.results["actions"] # type: ignore
actions.append(action)
def _collect_all_options(
self, active_options: list[OptionGroup]
) -> dict[str, Option]:
all_options = {}
for options in active_options:
for option in options.options:
all_options[option.name] = option
return all_options
def _collect_all_module_params(self):
def _collect_all_module_params(self) -> set[str]:
all_module_options = set()
for option, data in self.module.argument_spec.items():
all_module_options.add(option)
@@ -152,7 +196,7 @@ class ContainerManager(DockerBaseClass):
all_module_options.add(alias)
return all_module_options
def _parse_comparisons(self):
def _parse_comparisons(self) -> None:
# Keep track of all module params and all option aliases
all_module_options = self._collect_all_module_params()
comp_aliases = {}
@@ -163,10 +207,11 @@ class ContainerManager(DockerBaseClass):
for alias in option.ansible_aliases:
comp_aliases[alias] = option_name
# Process comparisons specified by user
if self.module.params.get("comparisons"):
comparisons: dict[str, t.Any] | None = self.module.params.get("comparisons")
if comparisons:
# If '*' appears in comparisons, process it first
if "*" in self.module.params["comparisons"]:
value = self.module.params["comparisons"]["*"]
if "*" in comparisons:
value = comparisons["*"]
if value not in ("strict", "ignore"):
self.fail(
"The wildcard can only be used with comparison modes 'strict' and 'ignore'!"
@@ -179,8 +224,8 @@ class ContainerManager(DockerBaseClass):
continue
option.comparison = value
# Now process all other comparisons.
comp_aliases_used = {}
for key, value in self.module.params["comparisons"].items():
comp_aliases_used: dict[str, str] = {}
for key, value in comparisons.items():
if key == "*":
continue
# Find main key
@@ -220,7 +265,7 @@ class ContainerManager(DockerBaseClass):
option.copy_comparison_from
].comparison
def _update_params(self):
def _update_params(self) -> None:
if (
self.param_networks_cli_compatible is True
and self.module.params["networks"]
@@ -247,12 +292,14 @@ class ContainerManager(DockerBaseClass):
if self.module.params[param] is None:
self.module.params[param] = value
def fail(self, *args, **kwargs):
self.client.fail(*args, **kwargs)
def fail(self, *args, **kwargs) -> t.NoReturn:
# mypy doesn't know that Client has fail() method
raise self.client.fail(*args, **kwargs) # type: ignore
def run(self):
def run(self) -> None:
if self.param_state in ("stopped", "started", "present", "healthy"):
self.present(self.param_state)
# mypy doesn't get that self.param_state has only one of the above values
self.present(self.param_state) # type: ignore
elif self.param_state == "absent":
self.absent()
@@ -270,15 +317,16 @@ class ContainerManager(DockerBaseClass):
def wait_for_state(
self,
container_id,
complete_states=None,
wait_states=None,
accept_removal=False,
max_wait=None,
health_state=False,
):
container_id: str,
*,
complete_states: Sequence[str | None] | None = None,
wait_states: Sequence[str | None] | None = None,
accept_removal: bool = False,
max_wait: int | float | None = None,
health_state: bool = False,
) -> dict[str, t.Any] | None:
delay = 1.0
total_wait = 0
total_wait = 0.0
while True:
# Inspect container
result = self.engine_driver.inspect_container_by_id(
@@ -314,7 +362,9 @@ class ContainerManager(DockerBaseClass):
# code will have slept for ~1.5 minutes.)
delay = min(delay * 1.1, 10)
def _collect_params(self, active_options):
def _collect_params(
self, active_options: list[OptionGroup]
) -> list[tuple[OptionGroup, dict[str, t.Any]]]:
parameters = []
for options in active_options:
values = {}
@@ -336,21 +386,25 @@ class ContainerManager(DockerBaseClass):
parameters.append((options, values))
return parameters
def _needs_container_image(self):
def _needs_container_image(self) -> bool:
assert self.parameters is not None
for options, values in self.parameters:
engine = options.get_engine(self.engine_driver.name)
if engine.needs_container_image(values):
return True
return False
def _needs_host_info(self):
def _needs_host_info(self) -> bool:
assert self.parameters is not None
for options, values in self.parameters:
engine = options.get_engine(self.engine_driver.name)
if engine.needs_host_info(values):
return True
return False
def present(self, state):
def present(
self, state: t.Literal["stopped", "started", "present", "healthy"]
) -> None:
self.parameters = self._collect_params(self.options)
container = self._get_container(self.param_name)
was_running = container.running
@@ -382,6 +436,7 @@ class ContainerManager(DockerBaseClass):
self.diff_tracker.add("exists", parameter=True, active=False)
if container.removing and not self.check_mode:
# Wait for container to be removed before trying to create it
assert container.id is not None
self.wait_for_state(
container.id,
wait_states=["removing"],
@@ -394,6 +449,7 @@ class ContainerManager(DockerBaseClass):
container_created = True
else:
# Existing container
assert container.id is not None
different, differences = self.has_different_configuration(
container, container_image, comparison_image, host_info
)
@@ -453,13 +509,16 @@ class ContainerManager(DockerBaseClass):
if state in ("started", "healthy") and not container.running:
self.diff_tracker.add("running", parameter=True, active=was_running)
assert container.id is not None
container = self.container_start(container.id)
elif state in ("started", "healthy") and self.param_restart:
self.diff_tracker.add("running", parameter=True, active=was_running)
self.diff_tracker.add("restarted", parameter=True, active=False)
assert container.id is not None
container = self.container_restart(container.id)
elif state == "stopped" and container.running:
self.diff_tracker.add("running", parameter=False, active=was_running)
assert container.id is not None
self.container_stop(container.id)
container = self._get_container(container.id)
@@ -472,6 +531,7 @@ class ContainerManager(DockerBaseClass):
"paused", parameter=self.param_paused, active=was_paused
)
if not self.check_mode:
assert container.id is not None
try:
if self.param_paused:
self.engine_driver.pause_container(
@@ -487,12 +547,13 @@ class ContainerManager(DockerBaseClass):
)
container = self._get_container(container.id)
self.results["changed"] = True
self.results["actions"].append({"set_paused": self.param_paused})
self._add_action({"set_paused": self.param_paused})
self.facts = container.raw
if state == "healthy" and not self.check_mode:
# `None` means that no health check enabled; simply treat this as 'healthy'
assert container.id is not None
inspect_result = self.wait_for_state(
container.id,
wait_states=["starting", "unhealthy"],
@@ -504,41 +565,51 @@ class ContainerManager(DockerBaseClass):
# Return the latest inspection results retrieved
self.facts = inspect_result
def absent(self):
def absent(self) -> None:
container = self._get_container(self.param_name)
if container.exists:
assert container.id is not None
if container.running:
self.diff_tracker.add("running", parameter=False, active=True)
self.container_stop(container.id)
self.diff_tracker.add("exists", parameter=False, active=True)
self.container_remove(container.id)
def _output_logs(self, msg):
def _output_logs(self, msg: str | bytes) -> None:
self.module.log(msg=msg)
def _get_container(self, container):
def _get_container(self, container: str) -> Container:
"""
Expects container ID or Name. Returns a container object
"""
container = self.engine_driver.inspect_container_by_name(self.client, container)
return Container(container, self.engine_driver)
container_data = self.engine_driver.inspect_container_by_name(
self.client, container
)
return Container(container_data, self.engine_driver)
def _get_container_image(self, container, fallback=None):
def _get_container_image(
self, container: Container, fallback: dict[str, t.Any] | None = None
) -> dict[str, t.Any] | None:
if not container.exists or container.removing:
return fallback
image = container.image
assert image is not None
if is_image_name_id(image):
image = self.engine_driver.inspect_image_by_id(self.client, image)
image_data = self.engine_driver.inspect_image_by_id(self.client, image)
else:
repository, tag = parse_repository_tag(image)
if not tag:
tag = "latest"
image = self.engine_driver.inspect_image_by_name(
image_data = self.engine_driver.inspect_image_by_name(
self.client, repository, tag
)
return image or fallback
return image_data or fallback
def _get_image(self, container, needs_container_image=False):
def _get_image(
self, container: Container, needs_container_image: bool = False
) -> tuple[
dict[str, t.Any] | None, dict[str, t.Any] | None, dict[str, t.Any] | None
]:
image_parameter = self.param_image
get_container_image = needs_container_image or not image_parameter
container_image = (
@@ -553,7 +624,7 @@ class ContainerManager(DockerBaseClass):
if is_image_name_id(image_parameter):
image = self.engine_driver.inspect_image_by_id(self.client, image_parameter)
if image is None:
self.client.fail(f"Cannot find image with ID {image_parameter}")
self.fail(f"Cannot find image with ID {image_parameter}")
else:
repository, tag = parse_repository_tag(image_parameter)
if not tag:
@@ -562,7 +633,7 @@ class ContainerManager(DockerBaseClass):
self.client, repository, tag
)
if not image and self.param_pull == "never":
self.client.fail(
self.fail(
f"Cannot find image with name {repository}:{tag}, and pull=never"
)
if not image or self.param_pull == "always":
@@ -576,12 +647,12 @@ class ContainerManager(DockerBaseClass):
)
if already_to_latest:
self.results["changed"] = False
self.results["actions"].append(
self._add_action(
{"pulled_image": f"{repository}:{tag}", "changed": False}
)
else:
self.results["changed"] = True
self.results["actions"].append(
self._add_action(
{"pulled_image": f"{repository}:{tag}", "changed": True}
)
elif not image or self.param_pull_check_mode_behavior == "always":
@@ -589,10 +660,10 @@ class ContainerManager(DockerBaseClass):
# pull. (Implicitly: if the image is there, claim it already was latest unless
# pull_check_mode_behavior == 'always'.)
self.results["changed"] = True
action = {"pulled_image": f"{repository}:{tag}"}
action: dict[str, t.Any] = {"pulled_image": f"{repository}:{tag}"}
if not image:
action["changed"] = True
self.results["actions"].append(action)
self._add_action(action)
self.log("image")
self.log(image, pretty_print=True)
@@ -605,7 +676,9 @@ class ContainerManager(DockerBaseClass):
return image, container_image, comparison_image
def _image_is_different(self, image, container):
def _image_is_different(
self, image: dict[str, t.Any] | None, container: Container
) -> bool:
if image and image.get("Id"):
if container and container.image:
if image.get("Id") != container.image:
@@ -615,8 +688,9 @@ class ContainerManager(DockerBaseClass):
return True
return False
def _compose_create_parameters(self, image):
params = {}
def _compose_create_parameters(self, image: str) -> dict[str, t.Any]:
params: dict[str, t.Any] = {}
assert self.parameters is not None
for options, values in self.parameters:
engine = options.get_engine(self.engine_driver.name)
if engine.can_set_value(self.engine_driver.get_api_version(self.client)):
@@ -632,15 +706,16 @@ class ContainerManager(DockerBaseClass):
def _record_differences(
self,
differences,
options,
param_values,
engine,
container,
container_image,
image,
host_info,
differences: DifferenceTracker,
options: OptionGroup,
param_values: dict[str, t.Any],
engine: Engine,
container: Container,
container_image: dict[str, t.Any] | None,
image: dict[str, t.Any] | None,
host_info: dict[str, t.Any] | None,
):
assert container.raw is not None
container_values = engine.get_value(
self.module,
container.raw,
@@ -709,9 +784,16 @@ class ContainerManager(DockerBaseClass):
c = sorted(c, key=sort_key_fn)
differences.add(option.name, parameter=p, active=c)
def has_different_configuration(self, container, container_image, image, host_info):
def has_different_configuration(
self,
container: Container,
container_image: dict[str, t.Any] | None,
image: dict[str, t.Any] | None,
host_info: dict[str, t.Any] | None,
) -> tuple[bool, DifferenceTracker]:
differences = DifferenceTracker()
update_differences = DifferenceTracker()
assert self.parameters is not None
for options, param_values in self.parameters:
engine = options.get_engine(self.engine_driver.name)
if engine.can_update_value(self.engine_driver.get_api_version(self.client)):
@@ -743,9 +825,14 @@ class ContainerManager(DockerBaseClass):
return has_differences, differences
def has_different_resource_limits(
self, container, container_image, image, host_info
):
self,
container: Container,
container_image: dict[str, t.Any] | None,
image: dict[str, t.Any] | None,
host_info: dict[str, t.Any] | None,
) -> tuple[bool, DifferenceTracker]:
differences = DifferenceTracker()
assert self.parameters is not None
for options, param_values in self.parameters:
engine = options.get_engine(self.engine_driver.name)
if not engine.can_update_value(
@@ -765,8 +852,9 @@ class ContainerManager(DockerBaseClass):
has_differences = not differences.empty
return has_differences, differences
def _compose_update_parameters(self):
result = {}
def _compose_update_parameters(self) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
assert self.parameters is not None
for options, values in self.parameters:
engine = options.get_engine(self.engine_driver.name)
if not engine.can_update_value(
@@ -782,7 +870,13 @@ class ContainerManager(DockerBaseClass):
)
return result
def update_limits(self, container, container_image, image, host_info):
def update_limits(
self,
container: Container,
container_image: dict[str, t.Any] | None,
image: dict[str, t.Any] | None,
host_info: dict[str, t.Any] | None,
) -> Container:
limits_differ, different_limits = self.has_different_resource_limits(
container, container_image, image, host_info
)
@@ -793,20 +887,24 @@ class ContainerManager(DockerBaseClass):
)
self.diff_tracker.merge(different_limits)
if limits_differ and not self.check_mode:
assert container.id is not None
self.container_update(container.id, self._compose_update_parameters())
return self._get_container(container.id)
return container
def has_network_differences(self, container):
def has_network_differences(
self, container: Container
) -> tuple[bool, list[dict[str, t.Any]]]:
"""
Check if the container is connected to requested networks with expected options: links, aliases, ipv4, ipv6
"""
different = False
differences = []
differences: list[dict[str, t.Any]] = []
if not self.module.params["networks"]:
return different, differences
assert container.container is not None
if not container.container.get("NetworkSettings"):
self.fail(
"has_missing_networks: Error parsing container properties. NetworkSettings missing."
@@ -869,13 +967,16 @@ class ContainerManager(DockerBaseClass):
)
return different, differences
def has_extra_networks(self, container):
def has_extra_networks(
self, container: Container
) -> tuple[bool, list[dict[str, t.Any]]]:
"""
Check if the container is connected to non-requested networks
"""
extra_networks = []
extra_networks: list[dict[str, t.Any]] = []
extra = False
assert container.container is not None
if not container.container.get("NetworkSettings"):
self.fail(
"has_extra_networks: Error parsing container properties. NetworkSettings missing."
@@ -896,7 +997,9 @@ class ContainerManager(DockerBaseClass):
)
return extra, extra_networks
def update_networks(self, container, container_created):
def update_networks(
self, container: Container, container_created: bool
) -> Container:
updated_container = container
if self.all_options["networks"].comparison != "ignore" or container_created:
has_network_differences, network_differences = self.has_network_differences(
@@ -939,13 +1042,14 @@ class ContainerManager(DockerBaseClass):
updated_container = self._purge_networks(container, extra_networks)
return updated_container
def _add_networks(self, container, differences):
def _add_networks(
self, container: Container, differences: list[dict[str, t.Any]]
) -> Container:
assert container.id is not None
for diff in differences:
# remove the container from the network, if connected
if diff.get("container"):
self.results["actions"].append(
{"removed_from_network": diff["parameter"]["name"]}
)
self._add_action({"removed_from_network": diff["parameter"]["name"]})
if not self.check_mode:
try:
self.engine_driver.disconnect_container_from_network(
@@ -956,7 +1060,7 @@ class ContainerManager(DockerBaseClass):
f"Error disconnecting container from network {diff['parameter']['name']} - {exc}"
)
# connect to the network
self.results["actions"].append(
self._add_action(
{
"added_to_network": diff["parameter"]["name"],
"network_parameters": diff["parameter"],
@@ -982,9 +1086,12 @@ class ContainerManager(DockerBaseClass):
)
return self._get_container(container.id)
def _purge_networks(self, container, networks):
def _purge_networks(
self, container: Container, networks: list[dict[str, t.Any]]
) -> Container:
assert container.id is not None
for network in networks:
self.results["actions"].append({"removed_from_network": network["name"]})
self._add_action({"removed_from_network": network["name"]})
if not self.check_mode:
try:
self.engine_driver.disconnect_container_from_network(
@@ -996,7 +1103,7 @@ class ContainerManager(DockerBaseClass):
)
return self._get_container(container.id)
def container_create(self, image):
def container_create(self, image: str) -> Container | None:
create_parameters = self._compose_create_parameters(image)
self.log("create container")
self.log(f"image: {image} parameters:")
@@ -1014,7 +1121,7 @@ class ContainerManager(DockerBaseClass):
for key, value in network.items()
if key not in ("name", "id")
}
self.results["actions"].append(
self._add_action(
{
"created": "Created container",
"create_parameters": create_parameters,
@@ -1022,7 +1129,6 @@ class ContainerManager(DockerBaseClass):
}
)
self.results["changed"] = True
new_container = None
if not self.check_mode:
try:
container_id = self.engine_driver.create_container(
@@ -1031,11 +1137,11 @@ class ContainerManager(DockerBaseClass):
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error creating container: {exc}")
return self._get_container(container_id)
return new_container
return None
def container_start(self, container_id):
def container_start(self, container_id: str) -> Container:
self.log(f"start container {container_id}")
self.results["actions"].append({"started": container_id})
self._add_action({"started": container_id})
self.results["changed"] = True
if not self.check_mode:
try:
@@ -1047,9 +1153,11 @@ class ContainerManager(DockerBaseClass):
status = self.engine_driver.wait_for_container(
self.client, container_id
)
self.client.fail_results["status"] = status
# mypy doesn't know that Client has fail_results property
self.client.fail_results["status"] = status # type: ignore
self.results["status"] = status
output: str | bytes
if self.module.params["auto_remove"]:
output = "Cannot retrieve result as auto_remove is enabled"
if self.param_output_logs:
@@ -1077,12 +1185,14 @@ class ContainerManager(DockerBaseClass):
return insp
return self._get_container(container_id)
def container_remove(self, container_id, link=False, force=False):
def container_remove(
self, container_id: str, link: bool = False, force: bool = False
) -> None:
volume_state = not self.param_keep_volumes
self.log(
f"remove container container:{container_id} v:{volume_state} link:{link} force{force}"
)
self.results["actions"].append(
self._add_action(
{
"removed": container_id,
"volume_state": volume_state,
@@ -1101,13 +1211,15 @@ class ContainerManager(DockerBaseClass):
force=force,
)
except Exception as exc: # pylint: disable=broad-exception-caught
self.client.fail(f"Error removing container {container_id}: {exc}")
self.fail(f"Error removing container {container_id}: {exc}")
def container_update(self, container_id, update_parameters):
def container_update(
self, container_id: str, update_parameters: dict[str, t.Any]
) -> Container:
if update_parameters:
self.log(f"update container {container_id}")
self.log(update_parameters, pretty_print=True)
self.results["actions"].append(
self._add_action(
{"updated": container_id, "update_parameters": update_parameters}
)
self.results["changed"] = True
@@ -1120,10 +1232,8 @@ class ContainerManager(DockerBaseClass):
self.fail(f"Error updating container {container_id}: {exc}")
return self._get_container(container_id)
def container_kill(self, container_id):
self.results["actions"].append(
{"killed": container_id, "signal": self.param_kill_signal}
)
def container_kill(self, container_id: str) -> None:
self._add_action({"killed": container_id, "signal": self.param_kill_signal})
self.results["changed"] = True
if not self.check_mode:
try:
@@ -1133,8 +1243,8 @@ class ContainerManager(DockerBaseClass):
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error killing container {container_id}: {exc}")
def container_restart(self, container_id):
self.results["actions"].append(
def container_restart(self, container_id: str) -> Container:
self._add_action(
{"restarted": container_id, "timeout": self.module.params["stop_timeout"]}
)
self.results["changed"] = True
@@ -1147,11 +1257,11 @@ class ContainerManager(DockerBaseClass):
self.fail(f"Error restarting container {container_id}: {exc}")
return self._get_container(container_id)
def container_stop(self, container_id):
def container_stop(self, container_id: str) -> None:
if self.param_force_kill:
self.container_kill(container_id)
return
self.results["actions"].append(
self._add_action(
{"stopped": container_id, "timeout": self.module.params["stop_timeout"]}
)
self.results["changed"] = True
@@ -1164,7 +1274,7 @@ class ContainerManager(DockerBaseClass):
self.fail(f"Error stopping container {container_id}: {exc}")
def run_module(engine_driver):
def run_module(engine_driver: EngineDriver) -> None:
module, active_options, client = engine_driver.setup(
argument_spec={
"cleanup": {"type": "bool", "default": False},
@@ -1228,7 +1338,7 @@ def run_module(engine_driver):
],
)
def execute():
def execute() -> t.NoReturn:
cm = ContainerManager(module, engine_driver, client, active_options)
cm.run()
module.exit_json(**sanitize_result(cm.results))