mirror of
https://github.com/ansible-collections/community.docker.git
synced 2025-12-15 19:42:06 +00:00
* Add debug flag to failing task.
* Add more debug output.
* Fix pull idempotency.
* Revert "Add more debug output."
This reverts commit 64020149bf.
* Fix casing.
* Remove unreliable test.
* Add 'debug: true' to all tasks.
* Reformat.
* Fix idempotency problem for IPv6 addresses.
* Fix expose ranges handling.
* Update changelog fragment to also mention other affected modules.
1353 lines
56 KiB
Python
1353 lines
56 KiB
Python
# Copyright (c) 2022 Felix Fontein <felix@fontein.de>
|
|
# Copyright 2016 Red Hat | Ansible
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
# Note that this module util is **PRIVATE** to the collection. It can have breaking changes at any time.
|
|
# Do not use this from other collections or standalone plugins/modules!
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import typing as t
|
|
from time import sleep
|
|
|
|
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,
|
|
compare_generic,
|
|
is_image_name_id,
|
|
normalize_ip_address,
|
|
sanitize_result,
|
|
)
|
|
|
|
|
|
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: dict[str, t.Any] | None, engine_driver: EngineDriver
|
|
) -> None:
|
|
super().__init__()
|
|
self.raw = container
|
|
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:
|
|
self.id = engine_driver.get_container_id(container)
|
|
self.image = engine_driver.get_image_from_container(container)
|
|
self.image_name = engine_driver.get_image_name_from_container(container)
|
|
self.log(self.container, pretty_print=True)
|
|
|
|
@property
|
|
def exists(self) -> bool:
|
|
return bool(self.container)
|
|
|
|
@property
|
|
def removing(self) -> bool:
|
|
return (
|
|
self.engine_driver.is_container_removing(self.container)
|
|
if self.container
|
|
else False
|
|
)
|
|
|
|
@property
|
|
def running(self) -> bool:
|
|
return (
|
|
self.engine_driver.is_container_running(self.container)
|
|
if self.container
|
|
else False
|
|
)
|
|
|
|
@property
|
|
def paused(self) -> bool:
|
|
return (
|
|
self.engine_driver.is_container_paused(self.container)
|
|
if self.container
|
|
else False
|
|
)
|
|
|
|
|
|
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
|
|
self.client = client
|
|
self.options = active_options
|
|
self.all_options = self._collect_all_options(active_options)
|
|
self.check_mode = self.module.check_mode
|
|
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: 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_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: 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: dict[str, t.Any] = {}
|
|
self.diff_tracker = DifferenceTracker()
|
|
self.facts: dict[str, t.Any] | None = {}
|
|
if self.param_default_host_ip:
|
|
valid_ip = False
|
|
if re.match(
|
|
r"^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$", self.param_default_host_ip
|
|
):
|
|
valid_ip = True
|
|
if re.match(r"^\[[0-9a-fA-F:]+\]$", self.param_default_host_ip):
|
|
valid_ip = True
|
|
if re.match(r"^[0-9a-fA-F:]+$", self.param_default_host_ip):
|
|
self.param_default_host_ip = f"[{self.param_default_host_ip}]"
|
|
valid_ip = True
|
|
if not valid_ip:
|
|
self.fail(
|
|
"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: list[tuple[OptionGroup, dict[str, t.Any]]] | None = None
|
|
|
|
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) -> set[str]:
|
|
all_module_options = set()
|
|
for option, data in self.module.argument_spec.items():
|
|
all_module_options.add(option)
|
|
if "aliases" in data:
|
|
for alias in data["aliases"]:
|
|
all_module_options.add(alias)
|
|
return all_module_options
|
|
|
|
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 = {}
|
|
for option_name, option in self.all_options.items():
|
|
if option.not_an_ansible_option:
|
|
continue
|
|
comp_aliases[option_name] = option_name
|
|
for alias in option.ansible_aliases:
|
|
comp_aliases[alias] = option_name
|
|
# Process comparisons specified by user
|
|
comparisons: dict[str, t.Any] | None = self.module.params.get("comparisons")
|
|
if comparisons:
|
|
# If '*' appears in comparisons, process it first
|
|
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'!"
|
|
)
|
|
for option in self.all_options.values():
|
|
# `networks` is special: only update if
|
|
# some value is actually specified
|
|
if (
|
|
option.name == "networks"
|
|
and self.module.params["networks"] is None
|
|
):
|
|
continue
|
|
option.comparison = value
|
|
# Now process all other comparisons.
|
|
comp_aliases_used: dict[str, str] = {}
|
|
for key, value in comparisons.items():
|
|
if key == "*":
|
|
continue
|
|
# Find main key
|
|
key_main = comp_aliases.get(key)
|
|
if key_main is None:
|
|
if key_main in all_module_options:
|
|
self.fail(
|
|
f"The module option '{key}' cannot be specified in the comparisons dict, "
|
|
"since it does not correspond to container's state!"
|
|
)
|
|
if (
|
|
key not in self.all_options
|
|
or self.all_options[key].not_an_ansible_option
|
|
):
|
|
self.fail(f"Unknown module option '{key}' in comparisons dict!")
|
|
key_main = key
|
|
if key_main in comp_aliases_used:
|
|
self.fail(
|
|
f"Both '{key}' and '{comp_aliases_used[key_main]}' (aliases of {key_main}) are specified in comparisons dict!"
|
|
)
|
|
comp_aliases_used[key_main] = key
|
|
# Check value and update accordingly
|
|
if value in ("strict", "ignore"):
|
|
self.all_options[key_main].comparison = value
|
|
elif value == "allow_more_present":
|
|
if self.all_options[key_main].comparison_type == "value":
|
|
self.fail(
|
|
f"Option '{key}' is a value and not a set/list/dict, so its comparison cannot be {value}"
|
|
)
|
|
self.all_options[key_main].comparison = value
|
|
else:
|
|
self.fail(f"Unknown comparison mode '{value}'!")
|
|
# Copy values
|
|
for option in self.all_options.values():
|
|
if option.copy_comparison_from is not None:
|
|
option.comparison = self.all_options[
|
|
option.copy_comparison_from
|
|
].comparison
|
|
|
|
def _update_params(self) -> None:
|
|
if (
|
|
self.param_networks_cli_compatible is True
|
|
and self.module.params["networks"]
|
|
and self.module.params["network_mode"] is None
|
|
):
|
|
# Same behavior as Docker CLI: if networks are specified, use the name of the first network as the value for network_mode
|
|
# (assuming no explicit value is specified for network_mode)
|
|
self.module.params["network_mode"] = self.module.params["networks"][0][
|
|
"name"
|
|
]
|
|
if self.param_container_default_behavior == "compatibility":
|
|
old_default_values = {
|
|
"auto_remove": False,
|
|
"detach": True,
|
|
"init": False,
|
|
"interactive": False,
|
|
"memory": "0",
|
|
"paused": False,
|
|
"privileged": False,
|
|
"read_only": False,
|
|
"tty": False,
|
|
}
|
|
for param, value in old_default_values.items():
|
|
if self.module.params[param] is None:
|
|
self.module.params[param] = value
|
|
|
|
def fail(self, *args: str, **kwargs: t.Any) -> t.NoReturn:
|
|
# mypy doesn't know that Client has fail() method
|
|
raise self.client.fail(*args, **kwargs) # type: ignore
|
|
|
|
def run(self) -> None:
|
|
if self.param_state in ("stopped", "started", "present", "healthy"):
|
|
# 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()
|
|
|
|
if not self.check_mode and not self.param_debug:
|
|
self.results.pop("actions")
|
|
|
|
if self.module._diff or self.param_debug:
|
|
self.diff["before"], self.diff["after"] = (
|
|
self.diff_tracker.get_before_after()
|
|
)
|
|
self.results["diff"] = self.diff
|
|
|
|
if self.facts:
|
|
self.results["container"] = self.facts
|
|
|
|
def wait_for_state(
|
|
self,
|
|
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.0
|
|
while True:
|
|
# Inspect container
|
|
result = self.engine_driver.inspect_container_by_id(
|
|
self.client, container_id
|
|
)
|
|
if result is None:
|
|
if accept_removal:
|
|
return result
|
|
msg = f'Encontered vanished container while waiting for container "{container_id}"'
|
|
self.fail(msg)
|
|
# Check container state
|
|
state_info = result.get("State") or {}
|
|
if health_state:
|
|
state_info = state_info.get("Health") or {}
|
|
state = state_info.get("Status")
|
|
if complete_states is not None and state in complete_states:
|
|
return result
|
|
if wait_states is not None and state not in wait_states:
|
|
msg = f'Encontered unexpected state "{state}" while waiting for container "{container_id}"'
|
|
self.fail(msg, container=result)
|
|
# Wait
|
|
if max_wait is not None:
|
|
if total_wait > max_wait or delay < 1e-4:
|
|
msg = f'Timeout of {max_wait} seconds exceeded while waiting for container "{container_id}"'
|
|
self.fail(msg, container=result)
|
|
if total_wait + delay > max_wait:
|
|
delay = max_wait - total_wait
|
|
sleep(delay)
|
|
total_wait += delay
|
|
# Exponential backoff, but never wait longer than 10 seconds
|
|
# (1.1**24 < 10, 1.1**25 > 10, so it will take 25 iterations
|
|
# until the maximal 10 seconds delay is reached. By then, the
|
|
# code will have slept for ~1.5 minutes.)
|
|
delay = min(delay * 1.1, 10)
|
|
|
|
def _collect_params(
|
|
self, active_options: list[OptionGroup]
|
|
) -> list[tuple[OptionGroup, dict[str, t.Any]]]:
|
|
parameters = []
|
|
for options in active_options:
|
|
values = {}
|
|
engine = options.get_engine(self.engine_driver.name)
|
|
for option in options.all_options:
|
|
if (
|
|
not option.not_an_ansible_option
|
|
and self.module.params[option.name] is not None
|
|
):
|
|
values[option.name] = self.module.params[option.name]
|
|
values = options.preprocess(self.module, values)
|
|
engine.preprocess_value(
|
|
self.module,
|
|
self.client,
|
|
self.engine_driver.get_api_version(self.client),
|
|
options.options,
|
|
values,
|
|
)
|
|
parameters.append((options, values))
|
|
return parameters
|
|
|
|
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) -> 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: 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
|
|
was_paused = container.paused
|
|
container_created = False
|
|
|
|
# If the image parameter was passed then we need to deal with the image
|
|
# version comparison. Otherwise we handle this depending on whether
|
|
# the container already runs or not; in the former case, in case the
|
|
# container needs to be restarted, we use the existing container's
|
|
# image ID.
|
|
image, container_image, comparison_image = self._get_image(
|
|
container, needs_container_image=self._needs_container_image()
|
|
)
|
|
self.log(image, pretty_print=True)
|
|
host_info = (
|
|
self.engine_driver.get_host_info(self.client)
|
|
if self._needs_host_info()
|
|
else None
|
|
)
|
|
if not container.exists or container.removing:
|
|
# New container
|
|
if container.removing:
|
|
self.log("Found container in removal phase")
|
|
else:
|
|
self.log("No container found")
|
|
if not self.param_image:
|
|
self.fail("Cannot create container when image is not specified!")
|
|
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"],
|
|
accept_removal=True,
|
|
max_wait=self.param_removal_wait_timeout,
|
|
)
|
|
new_container = self.container_create(self.param_image)
|
|
if new_container:
|
|
container = new_container
|
|
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
|
|
)
|
|
image_different = False
|
|
if self.all_options["image"].comparison == "strict":
|
|
image_different = self._image_is_different(image, container)
|
|
if (
|
|
self.param_image_name_mismatch == "recreate"
|
|
and self.param_image is not None
|
|
and self.param_image != container.image_name
|
|
):
|
|
different = True
|
|
self.diff_tracker.add(
|
|
"image_name",
|
|
parameter=self.param_image,
|
|
active=container.image_name,
|
|
)
|
|
if image_different or different or self.param_recreate:
|
|
self.diff_tracker.merge(differences)
|
|
self.diff["differences"] = (
|
|
differences.get_legacy_docker_container_diffs()
|
|
)
|
|
if image_different:
|
|
self.diff["image_different"] = True
|
|
self.log("differences")
|
|
self.log(
|
|
differences.get_legacy_docker_container_diffs(), pretty_print=True
|
|
)
|
|
image_to_use = self.param_image
|
|
if not image_to_use and container and container.image:
|
|
image_to_use = container.image
|
|
if not image_to_use:
|
|
self.fail(
|
|
"Cannot recreate container when image is not specified or cannot be extracted from current container!"
|
|
)
|
|
if container.running:
|
|
self.container_stop(container.id)
|
|
self.container_remove(container.id)
|
|
if not self.check_mode:
|
|
self.wait_for_state(
|
|
container.id,
|
|
wait_states=["removing"],
|
|
accept_removal=True,
|
|
max_wait=self.param_removal_wait_timeout,
|
|
)
|
|
new_container = self.container_create(image_to_use)
|
|
if new_container:
|
|
container = new_container
|
|
container_created = True
|
|
comparison_image = image
|
|
|
|
if container and container.exists:
|
|
container = self.update_limits(
|
|
container, container_image, comparison_image, host_info
|
|
)
|
|
container = self.update_networks(container, container_created)
|
|
|
|
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)
|
|
|
|
if (
|
|
state in ("started", "healthy")
|
|
and self.param_paused is not None
|
|
and container.paused != self.param_paused
|
|
):
|
|
self.diff_tracker.add(
|
|
"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(
|
|
self.client, container.id
|
|
)
|
|
else:
|
|
self.engine_driver.unpause_container(
|
|
self.client, container.id
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(
|
|
f"Error {'pausing' if self.param_paused else 'unpausing'} container {container.id}: {exc}"
|
|
)
|
|
container = self._get_container(container.id)
|
|
self.results["changed"] = True
|
|
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"],
|
|
complete_states=["healthy", None],
|
|
max_wait=self.param_healthy_wait_timeout,
|
|
health_state=True,
|
|
)
|
|
if inspect_result:
|
|
# Return the latest inspection results retrieved
|
|
self.facts = inspect_result
|
|
|
|
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: str | bytes) -> None:
|
|
self.module.log(msg=msg)
|
|
|
|
def _get_container(self, container: str) -> Container:
|
|
"""
|
|
Expects container ID or Name. Returns a container object
|
|
"""
|
|
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: 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_data = self.engine_driver.inspect_image_by_id(self.client, image)
|
|
else:
|
|
repository, tag = parse_repository_tag(image)
|
|
if not tag:
|
|
tag = "latest"
|
|
image_data = self.engine_driver.inspect_image_by_name(
|
|
self.client, repository, tag
|
|
)
|
|
return image_data or fallback
|
|
|
|
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 = (
|
|
self._get_container_image(container) if get_container_image else None
|
|
)
|
|
if container_image:
|
|
self.log("current image")
|
|
self.log(container_image, pretty_print=True)
|
|
if not image_parameter:
|
|
self.log("No image specified")
|
|
return None, container_image, container_image
|
|
if is_image_name_id(image_parameter):
|
|
image = self.engine_driver.inspect_image_by_id(self.client, image_parameter)
|
|
if image is None:
|
|
self.fail(f"Cannot find image with ID {image_parameter}")
|
|
else:
|
|
repository, tag = parse_repository_tag(image_parameter)
|
|
if not tag:
|
|
tag = "latest"
|
|
image = self.engine_driver.inspect_image_by_name(
|
|
self.client, repository, tag
|
|
)
|
|
if not image and self.param_pull == "never":
|
|
self.fail(
|
|
f"Cannot find image with name {repository}:{tag}, and pull=never"
|
|
)
|
|
if not image or self.param_pull == "always":
|
|
if not self.check_mode:
|
|
self.log("Pull the image.")
|
|
image, already_to_latest = self.engine_driver.pull_image(
|
|
self.client,
|
|
repository,
|
|
tag,
|
|
image_platform=self.module.params["platform"],
|
|
)
|
|
if already_to_latest:
|
|
self.results["changed"] = False
|
|
self._add_action(
|
|
{"pulled_image": f"{repository}:{tag}", "changed": False}
|
|
)
|
|
else:
|
|
self.results["changed"] = True
|
|
self._add_action(
|
|
{"pulled_image": f"{repository}:{tag}", "changed": True}
|
|
)
|
|
elif not image or self.param_pull_check_mode_behavior == "always":
|
|
# If the image is not there, or pull_check_mode_behavior == 'always', claim we'll
|
|
# pull. (Implicitly: if the image is there, claim it already was latest unless
|
|
# pull_check_mode_behavior == 'always'.)
|
|
self.results["changed"] = True
|
|
action: dict[str, t.Any] = {"pulled_image": f"{repository}:{tag}"}
|
|
if not image:
|
|
action["changed"] = True
|
|
self._add_action(action)
|
|
|
|
self.log("image")
|
|
self.log(image, pretty_print=True)
|
|
|
|
comparison_image = image
|
|
if self.param_image_comparison == "current-image":
|
|
if not get_container_image:
|
|
container_image = self._get_container_image(container)
|
|
comparison_image = container_image
|
|
|
|
return image, container_image, comparison_image
|
|
|
|
def _image_is_different(
|
|
self, image: dict[str, t.Any] | None, container: Container
|
|
) -> bool:
|
|
if (
|
|
image
|
|
and image.get("Id")
|
|
and container
|
|
and container.image
|
|
and image.get("Id") != container.image
|
|
):
|
|
self.diff_tracker.add(
|
|
"image", parameter=image.get("Id"), active=container.image
|
|
)
|
|
return True
|
|
return False
|
|
|
|
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)):
|
|
engine.set_value(
|
|
self.module,
|
|
params,
|
|
self.engine_driver.get_api_version(self.client),
|
|
options.options,
|
|
values,
|
|
)
|
|
params["Image"] = image
|
|
return params
|
|
|
|
def _record_differences(
|
|
self,
|
|
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,
|
|
) -> None:
|
|
assert container.raw is not None
|
|
container_values = engine.get_value(
|
|
self.module,
|
|
container.raw,
|
|
self.engine_driver.get_api_version(self.client),
|
|
options.options,
|
|
container_image,
|
|
host_info,
|
|
)
|
|
expected_values = engine.get_expected_values(
|
|
self.module,
|
|
self.client,
|
|
self.engine_driver.get_api_version(self.client),
|
|
options.options,
|
|
image,
|
|
param_values.copy(),
|
|
host_info,
|
|
)
|
|
for option in options.options:
|
|
if option.name in expected_values:
|
|
param_value = expected_values[option.name]
|
|
container_value = container_values.get(option.name)
|
|
match = engine.compare_value(option, param_value, container_value)
|
|
|
|
if not match:
|
|
# No match.
|
|
if engine.ignore_mismatching_result(
|
|
self.module,
|
|
self.client,
|
|
self.engine_driver.get_api_version(self.client),
|
|
option,
|
|
image,
|
|
container_value,
|
|
param_value,
|
|
host_info,
|
|
):
|
|
# Ignore the result
|
|
continue
|
|
|
|
# Record the differences
|
|
p = param_value
|
|
c = container_value
|
|
if option.comparison_type == "set":
|
|
# Since the order does not matter, sort so that the diff output is better.
|
|
if p is not None:
|
|
p = sorted(p)
|
|
if c is not None:
|
|
c = sorted(c)
|
|
elif option.comparison_type == "set(dict)":
|
|
# Since the order does not matter, sort so that the diff output is better.
|
|
if option.name == "expected_mounts":
|
|
# For selected values, use one entry as key
|
|
def sort_key_fn(x: dict[str, t.Any]) -> t.Any:
|
|
return x["target"]
|
|
|
|
else:
|
|
# We sort the list of dictionaries by using the sorted items of a dict as its key.
|
|
def sort_key_fn(x: dict[str, t.Any]) -> t.Any:
|
|
return sorted(
|
|
(a, to_text(b, errors="surrogate_or_strict"))
|
|
for a, b in x.items()
|
|
)
|
|
|
|
if p is not None:
|
|
p = sorted(p, key=sort_key_fn)
|
|
if c is not None:
|
|
c = sorted(c, key=sort_key_fn)
|
|
differences.add(option.name, parameter=p, active=c)
|
|
|
|
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)):
|
|
self._record_differences(
|
|
update_differences,
|
|
options,
|
|
param_values,
|
|
engine,
|
|
container,
|
|
container_image,
|
|
image,
|
|
host_info,
|
|
)
|
|
else:
|
|
self._record_differences(
|
|
differences,
|
|
options,
|
|
param_values,
|
|
engine,
|
|
container,
|
|
container_image,
|
|
image,
|
|
host_info,
|
|
)
|
|
has_differences = not differences.empty
|
|
# Only consider differences of properties that can be updated when there are also other differences
|
|
if has_differences:
|
|
differences.merge(update_differences)
|
|
return has_differences, differences
|
|
|
|
def has_different_resource_limits(
|
|
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(
|
|
self.engine_driver.get_api_version(self.client)
|
|
):
|
|
continue
|
|
self._record_differences(
|
|
differences,
|
|
options,
|
|
param_values,
|
|
engine,
|
|
container,
|
|
container_image,
|
|
image,
|
|
host_info,
|
|
)
|
|
has_differences = not differences.empty
|
|
return has_differences, differences
|
|
|
|
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(
|
|
self.engine_driver.get_api_version(self.client)
|
|
):
|
|
continue
|
|
engine.update_value(
|
|
self.module,
|
|
result,
|
|
self.engine_driver.get_api_version(self.client),
|
|
options.options,
|
|
values,
|
|
)
|
|
return result
|
|
|
|
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
|
|
)
|
|
if limits_differ:
|
|
self.log("limit differences:")
|
|
self.log(
|
|
different_limits.get_legacy_docker_container_diffs(), pretty_print=True
|
|
)
|
|
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: 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: 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."
|
|
)
|
|
|
|
connected_networks = container.container["NetworkSettings"]["Networks"]
|
|
for network in self.module.params["networks"]:
|
|
network_info = connected_networks.get(network["name"])
|
|
if network_info is None:
|
|
different = True
|
|
differences.append({"parameter": network, "container": None})
|
|
else:
|
|
diff = False
|
|
network_info_ipam = network_info.get("IPAMConfig") or {}
|
|
if network.get("ipv4_address") and normalize_ip_address(
|
|
network["ipv4_address"]
|
|
) != normalize_ip_address(network_info_ipam.get("IPv4Address")):
|
|
diff = True
|
|
if network.get("ipv6_address") and normalize_ip_address(
|
|
network["ipv6_address"]
|
|
) != normalize_ip_address(network_info_ipam.get("IPv6Address")):
|
|
diff = True
|
|
if network.get("aliases") and not compare_generic(
|
|
network["aliases"],
|
|
network_info.get("Aliases"),
|
|
"allow_more_present",
|
|
"set",
|
|
):
|
|
diff = True
|
|
if network.get("links"):
|
|
expected_links = []
|
|
for link, alias in network["links"]:
|
|
expected_links.append(f"{link}:{alias}")
|
|
if not compare_generic(
|
|
expected_links,
|
|
network_info.get("Links"),
|
|
"allow_more_present",
|
|
"set",
|
|
):
|
|
diff = True
|
|
if network.get("mac_address") and network[
|
|
"mac_address"
|
|
] != network_info.get("MacAddress"):
|
|
diff = True
|
|
if diff:
|
|
different = True
|
|
differences.append(
|
|
{
|
|
"parameter": network,
|
|
"container": {
|
|
"name": network["name"],
|
|
"ipv4_address": network_info_ipam.get("IPv4Address"),
|
|
"ipv6_address": network_info_ipam.get("IPv6Address"),
|
|
"aliases": network_info.get("Aliases"),
|
|
"links": network_info.get("Links"),
|
|
"mac_address": network_info.get("MacAddress"),
|
|
},
|
|
}
|
|
)
|
|
return different, differences
|
|
|
|
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: 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."
|
|
)
|
|
|
|
connected_networks = container.container["NetworkSettings"].get("Networks")
|
|
if connected_networks:
|
|
for network, network_config in connected_networks.items():
|
|
keep = False
|
|
if self.module.params["networks"]:
|
|
for expected_network in self.module.params["networks"]:
|
|
if expected_network["name"] == network:
|
|
keep = True
|
|
if not keep:
|
|
extra = True
|
|
extra_networks.append(
|
|
{"name": network, "id": network_config["NetworkID"]}
|
|
)
|
|
return extra, extra_networks
|
|
|
|
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(
|
|
container
|
|
)
|
|
if has_network_differences:
|
|
if self.diff.get("differences"):
|
|
self.diff["differences"].append(
|
|
{"network_differences": network_differences}
|
|
)
|
|
else:
|
|
self.diff["differences"] = [
|
|
{"network_differences": network_differences}
|
|
]
|
|
for netdiff in network_differences:
|
|
self.diff_tracker.add(
|
|
f"network.{netdiff['parameter']['name']}",
|
|
parameter=netdiff["parameter"],
|
|
active=netdiff["container"],
|
|
)
|
|
self.results["changed"] = True
|
|
updated_container = self._add_networks(container, network_differences)
|
|
|
|
purge_networks = (
|
|
self.all_options["networks"].comparison == "strict"
|
|
and self.module.params["networks"] is not None
|
|
)
|
|
if purge_networks:
|
|
has_extra_networks, extra_networks = self.has_extra_networks(container)
|
|
if has_extra_networks:
|
|
if self.diff.get("differences"):
|
|
self.diff["differences"].append({"purge_networks": extra_networks})
|
|
else:
|
|
self.diff["differences"] = [{"purge_networks": extra_networks}]
|
|
for extra_network in extra_networks:
|
|
self.diff_tracker.add(
|
|
f"network.{extra_network['name']}", active=extra_network
|
|
)
|
|
self.results["changed"] = True
|
|
updated_container = self._purge_networks(container, extra_networks)
|
|
return updated_container
|
|
|
|
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._add_action({"removed_from_network": diff["parameter"]["name"]})
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.disconnect_container_from_network(
|
|
self.client, container.id, diff["parameter"]["id"]
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(
|
|
f"Error disconnecting container from network {diff['parameter']['name']} - {exc}"
|
|
)
|
|
# connect to the network
|
|
self._add_action(
|
|
{
|
|
"added_to_network": diff["parameter"]["name"],
|
|
"network_parameters": diff["parameter"],
|
|
}
|
|
)
|
|
if not self.check_mode:
|
|
params = {
|
|
key: value
|
|
for key, value in diff["parameter"].items()
|
|
if key not in ("id", "name")
|
|
}
|
|
try:
|
|
self.log(
|
|
f"Connecting container to network {diff['parameter']['id']}"
|
|
)
|
|
self.log(params, pretty_print=True)
|
|
self.engine_driver.connect_container_to_network(
|
|
self.client, container.id, diff["parameter"]["id"], params
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(
|
|
f"Error connecting container to network {diff['parameter']['name']} - {exc}"
|
|
)
|
|
return self._get_container(container.id)
|
|
|
|
def _purge_networks(
|
|
self, container: Container, networks: list[dict[str, t.Any]]
|
|
) -> Container:
|
|
assert container.id is not None
|
|
for network in networks:
|
|
self._add_action({"removed_from_network": network["name"]})
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.disconnect_container_from_network(
|
|
self.client, container.id, network["name"]
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(
|
|
f"Error disconnecting container from network {network['name']} - {exc}"
|
|
)
|
|
return self._get_container(container.id)
|
|
|
|
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:")
|
|
self.log(create_parameters, pretty_print=True)
|
|
networks = {}
|
|
if self.param_networks_cli_compatible and self.module.params["networks"]:
|
|
network_list = self.module.params["networks"]
|
|
if not self.engine_driver.create_container_supports_more_than_one_network(
|
|
self.client
|
|
):
|
|
network_list = network_list[:1]
|
|
for network in network_list:
|
|
networks[network["name"]] = {
|
|
key: value
|
|
for key, value in network.items()
|
|
if key not in ("name", "id")
|
|
}
|
|
self._add_action(
|
|
{
|
|
"created": "Created container",
|
|
"create_parameters": create_parameters,
|
|
"networks": networks,
|
|
}
|
|
)
|
|
self.results["changed"] = True
|
|
if not self.check_mode:
|
|
try:
|
|
container_id = self.engine_driver.create_container(
|
|
self.client, self.param_name, create_parameters, networks=networks
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error creating container: {exc}")
|
|
return self._get_container(container_id)
|
|
return None
|
|
|
|
def container_start(self, container_id: str) -> Container:
|
|
self.log(f"start container {container_id}")
|
|
self._add_action({"started": container_id})
|
|
self.results["changed"] = True
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.start_container(self.client, container_id)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error starting container {container_id}: {exc}")
|
|
|
|
if self.module.params["detach"] is False:
|
|
status = self.engine_driver.wait_for_container(
|
|
self.client, container_id
|
|
)
|
|
# 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:
|
|
self.module.warn(
|
|
"Cannot output_logs if auto_remove is enabled!"
|
|
)
|
|
else:
|
|
output, real_output = self.engine_driver.get_container_output(
|
|
self.client, container_id
|
|
)
|
|
if real_output and self.param_output_logs:
|
|
self._output_logs(msg=output)
|
|
|
|
if self.param_cleanup:
|
|
self.container_remove(container_id, force=True)
|
|
insp = self._get_container(container_id)
|
|
if insp.raw:
|
|
insp.raw["Output"] = output
|
|
else:
|
|
insp.raw = {"Output": output}
|
|
if status != 0:
|
|
# Set `failed` to True and return output as msg
|
|
self.results["failed"] = True
|
|
self.results["msg"] = output
|
|
return insp
|
|
return self._get_container(container_id)
|
|
|
|
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._add_action(
|
|
{
|
|
"removed": container_id,
|
|
"volume_state": volume_state,
|
|
"link": link,
|
|
"force": force,
|
|
}
|
|
)
|
|
self.results["changed"] = True
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.remove_container(
|
|
self.client,
|
|
container_id,
|
|
remove_volumes=volume_state,
|
|
link=link,
|
|
force=force,
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error removing container {container_id}: {exc}")
|
|
|
|
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._add_action(
|
|
{"updated": container_id, "update_parameters": update_parameters}
|
|
)
|
|
self.results["changed"] = True
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.update_container(
|
|
self.client, container_id, update_parameters
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error updating container {container_id}: {exc}")
|
|
return self._get_container(container_id)
|
|
|
|
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:
|
|
self.engine_driver.kill_container(
|
|
self.client, container_id, kill_signal=self.param_kill_signal
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error killing container {container_id}: {exc}")
|
|
|
|
def container_restart(self, container_id: str) -> Container:
|
|
self._add_action(
|
|
{"restarted": container_id, "timeout": self.module.params["stop_timeout"]}
|
|
)
|
|
self.results["changed"] = True
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.restart_container(
|
|
self.client, container_id, self.module.params["stop_timeout"] or 10
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error restarting container {container_id}: {exc}")
|
|
return self._get_container(container_id)
|
|
|
|
def container_stop(self, container_id: str) -> None:
|
|
if self.param_force_kill:
|
|
self.container_kill(container_id)
|
|
return
|
|
self._add_action(
|
|
{"stopped": container_id, "timeout": self.module.params["stop_timeout"]}
|
|
)
|
|
self.results["changed"] = True
|
|
if not self.check_mode:
|
|
try:
|
|
self.engine_driver.stop_container(
|
|
self.client, container_id, self.module.params["stop_timeout"]
|
|
)
|
|
except Exception as exc: # pylint: disable=broad-exception-caught
|
|
self.fail(f"Error stopping container {container_id}: {exc}")
|
|
|
|
|
|
def run_module(engine_driver: EngineDriver) -> None:
|
|
module, active_options, client = engine_driver.setup(
|
|
argument_spec={
|
|
"cleanup": {"type": "bool", "default": False},
|
|
"comparisons": {"type": "dict"},
|
|
"container_default_behavior": {
|
|
"type": "str",
|
|
"default": "no_defaults",
|
|
"choices": ["compatibility", "no_defaults"],
|
|
},
|
|
"command_handling": {
|
|
"type": "str",
|
|
"choices": ["compatibility", "correct"],
|
|
"default": "correct",
|
|
},
|
|
"default_host_ip": {"type": "str"},
|
|
"force_kill": {"type": "bool", "default": False, "aliases": ["forcekill"]},
|
|
"image": {"type": "str"},
|
|
"image_comparison": {
|
|
"type": "str",
|
|
"choices": ["desired-image", "current-image"],
|
|
"default": "desired-image",
|
|
},
|
|
"image_label_mismatch": {
|
|
"type": "str",
|
|
"choices": ["ignore", "fail"],
|
|
"default": "ignore",
|
|
},
|
|
"image_name_mismatch": {
|
|
"type": "str",
|
|
"choices": ["ignore", "recreate"],
|
|
"default": "recreate",
|
|
},
|
|
"keep_volumes": {"type": "bool", "default": True},
|
|
"kill_signal": {"type": "str"},
|
|
"name": {"type": "str", "required": True},
|
|
"networks_cli_compatible": {"type": "bool", "default": True},
|
|
"output_logs": {"type": "bool", "default": False},
|
|
"paused": {"type": "bool"},
|
|
"pull": {
|
|
"type": "raw",
|
|
"choices": ["never", "missing", "always", True, False],
|
|
"default": "missing",
|
|
},
|
|
"pull_check_mode_behavior": {
|
|
"type": "str",
|
|
"choices": ["image_not_present", "always"],
|
|
"default": "image_not_present",
|
|
},
|
|
"recreate": {"type": "bool", "default": False},
|
|
"removal_wait_timeout": {"type": "float"},
|
|
"restart": {"type": "bool", "default": False},
|
|
"state": {
|
|
"type": "str",
|
|
"default": "started",
|
|
"choices": ["absent", "present", "healthy", "started", "stopped"],
|
|
},
|
|
"healthy_wait_timeout": {"type": "float", "default": 300},
|
|
},
|
|
required_if=[
|
|
("state", "present", ["image"]),
|
|
],
|
|
)
|
|
|
|
def execute() -> t.NoReturn:
|
|
cm = ContainerManager(module, engine_driver, client, active_options)
|
|
cm.run()
|
|
module.exit_json(**sanitize_result(cm.results))
|
|
|
|
engine_driver.run(execute, client)
|