diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:22 +0000 |
commit | 38b7c80217c4e72b1d8988eb1e60bb6e77334114 (patch) | |
tree | 356e9fd3762877d07cde52d21e77070aeff7e789 /ansible_collections/containers/podman/plugins | |
parent | Adding upstream version 7.7.0+dfsg. (diff) | |
download | ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.tar.xz ansible-38b7c80217c4e72b1d8988eb1e60bb6e77334114.zip |
Adding upstream version 9.4.0+dfsg.upstream/9.4.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/containers/podman/plugins')
21 files changed, 1243 insertions, 132 deletions
diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/common.py b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py index dba3aff65..cbb6b080e 100644 --- a/ansible_collections/containers/podman/plugins/module_utils/podman/common.py +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/common.py @@ -19,6 +19,25 @@ except ImportError: ' < 2.11, you need to use Python < 3.12 with ' 'distutils.version present'), exc) +ARGUMENTS_OPTS_DICT = { + '--attach': ['--attach', '-a'], + '--cpu-shares': ['--cpu-shares', '-c'], + '--detach': ['--detach', '-d'], + '--env': ['--env', '-e'], + '--hostname': ['--hostname', '-h'], + '--interactive': ['--interactive', '-i'], + '--label': ['--label', '-l'], + '--memory': ['--memory', '-m'], + '--network': ['--network', '--net'], + '--publish': ['--publish', '-p'], + '--publish-all': ['--publish-all', '-P'], + '--quiet': ['--quiet', '-q'], + '--tty': ['--tty', '-t'], + '--user': ['--user', '-u'], + '--volume': ['--volume', '-v'], + '--workdir': ['--workdir', '-w'], +} + def run_podman_command(module, executable='podman', args=None, expected_rc=0, ignore_errors=False): if not isinstance(executable, list): @@ -50,8 +69,15 @@ def run_generate_systemd_command(module, module_params, name, version): command.extend([ '--restart-policy', sysconf['restart_policy']]) - if sysconf.get('time'): - command.extend(['--time', str(sysconf['time'])]) + if sysconf.get('restart_sec') is not None: + command.extend(['--restart-sec=%s' % sysconf['restart_sec']]) + if (sysconf.get('stop_timeout') is not None) or (sysconf.get('time') is not None): + # Select correct parameter name based on version + arg_name = 'stop-timeout' if gt4ver else 'time' + arg_value = sysconf.get('stop_timeout') if sysconf.get('stop_timeout') is not None else sysconf.get('time') + command.extend(['--%s=%s' % (arg_name, arg_value)]) + if sysconf.get('start_timeout') is not None: + command.extend(['--start-timeout=%s' % sysconf['start_timeout']]) if sysconf.get('no_header'): command.extend(['--no-header']) if sysconf.get('names', True): @@ -96,34 +122,103 @@ def run_generate_systemd_command(module, module_params, name, version): return rc, systemd, err +def compare_systemd_file_content(file_path, file_content): + if not os.path.exists(file_path): + # File does not exist, so all lines in file_content are different + return '', file_content + # Read the file + with open(file_path, 'r') as unit_file: + current_unit_file_content = unit_file.read() + + # Function to remove comments from file content + def remove_comments(content): + return "\n".join([line for line in content.splitlines() if not line.startswith('#')]) + + # Remove comments from both file contents before comparison + current_unit_file_content_nocmnt = remove_comments(current_unit_file_content) + unit_content_nocmnt = remove_comments(file_content) + if current_unit_file_content_nocmnt == unit_content_nocmnt: + return None + + # Get the different lines between the two contents + diff_in_file = [line + for line in unit_content_nocmnt.splitlines() + if line not in current_unit_file_content_nocmnt.splitlines()] + diff_in_string = [line + for line in current_unit_file_content_nocmnt.splitlines() + if line not in unit_content_nocmnt.splitlines()] + + return diff_in_string, diff_in_file + + def generate_systemd(module, module_params, name, version): - empty = {} + result = { + 'changed': False, + 'systemd': {}, + 'diff': {}, + } sysconf = module_params['generate_systemd'] rc, systemd, err = run_generate_systemd_command(module, module_params, name, version) if rc != 0: module.log( "PODMAN-CONTAINER-DEBUG: Error generating systemd: %s" % err) - return empty + if sysconf: + module.fail_json(msg="Error generating systemd: %s" % err) + return result else: try: data = json.loads(systemd) + result['systemd'] = data if sysconf.get('path'): full_path = os.path.expanduser(sysconf['path']) if not os.path.exists(full_path): os.makedirs(full_path) + result['changed'] = True if not os.path.isdir(full_path): module.fail_json("Path %s is not a directory! " "Can not save systemd unit files there!" % full_path) for file_name, file_content in data.items(): file_name += ".service" + if not os.path.exists(os.path.join(full_path, file_name)): + result['changed'] = True + if result['diff'].get('before') is None: + result['diff'] = {'before': {}, 'after': {}} + result['diff']['before'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): ''}) + result['diff']['after'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): file_content}) + + else: + diff_ = compare_systemd_file_content(os.path.join(full_path, file_name), file_content) + if diff_: + result['changed'] = True + if result['diff'].get('before') is None: + result['diff'] = {'before': {}, 'after': {}} + result['diff']['before'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): "\n".join(diff_[0])}) + result['diff']['after'].update( + {'systemd_{file_name}.service'.format(file_name=file_name): "\n".join(diff_[1])}) with open(os.path.join(full_path, file_name), 'w') as f: f.write(file_content) - return data + diff_before = "\n".join( + ["{j} - {k}".format(j=j, k=k) + for j, k in result['diff'].get('before', {}).items() if 'PIDFile' not in k]).strip() + diff_after = "\n".join( + ["{j} - {k}".format(j=j, k=k) + for j, k in result['diff'].get('after', {}).items() if 'PIDFile' not in k]).strip() + if diff_before or diff_after: + result['diff']['before'] = diff_before + "\n" + result['diff']['after'] = diff_after + "\n" + else: + result['diff'] = {} + return result except Exception as e: module.log( "PODMAN-CONTAINER-DEBUG: Error writing systemd: %s" % e) - return empty + if sysconf: + module.fail_json(msg="Error writing systemd: %s" % e) + return result def delete_systemd(module, module_params, name, version): @@ -230,3 +325,15 @@ def normalize_signal(signal_name_or_number): if signal_name not in _signal_map: raise RuntimeError("Unknown signal '{0}'".format(signal_name_or_number)) return str(_signal_map[signal_name]) + + +def get_podman_version(module, fail=True): + executable = module.params['executable'] if module.params['executable'] else 'podman' + rc, out, err = module.run_command( + [executable, b'--version']) + if rc != 0 or not out or "version" not in out: + if fail: + module.fail_json(msg="'%s --version' run failed! Error: %s" % + (executable, err)) + return None + return out.split("version")[1].strip() diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py index 1ba28f4c8..ff4c18629 100644 --- a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_container_lib.py @@ -9,6 +9,7 @@ from ansible_collections.containers.podman.plugins.module_utils.podman.common im from ansible_collections.containers.podman.plugins.module_utils.podman.common import generate_systemd from ansible_collections.containers.podman.plugins.module_utils.podman.common import delete_systemd from ansible_collections.containers.podman.plugins.module_utils.podman.common import normalize_signal +from ansible_collections.containers.podman.plugins.module_utils.podman.common import ARGUMENTS_OPTS_DICT __metaclass__ = type @@ -19,6 +20,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( 'absent', 'present', 'stopped', 'started', 'created']), image=dict(type='str'), annotation=dict(type='dict'), + attach=dict(type='list', elements='str', choices=['stdout', 'stderr', 'stdin']), authfile=dict(type='path'), blkio_weight=dict(type='int'), blkio_weight_device=dict(type='dict'), @@ -32,12 +34,16 @@ ARGUMENTS_SPEC_CONTAINER = dict( conmon_pidfile=dict(type='path'), command=dict(type='raw'), cpu_period=dict(type='int'), + cpu_quota=dict(type='int'), cpu_rt_period=dict(type='int'), cpu_rt_runtime=dict(type='int'), cpu_shares=dict(type='int'), cpus=dict(type='str'), cpuset_cpus=dict(type='str'), cpuset_mems=dict(type='str'), + delete_depend=dict(type='bool'), + delete_time=dict(type='str'), + delete_volumes=dict(type='bool'), detach=dict(type='bool', default=True), debug=dict(type='bool', default=False), detach_keys=dict(type='str', no_log=False), @@ -51,13 +57,14 @@ ARGUMENTS_SPEC_CONTAINER = dict( dns_search=dict(type='str', aliases=['dns_search_domains']), entrypoint=dict(type='str'), env=dict(type='dict'), - env_file=dict(type='path'), + env_file=dict(type='list', elements='path', aliases=['env_files']), env_host=dict(type='bool'), etc_hosts=dict(type='dict', aliases=['add_hosts']), expose=dict(type='list', elements='str', aliases=[ 'exposed', 'exposed_ports']), force_restart=dict(type='bool', default=False, aliases=['restart']), + force_delete=dict(type='bool', default=True), generate_systemd=dict(type='dict', default={}), gidmap=dict(type='list', elements='str'), group_add=dict(type='list', elements='str', aliases=['groups']), @@ -66,6 +73,8 @@ ARGUMENTS_SPEC_CONTAINER = dict( healthcheck_retries=dict(type='int'), healthcheck_start_period=dict(type='str'), healthcheck_timeout=dict(type='str'), + healthcheck_failure_action=dict(type='str', choices=[ + 'none', 'kill', 'restart', 'stop']), hooks_dir=dict(type='list', elements='str'), hostname=dict(type='str'), http_proxy=dict(type='bool'), @@ -112,6 +121,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( recreate=dict(type='bool', default=False), requires=dict(type='list', elements='str'), restart_policy=dict(type='str'), + restart_time=dict(type='str'), rm=dict(type='bool', aliases=['remove', 'auto_remove']), rootfs=dict(type='bool'), secrets=dict(type='list', elements='str', no_log=True), @@ -121,6 +131,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( sig_proxy=dict(type='bool'), stop_signal=dict(type='int'), stop_timeout=dict(type='int'), + stop_time=dict(type='str'), subgidname=dict(type='str'), subuidname=dict(type='str'), sysctl=dict(type='dict'), @@ -227,12 +238,35 @@ class PodmanModuleParams: def start_stop_delete(self): + def complete_params(cmd): + if self.params['attach'] and self.action == 'start': + cmd.append('--attach') + if self.params['detach'] is False and self.action == 'start' and '--attach' not in cmd: + cmd.append('--attach') + if self.params['detach_keys'] and self.action == 'start': + cmd += ['--detach-keys', self.params['detach_keys']] + if self.params['sig_proxy'] and self.action == 'start': + cmd.append('--sig-proxy') + if self.params['stop_time'] and self.action == 'stop': + cmd += ['--time', self.params['stop_time']] + if self.params['restart_time'] and self.action == 'restart': + cmd += ['--time', self.params['restart_time']] + if self.params['delete_depend'] and self.action == 'delete': + cmd.append('--depend') + if self.params['delete_time'] and self.action == 'delete': + cmd += ['--time', self.params['delete_time']] + if self.params['delete_volumes'] and self.action == 'delete': + cmd.append('--volumes') + if self.params['force_delete'] and self.action == 'delete': + cmd.append('--force') + return cmd + if self.action in ['stop', 'start', 'restart']: - cmd = [self.action, self.params['name']] + cmd = complete_params([self.action]) + [self.params['name']] return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] if self.action == 'delete': - cmd = ['rm', '-f', self.params['name']] + cmd = complete_params(['rm']) + [self.params['name']] return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] def check_version(self, param, minv=None, maxv=None): @@ -252,6 +286,11 @@ class PodmanModuleParams: c += ['--annotation', '='.join(annotate)] return c + def addparam_attach(self, c): + for attach in self.params['attach']: + c += ['--attach=%s' % attach] + return c + def addparam_authfile(self, c): return c + ['--authfile', self.params['authfile']] @@ -293,6 +332,9 @@ class PodmanModuleParams: def addparam_cpu_period(self, c): return c + ['--cpu-period', self.params['cpu_period']] + def addparam_cpu_quota(self, c): + return c + ['--cpu-quota', self.params['cpu_quota']] + def addparam_cpu_rt_period(self, c): return c + ['--cpu-rt-period', self.params['cpu_rt_period']] @@ -312,6 +354,9 @@ class PodmanModuleParams: return c + ['--cpuset-mems', self.params['cpuset_mems']] def addparam_detach(self, c): + # Remove detach from create command and don't set if attach is true + if self.action == 'create' or self.params['attach']: + return c return c + ['--detach=%s' % self.params['detach']] def addparam_detach_keys(self, c): @@ -362,7 +407,9 @@ class PodmanModuleParams: return c def addparam_env_file(self, c): - return c + ['--env-file', self.params['env_file']] + for env_file in self.params['env_file']: + c += ['--env-file', env_file] + return c def addparam_env_host(self, c): self.check_version('--env-host', minv='1.5.0') @@ -407,6 +454,10 @@ class PodmanModuleParams: return c + ['--healthcheck-timeout', self.params['healthcheck_timeout']] + def addparam_healthcheck_failure_action(self, c): + return c + ['--health-on-failure', + self.params['healthcheck_failure_action']] + def addparam_hooks_dir(self, c): for hook_dir in self.params['hooks_dir']: c += ['--hooks-dir=%s' % hook_dir] @@ -722,6 +773,35 @@ class PodmanContainerDiff: params_with_defaults[p] = self.module_params[p] return params_with_defaults + def _createcommand(self, argument): + """Returns list of values for given argument from CreateCommand + from Podman container inspect output. + + Args: + argument (str): argument name + + Returns: + + all_values: list of values for given argument from createcommand + """ + if "createcommand" not in self.info["config"]: + return [] + cr_com = self.info["config"]["createcommand"] + argument_values = ARGUMENTS_OPTS_DICT.get(argument, [argument]) + all_values = [] + for arg in argument_values: + for ind, cr_opt in enumerate(cr_com): + if arg == cr_opt: + # This is a key=value argument + if not cr_com[ind + 1].startswith("-"): + all_values.append(cr_com[ind + 1]) + else: + # This is a false/true switching argument + return [True] + if cr_opt.startswith("%s=" % arg): + all_values.append(cr_opt.split("=", 1)[1]) + return all_values + def _diff_update_and_compare(self, param_name, before, after): if before != after: self.diff['before'].update({param_name: before}) @@ -737,7 +817,7 @@ class PodmanContainerDiff: return self._diff_update_and_compare('annotation', before, after) def diffparam_env_host(self): - # It's impossible to get from inspest, recreate it if not default + # It's impossible to get from inspect, recreate it if not default before = False after = self.params['env_host'] return self._diff_update_and_compare('env_host', before, after) @@ -826,9 +906,16 @@ class PodmanContainerDiff: def diffparam_cpu_period(self): before = self.info['hostconfig']['cpuperiod'] - after = self.params['cpu_period'] + # if cpu_period left to default keep settings + after = self.params['cpu_period'] or before return self._diff_update_and_compare('cpu_period', before, after) + def diffparam_cpu_quota(self): + before = self.info['hostconfig']['cpuquota'] + # if cpu_quota left to default keep settings + after = self.params['cpu_quota'] or before + return self._diff_update_and_compare('cpu_quota', before, after) + def diffparam_cpu_rt_period(self): before = self.info['hostconfig']['cpurealtimeperiod'] after = self.params['cpu_rt_period'] @@ -845,8 +932,9 @@ class PodmanContainerDiff: return self._diff_update_and_compare('cpu_shares', before, after) def diffparam_cpus(self): - before = int(self.info['hostconfig']['nanocpus']) / 1000000000 - after = self.params['cpus'] + before = self.info['hostconfig']['nanocpus'] / 1000000000 + # if cpus left to default keep settings + after = float(self.params['cpus'] or before) return self._diff_update_and_compare('cpus', before, after) def diffparam_cpuset_cpus(self): @@ -863,16 +951,13 @@ class PodmanContainerDiff: before = [":".join([i['pathonhost'], i['pathincontainer']]) for i in self.info['hostconfig']['devices']] if not before and 'createcommand' in self.info['config']: - cr_com = self.info['config']['createcommand'] - if '--device' in cr_com: - before = [cr_com[k + 1].lower() - for k, i in enumerate(cr_com) if i == '--device'] + before = [i.lower() for i in self._createcommand('--device')] before = [":".join((i, i)) if len(i.split(":")) == 1 else i for i in before] after = [":".join(i.split(":")[:2]) for i in self.params['device']] after = [":".join((i, i)) if len(i.split(":")) == 1 else i for i in after] - after = [i.lower() for i in after] + before, after = [i.lower() for i in before], [i.lower() for i in after] before, after = sorted(list(set(before))), sorted(list(set(after))) return self._diff_update_and_compare('devices', before, after) @@ -931,15 +1016,23 @@ class PodmanContainerDiff: # Healthcheck is only defined in container config if a healthcheck # was configured; otherwise the config key isn't part of the config. def diffparam_healthcheck(self): + before = '' if 'healthcheck' in self.info['config']: # the "test" key is a list of 2 items where the first one is # "CMD-SHELL" and the second one is the actual healthcheck command. - before = self.info['config']['healthcheck']['test'][1] - else: - before = '' + if len(self.info['config']['healthcheck']['test']) > 1: + before = self.info['config']['healthcheck']['test'][1] after = self.params['healthcheck'] or before return self._diff_update_and_compare('healthcheck', before, after) + def diffparam_healthcheck_failure_action(self): + if 'healthcheckonfailureaction' in self.info['config']: + before = self.info['config']['healthcheckonfailureaction'] + else: + before = '' + after = self.params['healthcheck_failure_action'] or before + return self._diff_update_and_compare('healthcheckonfailureaction', before, after) + # Because of hostname is random generated, this parameter has partial idempotency only. def diffparam_hostname(self): before = self.info['config']['hostname'] @@ -1066,9 +1159,8 @@ class PodmanContainerDiff: if macs: before = macs[0] if not before and 'createcommand' in self.info['config']: - cr_com = self.info['config']['createcommand'] - if '--mac-address' in cr_com: - before = cr_com[cr_com.index('--mac-address') + 1].lower() + before = [i.lower() for i in self._createcommand('--mac-address')] + before = before[0] if before else '' if self.module_params['mac_address'] is not None: after = self.params['mac_address'] else: @@ -1084,11 +1176,10 @@ class PodmanContainerDiff: before = [] # Special case for options for slirp4netns rootless networking from v2 if net_mode_before == 'slirp4netns' and 'createcommand' in self.info['config']: - cr_com = self.info['config']['createcommand'] - if '--network' in cr_com: - cr_net = cr_com[cr_com.index('--network') + 1].lower() - if 'slirp4netns:' in cr_net: - before = [cr_net] + cr_net = [i.lower() for i in self._createcommand('--network')] + for cr_net_opt in cr_net: + if 'slirp4netns:' in cr_net_opt: + before = [cr_net_opt] after = self.params['network'] or [] # If container is in pod and no networks are provided if not self.module_params['network'] and self.params['pod']: @@ -1124,8 +1215,19 @@ class PodmanContainerDiff: return self._diff_update_and_compare('privileged', before, after) def diffparam_pid(self): + def get_container_id_by_name(name): + rc, podman_inspect_info, err = self.module.run_command( + [self.module.params["executable"], "inspect", name, "-f", "{{.Id}}"]) + if rc != 0: + return None + return podman_inspect_info.strip() + before = self.info['hostconfig']['pidmode'] after = self.params['pid'] + if after is not None and "container:" in after and "container:" in before: + if after.split(":")[1] == before.split(":")[1]: + return self._diff_update_and_compare('pid', before, after) + after = "container:" + get_container_id_by_name(after.split(":")[1]) return self._diff_update_and_compare('pid', before, after) # TODO(sshnaidm) Need to add port ranges support @@ -1150,7 +1252,7 @@ class PodmanContainerDiff: if image_ports: after += list(image_ports.keys()) after = [ - i.replace("/tcp", "").replace("[", "").replace("]", "") + i.replace("/tcp", "").replace("[", "").replace("]", "").replace("0.0.0.0:", "") for i in after] # No support for port ranges yet for ports in after: @@ -1166,7 +1268,15 @@ class PodmanContainerDiff: def diffparam_restart_policy(self): before = self.info['hostconfig']['restartpolicy']['name'] + before_max_count = int(self.info['hostconfig']['restartpolicy'].get('maximumretrycount', 0)) after = self.params['restart_policy'] or "" + if ':' in after: + after, after_max_count = after.rsplit(':', 1) + after_max_count = int(after_max_count) + else: + after_max_count = 0 + before = "%s:%i" % (before, before_max_count) + after = "%s:%i" % (after, after_max_count) return self._diff_update_and_compare('restart_policy', before, after) def diffparam_rm(self): @@ -1175,11 +1285,16 @@ class PodmanContainerDiff: return self._diff_update_and_compare('rm', before, after) def diffparam_security_opt(self): - before = self.info['hostconfig']['securityopt'] - # In rootful containers with apparmor there is a default security opt - before = [o for o in before if 'apparmor=containers-default' not in o] - after = self.params['security_opt'] - before, after = sorted(list(set(before))), sorted(list(set(after))) + unsorted_before = self.info['hostconfig']['securityopt'] + unsorted_after = self.params['security_opt'] + # In rootful containers with apparmor there is a profile, "container-default", + # which is already added by default + # Since SElinux labels are basically annotations, they are merged in a single list + # element by podman so we need to split them in a (sorted) list if we want to compare it + # to the list we provide to the module + before = sorted(item for element in unsorted_before for item in element.split(',') + if 'apparmor=container-default' not in item) + after = sorted(list(set(unsorted_after))) return self._diff_update_and_compare('security_opt', before, after) def diffparam_stop_signal(self): @@ -1206,11 +1321,7 @@ class PodmanContainerDiff: after = self.params['ulimit'] or [] # In case of latest podman if 'createcommand' in self.info['config']: - ulimits = [] - for k, c in enumerate(self.info['config']['createcommand']): - if c == '--ulimit': - ulimits.append(self.info['config']['createcommand'][k + 1]) - before = ulimits + before = self._createcommand('--ulimit') before, after = sorted(before), sorted(after) return self._diff_update_and_compare('ulimit', before, after) if after: @@ -1428,8 +1539,6 @@ class PodmanContainer: self.version, self.module, ).construct_command_from_params() - if action == 'create': - b_command.remove(b'--detach=True') full_cmd = " ".join([self.module_params['executable']] + [to_native(i) for i in b_command]) self.actions.append(full_cmd) @@ -1449,7 +1558,7 @@ class PodmanContainer: self.stderr = err if rc != 0: self.module.fail_json( - msg="Can't %s container %s" % (action, self.name), + msg="Container %s exited with code %s when %sed" % (self.name, rc, action), stdout=out, stderr=err) def run(self): @@ -1547,11 +1656,19 @@ class PodmanManager: self.results.update({'diff': self.container.diff}) if self.module.params['debug'] or self.module_params['debug']: self.results.update({'podman_version': self.container.version}) + sysd = generate_systemd(self.module, + self.module_params, + self.name, + self.container.version) + self.results['changed'] = changed or sysd['changed'] self.results.update( - {'podman_systemd': generate_systemd(self.module, - self.module_params, - self.name, - self.container.version)}) + {'podman_systemd': sysd['systemd']}) + if sysd['diff']: + if 'diff' not in self.results: + self.results.update({'diff': sysd['diff']}) + else: + self.results['diff']['before'] += sysd['diff']['before'] + self.results['diff']['after'] += sysd['diff']['after'] def make_started(self): """Run actions if desired state is 'started'.""" @@ -1601,7 +1718,8 @@ class PodmanManager: self.results['actions'].append('started %s' % self.container.name) self.update_container_result() return - elif self.container.stopped and self.container.different: + elif self.container.stopped and \ + (self.container.different or self.recreate): self.container.recreate_run() self.results['actions'].append('recreated %s' % self.container.name) diff --git a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py index 0b4afc0bc..4106136e2 100644 --- a/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py +++ b/ansible_collections/containers/podman/plugins/module_utils/podman/podman_pod_lib.py @@ -27,10 +27,15 @@ ARGUMENTS_SPEC_POD = dict( recreate=dict(type='bool', default=False), add_host=dict(type='list', required=False, elements='str'), cgroup_parent=dict(type='str', required=False), + blkio_weight=dict(type='str', required=False), + blkio_weight_device=dict(type='list', elements='str', required=False), cpus=dict(type='str', required=False), cpuset_cpus=dict(type='str', required=False), + cpuset_mems=dict(type='str', required=False), + cpu_shares=dict(type='str', required=False), device=dict(type='list', elements='str', required=False), device_read_bps=dict(type='list', elements='str', required=False), + device_write_bps=dict(type='list', elements='str', required=False), dns=dict(type='list', elements='str', required=False), dns_opt=dict(type='list', elements='str', required=False), dns_search=dict(type='list', elements='str', required=False), @@ -46,6 +51,8 @@ ARGUMENTS_SPEC_POD = dict( label=dict(type='dict', required=False), label_file=dict(type='str', required=False), mac_address=dict(type='str', required=False), + memory=dict(type='str', required=False), + memory_swap=dict(type='str', required=False), name=dict(type='str', required=True), network=dict(type='list', elements='str', required=False), network_alias=dict(type='list', elements='str', required=False, @@ -135,25 +142,52 @@ class PodmanPodModuleParams: c += ['--add-host', g] return c + def addparam_blkio_weight(self, c): + self.check_version('--blkio-weight', minv='4.3.0') + return c + ['--blkio-weight', self.params['blkio_weight']] + + def addparam_blkio_weight_device(self, c): + self.check_version('--blkio-weight-device', minv='4.3.0') + for dev in self.params['blkio_weight_device']: + c += ['--blkio-weight-device', dev] + return c + def addparam_cgroup_parent(self, c): return c + ['--cgroup-parent', self.params['cgroup_parent']] def addparam_cpus(self, c): + self.check_version('--cpus', minv='4.2.0') return c + ['--cpus', self.params['cpus']] def addparam_cpuset_cpus(self, c): + self.check_version('--cpus', minv='4.2.0') return c + ['--cpuset-cpus', self.params['cpuset_cpus']] + def addparam_cpuset_mems(self, c): + self.check_version('--cpuset-mems', minv='4.3.0') + return c + ['--cpuset-mems', self.params['cpuset_mems']] + + def addparam_cpu_shares(self, c): + self.check_version('--cpu-shares', minv='4.3.0') + return c + ['--cpu-shares', self.params['cpu_shares']] + def addparam_device(self, c): for dev in self.params['device']: c += ['--device', dev] return c def addparam_device_read_bps(self, c): + self.check_version('--device-read-bps', minv='4.3.0') for dev in self.params['device_read_bps']: c += ['--device-read-bps', dev] return c + def addparam_device_write_bps(self, c): + self.check_version('--device-write-bps', minv='4.3.0') + for dev in self.params['device_write_bps']: + c += ['--device-write-bps', dev] + return c + def addparam_dns(self, c): for g in self.params['dns']: c += ['--dns', g] @@ -209,6 +243,14 @@ class PodmanPodModuleParams: def addparam_mac_address(self, c): return c + ['--mac-address', self.params['mac_address']] + def addparam_memory(self, c): + self.check_version('--memory', minv='4.2.0') + return c + ['--memory', self.params['memory']] + + def addparam_memory_swap(self, c): + self.check_version('--memory-swap', minv='4.3.0') + return c + ['--memory-swap', self.params['memory_swap']] + def addparam_name(self, c): return c + ['--name', self.params['name']] @@ -323,10 +365,8 @@ class PodmanPodDiff: return self._diff_update_and_compare('add_host', before, after) def diffparam_cgroup_parent(self): - if 'cgroupparent' in self.info: - before = self.info['cgroupparent'] - elif 'config' in self.info and self.info['config'].get('cgroupparent'): - before = self.info['config']['cgroupparent'] + before = (self.info.get('cgroupparent', '') + or self.info.get('hostconfig', {}).get('cgroupparent', '')) after = self.params['cgroup_parent'] or before return self._diff_update_and_compare('cgroup_parent', before, after) @@ -511,7 +551,7 @@ class PodmanPodDiff: # TODO: find out why on Ubuntu the 'net' is not present if 'net' not in before: after.remove('net') - if self.params["uidmap"] or self.params["gidmap"]: + if self.params["uidmap"] or self.params["gidmap"] or self.params["userns"]: after.append('user') before, after = sorted(list(set(before))), sorted(list(set(after))) @@ -759,11 +799,19 @@ class PodmanPodManager: self.results.update({'diff': self.pod.diff}) if self.module.params['debug'] or self.module_params['debug']: self.results.update({'podman_version': self.pod.version}) + sysd = generate_systemd(self.module, + self.module_params, + self.name, + self.pod.version) + self.results['changed'] = changed or sysd['changed'] self.results.update( - {'podman_systemd': generate_systemd(self.module, - self.module_params, - self.name, - self.pod.version)}) + {'podman_systemd': sysd['systemd']}) + if sysd['diff']: + if 'diff' not in self.results: + self.results.update({'diff': sysd['diff']}) + else: + self.results['diff']['before'] += sysd['diff']['before'] + self.results['diff']['after'] += sysd['diff']['after'] def execute(self): """Execute the desired action according to map of actions & states.""" diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container.py b/ansible_collections/containers/podman/plugins/modules/podman_container.py index 7878352da..51cb57a53 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_container.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_container.py @@ -76,6 +76,15 @@ options: - Add an annotation to the container. The format is key value, multiple times. type: dict + attach: + description: + - Attach to STDIN, STDOUT or STDERR. The default in Podman is false. + type: list + elements: str + choices: + - stdin + - stdout + - stderr authfile: description: - Path of the authentication file. Default is @@ -149,7 +158,11 @@ options: type: raw cpu_period: description: - - Limit the CPU real-time period in microseconds + - Limit the CPU CFS (Completely Fair Scheduler) period + type: int + cpu_quota: + description: + - Limit the CPU CFS (Completely Fair Scheduler) quota type: int cpu_rt_period: description: @@ -180,6 +193,22 @@ options: - Memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems. type: str + delete_depend: + description: + - Remove selected container and recursively remove all containers that depend on it. + Applies to "delete" command. + type: bool + delete_time: + description: + - Seconds to wait before forcibly stopping the container. Use -1 for infinite wait. + Applies to "delete" command. + type: str + delete_volumes: + description: + - Remove anonymous volumes associated with the container. + This does not include named volumes created with podman volume create, + or the --volume option of podman run and podman create. + type: bool detach: description: - Run container in detach mode @@ -262,7 +291,14 @@ options: - Read in a line delimited file of environment variables. Doesn't support idempotency. If users changes the file with environment variables it's on them to recreate the container. - type: path + The file must be present on the REMOTE machine where actual podman is + running, not on the controller machine where Ansible is executing. + If you need to copy the file from controller to remote machine, use the + copy or slurp module. + type: list + elements: path + aliases: + - env_files env_host: description: - Use all current host environment variables in container. @@ -292,6 +328,11 @@ options: default: False aliases: - restart + force_delete: + description: + - Force deletion of container when it's being deleted. + type: bool + default: True generate_systemd: description: - Generate systemd unit file for container. @@ -319,11 +360,21 @@ options: - 'on-watchdog' - 'on-abort' - 'always' - time: + restart_sec: + description: Set the systemd service restartsec value. + type: int + required: false + start_timeout: + description: Override the default start timeout for the container with the given value. + type: int + required: false + stop_timeout: description: - - Override the default stop timeout for the container with the given value. + - Override the default stop timeout for the container with the given value. Called `time` before version 4. type: int required: false + aliases: + - time no_header: description: - Do not generate the header including meta data such as the Podman version and the timestamp. @@ -416,6 +467,17 @@ options: is considered failed. Like start-period, the value can be expressed in a time format such as 1m22s. The default value is 30s type: str + healthcheck_failure_action: + description: + - The action to be taken when the container is considered unhealthy. The action must be one of + "none", "kill", "restart", or "stop". + The default policy is "none". + type: str + choices: + - 'none' + - 'kill' + - 'restart' + - 'stop' hooks_dir: description: - Each .json file in the path configures a hook for Podman containers. @@ -693,6 +755,11 @@ options: * always - Restart containers when they exit, regardless of status, retrying indefinitely type: str + restart_time: + description: + - Seconds to wait before forcibly stopping the container when restarting. Use -1 for infinite wait. + Applies to "restarted" status. + type: str rm: description: - Automatically remove the container when it exits. The default is false. @@ -740,6 +807,11 @@ options: description: - Signal to stop a container. Default is SIGTERM. type: int + stop_time: + description: + - Seconds to wait before forcibly stopping the container. Use -1 for infinite wait. + Applies to "stopped" status. + type: str stop_timeout: description: - Timeout (in seconds) to stop a container. Default is 10. @@ -860,7 +932,7 @@ EXAMPLES = r""" generate_systemd: path: /tmp/ restart_policy: always - time: 120 + stop_timeout: 120 names: true container_prefix: ainer @@ -912,6 +984,16 @@ EXAMPLES = r""" image: busybox log_options: path=/var/log/container/mycontainer.json log_driver: k8s-file + +- name: Run container with complex command with quotes + containers.podman.podman_container: + name: mycontainer + image: certbot/certbot + command: + - renew + - --deploy-hook + - "echo 1 > /var/lib/letsencrypt/complete" + """ RETURN = r""" diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py b/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py new file mode 100644 index 000000000..d30e85cdb --- /dev/null +++ b/ansible_collections/containers/podman/plugins/modules/podman_container_exec.py @@ -0,0 +1,244 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2023, Takuya Nishimura <@nishipy> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = r''' +module: podman_container_exec +author: + - Takuya Nishimura (@nishipy) +short_description: Executes a command in a running container. +description: + - Executes a command in a running container. +options: + name: + description: + - Name of the container where the command is executed. + type: str + required: true + command: + description: + - The command to run in the container. + - One of the I(command) or I(args) is required. + type: str + argv: + description: + - Passes the command as a list rather than a string. + - One of the I(command) or I(args) is required. + type: list + elements: str + detach: + description: + - If true, the command runs in the background. + - The exec session is automatically removed when it completes. + type: bool + default: false + env: + description: + - Set environment variables. + type: dict + privileged: + description: + - Give extended privileges to the container. + type: bool + default: false + tty: + description: + - Allocate a pseudo-TTY. + type: bool + default: false + user: + description: + - The username or UID used and, optionally, the groupname or GID for the specified command. + - Both user and group may be symbolic or numeric. + type: str + workdir: + description: + - Working directory inside the container. + type: str +requirements: + - podman +notes: + - See L(the Podman documentation,https://docs.podman.io/en/latest/markdown/podman-exec.1.html) for details of podman-exec(1). +''' + +EXAMPLES = r''' +- name: Execute a command with workdir + containers.podman.podman_container_exec: + name: ubi8 + command: "cat redhat-release" + workdir: /etc + +- name: Execute a command with a list of args and environment variables + containers.podman.podman_container_exec: + name: test_container + argv: + - /bin/sh + - -c + - echo $HELLO $BYE + env: + HELLO: hello world + BYE: goodbye world + +- name: Execute command in background by using detach + containers.podman.podman_container_exec: + name: detach_container + command: "cat redhat-release" + detach: true +''' + +RETURN = r''' +stdout: + type: str + returned: success + description: + - The standard output of the command executed in the container. +stderr: + type: str + returned: success + description: + - The standard output of the command executed in the container. +rc: + type: int + returned: success + sample: 0 + description: + - The exit code of the command executed in the container. +exec_id: + type: str + returned: success and I(detach=true) + sample: f99002e34c1087fd1aa08d5027e455bf7c2d6b74f019069acf6462a96ddf2a47 + description: + - The ID of the exec session. +''' + + +import shlex +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.containers.podman.plugins.module_utils.podman.common import run_podman_command + + +def run_container_exec(module: AnsibleModule) -> dict: + ''' + Execute podman-container-exec for the given options + ''' + exec_with_args = ['container', 'exec'] + # podman_container_exec always returns changed=true + changed = True + exec_options = [] + + name = module.params['name'] + argv = module.params['argv'] + command = module.params['command'] + detach = module.params['detach'] + env = module.params['env'] + privileged = module.params['privileged'] + tty = module.params['tty'] + user = module.params['user'] + workdir = module.params['workdir'] + + if command is not None: + argv = shlex.split(command) + + if detach: + exec_options.append('--detach') + + if env is not None: + for key, value in env.items(): + if not isinstance(value, string_types): + module.fail_json( + msg="Specify string value %s on the env field" % (value)) + + to_text(value, errors='surrogate_or_strict') + exec_options += ['--env', + '%s="%s"' % (key, value)] + + if privileged: + exec_options.append('--privileged') + + if tty: + exec_options.append('--tty') + + if user is not None: + exec_options += ['--user', + to_text(user, errors='surrogate_or_strict')] + + if workdir is not None: + exec_options += ['--workdir', + to_text(workdir, errors='surrogate_or_strict')] + + exec_options.append(name) + exec_options.extend(argv) + + exec_with_args.extend(exec_options) + + rc, stdout, stderr = run_podman_command( + module=module, executable='podman', args=exec_with_args) + + result = { + 'changed': changed, + 'podman_command': exec_options, + 'rc': rc, + 'stdout': stdout, + 'stderr': stderr, + } + + if detach: + result['exec_id'] = stdout.replace('\n', '') + + return result + + +def main(): + argument_spec = { + 'name': { + 'type': 'str', + 'required': True, + }, + 'command': { + 'type': 'str', + }, + 'argv': { + 'type': 'list', + 'elements': 'str', + }, + 'detach': { + 'type': 'bool', + 'default': False, + }, + 'env': { + 'type': 'dict', + }, + 'privileged': { + 'type': 'bool', + 'default': False, + }, + 'tty': { + 'type': 'bool', + 'default': False, + }, + 'user': { + 'type': 'str', + }, + 'workdir': { + 'type': 'str', + }, + } + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + required_one_of=[('argv', 'command')], + ) + + result = run_container_exec(module) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_container_info.py b/ansible_collections/containers/podman/plugins/modules/podman_container_info.py index bbdd29fb9..dd361c449 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_container_info.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_container_info.py @@ -50,7 +50,7 @@ EXAMPLES = r""" RETURN = r""" containers: - description: Facts from all or specificed containers + description: Facts from all or specified containers returned: always type: list elements: dict diff --git a/ansible_collections/containers/podman/plugins/modules/podman_containers.py b/ansible_collections/containers/podman/plugins/modules/podman_containers.py index c67aee344..7f418a67b 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_containers.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_containers.py @@ -41,7 +41,7 @@ EXAMPLES = ''' - name: web image: nginx - name: test - image: python:3-alpine + image: python:3.10-alpine command: python -V ''' diff --git a/ansible_collections/containers/podman/plugins/modules/podman_export.py b/ansible_collections/containers/podman/plugins/modules/podman_export.py index e2bb19614..dda0099cb 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_export.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_export.py @@ -24,7 +24,10 @@ options: description: - Container to export. type: str - required: true + volume: + description: + - Volume to export. + type: str force: description: - Force saving to file even if it exists. @@ -48,6 +51,9 @@ EXAMPLES = ''' - containers.podman.podman_export: dest: /path/to/tar/file container: container-name +- containers.podman.podman_export: + dest: /path/to/tar/file + volume: volume-name ''' import os # noqa: E402 @@ -57,8 +63,16 @@ from ..module_utils.podman.common import remove_file_or_dir # noqa: E402 def export(module, executable): changed = False - command = [executable, 'export'] - command += ['-o=%s' % module.params['dest'], module.params['container']] + export_type = '' + command = [] + if module.params['container']: + export_type = 'container' + command = [executable, 'export'] + else: + export_type = 'volume' + command = [executable, 'volume', 'export'] + + command += ['-o=%s' % module.params['dest'], module.params[export_type]] if module.params['force']: dest = module.params['dest'] if os.path.exists(dest): @@ -75,8 +89,8 @@ def export(module, executable): return changed, '', '' rc, out, err = module.run_command(command) if rc != 0: - module.fail_json(msg="Error exporting container %s: %s" % ( - module.params['container'], err)) + module.fail_json(msg="Error exporting %s %s: %s" % (export_type, + module.params['container'], err)) return changed, out, err @@ -84,11 +98,18 @@ def main(): module = AnsibleModule( argument_spec=dict( dest=dict(type='str', required=True), - container=dict(type='str', required=True), + container=dict(type='str'), + volume=dict(type='str'), force=dict(type='bool', default=True), executable=dict(type='str', default='podman') ), supports_check_mode=True, + mutually_exclusive=[ + ('container', 'volume'), + ], + required_one_of=[ + ('container', 'volume'), + ], ) executable = module.get_bin_path(module.params['executable'], required=True) diff --git a/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py b/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py index 9c9bc7b27..486a18a86 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_generate_systemd.py @@ -27,6 +27,12 @@ options: - Use C(/etc/systemd/system) for the system-wide systemd instance. - Use C(/etc/systemd/user) or C(~/.config/systemd/user) for use with per-user instances of systemd. type: path + force: + description: + - Replace the systemd unit file(s) even if it already exists. + - This works with dest option. + type: bool + default: false new: description: - Generate unit files that create containers and pods, not only start them. @@ -219,7 +225,7 @@ podman_command: import os from ansible.module_utils.basic import AnsibleModule import json - +from ansible_collections.containers.podman.plugins.module_utils.podman.common import compare_systemd_file_content RESTART_POLICY_CHOICES = [ 'no-restart', @@ -388,7 +394,7 @@ def generate_systemd(module): # In case of error in running the command if return_code != 0: - # Print informations about the error and return and empty dictionary + # Print information about the error and return and empty dictionary message = 'Error generating systemd .service unit(s).' message += ' Command executed: {command_str}' message += ' Command returned with code: {return_code}.' @@ -425,7 +431,7 @@ def generate_systemd(module): changed = True # If destination exist but not a directory if not os.path.isdir(systemd_units_dest): - # Stop and tell user that the destination is not a directry + # Stop and tell user that the destination is not a directory message = "Destination {systemd_units_dest} is not a directory." message += " Can't save systemd unit files in." module.fail_json( @@ -446,26 +452,13 @@ def generate_systemd(module): unit_file_name, ) - # See if we need to write the unit file, default yes - need_to_write_file = True - # If the unit file already exist, compare it with the - # generated content - if os.path.exists(unit_file_full_path): - # Read the file - with open(unit_file_full_path, 'r') as unit_file: - current_unit_file_content = unit_file.read() - # If current unit file content is the same as the - # generated content - # Remove comments from files, before comparing - current_unit_file_content_nocmnt = "\n".join([ - line for line in current_unit_file_content.splitlines() - if not line.startswith('#')]) - unit_content_nocmnt = "\n".join([ - line for line in unit_content.splitlines() - if not line.startswith('#')]) - if current_unit_file_content_nocmnt == unit_content_nocmnt: - # We don't need to write it - need_to_write_file = False + if module.params['force']: + # Force to replace the existing unit file + need_to_write_file = True + else: + # See if we need to write the unit file, default yes + need_to_write_file = bool(compare_systemd_file_content( + unit_file_full_path, unit_content)) # Write the file, if needed if need_to_write_file: @@ -506,6 +499,11 @@ def run_module(): 'required': False, 'default': False, }, + 'force': { + 'type': 'bool', + 'required': False, + 'default': False, + }, 'restart_policy': { 'type': 'str', 'required': False, diff --git a/ansible_collections/containers/podman/plugins/modules/podman_image.py b/ansible_collections/containers/podman/plugins/modules/podman_image.py index d66ff5d49..6305a5d5b 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_image.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_image.py @@ -17,7 +17,7 @@ DOCUMENTATION = r''' options: arch: description: - - CPU architecutre for the container image + - CPU architecture for the container image type: str name: description: @@ -132,6 +132,10 @@ DOCUMENTATION = r''' description: - Extra args to pass to build, if executed. Does not idempotently check for new build args. type: str + target: + description: + - Specify the target build stage to build. + type: str push_args: description: Arguments that control pushing images. type: dict @@ -512,6 +516,8 @@ class PodmanImageManager(object): if not self.module.check_mode: self.results['image'], output = self.push_image() self.results['stdout'] += "\n" + output + if image and not self.results.get('image'): + self.results['image'] = image def absent(self): image = self.find_image() @@ -536,12 +542,21 @@ class PodmanImageManager(object): image_name = self.image_name args = ['image', 'ls', image_name, '--format', 'json'] rc, images, err = self._run(args, ignore_errors=True) - images = json.loads(images) + try: + images = json.loads(images) + except json.decoder.JSONDecodeError: + self.module.fail_json(msg='Failed to parse JSON output from podman image ls: {out}'.format(out=images)) + if len(images) == 0: + # Let's find out if image exists + rc, out, err = self._run(['image', 'exists', image_name], ignore_errors=True) + if rc == 0: + inspect_json = self.inspect_image(image_name) + else: + return None if len(images) > 0: inspect_json = self.inspect_image(image_name) - if self._is_target_arch(inspect_json, self.arch) or not self.arch: - return images - + if self._is_target_arch(inspect_json, self.arch) or not self.arch: + return images or inspect_json return None def _is_target_arch(self, inspect_json=None, arch=None): @@ -565,7 +580,10 @@ class PodmanImageManager(object): image_name = self.image_name args = ['inspect', image_name, '--format', 'json'] rc, image_data, err = self._run(args) - image_data = json.loads(image_data) + try: + image_data = json.loads(image_data) + except json.decoder.JSONDecodeError: + self.module.fail_json(msg='Failed to parse JSON output from podman inspect: {out}'.format(out=image_data)) if len(image_data) > 0: return image_data else: @@ -656,6 +674,10 @@ class PodmanImageManager(object): if extra_args: args.extend(shlex.split(extra_args)) + target = self.build.get('target') + if target: + args.extend(['--target', target]) + args.append(self.path) rc, out, err = self._run(args, ignore_errors=True) @@ -812,6 +834,7 @@ def main(): rm=dict(type='bool', default=True), volume=dict(type='list', elements='str'), extra_args=dict(type='str'), + target=dict(type='str'), ), ), push_args=dict( diff --git a/ansible_collections/containers/podman/plugins/modules/podman_import.py b/ansible_collections/containers/podman/plugins/modules/podman_import.py index 5090b177c..6a408c08e 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_import.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_import.py @@ -29,6 +29,10 @@ options: - Set changes as list of key-value pairs, see example. type: list elements: dict + volume: + description: + - Volume to import, cannot be used with change and commit_message + type: str executable: description: - Path to C(podman) executable if it is not in the C($PATH) on the @@ -95,6 +99,9 @@ EXAMPLES = ''' - "CMD": /bin/bash - "User": root commit_message: "Importing image" +- containers.podman.podman_import: + src: /path/to/tar/file + volume: myvolume ''' import json # noqa: E402 @@ -128,25 +135,55 @@ def load(module, executable): return changed, out, err, info, command +def volume_load(module, executable): + changed = True + command = [executable, 'volume', 'import', module.params['volume'], module.params['src']] + src = module.params['src'] + if module.check_mode: + return changed, '', '', '', command + rc, out, err = module.run_command(command) + if rc != 0: + module.fail_json(msg="Error importing volume %s: %s" % (src, err)) + rc, out2, err2 = module.run_command([executable, 'volume', 'inspect', module.params['volume']]) + if rc != 0: + module.fail_json(msg="Volume %s inspection failed: %s" % (module.params['volume'], err2)) + try: + info = json.loads(out2)[0] + except Exception as e: + module.fail_json(msg="Could not parse JSON from volume %s: %s" % (module.params['volume'], e)) + return changed, out, err, info, command + + def main(): module = AnsibleModule( argument_spec=dict( src=dict(type='str', required=True), commit_message=dict(type='str'), change=dict(type='list', elements='dict'), - executable=dict(type='str', default='podman') + executable=dict(type='str', default='podman'), + volume=dict(type='str', required=False), ), + mutually_exclusive=[ + ('volume', 'commit_message'), + ('volume', 'change'), + ], supports_check_mode=True, ) executable = module.get_bin_path(module.params['executable'], required=True) - changed, out, err, image_info, command = load(module, executable) + volume_info = '' + image_info = '' + if module.params['volume']: + changed, out, err, volume_info, command = volume_load(module, executable) + else: + changed, out, err, image_info, command = load(module, executable) results = { "changed": changed, "stdout": out, "stderr": err, "image": image_info, + "volume": volume_info, "podman_command": " ".join(command) } diff --git a/ansible_collections/containers/podman/plugins/modules/podman_login.py b/ansible_collections/containers/podman/plugins/modules/podman_login.py index be417c761..8ae8418a9 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_login.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_login.py @@ -75,7 +75,7 @@ EXAMPLES = r""" username: user password: 'p4ssw0rd' -- name: Login to default registry and create ${XDG_RUNTIME_DIR}/containers/auth.json +- name: Login to quay.io and create ${XDG_RUNTIME_DIR}/containers/auth.json containers.podman.podman_login: username: user password: 'p4ssw0rd' diff --git a/ansible_collections/containers/podman/plugins/modules/podman_network.py b/ansible_collections/containers/podman/plugins/modules/podman_network.py index 846524b65..3f52af4ce 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_network.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_network.py @@ -37,10 +37,22 @@ options: description: - Driver to manage the network (default "bridge") type: str + force: + description: + - Remove all containers that use the network. + If the container is running, it is stopped and removed. + default: False + type: bool gateway: description: - IPv4 or IPv6 gateway for the subnet type: str + interface_name: + description: + - For bridge, it uses the bridge interface name. + For macvlan, it is the parent device on the host (it is the same + as 'opt.parent') + type: str internal: description: - Restrict external access from this network (default "false") @@ -92,7 +104,8 @@ options: required: false parent: description: - - The host device which should be used for the macvlan interface. + - The host device which should be used for the macvlan interface + (it is the same as 'interface' in that case). Defaults to the default route interface. type: str required: false @@ -219,14 +232,15 @@ class PodmanNetworkModuleParams: list -- list of byte strings for Popen command """ if self.action in ['delete']: - return self._simple_action() + return self._delete_action() if self.action in ['create']: return self._create_action() - def _simple_action(self): - if self.action == 'delete': - cmd = ['rm', '-f', self.params['name']] - return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] + def _delete_action(self): + cmd = ['rm', self.params['name']] + if self.params['force']: + cmd += ['--force'] + return [to_bytes(i, errors='surrogate_or_strict') for i in cmd] def _create_action(self): cmd = [self.action, self.params['name']] @@ -270,6 +284,9 @@ class PodmanNetworkModuleParams: def addparam_macvlan(self, c): return c + ['--macvlan', self.params['macvlan']] + def addparam_interface_name(self, c): + return c + ['--interface-name', self.params['interface_name']] + def addparam_internal(self, c): return c + ['--internal=%s' % self.params['internal']] @@ -291,7 +308,6 @@ class PodmanNetworkDefaults: self.version = podman_version self.defaults = { 'driver': 'bridge', - 'disable_dns': False, 'internal': False, 'ipv6': False } @@ -334,6 +350,9 @@ class PodmanNetworkDiff: if LooseVersion(self.version) >= LooseVersion('4.0.0'): before = not self.info.get('dns_enabled', True) after = self.params['disable_dns'] + # compare only if set explicitly + if self.params['disable_dns'] is None: + after = before return self._diff_update_and_compare('disable_dns', before, after) before = after = self.params['disable_dns'] return self._diff_update_and_compare('disable_dns', before, after) @@ -642,7 +661,9 @@ def main(): name=dict(type='str', required=True), disable_dns=dict(type='bool', required=False), driver=dict(type='str', required=False), + force=dict(type='bool', default=False), gateway=dict(type='str', required=False), + interface_name=dict(type='str', required=False), internal=dict(type='bool', required=False), ip_range=dict(type='str', required=False), ipv6=dict(type='bool', required=False), diff --git a/ansible_collections/containers/podman/plugins/modules/podman_play.py b/ansible_collections/containers/podman/plugins/modules/podman_play.py index 04a30441b..10a9a06fa 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_play.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_play.py @@ -29,6 +29,12 @@ options: - Path to file with YAML configuration for a Pod. type: path required: True + annotation: + description: + - Add an annotation to the container or pod. + type: dict + aliases: + - annotations authfile: description: - Path of the authentication file. Default is ${XDG_RUNTIME_DIR}/containers/auth.json, @@ -37,6 +43,11 @@ options: Note - You can also override the default path of the authentication file by setting the REGISTRY_AUTH_FILE environment variable. export REGISTRY_AUTH_FILE=path type: path + build: + description: + - Build images even if they are found in the local storage. + - It is required to exist subdirectories matching the image names to be build. + type: bool cert_dir: description: - Use certificates at path (*.crt, *.cert, *.key) to connect to the registry. @@ -51,6 +62,11 @@ options: Kubernetes configmap YAMLs type: list elements: path + context_dir: + description: + - Use path as the build context directory for each image. + Requires build option be true. + type: path seccomp_profile_root: description: - Directory path for seccomp profiles (default is "/var/lib/kubelet/seccomp"). @@ -68,6 +84,28 @@ options: description: - Set logging driver for all created containers. type: str + log_opt: + description: + - Logging driver specific options. Set custom logging configuration. + type: dict + aliases: + - log_options + suboptions: + path: + description: + - specify a path to the log file (e.g. /var/log/container/mycontainer.json). + type: str + required: false + max_size: + description: + - Specify a max size of the log file (e.g 10mb). + type: str + required: false + tag: + description: + - specify a custom log tag for the container. This option is currently supported only by the journald log driver in Podman. + type: str + required: false log_level: description: - Set logging level for podman calls. Log messages above specified level @@ -128,6 +166,18 @@ EXAMPLES = ''' kube_file: ~/kube.yaml state: started +- name: Recreate pod from a kube file with options + containers.podman.podman_play: + kube_file: ~/kube.yaml + state: started + recreate: true + annotations: + greeting: hello + greet_to: world + userns: host + log_opt: + path: /tmp/my-container.log + max_size: 10mb ''' import re # noqa: F402 try: @@ -148,6 +198,9 @@ class PodmanKubeManagement: self.command = [self.executable, 'play', 'kube'] creds = [] # pod_name = extract_pod_name(module.params['kube_file']) + if self.module.params['annotation']: + for k, v in self.module.params['annotation'].items(): + self.command.extend(['--annotation', '"{k}={v}"'.format(k=k, v=v)]) if self.module.params['username']: creds += [self.module.params['username']] if self.module.params['password']: @@ -160,11 +213,16 @@ class PodmanKubeManagement: if self.module.params['configmap']: configmaps = ",".join(self.module.params['configmap']) self.command.extend(['--configmap=%s' % configmaps]) + if self.module.params['log_opt']: + for k, v in self.module.params['log_opt'].items(): + self.command.extend(['--log-opt', '{k}={v}'.format(k=k.replace('_', '-'), v=v)]) start = self.module.params['state'] == 'started' self.command.extend(['--start=%s' % str(start).lower()]) for arg, param in { '--authfile': 'authfile', + '--build': 'build', '--cert-dir': 'cert_dir', + '--context-dir': 'context_dir', '--log-driver': 'log_driver', '--seccomp-profile-root': 'seccomp_profile_root', '--tls-verify': 'tls_verify', @@ -264,15 +322,22 @@ class PodmanKubeManagement: def main(): module = AnsibleModule( argument_spec=dict( + annotation=dict(type='dict', aliases=['annotations']), executable=dict(type='str', default='podman'), kube_file=dict(type='path', required=True), authfile=dict(type='path'), + build=dict(type='bool'), cert_dir=dict(type='path'), configmap=dict(type='list', elements='path'), + context_dir=dict(type='path'), seccomp_profile_root=dict(type='path'), username=dict(type='str'), password=dict(type='str', no_log=True), log_driver=dict(type='str'), + log_opt=dict(type='dict', aliases=['log_options'], options=dict( + path=dict(type='str'), + max_size=dict(type='str'), + tag=dict(type='str'))), network=dict(type='list', elements='str'), state=dict( type='str', diff --git a/ansible_collections/containers/podman/plugins/modules/podman_pod.py b/ansible_collections/containers/podman/plugins/modules/podman_pod.py index ab475de99..7b57fd302 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_pod.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_pod.py @@ -42,6 +42,18 @@ options: type: list elements: str required: false + blkio_weight: + description: + - Block IO relative weight. The weight is a value between 10 and 1000. + - This option is not supported on cgroups V1 rootless systems. + type: str + required: false + blkio_weight_device: + description: + - Block IO relative device weight. + type: list + elements: str + required: false cgroup_parent: description: - Path to cgroups under which the cgroup for the pod will be created. If the path @@ -61,6 +73,16 @@ options: Unlike `cpus` this is of type string and parsed as a list of numbers. Format is 0-3,0,1 required: false type: str + cpuset_mems: + description: + - Memory nodes in which to allow execution (0-3, 0,1). Only effective on NUMA systems. + required: false + type: str + cpu_shares: + description: + - CPU shares (relative weight). + required: false + type: str device: description: - Add a host device to the pod. Optional permissions parameter can be used to specify @@ -74,6 +96,12 @@ options: elements: str required: false type: list + device_write_bps: + description: + - Limit write rate (in bytes per second) to a device. + type: list + elements: str + required: false dns: description: - Set custom DNS servers in the /etc/resolv.conf file that will be shared between @@ -123,11 +151,21 @@ options: - 'on-watchdog' - 'on-abort' - 'always' - time: + restart_sec: + description: Set the systemd service restartsec value. + type: int + required: false + start_timeout: + description: Override the default start timeout for the container with the given value. + type: int + required: false + stop_timeout: description: - - Override the default stop timeout for the container with the given value. + - Override the default stop timeout for the container with the given value. Called `time` before version 4. type: int required: false + aliases: + - time no_header: description: - Do not generate the header including meta data such as the Podman version and the timestamp. @@ -242,6 +280,18 @@ options: - Set a static MAC address for the pod's shared network. type: str required: false + memory: + description: + - Set memory limit. + - A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + type: str + required: false + memory_swap: + description: + - Set limit value equal to memory plus swap. + - A unit can be b (bytes), k (kibibytes), m (mebibytes), or g (gibibytes). + type: str + required: false name: description: - Assign a name to the pod. diff --git a/ansible_collections/containers/podman/plugins/modules/podman_prune.py b/ansible_collections/containers/podman/plugins/modules/podman_prune.py index ee4c68a93..3fe3b7539 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_prune.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_prune.py @@ -66,17 +66,17 @@ options: type: dict system: description: - - Wheter to prune unused pods, containers, image, networks and volume data + - Whether to prune unused pods, containers, image, networks and volume data type: bool default: false system_all: description: - - Wheter to prune all unused images, not only dangling images. + - Whether to prune all unused images, not only dangling images. type: bool default: false system_volumes: description: - - Wheter to prune volumes currently unused by any container. + - Whether to prune volumes currently unused by any container. type: bool default: false volume: diff --git a/ansible_collections/containers/podman/plugins/modules/podman_runlabel.py b/ansible_collections/containers/podman/plugins/modules/podman_runlabel.py new file mode 100644 index 000000000..e5b6cf32f --- /dev/null +++ b/ansible_collections/containers/podman/plugins/modules/podman_runlabel.py @@ -0,0 +1,86 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# Copyright (c) 2023, Pavel Dostal <@pdostal> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = r''' +module: podman_runlabel +short_description: Run given label from given image +author: Pavel Dostal (@pdostal) +description: + - podman container runlabel runs selected label from given image +options: + image: + description: + - Image to get the label from. + type: str + required: true + label: + description: + - Label to run. + type: str + required: true + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the + machine running C(podman) + default: 'podman' + type: str +requirements: + - "Podman installed on host" +''' + +RETURN = ''' +''' + +EXAMPLES = ''' +# What modules does for example +- containers.podman.podman_runlabel: + image: docker.io/continuumio/miniconda3 + label: INSTALL +''' + +from ansible.module_utils.basic import AnsibleModule # noqa: E402 + + +def runlabel(module, executable): + changed = False + command = [executable, 'container', 'runlabel'] + command.append(module.params['label']) + command.append(module.params['image']) + rc, out, err = module.run_command(command) + if rc == 0: + changed = True + else: + module.fail_json(msg="Error running the runlabel from image %s: %s" % ( + module.params['image'], err)) + return changed, out, err + + +def main(): + module = AnsibleModule( + argument_spec=dict( + image=dict(type='str', required=True), + label=dict(type='str', required=True), + executable=dict(type='str', default='podman') + ), + supports_check_mode=False, + ) + + executable = module.get_bin_path(module.params['executable'], required=True) + changed, out, err = runlabel(module, executable) + + results = { + "changed": changed, + "stdout": out, + "stderr": err + } + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_save.py b/ansible_collections/containers/podman/plugins/modules/podman_save.py index bc7ce252c..e23f31021 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_save.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_save.py @@ -20,7 +20,8 @@ options: image: description: - Image to save. - type: str + type: list + elements: str required: true compress: description: @@ -70,9 +71,14 @@ RETURN = ''' EXAMPLES = ''' # What modules does for example - containers.podman.podman_save: - dest: /path/to/tar/file - compress: true - format: oci-dir + image: nginx + dest: /tmp/file123.tar +- containers.podman.podman_save: + image: + - nginx + - fedora + dest: /tmp/file456.tar + multi_image_archive: true ''' import os # noqa: E402 @@ -92,7 +98,8 @@ def save(module, executable): for param in module.params: if module.params[param] is not None and param in cmd_args: command += cmd_args[param] - command.append(module.params['image']) + for img in module.params['image']: + command.append(img) if module.params['force']: dest = module.params['dest'] if os.path.exists(dest): @@ -116,7 +123,7 @@ def save(module, executable): def main(): module = AnsibleModule( argument_spec=dict( - image=dict(type='str', required=True), + image=dict(type='list', elements='str', required=True), compress=dict(type='bool'), dest=dict(type='str', required=True, aliases=['path']), format=dict(type='str', choices=['docker-archive', 'oci-archive', 'oci-dir', 'docker-dir']), diff --git a/ansible_collections/containers/podman/plugins/modules/podman_secret.py b/ansible_collections/containers/podman/plugins/modules/podman_secret.py index fc8ec1f1d..a31aae9dc 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_secret.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_secret.py @@ -61,6 +61,15 @@ options: choices: - absent - present + labels: + description: + - Labels to set on the secret. + type: dict + debug: + description: + - Enable debug mode for module. + type: bool + default: False ''' EXAMPLES = r""" @@ -91,19 +100,75 @@ EXAMPLES = r""" """ from ansible.module_utils.basic import AnsibleModule +from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion +from ansible_collections.containers.podman.plugins.module_utils.podman.common import get_podman_version +diff = {"before": '', "after": ''} -def podman_secret_create(module, executable, name, data, force, skip, - driver, driver_opts): - if force: - module.run_command([executable, 'secret', 'rm', name]) - if skip: + +def podman_secret_exists(module, executable, name, version): + if version is None or LooseVersion(version) < LooseVersion('4.5.0'): rc, out, err = module.run_command( [executable, 'secret', 'ls', "--format", "{{.Name}}"]) - if name in [i.strip() for i in out.splitlines()]: - return { - "changed": False, - } + return name in [i.strip() for i in out.splitlines()] + rc, out, err = module.run_command( + [executable, 'secret', 'exists', name]) + return rc == 0 + + +def need_update(module, executable, name, data, driver, driver_opts, debug, labels): + + cmd = [executable, 'secret', 'inspect', '--showsecret', name] + rc, out, err = module.run_command(cmd) + if rc != 0: + if debug: + module.log("PODMAN-SECRET-DEBUG: Unable to get secret info: %s" % err) + return True + try: + secret = module.from_json(out)[0] + # We support only file driver for now + if (driver and driver != 'file') or secret['Spec']['Driver']['Name'] != 'file': + if debug: + module.log("PODMAN-SECRET-DEBUG: Idempotency of driver %s is not supported" % driver) + return True + if secret['SecretData'] != data: + diff['after'] = "<different-secret>" + diff['before'] = "<secret>" + return True + if driver_opts: + for k, v in driver_opts.items(): + if secret['Spec']['Driver']['Options'].get(k) != v: + diff['after'] = "=".join([k, v]) + diff['before'] = "=".join( + [k, secret['Spec']['Driver']['Options'].get(k)]) + return True + if labels: + for k, v in labels.items(): + if secret['Spec']['Labels'].get(k) != v: + diff['after'] = "=".join([k, v]) + diff['before'] = "=".join( + [k, secret['Spec']['Labels'].get(k)]) + return True + except Exception: + return True + return False + + +def podman_secret_create(module, executable, name, data, force, skip, + driver, driver_opts, debug, labels): + podman_version = get_podman_version(module, fail=False) + if (podman_version is not None and + LooseVersion(podman_version) >= LooseVersion('4.7.0') + and (driver is None or driver == 'file')): + if not skip and need_update(module, executable, name, data, driver, driver_opts, debug, labels): + podman_secret_remove(module, executable, name) + else: + return {"changed": False} + else: + if force: + podman_secret_remove(module, executable, name) + if skip and podman_secret_exists(module, executable, name, podman_version): + return {"changed": False} cmd = [executable, 'secret', 'create'] if driver: @@ -112,6 +177,10 @@ def podman_secret_create(module, executable, name, data, force, skip, if driver_opts: cmd.append('--driver-opts') cmd.append(",".join("=".join(i) for i in driver_opts.items())) + if labels: + for k, v in labels.items(): + cmd.append('--label') + cmd.append("=".join([k, v])) cmd.append(name) cmd.append('-') @@ -121,6 +190,10 @@ def podman_secret_create(module, executable, name, data, force, skip, return { "changed": True, + "diff": { + "before": diff['before'] + "\n", + "after": diff['after'] + "\n", + }, } @@ -150,6 +223,8 @@ def main(): skip_existing=dict(type='bool', default=False), driver=dict(type='str'), driver_opts=dict(type='dict'), + labels=dict(type='dict'), + debug=dict(type='bool', default=False), ), ) @@ -165,9 +240,11 @@ def main(): skip = module.params['skip_existing'] driver = module.params['driver'] driver_opts = module.params['driver_opts'] + debug = module.params['debug'] + labels = module.params['labels'] results = podman_secret_create(module, executable, name, data, force, skip, - driver, driver_opts) + driver, driver_opts, debug, labels) else: results = podman_secret_remove(module, executable, name) diff --git a/ansible_collections/containers/podman/plugins/modules/podman_secret_info.py b/ansible_collections/containers/podman/plugins/modules/podman_secret_info.py new file mode 100644 index 000000000..ebe854241 --- /dev/null +++ b/ansible_collections/containers/podman/plugins/modules/podman_secret_info.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# Copyright (c) 2024 Red Hat +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +module: podman_secret_info +author: + - "Sagi Shnaidman (@sshnaidm)" +short_description: Gather info about podman secrets +notes: [] +description: + - Gather info about podman secrets with podman inspect command. +requirements: + - "Podman installed on host" +options: + name: + description: + - Name of the secret + type: str + showsecret: + description: + - Show secret data value + type: bool + default: False + executable: + description: + - Path to C(podman) executable if it is not in the C($PATH) on the + machine running C(podman) + default: 'podman' + type: str +''' + +EXAMPLES = r""" +- name: Gather info about all present secrets + podman_secret_info: + +- name: Gather info about specific secret + podman_secret_info: + name: specific_secret +""" + +RETURN = r""" +secrets: + description: Facts from all or specified secrets + returned: always + type: list + sample: [ + { + "ID": "06068c676e9a7f1c7dc0da8dd", + "CreatedAt": "2024-01-28T20:32:08.31857841+02:00", + "UpdatedAt": "2024-01-28T20:32:08.31857841+02:00", + "Spec": { + "Name": "secret_name", + "Driver": { + "Name": "file", + "Options": { + "path": "/home/user/.local/share/containers/storage/secrets/filedriver" + } + }, + "Labels": {} + } + } + ] +""" + +import json +from ansible.module_utils.basic import AnsibleModule + + +def get_secret_info(module, executable, show, name): + command = [executable, 'secret', 'inspect'] + if show: + command.append('--showsecret') + if name: + command.append(name) + else: + all_names = [executable, 'secret', 'ls', '-q'] + rc, out, err = module.run_command(all_names) + name = out.split() + if not name: + return [], out, err + command.extend(name) + rc, out, err = module.run_command(command) + if rc != 0 or 'no secret with name or id' in err: + module.fail_json(msg="Unable to gather info for %s: %s" % (name or 'all secrets', err)) + if not out or json.loads(out) is None: + return [], out, err + return json.loads(out), out, err + + +def main(): + module = AnsibleModule( + argument_spec=dict( + executable=dict(type='str', default='podman'), + name=dict(type='str'), + showsecret=dict(type='bool', default=False), + ), + supports_check_mode=True, + ) + + name = module.params['name'] + showsecret = module.params['showsecret'] + executable = module.get_bin_path(module.params['executable'], required=True) + + inspect_results, out, err = get_secret_info(module, executable, showsecret, name) + + results = { + "changed": False, + "secrets": inspect_results, + "stderr": err, + } + + module.exit_json(**results) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/containers/podman/plugins/modules/podman_volume.py b/ansible_collections/containers/podman/plugins/modules/podman_volume.py index c533091e1..b4d5062fa 100644 --- a/ansible_collections/containers/podman/plugins/modules/podman_volume.py +++ b/ansible_collections/containers/podman/plugins/modules/podman_volume.py @@ -327,7 +327,13 @@ class PodmanVolume: # pylint: disable=unused-variable rc, out, err = self.module.run_command( [self.module.params['executable'], b'volume', b'inspect', self.name]) - return json.loads(out)[0] if rc == 0 else {} + if rc == 0: + data = json.loads(out) + if data: + data = data[0] + if data.get("Name") == self.name: + return data + return {} def _get_podman_version(self): # pylint: disable=unused-variable |