# Copyright (c) 2022 Felix Fontein # 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 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._util import ( DifferenceTracker, DockerBaseClass, compare_generic, is_image_name_id, sanitize_result, ) class Container(DockerBaseClass): def __init__(self, container, engine_driver): super().__init__() self.raw = container self.id = None self.image = None self.image_name = 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): return True if self.container else False @property def removing(self): return ( self.engine_driver.is_container_removing(self.container) if self.container else False ) @property def running(self): return ( self.engine_driver.is_container_running(self.container) if self.container else False ) @property def paused(self): return ( self.engine_driver.is_container_paused(self.container) if self.container else False ) class ContainerManager(DockerBaseClass): def __init__(self, module, engine_driver, client, active_options): 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 = self.module.params["cleanup"] self.param_container_default_behavior = self.module.params[ "container_default_behavior" ] self.param_default_host_ip = self.module.params["default_host_ip"] self.param_debug = self.module.params["debug"] self.param_force_kill = self.module.params["force_kill"] self.param_image = self.module.params["image"] self.param_image_comparison = self.module.params["image_comparison"] self.param_image_label_mismatch = self.module.params["image_label_mismatch"] self.param_image_name_mismatch = self.module.params["image_name_mismatch"] self.param_keep_volumes = self.module.params["keep_volumes"] self.param_kill_signal = self.module.params["kill_signal"] self.param_name = self.module.params["name"] self.param_networks_cli_compatible = self.module.params[ "networks_cli_compatible" ] self.param_output_logs = self.module.params["output_logs"] self.param_paused = self.module.params["paused"] self.param_pull = self.module.params["pull"] if self.param_pull is True: self.param_pull = "always" if self.param_pull is False: self.param_pull = "missing" self.param_pull_check_mode_behavior = self.module.params[ "pull_check_mode_behavior" ] self.param_recreate = self.module.params["recreate"] self.param_removal_wait_timeout = self.module.params["removal_wait_timeout"] self.param_healthy_wait_timeout = self.module.params["healthy_wait_timeout"] if self.param_healthy_wait_timeout <= 0: self.param_healthy_wait_timeout = None self.param_restart = self.module.params["restart"] self.param_state = self.module.params["state"] self._parse_comparisons() self._update_params() self.results = {"changed": False, "actions": []} self.diff = {} self.diff_tracker = DifferenceTracker() self.facts = {} 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.' ) def _collect_all_options(self, active_options): 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): 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): # 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 if self.module.params.get("comparisons"): # If '*' appears in comparisons, process it first if "*" in self.module.params["comparisons"]: value = self.module.params["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(): if option.name == "networks": # `networks` is special: only update if # some value is actually specified if self.module.params["networks"] is None: continue option.comparison = value # Now process all other comparisons. comp_aliases_used = {} for key, value in self.module.params["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): 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 = dict( 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, **kwargs): self.client.fail(*args, **kwargs) def run(self): if self.param_state in ("stopped", "started", "present", "healthy"): self.present(self.param_state) 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, complete_states=None, wait_states=None, accept_removal=False, max_wait=None, health_state=False, ): delay = 1.0 total_wait = 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): 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): 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): 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): 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 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 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) 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) container = self.container_restart(container.id) elif state == "stopped" and container.running: self.diff_tracker.add("running", parameter=False, active=was_running) 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: 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: 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.results["actions"].append(dict(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' 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): container = self._get_container(self.param_name) if container.exists: 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): self.module.log(msg=msg) def _get_container(self, container): """ Expects container ID or Name. Returns a container object """ container = self.engine_driver.inspect_container_by_name(self.client, container) return Container(container, self.engine_driver) def _get_container_image(self, container, fallback=None): if not container.exists or container.removing: return fallback image = container.image if is_image_name_id(image): image = self.engine_driver.inspect_image_by_id(self.client, image) else: repository, tag = parse_repository_tag(image) if not tag: tag = "latest" image = self.engine_driver.inspect_image_by_name( self.client, repository, tag ) return image or fallback def _get_image(self, container, needs_container_image=False): 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.client.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.client.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, alreadyToLatest = self.engine_driver.pull_image( self.client, repository, tag, image_platform=self.module.params["platform"], ) if alreadyToLatest: self.results["changed"] = False self.results["actions"].append( dict(pulled_image=f"{repository}:{tag}", changed=False) ) else: self.results["changed"] = True self.results["actions"].append( dict(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(pulled_image=f"{repository}:{tag}") if not image: action["changed"] = True self.results["actions"].append(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, container): if image and image.get("Id"): if container and container.image: if 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): params = {} 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, options, param_values, engine, container, container_image, image, host_info, ): 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): 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): 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_image, image, host_info): differences = DifferenceTracker() update_differences = DifferenceTracker() 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_image, image, host_info ): differences = DifferenceTracker() 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): result = {} 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_image, image, host_info): 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: self.container_update(container.id, self._compose_update_parameters()) return self._get_container(container.id) return container def has_network_differences(self, container): """ Check if the container is connected to requested networks with expected options: links, aliases, ipv4, ipv6 """ different = False differences = [] if not self.module.params["networks"]: return different, differences 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(dict(parameter=network, container=None)) else: diff = False network_info_ipam = network_info.get("IPAMConfig") or {} if network.get("ipv4_address") and network[ "ipv4_address" ] != network_info_ipam.get("IPv4Address"): diff = True if network.get("ipv6_address") and network[ "ipv6_address" ] != network_info_ipam.get("IPv6Address"): diff = True if network.get("aliases"): if 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( dict( parameter=network, container=dict( 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): """ Check if the container is connected to non-requested networks """ extra_networks = [] extra = False 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( dict(name=network, id=network_config["NetworkID"]) ) return extra, extra_networks def update_networks(self, container, container_created): 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( dict(network_differences=network_differences) ) else: self.diff["differences"] = [ dict(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(dict(purge_networks=extra_networks)) else: self.diff["differences"] = [dict(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, differences): for diff in differences: # remove the container from the network, if connected if diff.get("container"): self.results["actions"].append( dict(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: self.fail( f"Error disconnecting container from network {diff['parameter']['name']} - {exc}" ) # connect to the network self.results["actions"].append( dict( 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: self.fail( f"Error connecting container to network {diff['parameter']['name']} - {exc}" ) return self._get_container(container.id) def _purge_networks(self, container, networks): for network in networks: self.results["actions"].append(dict(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: self.fail( f"Error disconnecting container from network {network['name']} - {exc}" ) return self._get_container(container.id) def container_create(self, image): 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.results["actions"].append( dict( created="Created container", create_parameters=create_parameters, networks=networks, ) ) self.results["changed"] = True new_container = None 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: self.fail(f"Error creating container: {exc}") return self._get_container(container_id) return new_container def container_start(self, container_id): self.log(f"start container {container_id}") self.results["actions"].append(dict(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: 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 ) self.client.fail_results["status"] = status self.results["status"] = status 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 = dict(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, link=False, force=False): volume_state = not self.param_keep_volumes self.log( f"remove container container:{container_id} v:{volume_state} link:{link} force{force}" ) self.results["actions"].append( dict( 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: self.client.fail(f"Error removing container {container_id}: {exc}") def container_update(self, container_id, update_parameters): if update_parameters: self.log(f"update container {container_id}") self.log(update_parameters, pretty_print=True) self.results["actions"].append( dict(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: self.fail(f"Error updating container {container_id}: {exc}") return self._get_container(container_id) def container_kill(self, container_id): self.results["actions"].append( dict(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: self.fail(f"Error killing container {container_id}: {exc}") def container_restart(self, container_id): self.results["actions"].append( dict(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: self.fail(f"Error restarting container {container_id}: {exc}") return self._get_container(container_id) def container_stop(self, container_id): if self.param_force_kill: self.container_kill(container_id) return self.results["actions"].append( dict(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: self.fail(f"Error stopping container {container_id}: {exc}") def run_module(engine_driver): module, active_options, client = engine_driver.setup( argument_spec=dict( cleanup=dict(type="bool", default=False), comparisons=dict(type="dict"), container_default_behavior=dict( type="str", default="no_defaults", choices=["compatibility", "no_defaults"], ), command_handling=dict( type="str", choices=["compatibility", "correct"], default="correct" ), default_host_ip=dict(type="str"), force_kill=dict(type="bool", default=False, aliases=["forcekill"]), image=dict(type="str"), image_comparison=dict( type="str", choices=["desired-image", "current-image"], default="desired-image", ), image_label_mismatch=dict( type="str", choices=["ignore", "fail"], default="ignore" ), image_name_mismatch=dict( type="str", choices=["ignore", "recreate"], default="recreate" ), keep_volumes=dict(type="bool", default=True), kill_signal=dict(type="str"), name=dict(type="str", required=True), networks_cli_compatible=dict(type="bool", default=True), output_logs=dict(type="bool", default=False), paused=dict(type="bool"), pull=dict( type="raw", choices=["never", "missing", "always", True, False], default="missing", ), pull_check_mode_behavior=dict( type="str", choices=["image_not_present", "always"], default="image_not_present", ), recreate=dict(type="bool", default=False), removal_wait_timeout=dict(type="float"), restart=dict(type="bool", default=False), state=dict( type="str", default="started", choices=["absent", "present", "healthy", "started", "stopped"], ), healthy_wait_timeout=dict(type="float", default=300), ), required_if=[ ("state", "present", ["image"]), ], ) def execute(): cm = ContainerManager(module, engine_driver, client, active_options) cm.run() module.exit_json(**sanitize_result(cm.results)) engine_driver.run(execute, client)