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
+1 -1
View File
@@ -80,7 +80,7 @@ import re
from ansible.module_utils.basic import AnsibleModule
def main():
def main() -> None:
module = AnsibleModule({}, supports_check_mode=True)
cpuset_path = "/proc/self/cpuset"
+39 -32
View File
@@ -437,6 +437,7 @@ actions:
"""
import traceback
import typing as t
from ansible.module_utils.common.validation import check_type_int
@@ -455,26 +456,32 @@ from ansible_collections.community.docker.plugins.module_utils._version import (
class ServicesManager(BaseComposeManager):
def __init__(self, client):
def __init__(self, client: AnsibleModuleDockerClient) -> None:
super().__init__(client)
parameters = self.client.module.params
self.state = parameters["state"]
self.dependencies = parameters["dependencies"]
self.pull = parameters["pull"]
self.build = parameters["build"]
self.ignore_build_events = parameters["ignore_build_events"]
self.recreate = parameters["recreate"]
self.remove_images = parameters["remove_images"]
self.remove_volumes = parameters["remove_volumes"]
self.remove_orphans = parameters["remove_orphans"]
self.renew_anon_volumes = parameters["renew_anon_volumes"]
self.timeout = parameters["timeout"]
self.services = parameters["services"] or []
self.scale = parameters["scale"] or {}
self.wait = parameters["wait"]
self.wait_timeout = parameters["wait_timeout"]
self.yes = parameters["assume_yes"]
self.state: t.Literal["absent", "present", "stopped", "restarted"] = parameters[
"state"
]
self.dependencies: bool = parameters["dependencies"]
self.pull: t.Literal["always", "missing", "never", "policy"] = parameters[
"pull"
]
self.build: t.Literal["always", "never", "policy"] = parameters["build"]
self.ignore_build_events: bool = parameters["ignore_build_events"]
self.recreate: t.Literal["always", "never", "auto"] = parameters["recreate"]
self.remove_images: t.Literal["all", "local"] | None = parameters[
"remove_images"
]
self.remove_volumes: bool = parameters["remove_volumes"]
self.remove_orphans: bool = parameters["remove_orphans"]
self.renew_anon_volumes: bool = parameters["renew_anon_volumes"]
self.timeout: int | None = parameters["timeout"]
self.services: list[str] = parameters["services"] or []
self.scale: dict[str, t.Any] = parameters["scale"] or {}
self.wait: bool = parameters["wait"]
self.wait_timeout: int | None = parameters["wait_timeout"]
self.yes: bool = parameters["assume_yes"]
if self.compose_version < LooseVersion("2.32.0") and self.yes:
self.fail(
f"assume_yes=true needs Docker Compose 2.32.0 or newer, not version {self.compose_version}"
@@ -491,7 +498,7 @@ class ServicesManager(BaseComposeManager):
self.fail(f"The value {value!r} for `scale[{key!r}]` is negative")
self.scale[key] = value
def run(self):
def run(self) -> dict[str, t.Any]:
if self.state == "present":
result = self.cmd_up()
elif self.state == "stopped":
@@ -508,7 +515,7 @@ class ServicesManager(BaseComposeManager):
self.cleanup_result(result)
return result
def get_up_cmd(self, dry_run, no_start=False):
def get_up_cmd(self, dry_run: bool, no_start: bool = False) -> list[str]:
args = self.get_base_args() + ["up", "--detach", "--no-color", "--quiet-pull"]
if self.pull != "policy":
args.extend(["--pull", self.pull])
@@ -549,8 +556,8 @@ class ServicesManager(BaseComposeManager):
args.append(service)
return args
def cmd_up(self):
result = {}
def cmd_up(self) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
args = self.get_up_cmd(self.check_mode)
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
@@ -566,7 +573,7 @@ class ServicesManager(BaseComposeManager):
self.update_failed(result, events, args, stdout, stderr, rc)
return result
def get_stop_cmd(self, dry_run):
def get_stop_cmd(self, dry_run: bool) -> list[str]:
args = self.get_base_args() + ["stop"]
if self.timeout is not None:
args.extend(["--timeout", f"{self.timeout}"])
@@ -577,17 +584,17 @@ class ServicesManager(BaseComposeManager):
args.append(service)
return args
def _are_containers_stopped(self):
def _are_containers_stopped(self) -> bool:
for container in self.list_containers_raw():
if container["State"] not in ("created", "exited", "stopped", "killed"):
return False
return True
def cmd_stop(self):
def cmd_stop(self) -> dict[str, t.Any]:
# Since 'docker compose stop' **always** claims it is stopping containers, even if they are already
# stopped, we have to do this a bit more complicated.
result = {}
result: dict[str, t.Any] = {}
# Make sure all containers are created
args_1 = self.get_up_cmd(self.check_mode, no_start=True)
rc_1, stdout_1, stderr_1 = self.client.call_cli(*args_1, cwd=self.project_src)
@@ -630,7 +637,7 @@ class ServicesManager(BaseComposeManager):
)
return result
def get_restart_cmd(self, dry_run):
def get_restart_cmd(self, dry_run: bool) -> list[str]:
args = self.get_base_args() + ["restart"]
if not self.dependencies:
args.append("--no-deps")
@@ -643,8 +650,8 @@ class ServicesManager(BaseComposeManager):
args.append(service)
return args
def cmd_restart(self):
result = {}
def cmd_restart(self) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
args = self.get_restart_cmd(self.check_mode)
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
@@ -653,7 +660,7 @@ class ServicesManager(BaseComposeManager):
self.update_failed(result, events, args, stdout, stderr, rc)
return result
def get_down_cmd(self, dry_run):
def get_down_cmd(self, dry_run: bool) -> list[str]:
args = self.get_base_args() + ["down"]
if self.remove_orphans:
args.append("--remove-orphans")
@@ -670,8 +677,8 @@ class ServicesManager(BaseComposeManager):
args.append(service)
return args
def cmd_down(self):
result = {}
def cmd_down(self) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
args = self.get_down_cmd(self.check_mode)
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
@@ -681,7 +688,7 @@ class ServicesManager(BaseComposeManager):
return result
def main():
def main() -> None:
argument_spec = {
"state": {
"type": "str",
+21 -17
View File
@@ -166,6 +166,7 @@ rc:
import shlex
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_text
@@ -180,29 +181,32 @@ from ansible_collections.community.docker.plugins.module_utils._compose_v2 impor
class ExecManager(BaseComposeManager):
def __init__(self, client):
def __init__(self, client: AnsibleModuleDockerClient) -> None:
super().__init__(client)
parameters = self.client.module.params
self.service = parameters["service"]
self.index = parameters["index"]
self.chdir = parameters["chdir"]
self.detach = parameters["detach"]
self.user = parameters["user"]
self.stdin = parameters["stdin"]
self.strip_empty_ends = parameters["strip_empty_ends"]
self.privileged = parameters["privileged"]
self.tty = parameters["tty"]
self.env = parameters["env"]
self.service: str = parameters["service"]
self.index: int | None = parameters["index"]
self.chdir: str | None = parameters["chdir"]
self.detach: bool = parameters["detach"]
self.user: str | None = parameters["user"]
self.stdin: str | None = parameters["stdin"]
self.strip_empty_ends: bool = parameters["strip_empty_ends"]
self.privileged: bool = parameters["privileged"]
self.tty: bool = parameters["tty"]
self.env: dict[str, t.Any] = parameters["env"]
self.argv = parameters["argv"]
self.argv: list[str]
if parameters["command"] is not None:
self.argv = shlex.split(parameters["command"])
else:
self.argv = parameters["argv"]
if self.detach and self.stdin is not None:
self.fail("If detach=true, stdin cannot be provided.")
if self.stdin is not None and parameters["stdin_add_newline"]:
stdin_add_newline: bool = parameters["stdin_add_newline"]
if self.stdin is not None and stdin_add_newline:
self.stdin += "\n"
if self.env is not None:
@@ -214,7 +218,7 @@ class ExecManager(BaseComposeManager):
)
self.env[name] = to_text(value, errors="surrogate_or_strict")
def get_exec_cmd(self, dry_run, no_start=False):
def get_exec_cmd(self, dry_run: bool) -> list[str]:
args = self.get_base_args(plain_progress=True) + ["exec"]
if self.index is not None:
args.extend(["--index", str(self.index)])
@@ -237,9 +241,9 @@ class ExecManager(BaseComposeManager):
args.extend(self.argv)
return args
def run(self):
def run(self) -> dict[str, t.Any]:
args = self.get_exec_cmd(self.check_mode)
kwargs = {
kwargs: dict[str, t.Any] = {
"cwd": self.project_src,
}
if self.stdin is not None:
@@ -262,7 +266,7 @@ class ExecManager(BaseComposeManager):
}
def main():
def main() -> None:
argument_spec = {
"service": {"type": "str", "required": True},
"index": {"type": "int"},
+10 -9
View File
@@ -111,6 +111,7 @@ actions:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._common_cli import (
AnsibleModuleDockerClient,
@@ -126,14 +127,14 @@ from ansible_collections.community.docker.plugins.module_utils._version import (
class PullManager(BaseComposeManager):
def __init__(self, client):
def __init__(self, client: AnsibleModuleDockerClient) -> None:
super().__init__(client)
parameters = self.client.module.params
self.policy = parameters["policy"]
self.ignore_buildable = parameters["ignore_buildable"]
self.include_deps = parameters["include_deps"]
self.services = parameters["services"] or []
self.policy: t.Literal["always", "missing"] = parameters["policy"]
self.ignore_buildable: bool = parameters["ignore_buildable"]
self.include_deps: bool = parameters["include_deps"]
self.services: list[str] = parameters["services"] or []
if self.policy != "always" and self.compose_version < LooseVersion("2.22.0"):
# https://github.com/docker/compose/pull/10981 - 2.22.0
@@ -146,7 +147,7 @@ class PullManager(BaseComposeManager):
f"--ignore-buildable is only supported since Docker Compose 2.15.0. {self.client.get_cli()} has version {self.compose_version}"
)
def get_pull_cmd(self, dry_run, no_start=False):
def get_pull_cmd(self, dry_run: bool):
args = self.get_base_args() + ["pull"]
if self.policy != "always":
args.extend(["--policy", self.policy])
@@ -161,8 +162,8 @@ class PullManager(BaseComposeManager):
args.append(service)
return args
def run(self):
result = {}
def run(self) -> dict[str, t.Any]:
result: dict[str, t.Any] = {}
args = self.get_pull_cmd(self.check_mode)
rc, stdout, stderr = self.client.call_cli(*args, cwd=self.project_src)
events = self.parse_events(stderr, dry_run=self.check_mode, nonzero_rc=rc != 0)
@@ -179,7 +180,7 @@ class PullManager(BaseComposeManager):
return result
def main():
def main() -> None:
argument_spec = {
"policy": {
"type": "str",
+34 -30
View File
@@ -239,6 +239,7 @@ rc:
import shlex
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_text
@@ -253,42 +254,45 @@ from ansible_collections.community.docker.plugins.module_utils._compose_v2 impor
class ExecManager(BaseComposeManager):
def __init__(self, client):
def __init__(self, client: AnsibleModuleDockerClient) -> None:
super().__init__(client)
parameters = self.client.module.params
self.service = parameters["service"]
self.build = parameters["build"]
self.cap_add = parameters["cap_add"]
self.cap_drop = parameters["cap_drop"]
self.entrypoint = parameters["entrypoint"]
self.interactive = parameters["interactive"]
self.labels = parameters["labels"]
self.name = parameters["name"]
self.no_deps = parameters["no_deps"]
self.publish = parameters["publish"]
self.quiet_pull = parameters["quiet_pull"]
self.remove_orphans = parameters["remove_orphans"]
self.do_cleanup = parameters["cleanup"]
self.service_ports = parameters["service_ports"]
self.use_aliases = parameters["use_aliases"]
self.volumes = parameters["volumes"]
self.chdir = parameters["chdir"]
self.detach = parameters["detach"]
self.user = parameters["user"]
self.stdin = parameters["stdin"]
self.strip_empty_ends = parameters["strip_empty_ends"]
self.tty = parameters["tty"]
self.env = parameters["env"]
self.service: str = parameters["service"]
self.build: bool = parameters["build"]
self.cap_add: list[str] | None = parameters["cap_add"]
self.cap_drop: list[str] | None = parameters["cap_drop"]
self.entrypoint: str | None = parameters["entrypoint"]
self.interactive: bool = parameters["interactive"]
self.labels: list[str] | None = parameters["labels"]
self.name: str | None = parameters["name"]
self.no_deps: bool = parameters["no_deps"]
self.publish: list[str] | None = parameters["publish"]
self.quiet_pull: bool = parameters["quiet_pull"]
self.remove_orphans: bool = parameters["remove_orphans"]
self.do_cleanup: bool = parameters["cleanup"]
self.service_ports: bool = parameters["service_ports"]
self.use_aliases: bool = parameters["use_aliases"]
self.volumes: list[str] | None = parameters["volumes"]
self.chdir: str | None = parameters["chdir"]
self.detach: bool = parameters["detach"]
self.user: str | None = parameters["user"]
self.stdin: str | None = parameters["stdin"]
self.strip_empty_ends: bool = parameters["strip_empty_ends"]
self.tty: bool = parameters["tty"]
self.env: dict[str, t.Any] | None = parameters["env"]
self.argv = parameters["argv"]
self.argv: list[str]
if parameters["command"] is not None:
self.argv = shlex.split(parameters["command"])
else:
self.argv = parameters["argv"]
if self.detach and self.stdin is not None:
self.fail("If detach=true, stdin cannot be provided.")
if self.stdin is not None and parameters["stdin_add_newline"]:
stdin_add_newline: bool = parameters["stdin_add_newline"]
if self.stdin is not None and stdin_add_newline:
self.stdin += "\n"
if self.env is not None:
@@ -300,7 +304,7 @@ class ExecManager(BaseComposeManager):
)
self.env[name] = to_text(value, errors="surrogate_or_strict")
def get_run_cmd(self, dry_run, no_start=False):
def get_run_cmd(self, dry_run: bool) -> list[str]:
args = self.get_base_args(plain_progress=True) + ["run"]
if self.build:
args.append("--build")
@@ -355,9 +359,9 @@ class ExecManager(BaseComposeManager):
args.extend(self.argv)
return args
def run(self):
def run(self) -> dict[str, t.Any]:
args = self.get_run_cmd(self.check_mode)
kwargs = {
kwargs: dict[str, t.Any] = {
"cwd": self.project_src,
}
if self.stdin is not None:
@@ -382,7 +386,7 @@ class ExecManager(BaseComposeManager):
}
def main():
def main() -> None:
argument_spec = {
"service": {"type": "str", "required": True},
"argv": {"type": "list", "elements": "str"},
+1 -1
View File
@@ -1355,7 +1355,7 @@ from ansible_collections.community.docker.plugins.module_utils._module_container
)
def main():
def main() -> None:
engine_driver = DockerAPIEngineDriver()
run_module(engine_driver)
+154 -119
View File
@@ -169,6 +169,7 @@ import io
import os
import stat
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.common.validation import check_type_int
@@ -198,23 +199,29 @@ from ansible_collections.community.docker.plugins.module_utils._scramble import
)
def are_fileobjs_equal(f1, f2):
if t.TYPE_CHECKING:
import tarfile
def are_fileobjs_equal(f1: t.IO[bytes], f2: t.IO[bytes]) -> bool:
"""Given two (buffered) file objects, compare their contents."""
f1on: t.IO[bytes] | None = f1
f2on: t.IO[bytes] | None = f2
blocksize = 65536
b1buf = b""
b2buf = b""
while True:
if f1 and len(b1buf) < blocksize:
f1b = f1.read(blocksize)
if f1on and len(b1buf) < blocksize:
f1b = f1on.read(blocksize)
if not f1b:
# f1 is EOF, so stop reading from it
f1 = None
f1on = None
b1buf += f1b
if f2 and len(b2buf) < blocksize:
f2b = f2.read(blocksize)
if f2on and len(b2buf) < blocksize:
f2b = f2on.read(blocksize)
if not f2b:
# f2 is EOF, so stop reading from it
f2 = None
f2on = None
b2buf += f2b
if not b1buf or not b2buf:
# At least one of f1 and f2 is EOF and all its data has
@@ -229,29 +236,33 @@ def are_fileobjs_equal(f1, f2):
b2buf = b2buf[buflen:]
def are_fileobjs_equal_read_first(f1, f2):
def are_fileobjs_equal_read_first(
f1: t.IO[bytes], f2: t.IO[bytes]
) -> tuple[bool, bytes]:
"""Given two (buffered) file objects, compare their contents.
Returns a tuple (is_equal, content_of_f1), where the first element indicates
whether the two file objects have the same content, and the second element is
the content of the first file object."""
f1on: t.IO[bytes] | None = f1
f2on: t.IO[bytes] | None = f2
blocksize = 65536
b1buf = b""
b2buf = b""
is_equal = True
content = []
while True:
if f1 and len(b1buf) < blocksize:
f1b = f1.read(blocksize)
if f1on and len(b1buf) < blocksize:
f1b = f1on.read(blocksize)
if not f1b:
# f1 is EOF, so stop reading from it
f1 = None
f1on = None
b1buf += f1b
if f2 and len(b2buf) < blocksize:
f2b = f2.read(blocksize)
if f2on and len(b2buf) < blocksize:
f2b = f2on.read(blocksize)
if not f2b:
# f2 is EOF, so stop reading from it
f2 = None
f2on = None
b2buf += f2b
if not b1buf or not b2buf:
# At least one of f1 and f2 is EOF and all its data has
@@ -269,13 +280,13 @@ def are_fileobjs_equal_read_first(f1, f2):
b2buf = b2buf[buflen:]
content.append(b1buf)
if f1:
content.append(f1.read())
if f1on:
content.append(f1on.read())
return is_equal, b"".join(content)
def is_container_file_not_regular_file(container_stat):
def is_container_file_not_regular_file(container_stat: dict[str, t.Any]) -> bool:
for bit in (
# https://pkg.go.dev/io/fs#FileMode
32 - 1, # ModeDir
@@ -292,7 +303,7 @@ def is_container_file_not_regular_file(container_stat):
return False
def get_container_file_mode(container_stat):
def get_container_file_mode(container_stat: dict[str, t.Any]) -> int:
mode = container_stat["mode"] & 0xFFF
if container_stat["mode"] & (1 << (32 - 9)) != 0: # ModeSetuid
mode |= stat.S_ISUID # set UID bit
@@ -303,7 +314,9 @@ def get_container_file_mode(container_stat):
return mode
def add_other_diff(diff, in_path, member):
def add_other_diff(
diff: dict[str, t.Any] | None, in_path: str, member: tarfile.TarInfo
) -> None:
if diff is None:
return
diff["before_header"] = in_path
@@ -326,14 +339,14 @@ def add_other_diff(diff, in_path, member):
def retrieve_diff(
client,
container,
container_path,
follow_links,
diff,
max_file_size_for_diff,
regular_stat=None,
link_target=None,
client: AnsibleDockerClient,
container: str,
container_path: str,
follow_links: bool,
diff: dict[str, t.Any] | None,
max_file_size_for_diff: int,
regular_stat: dict[str, t.Any] | None = None,
link_target: str | None = None,
):
if diff is None:
return
@@ -377,19 +390,21 @@ def retrieve_diff(
return
# We need to get hold of the content
def process_none(in_path):
def process_none(in_path: str) -> None:
diff["before"] = ""
def process_regular(in_path, tar, member):
def process_regular(
in_path: str, tar: tarfile.TarFile, member: tarfile.TarInfo
) -> None:
add_diff_dst_from_regular_member(
diff, max_file_size_for_diff, in_path, tar, member
)
def process_symlink(in_path, member):
def process_symlink(in_path: str, member: tarfile.TarInfo) -> None:
diff["before_header"] = in_path
diff["before"] = member.linkname
def process_other(in_path, member):
def process_other(in_path: str, member: tarfile.TarInfo) -> None:
add_other_diff(diff, in_path, member)
fetch_file_ex(
@@ -404,7 +419,7 @@ def retrieve_diff(
)
def is_binary(content):
def is_binary(content: bytes) -> bool:
if b"\x00" in content:
return True
# TODO: better detection
@@ -413,8 +428,13 @@ def is_binary(content):
def are_fileobjs_equal_with_diff_of_first(
f1, f2, size, diff, max_file_size_for_diff, container_path
):
f1: t.IO[bytes],
f2: t.IO[bytes],
size: int,
diff: dict[str, t.Any] | None,
max_file_size_for_diff: int,
container_path: str,
) -> bool:
if diff is None:
return are_fileobjs_equal(f1, f2)
if size > max_file_size_for_diff > 0:
@@ -430,15 +450,22 @@ def are_fileobjs_equal_with_diff_of_first(
def add_diff_dst_from_regular_member(
diff, max_file_size_for_diff, container_path, tar, member
):
diff: dict[str, t.Any] | None,
max_file_size_for_diff: int,
container_path: str,
tar: tarfile.TarFile,
member: tarfile.TarInfo,
) -> None:
if diff is None:
return
if member.size > max_file_size_for_diff > 0:
diff["dst_larger"] = max_file_size_for_diff
return
with tar.extractfile(member) as tar_f:
mf = tar.extractfile(member)
if not mf:
raise AssertionError("Member should be present for regular file")
with mf as tar_f:
content = tar_f.read()
if is_binary(content):
@@ -448,35 +475,35 @@ def add_diff_dst_from_regular_member(
diff["before"] = to_text(content)
def copy_dst_to_src(diff):
def copy_dst_to_src(diff: dict[str, t.Any] | None) -> None:
if diff is None:
return
for f, t in [
for frm, to in [
("dst_size", "src_size"),
("dst_binary", "src_binary"),
("before_header", "after_header"),
("before", "after"),
]:
if f in diff:
diff[t] = diff[f]
elif t in diff:
diff.pop(t)
if frm in diff:
diff[to] = diff[frm]
elif to in diff:
diff.pop(to)
def is_file_idempotent(
client,
container,
managed_path,
container_path,
follow_links,
local_follow_links,
client: AnsibleDockerClient,
container: str,
managed_path: str,
container_path: str,
follow_links: bool,
local_follow_links: bool,
owner_id,
group_id,
mode,
force=False,
diff=None,
max_file_size_for_diff=1,
):
force: bool | None = False,
diff: dict[str, t.Any] | None = None,
max_file_size_for_diff: int = 1,
) -> tuple[str, int, bool]:
# Retrieve information of local file
try:
file_stat = (
@@ -644,10 +671,12 @@ def is_file_idempotent(
return container_path, mode, False
# Fetch file from container
def process_none(in_path):
def process_none(in_path: str) -> tuple[str, int, bool]:
return container_path, mode, False
def process_regular(in_path, tar, member):
def process_regular(
in_path: str, tar: tarfile.TarFile, member: tarfile.TarInfo
) -> tuple[str, int, bool]:
# Check things like user/group ID and mode
if any(
[
@@ -663,14 +692,17 @@ def is_file_idempotent(
)
return container_path, mode, False
with tar.extractfile(member) as tar_f:
mf = tar.extractfile(member)
if mf is None:
raise AssertionError("Member should be present for regular file")
with mf as tar_f:
with open(managed_path, "rb") as local_f:
is_equal = are_fileobjs_equal_with_diff_of_first(
tar_f, local_f, member.size, diff, max_file_size_for_diff, in_path
)
return container_path, mode, is_equal
def process_symlink(in_path, member):
def process_symlink(in_path: str, member: tarfile.TarInfo) -> tuple[str, int, bool]:
if diff is not None:
diff["before_header"] = in_path
diff["before"] = member.linkname
@@ -689,7 +721,7 @@ def is_file_idempotent(
local_link_target = os.readlink(managed_path)
return container_path, mode, member.linkname == local_link_target
def process_other(in_path, member):
def process_other(in_path: str, member: tarfile.TarInfo) -> tuple[str, int, bool]:
add_other_diff(diff, in_path, member)
return container_path, mode, False
@@ -706,23 +738,21 @@ def is_file_idempotent(
def copy_file_into_container(
client,
container,
managed_path,
container_path,
follow_links,
local_follow_links,
client: AnsibleDockerClient,
container: str,
managed_path: str,
container_path: str,
follow_links: bool,
local_follow_links: bool,
owner_id,
group_id,
mode,
force=False,
diff=False,
max_file_size_for_diff=1,
):
if diff:
diff = {}
else:
diff = None
force: bool | None = False,
do_diff: bool = False,
max_file_size_for_diff: int = 1,
) -> t.NoReturn:
diff: dict[str, t.Any] | None
diff = {} if do_diff else None
container_path, mode, idempotent = is_file_idempotent(
client,
@@ -762,18 +792,18 @@ def copy_file_into_container(
def is_content_idempotent(
client,
container,
content,
container_path,
follow_links,
client: AnsibleDockerClient,
container: str,
content: bytes,
container_path: str,
follow_links: bool,
owner_id,
group_id,
mode,
force=False,
diff=None,
max_file_size_for_diff=1,
):
force: bool | None = False,
diff: dict[str, t.Any] | None = None,
max_file_size_for_diff: int = 1,
) -> tuple[str, int, bool]:
if diff is not None:
if len(content) > max_file_size_for_diff > 0:
diff["src_larger"] = max_file_size_for_diff
@@ -894,12 +924,14 @@ def is_content_idempotent(
return container_path, mode, False
# Fetch file from container
def process_none(in_path):
def process_none(in_path: str) -> tuple[str, int, bool]:
if diff is not None:
diff["before"] = ""
return container_path, mode, False
def process_regular(in_path, tar, member):
def process_regular(
in_path: str, tar: tarfile.TarFile, member: tarfile.TarInfo
) -> tuple[str, int, bool]:
# Check things like user/group ID and mode
if any(
[
@@ -914,7 +946,10 @@ def is_content_idempotent(
)
return container_path, mode, False
with tar.extractfile(member) as tar_f:
mf = tar.extractfile(member)
if mf is None:
raise AssertionError("Member should be present for regular file")
with mf as tar_f:
is_equal = are_fileobjs_equal_with_diff_of_first(
tar_f,
io.BytesIO(content),
@@ -925,14 +960,14 @@ def is_content_idempotent(
)
return container_path, mode, is_equal
def process_symlink(in_path, member):
def process_symlink(in_path: str, member: tarfile.TarInfo) -> tuple[str, int, bool]:
if diff is not None:
diff["before_header"] = in_path
diff["before"] = member.linkname
return container_path, mode, False
def process_other(in_path, member):
def process_other(in_path: str, member: tarfile.TarInfo) -> tuple[str, int, bool]:
add_other_diff(diff, in_path, member)
return container_path, mode, False
@@ -949,22 +984,19 @@ def is_content_idempotent(
def copy_content_into_container(
client,
container,
content,
container_path,
follow_links,
client: AnsibleDockerClient,
container: str,
content: bytes,
container_path: str,
follow_links: bool,
owner_id,
group_id,
mode,
force=False,
diff=False,
max_file_size_for_diff=1,
):
if diff:
diff = {}
else:
diff = None
force: bool | None = False,
do_diff: bool = False,
max_file_size_for_diff: int = 1,
) -> t.NoReturn:
diff: dict[str, t.Any] | None = {} if do_diff else None
container_path, mode, idempotent = is_content_idempotent(
client,
@@ -1007,7 +1039,7 @@ def copy_content_into_container(
client.module.exit_json(**result)
def parse_modern(mode):
def parse_modern(mode: str | int) -> int:
if isinstance(mode, str):
return int(to_native(mode), 8)
if isinstance(mode, int):
@@ -1015,13 +1047,13 @@ def parse_modern(mode):
raise TypeError(f"must be an octal string or an integer, got {mode!r}")
def parse_octal_string_only(mode):
def parse_octal_string_only(mode: str) -> int:
if isinstance(mode, str):
return int(to_native(mode), 8)
raise TypeError(f"must be an octal string, got {mode!r}")
def main():
def main() -> None:
argument_spec = {
"container": {"type": "str", "required": True},
"path": {"type": "path"},
@@ -1054,20 +1086,22 @@ def main():
},
)
container = client.module.params["container"]
managed_path = client.module.params["path"]
container_path = client.module.params["container_path"]
follow = client.module.params["follow"]
local_follow = client.module.params["local_follow"]
owner_id = client.module.params["owner_id"]
group_id = client.module.params["group_id"]
mode = client.module.params["mode"]
force = client.module.params["force"]
content = client.module.params["content"]
max_file_size_for_diff = client.module.params["_max_file_size_for_diff"] or 1
container: str = client.module.params["container"]
managed_path: str | None = client.module.params["path"]
container_path: str = client.module.params["container_path"]
follow: bool = client.module.params["follow"]
local_follow: bool = client.module.params["local_follow"]
owner_id: int | None = client.module.params["owner_id"]
group_id: int | None = client.module.params["group_id"]
mode: t.Any = client.module.params["mode"]
force: bool | None = client.module.params["force"]
content_str: str | None = client.module.params["content"]
max_file_size_for_diff: int = client.module.params["_max_file_size_for_diff"] or 1
if mode is not None:
mode_parse = client.module.params["mode_parse"]
mode_parse: t.Literal["legacy", "modern", "octal_string_only"] = (
client.module.params["mode_parse"]
)
try:
if mode_parse == "legacy":
mode = check_type_int(mode)
@@ -1080,14 +1114,15 @@ def main():
if mode < 0:
client.fail(f"'mode' must not be negative; got {mode}")
if content is not None:
content: bytes | None = None
if content_str is not None:
if client.module.params["content_is_b64"]:
try:
content = base64.b64decode(content)
content = base64.b64decode(content_str)
except Exception as e: # pylint: disable=broad-exception-caught
client.fail(f"Cannot Base64 decode the content option: {e}")
else:
content = to_bytes(content)
content = to_bytes(content_str)
if not container_path.startswith(os.path.sep):
container_path = os.path.join(os.path.sep, container_path)
@@ -1108,7 +1143,7 @@ def main():
group_id=group_id,
mode=mode,
force=force,
diff=client.module._diff,
do_diff=client.module._diff,
max_file_size_for_diff=max_file_size_for_diff,
)
elif managed_path is not None:
@@ -1123,7 +1158,7 @@ def main():
group_id=group_id,
mode=mode,
force=force,
diff=client.module._diff,
do_diff=client.module._diff,
max_file_size_for_diff=max_file_size_for_diff,
)
else:
+32 -19
View File
@@ -165,6 +165,7 @@ exec_id:
import shlex
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_text
@@ -185,7 +186,7 @@ from ansible_collections.community.docker.plugins.module_utils._socket_handler i
)
def main():
def main() -> None:
argument_spec = {
"container": {"type": "str", "required": True},
"argv": {"type": "list", "elements": "str"},
@@ -211,16 +212,16 @@ def main():
required_one_of=[("argv", "command")],
)
container = client.module.params["container"]
argv = client.module.params["argv"]
command = client.module.params["command"]
chdir = client.module.params["chdir"]
detach = client.module.params["detach"]
user = client.module.params["user"]
stdin = client.module.params["stdin"]
strip_empty_ends = client.module.params["strip_empty_ends"]
tty = client.module.params["tty"]
env = client.module.params["env"]
container: str = client.module.params["container"]
argv: list[str] | None = client.module.params["argv"]
command: str | None = client.module.params["command"]
chdir: str | None = client.module.params["chdir"]
detach: bool = client.module.params["detach"]
user: str | None = client.module.params["user"]
stdin: str | None = client.module.params["stdin"]
strip_empty_ends: bool = client.module.params["strip_empty_ends"]
tty: bool = client.module.params["tty"]
env: dict[str, t.Any] = client.module.params["env"]
if env is not None:
for name, value in list(env.items()):
@@ -233,6 +234,7 @@ def main():
if command is not None:
argv = shlex.split(command)
assert argv is not None
if detach and stdin is not None:
client.module.fail_json(msg="If detach=true, stdin cannot be provided.")
@@ -258,7 +260,7 @@ def main():
exec_data = client.post_json_to_json(
"/containers/{0}/exec", container, data=data
)
exec_id = exec_data["Id"]
exec_id: str = exec_data["Id"]
data = {
"Tty": tty,
@@ -269,6 +271,8 @@ def main():
client.module.exit_json(changed=True, exec_id=exec_id)
else:
stdout: bytes | None
stderr: bytes | None
if stdin and not detach:
exec_socket = client.post_json_to_stream_socket(
"/exec/{0}/start", exec_id, data=data
@@ -283,28 +287,37 @@ def main():
stdout, stderr = exec_socket_handler.consume()
finally:
exec_socket.close()
elif tty:
stdout, stderr = client.post_json_to_stream(
"/exec/{0}/start",
exec_id,
data=data,
stream=False,
tty=True,
demux=True,
)
else:
stdout, stderr = client.post_json_to_stream(
"/exec/{0}/start",
exec_id,
data=data,
stream=False,
tty=tty,
tty=False,
demux=True,
)
result = client.get_json("/exec/{0}/json", exec_id)
stdout = to_text(stdout or b"")
stderr = to_text(stderr or b"")
stdout_t = to_text(stdout or b"")
stderr_t = to_text(stderr or b"")
if strip_empty_ends:
stdout = stdout.rstrip("\r\n")
stderr = stderr.rstrip("\r\n")
stdout_t = stdout_t.rstrip("\r\n")
stderr_t = stderr_t.rstrip("\r\n")
client.module.exit_json(
changed=True,
stdout=stdout,
stderr=stderr,
stdout=stdout_t,
stderr=stderr_t,
rc=result.get("ExitCode") or 0,
)
except NotFound:
+3 -2
View File
@@ -86,7 +86,7 @@ from ansible_collections.community.docker.plugins.module_utils._common_api impor
)
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
}
@@ -96,8 +96,9 @@ def main():
supports_check_mode=True,
)
container_id: str = client.module.params["name"]
try:
container = client.get_container(client.module.params["name"])
container = client.get_container(container_id)
client.module.exit_json(
changed=False,
+26 -15
View File
@@ -173,6 +173,7 @@ current_context_name:
"""
import traceback
import typing as t
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
@@ -185,6 +186,7 @@ from ansible_collections.community.docker.plugins.module_utils._api.context.conf
)
from ansible_collections.community.docker.plugins.module_utils._api.context.context import (
IN_MEMORY,
Context,
)
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
ContextException,
@@ -192,7 +194,13 @@ from ansible_collections.community.docker.plugins.module_utils._api.errors impor
)
def tls_context_to_json(context):
if t.TYPE_CHECKING:
from ansible_collections.community.docker.plugins.module_utils._api.tls import (
TLSConfig,
)
def tls_context_to_json(context: TLSConfig | None) -> dict[str, t.Any] | None:
if context is None:
return None
return {
@@ -204,8 +212,8 @@ def tls_context_to_json(context):
}
def context_to_json(context, current):
module_config = {}
def context_to_json(context: Context, current: bool) -> dict[str, t.Any]:
module_config: dict[str, t.Any] = {}
if "docker" in context.endpoints:
endpoint = context.endpoints["docker"]
if isinstance(endpoint.get("Host"), str):
@@ -247,7 +255,7 @@ def context_to_json(context, current):
}
def main():
def main() -> None:
argument_spec = {
"only_current": {"type": "bool", "default": False},
"name": {"type": "str"},
@@ -262,28 +270,31 @@ def main():
],
)
only_current: bool = module.params["only_current"]
name: str | None = module.params["name"]
cli_context: str | None = module.params["cli_context"]
try:
if module.params["cli_context"]:
if cli_context:
current_context_name, current_context_source = (
module.params["cli_context"],
cli_context,
"cli_context module option",
)
else:
current_context_name, current_context_source = (
get_current_context_name_with_source()
)
if module.params["name"]:
contexts = [ContextAPI.get_context(module.params["name"])]
if not contexts[0]:
module.fail_json(
msg=f"There is no context of name {module.params['name']!r}"
)
elif module.params["only_current"]:
contexts = [ContextAPI.get_context(current_context_name)]
if not contexts[0]:
if name:
context_or_none = ContextAPI.get_context(name)
if not context_or_none:
module.fail_json(msg=f"There is no context of name {name!r}")
contexts = [context_or_none]
elif only_current:
context_or_none = ContextAPI.get_context(current_context_name)
if not context_or_none:
module.fail_json(
msg=f"There is no context of name {current_context_name!r}, which is configured as the default context ({current_context_source})",
)
contexts = [context_or_none]
else:
contexts = ContextAPI.contexts()
+15 -11
View File
@@ -212,6 +212,7 @@ disk_usage:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
APIError,
@@ -231,9 +232,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class DockerHostManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
super().__init__()
self.client = client
@@ -253,21 +252,21 @@ class DockerHostManager(DockerBaseClass):
for docker_object in listed_objects:
if self.client.module.params[docker_object]:
returned_name = docker_object
filter_name = docker_object + "_filters"
filter_name = f"{docker_object}_filters"
filters = clean_dict_booleans_for_docker_api(
client.module.params.get(filter_name), True
client.module.params.get(filter_name), allow_sequences=True
)
self.results[returned_name] = self.get_docker_items_list(
docker_object, filters
)
def get_docker_host_info(self):
def get_docker_host_info(self) -> dict[str, t.Any]:
try:
return self.client.info()
except APIError as exc:
self.client.fail(f"Error inspecting docker host: {exc}")
def get_docker_disk_usage_facts(self):
def get_docker_disk_usage_facts(self) -> dict[str, t.Any]:
try:
if self.verbose_output:
return self.client.df()
@@ -275,9 +274,13 @@ class DockerHostManager(DockerBaseClass):
except APIError as exc:
self.client.fail(f"Error inspecting docker host: {exc}")
def get_docker_items_list(self, docker_object=None, filters=None, verbose=False):
items = None
items_list = []
def get_docker_items_list(
self,
docker_object: str,
filters: dict[str, t.Any] | None = None,
verbose: bool = False,
) -> list[dict[str, t.Any]]:
items = []
header_containers = [
"Id",
@@ -329,6 +332,7 @@ class DockerHostManager(DockerBaseClass):
if self.verbose_output:
return items
items_list = []
for item in items:
item_record = {}
@@ -349,7 +353,7 @@ class DockerHostManager(DockerBaseClass):
return items_list
def main():
def main() -> None:
argument_spec = {
"containers": {"type": "bool", "default": False},
"containers_all": {"type": "bool", "default": False},
+80 -53
View File
@@ -367,6 +367,7 @@ import errno
import json
import os
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.common.text.formatters import human_to_bytes
@@ -411,7 +412,18 @@ from ansible_collections.community.docker.plugins.module_utils._version import (
)
def convert_to_bytes(value, module, name, unlimited_value=None):
if t.TYPE_CHECKING:
from collections.abc import Callable
from ansible.module_utils.basic import AnsibleModule
def convert_to_bytes(
value: str | None,
module: AnsibleModule,
name: str,
unlimited_value: int | None = None,
) -> int | None:
if value is None:
return value
try:
@@ -423,8 +435,7 @@ def convert_to_bytes(value, module, name, unlimited_value=None):
class ImageManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
"""
Configure a docker_image task.
@@ -441,12 +452,14 @@ class ImageManager(DockerBaseClass):
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.source = parameters["source"]
build = parameters["build"] or {}
pull = parameters["pull"] or {}
self.archive_path = parameters["archive_path"]
self.cache_from = build.get("cache_from")
self.container_limits = build.get("container_limits")
self.source: t.Literal["build", "load", "pull", "local"] | None = parameters[
"source"
]
build: dict[str, t.Any] = parameters["build"] or {}
pull: dict[str, t.Any] = parameters["pull"] or {}
self.archive_path: str | None = parameters["archive_path"]
self.cache_from: list[str] | None = build.get("cache_from")
self.container_limits: dict[str, t.Any] | None = build.get("container_limits")
if self.container_limits and "memory" in self.container_limits:
self.container_limits["memory"] = convert_to_bytes(
self.container_limits["memory"],
@@ -460,32 +473,36 @@ class ImageManager(DockerBaseClass):
"build.container_limits.memswap",
unlimited_value=-1,
)
self.dockerfile = build.get("dockerfile")
self.force_source = parameters["force_source"]
self.force_absent = parameters["force_absent"]
self.force_tag = parameters["force_tag"]
self.load_path = parameters["load_path"]
self.name = parameters["name"]
self.network = build.get("network")
self.extra_hosts = clean_dict_booleans_for_docker_api(build.get("etc_hosts"))
self.nocache = build.get("nocache", False)
self.build_path = build.get("path")
self.pull = build.get("pull")
self.target = build.get("target")
self.repository = parameters["repository"]
self.rm = build.get("rm", True)
self.state = parameters["state"]
self.tag = parameters["tag"]
self.http_timeout = build.get("http_timeout")
self.pull_platform = pull.get("platform")
self.push = parameters["push"]
self.buildargs = build.get("args")
self.build_platform = build.get("platform")
self.use_config_proxy = build.get("use_config_proxy")
self.shm_size = convert_to_bytes(
self.dockerfile: str | None = build.get("dockerfile")
self.force_source: bool = parameters["force_source"]
self.force_absent: bool = parameters["force_absent"]
self.force_tag: bool = parameters["force_tag"]
self.load_path: str | None = parameters["load_path"]
self.name: str = parameters["name"]
self.network: str | None = build.get("network")
self.extra_hosts: dict[str, str] = clean_dict_booleans_for_docker_api(
build.get("etc_hosts") # type: ignore
)
self.nocache: bool = build.get("nocache", False)
self.build_path: str | None = build.get("path")
self.pull: bool | None = build.get("pull")
self.target: str | None = build.get("target")
self.repository: str | None = parameters["repository"]
self.rm: bool = build.get("rm", True)
self.state: t.Literal["absent", "present"] = parameters["state"]
self.tag: str = parameters["tag"]
self.http_timeout: int | None = build.get("http_timeout")
self.pull_platform: str | None = pull.get("platform")
self.push: bool = parameters["push"]
self.buildargs: dict[str, t.Any] | None = build.get("args")
self.build_platform: str | None = build.get("platform")
self.use_config_proxy: bool | None = build.get("use_config_proxy")
self.shm_size: int | None = convert_to_bytes(
build.get("shm_size"), self.client.module, "build.shm_size"
)
self.labels = clean_dict_booleans_for_docker_api(build.get("labels"))
self.labels: dict[str, str] = clean_dict_booleans_for_docker_api(
build.get("labels") # type: ignore
)
# If name contains a tag, it takes precedence over tag parameter.
if not is_image_name_id(self.name):
@@ -507,10 +524,10 @@ class ImageManager(DockerBaseClass):
elif self.state == "absent":
self.absent()
def fail(self, msg):
def fail(self, msg: str) -> t.NoReturn:
self.client.fail(msg)
def present(self):
def present(self) -> None:
"""
Handles state = 'present', which includes building, loading or pulling an image,
depending on user provided parameters.
@@ -530,6 +547,7 @@ class ImageManager(DockerBaseClass):
)
# Build the image
assert self.build_path is not None
if not os.path.isdir(self.build_path):
self.fail(
f"Requested build path {self.build_path} could not be found or you do not have access."
@@ -546,6 +564,7 @@ class ImageManager(DockerBaseClass):
self.results.update(self.build_image())
elif self.source == "load":
assert self.load_path is not None
# Load the image from an archive
if not os.path.isfile(self.load_path):
self.fail(
@@ -596,7 +615,7 @@ class ImageManager(DockerBaseClass):
elif self.repository:
self.tag_image(self.name, self.tag, self.repository, push=self.push)
def absent(self):
def absent(self) -> None:
"""
Handles state = 'absent', which removes an image.
@@ -627,8 +646,11 @@ class ImageManager(DockerBaseClass):
@staticmethod
def archived_image_action(
failure_logger, archive_path, current_image_name, current_image_id
):
failure_logger: Callable[[str], None],
archive_path: str,
current_image_name: str,
current_image_id: str,
) -> str | None:
"""
If the archive is missing or requires replacement, return an action message.
@@ -667,7 +689,7 @@ class ImageManager(DockerBaseClass):
f"overwriting archive with image {archived.image_id} named {name}"
)
def archive_image(self, name, tag):
def archive_image(self, name: str, tag: str | None) -> None:
"""
Archive an image to a .tar file. Called when archive_path is passed.
@@ -676,6 +698,7 @@ class ImageManager(DockerBaseClass):
:param tag: Optional image tag; assumed to be "latest" if None
:type tag: str | None
"""
assert self.archive_path is not None
if not tag:
tag = "latest"
@@ -710,8 +733,8 @@ class ImageManager(DockerBaseClass):
self.client._get(
self.client._url("/images/{0}/get", image_name), stream=True
),
DEFAULT_DATA_CHUNK_SIZE,
False,
chunk_size=DEFAULT_DATA_CHUNK_SIZE,
decode=False,
)
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error getting image {image_name} - {exc}")
@@ -725,7 +748,7 @@ class ImageManager(DockerBaseClass):
self.results["image"] = image
def push_image(self, name, tag=None):
def push_image(self, name: str, tag: str | None = None) -> None:
"""
If the name of the image contains a repository path, then push the image.
@@ -799,7 +822,9 @@ class ImageManager(DockerBaseClass):
self.results["image"] = {}
self.results["image"]["push_status"] = status
def tag_image(self, name, tag, repository, push=False):
def tag_image(
self, name: str, tag: str, repository: str, push: bool = False
) -> None:
"""
Tag an image into a repository.
@@ -852,7 +877,7 @@ class ImageManager(DockerBaseClass):
self.push_image(repo, repo_tag)
@staticmethod
def _extract_output_line(line, output):
def _extract_output_line(line: dict[str, t.Any], output: list[str]):
"""
Extract text line from stream output and, if found, adds it to output.
"""
@@ -862,14 +887,15 @@ class ImageManager(DockerBaseClass):
text_line = line.get("stream") or line.get("status") or ""
output.extend(text_line.splitlines())
def build_image(self):
def build_image(self) -> dict[str, t.Any]:
"""
Build an image
:return: image dict
"""
assert self.build_path is not None
remote = context = None
headers = {}
headers: dict[str, str | bytes] = {}
buildargs = {}
if self.buildargs:
for key, value in self.buildargs.items():
@@ -898,12 +924,12 @@ class ImageManager(DockerBaseClass):
[line.strip() for line in f.read().splitlines()],
)
)
dockerfile = process_dockerfile(dockerfile, self.build_path)
dockerfile_data = process_dockerfile(dockerfile, self.build_path)
context = tar(
self.build_path, exclude=exclude, dockerfile=dockerfile, gzip=False
self.build_path, exclude=exclude, dockerfile=dockerfile_data, gzip=False
)
params = {
params: dict[str, t.Any] = {
"t": f"{self.name}:{self.tag}" if self.tag else self.name,
"remote": remote,
"q": False,
@@ -960,7 +986,7 @@ class ImageManager(DockerBaseClass):
if context is not None:
context.close()
build_output = []
build_output: list[str] = []
for line in self.client._stream_helper(response, decode=True):
# line = json.loads(line)
self.log(line, pretty_print=True)
@@ -982,14 +1008,15 @@ class ImageManager(DockerBaseClass):
"image": self.client.find_image(name=self.name, tag=self.tag),
}
def load_image(self):
def load_image(self) -> dict[str, t.Any] | None:
"""
Load an image from a .tar archive
:return: image dict
"""
# Load image(s) from file
load_output = []
assert self.load_path is not None
load_output: list[str] = []
has_output = False
try:
self.log(f"Opening image {self.load_path}")
@@ -1078,7 +1105,7 @@ class ImageManager(DockerBaseClass):
return self.client.find_image(self.name, self.tag)
def main():
def main() -> None:
argument_spec = {
"source": {"type": "str", "choices": ["build", "load", "pull", "local"]},
"build": {
+20 -10
View File
@@ -282,6 +282,7 @@ command:
import base64
import os
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.common.text.formatters import human_to_bytes
@@ -304,7 +305,16 @@ from ansible_collections.community.docker.plugins.module_utils._version import (
)
def convert_to_bytes(value, module, name, unlimited_value=None):
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
def convert_to_bytes(
value: str | None,
module: AnsibleModule,
name: str,
unlimited_value: int | None = None,
) -> int | None:
if value is None:
return value
try:
@@ -315,11 +325,11 @@ def convert_to_bytes(value, module, name, unlimited_value=None):
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
def dict_to_list(dictionary, concat="="):
def dict_to_list(dictionary: dict[str, t.Any], concat: str = "=") -> list[str]:
return [f"{k}{concat}{v}" for k, v in sorted(dictionary.items())]
def _quote_csv(text):
def _quote_csv(text: str) -> str:
if text.strip() == text and all(i not in text for i in '",\r\n'):
return text
text = text.replace('"', '""')
@@ -327,7 +337,7 @@ def _quote_csv(text):
class ImageBuilder(DockerBaseClass):
def __init__(self, client):
def __init__(self, client: AnsibleModuleDockerClient) -> None:
super().__init__()
self.client = client
self.check_mode = self.client.check_mode
@@ -420,14 +430,14 @@ class ImageBuilder(DockerBaseClass):
f" buildx plugin has version {buildx_version} which only supports one output."
)
def fail(self, msg, **kwargs):
def fail(self, msg: str, **kwargs: t.Any) -> t.NoReturn:
self.client.fail(msg, **kwargs)
def add_list_arg(self, args, option, values):
def add_list_arg(self, args: list[str], option: str, values: list[str]) -> None:
for value in values:
args.extend([option, value])
def add_args(self, args):
def add_args(self, args: list[str]) -> dict[str, t.Any]:
environ_update = {}
if not self.outputs:
args.extend(["--tag", f"{self.name}:{self.tag}"])
@@ -512,9 +522,9 @@ class ImageBuilder(DockerBaseClass):
)
return environ_update
def build_image(self):
def build_image(self) -> dict[str, t.Any]:
image = self.client.find_image(self.name, self.tag)
results = {
results: dict[str, t.Any] = {
"changed": False,
"actions": [],
"image": image or {},
@@ -547,7 +557,7 @@ class ImageBuilder(DockerBaseClass):
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"tag": {"type": "str", "default": "latest"},
+13 -12
View File
@@ -94,6 +94,7 @@ images:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.constants import (
DEFAULT_DATA_CHUNK_SIZE,
@@ -121,7 +122,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ImageExportManager(DockerBaseClass):
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
@@ -151,10 +152,10 @@ class ImageExportManager(DockerBaseClass):
if not self.names:
self.fail("At least one image name must be specified")
def fail(self, msg):
def fail(self, msg: str) -> t.NoReturn:
self.client.fail(msg)
def get_export_reason(self):
def get_export_reason(self) -> str | None:
if self.force:
return "Exporting since force=true"
@@ -178,13 +179,13 @@ class ImageExportManager(DockerBaseClass):
found = True
break
if not found:
return f'Overwriting archive since it contains unexpected image {archived_image.image_id} named {", ".join(archived_image.repo_tags)}'
return f"Overwriting archive since it contains unexpected image {archived_image.image_id} named {', '.join(archived_image.repo_tags)}"
if left_names:
return f"Overwriting archive since it is missing image(s) {', '.join([name['joined'] for name in left_names])}"
return None
def write_chunks(self, chunks):
def write_chunks(self, chunks: t.Generator[bytes]) -> None:
try:
with open(self.path, "wb") as fd:
for chunk in chunks:
@@ -192,7 +193,7 @@ class ImageExportManager(DockerBaseClass):
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error writing image archive {self.path} - {exc}")
def export_images(self):
def export_images(self) -> None:
image_names = [name["joined"] for name in self.names]
image_names_str = ", ".join(image_names)
if len(image_names) == 1:
@@ -202,8 +203,8 @@ class ImageExportManager(DockerBaseClass):
self.client._get(
self.client._url("/images/{0}/get", image_names[0]), stream=True
),
DEFAULT_DATA_CHUNK_SIZE,
False,
chunk_size=DEFAULT_DATA_CHUNK_SIZE,
decode=False,
)
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error getting image {image_names[0]} - {exc}")
@@ -216,15 +217,15 @@ class ImageExportManager(DockerBaseClass):
stream=True,
params={"names": image_names},
),
DEFAULT_DATA_CHUNK_SIZE,
False,
chunk_size=DEFAULT_DATA_CHUNK_SIZE,
decode=False,
)
except Exception as exc: # pylint: disable=broad-exception-caught
self.fail(f"Error getting images {image_names_str} - {exc}")
self.write_chunks(chunks)
def run(self):
def run(self) -> dict[str, t.Any]:
tag = self.tag
if not tag:
tag = "latest"
@@ -260,7 +261,7 @@ class ImageExportManager(DockerBaseClass):
return results
def main():
def main() -> None:
argument_spec = {
"path": {"type": "path"},
"force": {"type": "bool", "default": False},
+6 -7
View File
@@ -136,6 +136,7 @@ images:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
@@ -155,9 +156,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ImageManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
super().__init__()
self.client = client
@@ -170,10 +169,10 @@ class ImageManager(DockerBaseClass):
else:
self.results["images"] = self.get_all_images()
def fail(self, msg):
def fail(self, msg: str) -> t.NoReturn:
self.client.fail(msg)
def get_facts(self):
def get_facts(self) -> list[dict[str, t.Any]]:
"""
Lookup and inspect each image name found in the names parameter.
@@ -200,7 +199,7 @@ class ImageManager(DockerBaseClass):
results.append(image)
return results
def get_all_images(self):
def get_all_images(self) -> list[dict[str, t.Any]]:
results = []
params = {
"only_ids": 0,
@@ -218,7 +217,7 @@ class ImageManager(DockerBaseClass):
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "list", "elements": "str"},
}
+7 -6
View File
@@ -80,6 +80,7 @@ images:
import errno
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
@@ -95,7 +96,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ImageManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
super().__init__()
self.client = client
@@ -108,7 +109,7 @@ class ImageManager(DockerBaseClass):
self.load_images()
@staticmethod
def _extract_output_line(line, output):
def _extract_output_line(line: dict[str, t.Any], output: list[str]) -> None:
"""
Extract text line from stream output and, if found, adds it to output.
"""
@@ -118,12 +119,12 @@ class ImageManager(DockerBaseClass):
text_line = line.get("stream") or line.get("status") or ""
output.extend(text_line.splitlines())
def load_images(self):
def load_images(self) -> None:
"""
Load images from a .tar archive
"""
# Load image(s) from file
load_output = []
load_output: list[str] = []
try:
self.log(f"Opening image {self.path}")
with open(self.path, "rb") as image_tar:
@@ -179,7 +180,7 @@ class ImageManager(DockerBaseClass):
self.results["stdout"] = "\n".join(load_output)
def main():
def main() -> None:
client = AnsibleDockerClient(
argument_spec={
"path": {"type": "path", "required": True},
@@ -188,7 +189,7 @@ def main():
)
try:
results = {
results: dict[str, t.Any] = {
"image_names": [],
"images": [],
}
+18 -14
View File
@@ -91,6 +91,7 @@ image:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
@@ -114,7 +115,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
)
def image_info(image):
def image_info(image: dict[str, t.Any] | None) -> dict[str, t.Any]:
result = {}
if image:
result["id"] = image["Id"]
@@ -124,17 +125,17 @@ def image_info(image):
class ImagePuller(DockerBaseClass):
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters["name"]
self.tag = parameters["tag"]
self.platform = parameters["platform"]
self.pull_mode = parameters["pull"]
self.name: str = parameters["name"]
self.tag: str = parameters["tag"]
self.platform: str | None = parameters["platform"]
self.pull_mode: t.Literal["always", "not_present"] = parameters["pull"]
if is_image_name_id(self.name):
self.client.fail("Cannot pull an image by ID")
@@ -147,13 +148,15 @@ class ImagePuller(DockerBaseClass):
self.name = repo
self.tag = repo_tag
def pull(self):
def pull(self) -> dict[str, t.Any]:
image = self.client.find_image(name=self.name, tag=self.tag)
actions: list[str] = []
diff = {"before": image_info(image), "after": image_info(image)}
results = {
"changed": False,
"actions": [],
"actions": actions,
"image": image or {},
"diff": {"before": image_info(image), "after": image_info(image)},
"diff": diff,
}
if image and self.pull_mode == "not_present":
@@ -175,21 +178,22 @@ class ImagePuller(DockerBaseClass):
if compare_platform_strings(wanted_platform, image_platform):
return results
results["actions"].append(f"Pulled image {self.name}:{self.tag}")
actions.append(f"Pulled image {self.name}:{self.tag}")
if self.check_mode:
results["changed"] = True
results["diff"]["after"] = image_info({"Id": "unknown"})
diff["after"] = image_info({"Id": "unknown"})
else:
results["image"], not_changed = self.client.pull_image(
image, not_changed = self.client.pull_image(
self.name, tag=self.tag, image_platform=self.platform
)
results["image"] = image
results["changed"] = not not_changed
results["diff"]["after"] = image_info(results["image"])
diff["after"] = image_info(image)
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"tag": {"type": "str", "default": "latest"},
+10 -8
View File
@@ -73,6 +73,7 @@ image:
import base64
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.auth import (
get_config_header,
@@ -96,15 +97,15 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ImagePusher(DockerBaseClass):
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
self.check_mode = self.client.check_mode
parameters = self.client.module.params
self.name = parameters["name"]
self.tag = parameters["tag"]
self.name: str = parameters["name"]
self.tag: str = parameters["tag"]
if is_image_name_id(self.name):
self.client.fail("Cannot push an image by ID")
@@ -122,20 +123,21 @@ class ImagePusher(DockerBaseClass):
if not is_valid_tag(self.tag, allow_empty=False):
self.client.fail(f'"{self.tag}" is not a valid docker tag!')
def push(self):
def push(self) -> dict[str, t.Any]:
image = self.client.find_image(name=self.name, tag=self.tag)
if not image:
self.client.fail(f"Cannot find image {self.name}:{self.tag}")
results = {
actions: list[str] = []
results: dict[str, t.Any] = {
"changed": False,
"actions": [],
"actions": actions,
"image": image,
}
push_registry, push_repo = resolve_repository_name(self.name)
try:
results["actions"].append(f"Pushed image {self.name}:{self.tag}")
actions.append(f"Pushed image {self.name}:{self.tag}")
headers = {}
header = get_config_header(self.client, push_registry)
@@ -174,7 +176,7 @@ class ImagePusher(DockerBaseClass):
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"tag": {"type": "str", "default": "latest"},
+34 -29
View File
@@ -98,6 +98,7 @@ untagged:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
@@ -118,8 +119,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class ImageRemover(DockerBaseClass):
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
@@ -142,10 +142,10 @@ class ImageRemover(DockerBaseClass):
self.name = repo
self.tag = repo_tag
def fail(self, msg):
def fail(self, msg: str) -> t.NoReturn:
self.client.fail(msg)
def get_diff_state(self, image):
def get_diff_state(self, image: dict[str, t.Any] | None) -> dict[str, t.Any]:
if not image:
return {"exists": False}
return {
@@ -155,13 +155,16 @@ class ImageRemover(DockerBaseClass):
"digests": sorted(image.get("RepoDigests") or []),
}
def absent(self):
results = {
def absent(self) -> dict[str, t.Any]:
actions: list[str] = []
deleted: list[str] = []
untagged: list[str] = []
results: dict[str, t.Any] = {
"changed": False,
"actions": [],
"actions": actions,
"image": {},
"deleted": [],
"untagged": [],
"deleted": deleted,
"untagged": untagged,
}
name = self.name
@@ -172,16 +175,18 @@ class ImageRemover(DockerBaseClass):
if self.tag:
name = f"{self.name}:{self.tag}"
diff: dict[str, t.Any] = {}
if self.diff:
results["diff"] = {"before": self.get_diff_state(image)}
results["diff"] = diff
diff["before"] = self.get_diff_state(image)
if not image:
if self.diff:
results["diff"]["after"] = self.get_diff_state(image)
diff["after"] = self.get_diff_state(image)
return results
results["changed"] = True
results["actions"].append(f"Removed image {name}")
actions.append(f"Removed image {name}")
results["image"] = image
if not self.check_mode:
@@ -199,22 +204,22 @@ class ImageRemover(DockerBaseClass):
for entry in res:
if entry.get("Untagged"):
results["untagged"].append(entry["Untagged"])
untagged.append(entry["Untagged"])
if entry.get("Deleted"):
results["deleted"].append(entry["Deleted"])
deleted.append(entry["Deleted"])
results["untagged"] = sorted(results["untagged"])
results["deleted"] = sorted(results["deleted"])
untagged[:] = sorted(untagged)
deleted[:] = sorted(deleted)
if self.diff:
image_after = self.client.find_image_by_id(
image["Id"], accept_missing_image=True
)
results["diff"]["after"] = self.get_diff_state(image_after)
diff["after"] = self.get_diff_state(image_after)
elif is_image_name_id(name):
results["deleted"].append(image["Id"])
results["untagged"] = sorted(
deleted.append(image["Id"])
untagged[:] = sorted(
(image.get("RepoTags") or []) + (image.get("RepoDigests") or [])
)
if not self.force and results["untagged"]:
@@ -222,40 +227,40 @@ class ImageRemover(DockerBaseClass):
"Cannot delete image by ID that is still in use - use force=true"
)
if self.diff:
results["diff"]["after"] = self.get_diff_state({})
diff["after"] = self.get_diff_state({})
elif is_image_name_id(self.tag):
results["untagged"].append(name)
untagged.append(name)
if (
len(image.get("RepoTags") or []) < 1
and len(image.get("RepoDigests") or []) < 2
):
results["deleted"].append(image["Id"])
deleted.append(image["Id"])
if self.diff:
results["diff"]["after"] = self.get_diff_state(image)
diff["after"] = self.get_diff_state(image)
try:
results["diff"]["after"]["digests"].remove(name)
diff["after"]["digests"].remove(name)
except ValueError:
pass
else:
results["untagged"].append(name)
untagged.append(name)
if (
len(image.get("RepoTags") or []) < 2
and len(image.get("RepoDigests") or []) < 1
):
results["deleted"].append(image["Id"])
deleted.append(image["Id"])
if self.diff:
results["diff"]["after"] = self.get_diff_state(image)
diff["after"] = self.get_diff_state(image)
try:
results["diff"]["after"]["tags"].remove(name)
diff["after"]["tags"].remove(name)
except ValueError:
pass
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"tag": {"type": "str", "default": "latest"},
+29 -15
View File
@@ -101,6 +101,7 @@ tagged_images:
"""
import traceback
import typing as t
from ansible.module_utils.common.text.formatters import human_to_bytes
@@ -121,7 +122,16 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
)
def convert_to_bytes(value, module, name, unlimited_value=None):
if t.TYPE_CHECKING:
from ansible.module_utils.basic import AnsibleModule
def convert_to_bytes(
value: str | None,
module: AnsibleModule,
name: str,
unlimited_value: int | None = None,
) -> int | None:
if value is None:
return value
try:
@@ -132,8 +142,8 @@ def convert_to_bytes(value, module, name, unlimited_value=None):
module.fail_json(msg=f"Failed to convert {name} to bytes: {exc}")
def image_info(name, tag, image):
result = {"name": name, "tag": tag}
def image_info(name: str, tag: str, image: dict[str, t.Any] | None) -> dict[str, t.Any]:
result: dict[str, t.Any] = {"name": name, "tag": tag}
if image:
result["id"] = image["Id"]
else:
@@ -142,7 +152,7 @@ def image_info(name, tag, image):
class ImageTagger(DockerBaseClass):
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
@@ -179,10 +189,12 @@ class ImageTagger(DockerBaseClass):
)
self.repositories.append((repo, repo_tag))
def fail(self, msg):
def fail(self, msg: str) -> t.NoReturn:
self.client.fail(msg)
def tag_image(self, image, name, tag):
def tag_image(
self, image: dict[str, t.Any], name: str, tag: str
) -> tuple[bool, str, dict[str, t.Any] | None]:
tagged_image = self.client.find_image(name=name, tag=tag)
if tagged_image:
# Idempotency checks
@@ -220,20 +232,22 @@ class ImageTagger(DockerBaseClass):
return True, msg, tagged_image
def tag_images(self):
def tag_images(self) -> dict[str, t.Any]:
if is_image_name_id(self.name):
image = self.client.find_image_by_id(self.name, accept_missing_image=False)
else:
image = self.client.find_image(name=self.name, tag=self.tag)
if not image:
self.fail(f"Cannot find image {self.name}:{self.tag}")
assert image is not None
before = []
after = []
tagged_images = []
results = {
before: list[dict[str, t.Any]] = []
after: list[dict[str, t.Any]] = []
tagged_images: list[str] = []
actions: list[str] = []
results: dict[str, t.Any] = {
"changed": False,
"actions": [],
"actions": actions,
"image": image,
"tagged_images": tagged_images,
"diff": {"before": {"images": before}, "after": {"images": after}},
@@ -244,19 +258,19 @@ class ImageTagger(DockerBaseClass):
after.append(image_info(repository, tag, image if tagged else old_image))
if tagged:
results["changed"] = True
results["actions"].append(
actions.append(
f"Tagged image {image['Id']} as {repository}:{tag}: {msg}"
)
tagged_images.append(f"{repository}:{tag}")
else:
results["actions"].append(
actions.append(
f"Not tagged image {image['Id']} as {repository}:{tag}: {msg}"
)
return results
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
"tag": {"type": "str", "default": "latest"},
+28 -25
View File
@@ -120,6 +120,7 @@ import base64
import json
import os
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_bytes, to_text
@@ -154,11 +155,11 @@ class DockerFileStore:
program = "<legacy config>"
def __init__(self, config_path):
def __init__(self, config_path: str) -> None:
self._config_path = config_path
# Make sure we have a minimal config if none is available.
self._config = {"auths": {}}
self._config: dict[str, t.Any] = {"auths": {}}
try:
# Attempt to read the existing config.
@@ -172,14 +173,14 @@ class DockerFileStore:
self._config.update(config)
@property
def config_path(self):
def config_path(self) -> str:
"""
Return the config path configured in this DockerFileStore instance.
"""
return self._config_path
def get(self, server):
def get(self, server: str) -> dict[str, t.Any]:
"""
Retrieve credentials for `server` if there are any in the config file.
Otherwise raise a `StoreError`
@@ -193,7 +194,7 @@ class DockerFileStore:
return {"Username": username, "Secret": password}
def _write(self):
def _write(self) -> None:
"""
Write config back out to disk.
"""
@@ -209,7 +210,7 @@ class DockerFileStore:
finally:
os.close(f)
def store(self, server, username, password):
def store(self, server: str, username: str, password: str) -> None:
"""
Add a credentials for `server` to the current configuration.
"""
@@ -225,7 +226,7 @@ class DockerFileStore:
self._write()
def erase(self, server):
def erase(self, server: str) -> None:
"""
Remove credentials for the given server from the configuration.
"""
@@ -236,9 +237,7 @@ class DockerFileStore:
class LoginManager(DockerBaseClass):
def __init__(self, client, results):
def __init__(self, client: AnsibleDockerClient, results: dict[str, t.Any]) -> None:
super().__init__()
self.client = client
@@ -246,14 +245,14 @@ class LoginManager(DockerBaseClass):
parameters = self.client.module.params
self.check_mode = self.client.check_mode
self.registry_url = parameters.get("registry_url")
self.username = parameters.get("username")
self.password = parameters.get("password")
self.reauthorize = parameters.get("reauthorize")
self.config_path = parameters.get("config_path")
self.state = parameters.get("state")
self.registry_url: str = parameters.get("registry_url")
self.username: str | None = parameters.get("username")
self.password: str | None = parameters.get("password")
self.reauthorize: bool = parameters.get("reauthorize")
self.config_path: str = parameters.get("config_path")
self.state: t.Literal["present", "absent"] = parameters.get("state")
def run(self):
def run(self) -> None:
"""
Do the actual work of this task here. This allows instantiation for partial
testing.
@@ -264,10 +263,10 @@ class LoginManager(DockerBaseClass):
else:
self.logout()
def fail(self, msg):
def fail(self, msg: str) -> t.NoReturn:
self.client.fail(msg)
def _login(self, reauth):
def _login(self, reauth: bool) -> dict[str, t.Any]:
if self.config_path and os.path.exists(self.config_path):
self.client._auth_configs = auth.load_config(
self.config_path, credstore_env=self.client.credstore_env
@@ -297,7 +296,7 @@ class LoginManager(DockerBaseClass):
)
return self.client._result(response, get_json=True)
def login(self):
def login(self) -> None:
"""
Log into the registry with provided username/password. On success update the config
file with the new authorization.
@@ -331,7 +330,7 @@ class LoginManager(DockerBaseClass):
self.update_credentials()
def logout(self):
def logout(self) -> None:
"""
Log out of the registry. On success update the config file.
@@ -353,13 +352,16 @@ class LoginManager(DockerBaseClass):
store.erase(self.registry_url)
self.results["changed"] = True
def update_credentials(self):
def update_credentials(self) -> None:
"""
If the authorization is not stored attempt to store authorization values via
the appropriate credential helper or to the config file.
:return: None
"""
# This is only called from login()
assert self.username is not None
assert self.password is not None
# Check to see if credentials already exist.
store = self.get_credential_store_instance(self.registry_url, self.config_path)
@@ -385,7 +387,9 @@ class LoginManager(DockerBaseClass):
)
self.results["changed"] = True
def get_credential_store_instance(self, registry, dockercfg_path):
def get_credential_store_instance(
self, registry: str, dockercfg_path: str
) -> Store | DockerFileStore:
"""
Return an instance of docker.credentials.Store used by the given registry.
@@ -408,8 +412,7 @@ class LoginManager(DockerBaseClass):
return DockerFileStore(dockercfg_path)
def main():
def main() -> None:
argument_spec = {
"registry_url": {
"type": "str",
+56 -48
View File
@@ -284,6 +284,7 @@ network:
import re
import time
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native
@@ -303,29 +304,31 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass):
def __init__(self, client):
name: str
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
self.name = None
self.connected = None
self.config_from = None
self.config_only = None
self.driver = None
self.driver_options = None
self.ipam_driver = None
self.ipam_driver_options = None
self.ipam_config = None
self.appends = None
self.force = None
self.internal = None
self.labels = None
self.debug = None
self.enable_ipv4 = None
self.enable_ipv6 = None
self.scope = None
self.attachable = None
self.ingress = None
self.connected: list[str] = []
self.config_from: str | None = None
self.config_only: bool | None = None
self.driver: str = "bridge"
self.driver_options: dict[str, t.Any] = {}
self.ipam_driver: str | None = None
self.ipam_driver_options: dict[str, t.Any] | None = None
self.ipam_config: list[dict[str, t.Any]] | None = None
self.appends: bool = False
self.force: bool = False
self.internal: bool | None = None
self.labels: dict[str, t.Any] = {}
self.debug: bool = False
self.enable_ipv4: bool | None = None
self.enable_ipv6: bool | None = None
self.scope: t.Literal["local", "global", "swarm"] | None = None
self.attachable: bool | None = None
self.ingress: bool | None = None
self.state: t.Literal["present", "absent"] = "present"
for key, value in client.module.params.items():
setattr(self, key, value)
@@ -333,10 +336,10 @@ class TaskParameters(DockerBaseClass):
# config_only sets driver to 'null' (and scope to 'local') so force that here. Otherwise we get
# diffs of 'null' --> 'bridge' given that the driver option defaults to 'bridge'.
if self.config_only:
self.driver = "null"
self.driver = "null" # type: ignore[unreachable]
def container_names_in_network(network):
def container_names_in_network(network: dict[str, t.Any]) -> list[str]:
return (
[c["Name"] for c in network["Containers"].values()]
if network["Containers"]
@@ -348,7 +351,7 @@ CIDR_IPV4 = re.compile(r"^([0-9]{1,3}\.){3}[0-9]{1,3}/([0-9]|[1-2][0-9]|3[0-2])$
CIDR_IPV6 = re.compile(r"^[0-9a-fA-F:]+/([0-9]|[1-9][0-9]|1[0-2][0-9])$")
def validate_cidr(cidr):
def validate_cidr(cidr: str) -> t.Literal["ipv4", "ipv6"]:
"""Validate CIDR. Return IP version of a CIDR string on success.
:param cidr: Valid CIDR
@@ -364,7 +367,7 @@ def validate_cidr(cidr):
raise ValueError(f'"{cidr}" is not a valid CIDR')
def normalize_ipam_config_key(key):
def normalize_ipam_config_key(key: str) -> str:
"""Normalizes IPAM config keys returned by Docker API to match Ansible keys.
:param key: Docker API key
@@ -376,7 +379,7 @@ def normalize_ipam_config_key(key):
return special_cases.get(key, key.lower())
def dicts_are_essentially_equal(a, b):
def dicts_are_essentially_equal(a: dict[str, t.Any], b: dict[str, t.Any]):
"""Make sure that a is a subset of b, where None entries of a are ignored."""
for k, v in a.items():
if v is None:
@@ -387,15 +390,15 @@ def dicts_are_essentially_equal(a, b):
class DockerNetworkManager:
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
self.client = client
self.parameters = TaskParameters(client)
self.check_mode = self.client.check_mode
self.results = {"changed": False, "actions": []}
self.actions: list[str] = []
self.results: dict[str, t.Any] = {"changed": False, "actions": self.actions}
self.diff = self.client.module._diff
self.diff_tracker = DifferenceTracker()
self.diff_result = {}
self.diff_result: dict[str, t.Any] = {}
self.existing_network = self.get_existing_network()
@@ -429,10 +432,12 @@ class DockerNetworkManager:
)
self.results["diff"] = self.diff_result
def get_existing_network(self):
def get_existing_network(self) -> dict[str, t.Any] | None:
return self.client.get_network(name=self.parameters.name)
def has_different_config(self, net):
def has_different_config(
self, net: dict[str, t.Any]
) -> tuple[bool, DifferenceTracker]:
"""
Evaluates an existing network and returns a tuple containing a boolean
indicating if the configuration is different and a list of differences.
@@ -601,9 +606,9 @@ class DockerNetworkManager:
return not differences.empty, differences
def create_network(self):
def create_network(self) -> None:
if not self.existing_network:
data = {
data: dict[str, t.Any] = {
"Name": self.parameters.name,
"Driver": self.parameters.driver,
"Options": self.parameters.driver_options,
@@ -661,12 +666,12 @@ class DockerNetworkManager:
resp = self.client.post_json_to_json("/networks/create", data=data)
self.client.report_warnings(resp, ["Warning"])
self.existing_network = self.client.get_network(network_id=resp["Id"])
self.results["actions"].append(
self.actions.append(
f"Created network {self.parameters.name} with driver {self.parameters.driver}"
)
self.results["changed"] = True
def remove_network(self):
def remove_network(self) -> None:
if self.existing_network:
self.disconnect_all_containers()
if not self.check_mode:
@@ -674,15 +679,15 @@ class DockerNetworkManager:
if self.existing_network.get("Scope", "local") == "swarm":
while self.get_existing_network():
time.sleep(0.1)
self.results["actions"].append(f"Removed network {self.parameters.name}")
self.actions.append(f"Removed network {self.parameters.name}")
self.results["changed"] = True
def is_container_connected(self, container_name):
def is_container_connected(self, container_name: str) -> bool:
if not self.existing_network:
return False
return container_name in container_names_in_network(self.existing_network)
def is_container_exist(self, container_name):
def is_container_exist(self, container_name: str) -> bool:
try:
container = self.client.get_container(container_name)
return bool(container)
@@ -698,7 +703,7 @@ class DockerNetworkManager:
exception=traceback.format_exc(),
)
def connect_containers(self):
def connect_containers(self) -> None:
for name in self.parameters.connected:
if not self.is_container_connected(name) and self.is_container_exist(name):
if not self.check_mode:
@@ -709,11 +714,11 @@ class DockerNetworkManager:
self.client.post_json(
"/networks/{0}/connect", self.parameters.name, data=data
)
self.results["actions"].append(f"Connected container {name}")
self.actions.append(f"Connected container {name}")
self.results["changed"] = True
self.diff_tracker.add(f"connected.{name}", parameter=True, active=False)
def disconnect_missing(self):
def disconnect_missing(self) -> None:
if not self.existing_network:
return
containers = self.existing_network["Containers"]
@@ -724,26 +729,29 @@ class DockerNetworkManager:
if name not in self.parameters.connected:
self.disconnect_container(name)
def disconnect_all_containers(self):
containers = self.client.get_network(name=self.parameters.name)["Containers"]
def disconnect_all_containers(self) -> None:
network = self.client.get_network(name=self.parameters.name)
if not network:
return
containers = network["Containers"]
if not containers:
return
for cont in containers.values():
self.disconnect_container(cont["Name"])
def disconnect_container(self, container_name):
def disconnect_container(self, container_name: str) -> None:
if not self.check_mode:
data = {"Container": container_name, "Force": True}
self.client.post_json(
"/networks/{0}/disconnect", self.parameters.name, data=data
)
self.results["actions"].append(f"Disconnected container {container_name}")
self.actions.append(f"Disconnected container {container_name}")
self.results["changed"] = True
self.diff_tracker.add(
f"connected.{container_name}", parameter=False, active=True
)
def present(self):
def present(self) -> None:
different = False
differences = DifferenceTracker()
if self.existing_network:
@@ -771,14 +779,14 @@ class DockerNetworkManager:
network_facts = self.get_existing_network()
self.results["network"] = network_facts
def absent(self):
def absent(self) -> None:
self.diff_tracker.add(
"exists", parameter=False, active=self.existing_network is not None
)
self.remove_network()
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True, "aliases": ["network_name"]},
"config_from": {"type": "str"},
+1 -1
View File
@@ -107,7 +107,7 @@ from ansible_collections.community.docker.plugins.module_utils._common_api impor
)
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True},
}
+36 -30
View File
@@ -129,6 +129,7 @@ actions:
"""
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native
@@ -149,35 +150,36 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass):
def __init__(self, client):
plugin_name: str
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
self.plugin_name = None
self.alias = None
self.plugin_options = None
self.debug = None
self.force_remove = None
self.enable_timeout = None
self.alias: str | None = None
self.plugin_options: dict[str, t.Any] = {}
self.debug: bool = False
self.force_remove: bool = False
self.enable_timeout: int = 0
self.state: t.Literal["present", "absent", "enable", "disable"] = "present"
for key, value in client.module.params.items():
setattr(self, key, value)
def prepare_options(options):
def prepare_options(options: dict[str, t.Any] | None) -> list[str]:
return (
[f'{k}={v if v is not None else ""}' for k, v in options.items()]
[f"{k}={v if v is not None else ''}" for k, v in options.items()]
if options
else []
)
def parse_options(options_list):
def parse_options(options_list: list[str] | None) -> dict[str, str]:
return dict(x.split("=", 1) for x in options_list) if options_list else {}
class DockerPluginManager:
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
self.client = client
self.parameters = TaskParameters(client)
@@ -185,9 +187,9 @@ class DockerPluginManager:
self.check_mode = self.client.check_mode
self.diff = self.client.module._diff
self.diff_tracker = DifferenceTracker()
self.diff_result = {}
self.diff_result: dict[str, t.Any] = {}
self.actions = []
self.actions: list[str] = []
self.changed = False
self.existing_plugin = self.get_existing_plugin()
@@ -209,7 +211,7 @@ class DockerPluginManager:
)
self.diff = self.diff_result
def get_existing_plugin(self):
def get_existing_plugin(self) -> dict[str, t.Any] | None:
try:
return self.client.get_json("/plugins/{0}/json", self.preferred_name)
except NotFound:
@@ -217,12 +219,13 @@ class DockerPluginManager:
except APIError as e:
self.client.fail(to_native(e))
def has_different_config(self):
def has_different_config(self) -> DifferenceTracker:
"""
Return the list of differences between the current parameters and the existing plugin.
:return: list of options that differ
"""
assert self.existing_plugin is not None
differences = DifferenceTracker()
if self.parameters.plugin_options:
settings = self.existing_plugin.get("Settings")
@@ -249,7 +252,7 @@ class DockerPluginManager:
return differences
def install_plugin(self):
def install_plugin(self) -> None:
if not self.existing_plugin:
if not self.check_mode:
try:
@@ -297,7 +300,7 @@ class DockerPluginManager:
self.actions.append(f"Installed plugin {self.preferred_name}")
self.changed = True
def remove_plugin(self):
def remove_plugin(self) -> None:
force = self.parameters.force_remove
if self.existing_plugin:
if not self.check_mode:
@@ -311,7 +314,7 @@ class DockerPluginManager:
self.actions.append(f"Removed plugin {self.preferred_name}")
self.changed = True
def update_plugin(self):
def update_plugin(self) -> None:
if self.existing_plugin:
differences = self.has_different_config()
if not differences.empty:
@@ -328,7 +331,7 @@ class DockerPluginManager:
else:
self.client.fail("Cannot update the plugin: Plugin does not exist")
def present(self):
def present(self) -> None:
differences = DifferenceTracker()
if self.existing_plugin:
differences = self.has_different_config()
@@ -345,13 +348,10 @@ class DockerPluginManager:
if self.diff or self.check_mode or self.parameters.debug:
self.diff_tracker.merge(differences)
if not self.check_mode and not self.parameters.debug:
self.actions = None
def absent(self):
def absent(self) -> None:
self.remove_plugin()
def enable(self):
def enable(self) -> None:
timeout = self.parameters.enable_timeout
if self.existing_plugin:
if not self.existing_plugin.get("Enabled"):
@@ -380,7 +380,7 @@ class DockerPluginManager:
self.actions.append(f"Enabled plugin {self.preferred_name}")
self.changed = True
def disable(self):
def disable(self) -> None:
if self.existing_plugin:
if self.existing_plugin.get("Enabled"):
if not self.check_mode:
@@ -396,7 +396,7 @@ class DockerPluginManager:
self.client.fail("Plugin not found: Plugin does not exist.")
@property
def result(self):
def result(self) -> dict[str, t.Any]:
plugin_data = {}
if self.parameters.state != "absent":
try:
@@ -406,16 +406,22 @@ class DockerPluginManager:
except NotFound:
# This can happen in check mode
pass
result = {
result: dict[str, t.Any] = {
"actions": self.actions,
"changed": self.changed,
"diff": self.diff,
"plugin": plugin_data,
}
return dict((k, v) for k, v in result.items() if v is not None)
if (
self.parameters.state == "present"
and not self.check_mode
and not self.parameters.debug
):
result["actions"] = None
return {k: v for k, v in result.items() if v is not None}
def main():
def main() -> None:
argument_spec = {
"alias": {"type": "str"},
"plugin_name": {"type": "str", "required": True},
+1 -1
View File
@@ -247,7 +247,7 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
)
def main():
def main() -> None:
argument_spec = {
"containers": {"type": "bool", "default": False},
"containers_filters": {"type": "dict"},
+24 -22
View File
@@ -118,6 +118,7 @@ volume:
"""
import traceback
import typing as t
from ansible.module_utils.common.text.converters import to_native
@@ -137,31 +138,33 @@ from ansible_collections.community.docker.plugins.module_utils._util import (
class TaskParameters(DockerBaseClass):
def __init__(self, client):
volume_name: str
def __init__(self, client: AnsibleDockerClient) -> None:
super().__init__()
self.client = client
self.volume_name = None
self.driver = None
self.driver_options = None
self.labels = None
self.recreate = None
self.debug = None
self.driver: str = "local"
self.driver_options: dict[str, t.Any] = {}
self.labels: dict[str, t.Any] | None = None
self.recreate: t.Literal["always", "never", "options-changed"] = "never"
self.debug: bool = False
self.state: t.Literal["present", "absent"] = "present"
for key, value in client.module.params.items():
setattr(self, key, value)
class DockerVolumeManager:
def __init__(self, client):
def __init__(self, client: AnsibleDockerClient) -> None:
self.client = client
self.parameters = TaskParameters(client)
self.check_mode = self.client.check_mode
self.results = {"changed": False, "actions": []}
self.actions: list[str] = []
self.results: dict[str, t.Any] = {"changed": False, "actions": self.actions}
self.diff = self.client.module._diff
self.diff_tracker = DifferenceTracker()
self.diff_result = {}
self.diff_result: dict[str, t.Any] = {}
self.existing_volume = self.get_existing_volume()
@@ -178,7 +181,7 @@ class DockerVolumeManager:
)
self.results["diff"] = self.diff_result
def get_existing_volume(self):
def get_existing_volume(self) -> dict[str, t.Any] | None:
try:
volumes = self.client.get_json("/volumes")
except APIError as e:
@@ -193,12 +196,13 @@ class DockerVolumeManager:
return None
def has_different_config(self):
def has_different_config(self) -> DifferenceTracker:
"""
Return the list of differences between the current parameters and the existing volume.
:return: list of options that differ
"""
assert self.existing_volume is not None
differences = DifferenceTracker()
if (
self.parameters.driver
@@ -239,7 +243,7 @@ class DockerVolumeManager:
return differences
def create_volume(self):
def create_volume(self) -> None:
if not self.existing_volume:
if not self.check_mode:
try:
@@ -257,12 +261,12 @@ class DockerVolumeManager:
except APIError as e:
self.client.fail(to_native(e))
self.results["actions"].append(
self.actions.append(
f"Created volume {self.parameters.volume_name} with driver {self.parameters.driver}"
)
self.results["changed"] = True
def remove_volume(self):
def remove_volume(self) -> None:
if self.existing_volume:
if not self.check_mode:
try:
@@ -270,12 +274,10 @@ class DockerVolumeManager:
except APIError as e:
self.client.fail(to_native(e))
self.results["actions"].append(
f"Removed volume {self.parameters.volume_name}"
)
self.actions.append(f"Removed volume {self.parameters.volume_name}")
self.results["changed"] = True
def present(self):
def present(self) -> None:
differences = DifferenceTracker()
if self.existing_volume:
differences = self.has_different_config()
@@ -301,14 +303,14 @@ class DockerVolumeManager:
volume_facts = self.get_existing_volume()
self.results["volume"] = volume_facts
def absent(self):
def absent(self) -> None:
self.diff_tracker.add(
"exists", parameter=False, active=self.existing_volume is not None
)
self.remove_volume()
def main():
def main() -> None:
argument_spec = {
"volume_name": {"type": "str", "required": True, "aliases": ["name"]},
"state": {
+5 -2
View File
@@ -71,6 +71,7 @@ volume:
"""
import traceback
import typing as t
from ansible_collections.community.docker.plugins.module_utils._api.errors import (
DockerException,
@@ -82,7 +83,9 @@ from ansible_collections.community.docker.plugins.module_utils._common_api impor
)
def get_existing_volume(client, volume_name):
def get_existing_volume(
client: AnsibleDockerClient, volume_name: str
) -> dict[str, t.Any] | None:
try:
return client.get_json("/volumes/{0}", volume_name)
except NotFound:
@@ -91,7 +94,7 @@ def get_existing_volume(client, volume_name):
client.fail(f"Error inspecting volume: {exc}")
def main():
def main() -> None:
argument_spec = {
"name": {"type": "str", "required": True, "aliases": ["volume_name"]},
}