# 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 from __future__ import absolute_import, division, print_function __metaclass__ = type import re from time import sleep from ansible.module_utils.common.text.converters import to_native, to_text from ansible_collections.community.docker.plugins.module_utils.util import ( DifferenceTracker, DockerBaseClass, compare_generic, is_image_name_id, sanitize_result, ) from ansible_collections.community.docker.plugins.module_utils._api.utils.utils import parse_repository_tag class Container(DockerBaseClass): def __init__(self, container, engine_driver): super(Container, self).__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): 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_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 = '[{0}]'.format(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, ' 'or an IPv6 address. Got "{0}" instead.'.format(self.param_default_host_ip)) 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 legacy ignore options if self.module.params['ignore_image']: self.all_options['image'].comparison = 'ignore' if self.module.params['purge_networks']: self.all_options['networks'].comparison = 'strict' # 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("The module option '%s' cannot be specified in the comparisons dict, " "since it does not correspond to container's state!" % key) if key not in self.all_options or self.all_options[key].not_an_ansible_option: self.fail("Unknown module option '%s' in comparisons dict!" % key) key_main = key if key_main in comp_aliases_used: self.fail("Both '%s' and '%s' (aliases of %s) are specified in comparisons dict!" % (key, comp_aliases_used[key_main], key_main)) 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("Option '%s' is a value and not a set/list/dict, so its comparison cannot be %s" % (key, value)) self.all_options[key_main].comparison = value else: self.fail("Unknown comparison mode '%s'!" % 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 # Check legacy values if self.module.params['ignore_image'] and self.all_options['image'].comparison != 'ignore': self.module.warn('The ignore_image option has been overridden by the comparisons option!') if self.module.params['purge_networks'] and self.all_options['networks'].comparison != 'strict': self.module.warn('The purge_networks option has been overridden by the comparisons option!') 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'): 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): 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 msg = 'Encontered vanished container while waiting for container "{0}"' self.fail(msg.format(container_id)) # Check container state state = result.get('State', {}).get('Status') if complete_states is not None and state in complete_states: return if wait_states is not None and state not in wait_states: msg = 'Encontered unexpected state "{1}" while waiting for container "{0}"' self.fail(msg.format(container_id, state)) # Wait if max_wait is not None: if total_wait > max_wait: msg = 'Timeout of {1} seconds exceeded while waiting for container "{0}"' self.fail(msg.format(container_id, max_wait)) 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 != 'ignore' and self.param_image is not None and self.param_image != container.image_name: if self.param_image_name_mismatch == 'recreate': different = True self.diff_tracker.add('image_name', parameter=self.param_image, active=container.image_name) else: # The default has been deprecated! self.module.deprecate( 'The default value "ignore" for image_name_mismatch has been deprecated and will change to "recreate"' ' in community.docker 4.0.0. In the current situation, this would cause the container to be recreated' ' since the current container\'s image name "{active}" does not match the desired image name "{parameter}".'.format( parameter=self.param_image, active=container.image_name), version='4.0.0', collection_name='community.docker', ) 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 == 'started' and not container.running: self.diff_tracker.add('running', parameter=True, active=was_running) container = self.container_start(container.id) elif state == 'started' 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 == 'started' 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("Error %s container %s: %s" % ( "pausing" if self.param_paused else "unpausing", container.id, to_native(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 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("Cannot find image with ID %s" % (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("Cannot find image with name %s:%s, and pull=never" % (repository, tag)) 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, platform=self.module.params['platform']) if alreadyToLatest: self.results['changed'] = False self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag), changed=False)) else: self.results['changed'] = True self.results['actions'].append(dict(pulled_image="%s:%s" % (repository, tag), changed=True)) elif not image or self.param_pull_check_mode_behavior == 'always': # If the image isn't 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="%s:%s" % (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): # 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("%s:%s" % (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( 'network.{0}'.format(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 not purge_networks and self.module.params['purge_networks']: purge_networks = True self.module.deprecate( 'The purge_networks option is used while networks is not specified. In this case purge_networks=true cannot' ' be replaced by `networks: strict` in comparisons, which is necessary once purge_networks is removed.' ' Please modify the docker_container invocation by adding `networks: []`', version='4.0.0', collection_name='community.docker') 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( 'network.{0}'.format(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("Error disconnecting container from network %s - %s" % (diff['parameter']['name'], to_native(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("Connecting container to network %s" % 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("Error connecting container to network %s - %s" % (diff['parameter']['name'], to_native(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("Error disconnecting container from network %s - %s" % (network['name'], to_native(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("image: %s parameters:" % image) self.log(create_parameters, pretty_print=True) self.results['actions'].append(dict(created="Created container", create_parameters=create_parameters)) 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) except Exception as exc: self.fail("Error creating container: %s" % to_native(exc)) return self._get_container(container_id) return new_container def container_start(self, container_id): self.log("start container %s" % (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("Error starting container %s: %s" % (container_id, to_native(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("remove container container:%s v:%s link:%s force%s" % (container_id, volume_state, link, 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("Error removing container %s: %s" % (container_id, to_native(exc))) def container_update(self, container_id, update_parameters): if update_parameters: self.log("update container %s" % (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("Error updating container %s: %s" % (container_id, to_native(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("Error killing container %s: %s" % (container_id, to_native(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("Error restarting container %s: %s" % (container_id, to_native(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("Error stopping container %s: %s" % (container_id, to_native(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']), ignore_image=dict(type='bool', default=False, removed_in_version='4.0.0', removed_from_collection='community.docker'), 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']), 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'), purge_networks=dict(type='bool', default=False, removed_in_version='4.0.0', removed_from_collection='community.docker'), 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', 'started', 'stopped']), ), 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)