From 1476ebe685651043f46c806ee967433a3b8e7a3c Mon Sep 17 00:00:00 2001 From: Sergey <6213510+sshnaidm@users.noreply.github.com> Date: Mon, 22 Apr 2024 01:13:04 +0300 Subject: [PATCH] Add quadlet support for Podman modules (#722) Signed-off-by: Sagi Shnaidman --- .../podman/podman_container_lib.py | 16 +- plugins/module_utils/podman/podman_pod_lib.py | 16 +- plugins/module_utils/podman/quadlet.py | 636 ++++++++++++++++++ plugins/modules/podman_container.py | 36 + plugins/modules/podman_image.py | 33 +- plugins/modules/podman_network.py | 33 +- plugins/modules/podman_play.py | 39 +- plugins/modules/podman_pod.py | 21 +- plugins/modules/podman_volume.py | 31 +- .../targets/podman_container/tasks/main.yml | 265 +++++++- .../targets/podman_image/tasks/main.yml | 128 ++++ .../targets/podman_network/tasks/main.yml | 125 ++++ .../targets/podman_play/tasks/main.yml | 151 +++++ .../targets/podman_pod/tasks/main.yml | 145 ++++ .../targets/podman_volume/tasks/main.yml | 128 ++++ 15 files changed, 1790 insertions(+), 13 deletions(-) create mode 100644 plugins/module_utils/podman/quadlet.py diff --git a/plugins/module_utils/podman/podman_container_lib.py b/plugins/module_utils/podman/podman_container_lib.py index e1d1ede..bf42ffd 100644 --- a/plugins/module_utils/podman/podman_container_lib.py +++ b/plugins/module_utils/podman/podman_container_lib.py @@ -10,6 +10,9 @@ from ansible_collections.containers.podman.plugins.module_utils.podman.common im 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 +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import ContainerQuadlet + __metaclass__ = type @@ -17,7 +20,7 @@ ARGUMENTS_SPEC_CONTAINER = dict( name=dict(required=True, type='str'), executable=dict(default='podman', type='str'), state=dict(type='str', default='started', choices=[ - 'absent', 'present', 'stopped', 'started', 'created']), + 'absent', 'present', 'stopped', 'started', 'created', 'quadlet']), image=dict(type='str'), annotation=dict(type='dict'), attach=dict(type='list', elements='str', choices=['stdout', 'stderr', 'stdin']), @@ -116,6 +119,9 @@ ARGUMENTS_SPEC_CONTAINER = dict( publish=dict(type='list', elements='str', aliases=[ 'ports', 'published', 'published_ports']), publish_all=dict(type='bool'), + quadlet_dir=dict(type='path'), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str'), read_only=dict(type='bool'), read_only_tmpfs=dict(type='bool'), recreate=dict(type='bool', default=False), @@ -1681,6 +1687,9 @@ class PodmanManager: else: self.results['diff']['before'] += sysd['diff']['before'] self.results['diff']['after'] += sysd['diff']['after'] + quadlet = ContainerQuadlet(self.module_params) + quadlet_content = quadlet.create_quadlet_content() + self.results.update({'podman_quadlet': quadlet_content}) def make_started(self): """Run actions if desired state is 'started'.""" @@ -1812,6 +1821,10 @@ class PodmanManager: self.results.update({'container': {}, 'podman_actions': self.container.actions}) + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "container") + self.results.update(results_update) + def execute(self): """Execute the desired action according to map of actions & states.""" states_map = { @@ -1820,6 +1833,7 @@ class PodmanManager: 'absent': self.make_absent, 'stopped': self.make_stopped, 'created': self.make_created, + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() diff --git a/plugins/module_utils/podman/podman_pod_lib.py b/plugins/module_utils/podman/podman_pod_lib.py index 2058190..e003135 100644 --- a/plugins/module_utils/podman/podman_pod_lib.py +++ b/plugins/module_utils/podman/podman_pod_lib.py @@ -1,11 +1,12 @@ from __future__ import (absolute_import, division, print_function) -import json +import json # noqa: F402 from ansible.module_utils._text import to_bytes, to_native from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys 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.quadlet import create_quadlet_state, PodQuadlet __metaclass__ = type @@ -23,6 +24,7 @@ ARGUMENTS_SPEC_POD = dict( 'stopped', 'paused', 'unpaused', + 'quadlet' ]), recreate=dict(type='bool', default=False), add_host=dict(type='list', required=False, elements='str'), @@ -62,6 +64,9 @@ ARGUMENTS_SPEC_POD = dict( pod_id_file=dict(type='str', required=False), publish=dict(type='list', required=False, elements='str', aliases=['ports']), + quadlet_dir=dict(type='path'), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str'), share=dict(type='str', required=False), subgidname=dict(type='str', required=False), subuidname=dict(type='str', required=False), @@ -839,6 +844,9 @@ class PodmanPodManager: else: self.results['diff']['before'] += sysd['diff']['before'] self.results['diff']['after'] += sysd['diff']['after'] + quadlet = PodQuadlet(self.module_params) + quadlet_content = quadlet.create_quadlet_content() + self.results.update({'podman_quadlet': quadlet_content}) def execute(self): """Execute the desired action according to map of actions & states.""" @@ -851,7 +859,7 @@ class PodmanPodManager: 'killed': self.make_killed, 'paused': self.make_paused, 'unpaused': self.make_unpaused, - + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() @@ -953,3 +961,7 @@ class PodmanPodManager: self.results.update({'changed': True}) self.results.update({'pod': {}, 'podman_actions': self.pod.actions}) + + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "pod") + self.results.update(results_update) diff --git a/plugins/module_utils/podman/quadlet.py b/plugins/module_utils/podman/quadlet.py new file mode 100644 index 0000000..17764b6 --- /dev/null +++ b/plugins/module_utils/podman/quadlet.py @@ -0,0 +1,636 @@ +# Copyright (c) 2024 Sagi Shnaidman (@sshnaidm) +# 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 + +import os + +from ansible_collections.containers.podman.plugins.module_utils.podman.common import compare_systemd_file_content + +QUADLET_ROOT_PATH = "/etc/containers/systemd/" +QUADLET_NON_ROOT_PATH = "~/.config/containers/systemd/" + + +class Quadlet: + param_map = {} + + def __init__(self, section: str, params: dict): + self.section = section + self.custom_params = self.custom_prepare_params(params) + self.dict_params = self.prepare_params() + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for specific Quadlet types. + """ + # This should be implemented in child classes if needed. + return params + + def prepare_params(self) -> dict: + """ + Convert parameter values as per param_map. + """ + processed_params = [] + for param_key, quadlet_key in self.param_map.items(): + value = self.custom_params.get(param_key) + if value is not None: + if isinstance(value, list): + # Add an entry for each item in the list + for item in value: + processed_params.append([quadlet_key, item]) + else: + if isinstance(value, bool): + value = str(value).lower() + # Add a single entry for the key + processed_params.append([quadlet_key, value]) + return processed_params + + def create_quadlet_content(self) -> str: + """ + Construct the quadlet content as a string. + """ + custom_user_options = self.custom_params.get("quadlet_options") + custom_text = "\n" + "\n".join(custom_user_options) if custom_user_options else "" + return f"[{self.section}]\n" + "\n".join( + f"{key}={value}" for key, value in self.dict_params + ) + custom_text + "\n" + + def write_to_file(self, path: str): + """ + Write the quadlet content to a file at the specified path. + """ + content = self.create_quadlet_content() + with open(path, 'w') as file: + file.write(content) + + +class ContainerQuadlet(Quadlet): + param_map = { + 'cap_add': 'AddCapability', + 'device': 'AddDevice', + 'annotation': 'Annotation', + 'name': 'ContainerName', + # the following are not implemented yet in Podman module + 'AutoUpdate': 'AutoUpdate', + 'ContainersConfModule': 'ContainersConfModule', + # end of not implemented yet + 'dns': 'DNS', + 'dns_option': 'DNSOption', + 'dns_search': 'DNSSearch', + 'cap_drop': 'DropCapability', + 'entrypoint': 'Entrypoint', + 'env': 'Environment', + 'env_file': 'EnvironmentFile', + 'env_host': 'EnvironmentHost', + 'command': 'Exec', + 'expose': 'ExposeHostPort', + 'gidmap': 'GIDMap', + 'global_args': 'GlobalArgs', + 'group': 'Group', # Does not exist in module parameters + 'healthcheck': 'HealthCheckCmd', + 'healthcheck_interval': 'HealthInterval', + 'healthcheck_failure_action': 'HealthOnFailure', + 'healthcheck_retries': 'HealthRetries', + 'healthcheck_start_period': 'HealthStartPeriod', + 'healthcheck_timeout': 'HealthTimeout', + # the following are not implemented yet in Podman module + 'HealthStartupCmd': 'HealthStartupCmd', + 'HealthStartupInterval': 'HealthStartupInterval', + 'HealthStartupRetries': 'HealthStartupRetries', + 'HealthStartupSuccess': 'HealthStartupSuccess', + 'HealthStartupTimeout': 'HealthStartupTimeout', + # end of not implemented yet + 'hostname': 'HostName', + 'image': 'Image', + 'ip': 'IP', + # the following are not implemented yet in Podman module + 'IP6': 'IP6', + # end of not implemented yet + 'label': 'Label', + 'log_driver': 'LogDriver', + "Mask": "Mask", # add it in security_opt + 'mount': 'Mount', + 'network': 'Network', + 'no_new_privileges': 'NoNewPrivileges', + 'sdnotify': 'Notify', + 'pids_limit': 'PidsLimit', + 'pod': 'Pod', + 'publish': 'PublishPort', + # the following are not implemented yet in Podman module + "Pull": "Pull", + # end of not implemented yet + 'read_only': 'ReadOnly', + 'read_only_tmpfs': 'ReadOnlyTmpfs', + 'rootfs': 'Rootfs', + 'init': 'RunInit', + 'SeccompProfile': 'SeccompProfile', + 'secrets': 'Secret', + # All these are in security_opt + 'SecurityLabelDisable': 'SecurityLabelDisable', + 'SecurityLabelFileType': 'SecurityLabelFileType', + 'SecurityLabelLevel': 'SecurityLabelLevel', + 'SecurityLabelNested': 'SecurityLabelNested', + 'SecurityLabelType': 'SecurityLabelType', + 'shm_size': 'ShmSize', + 'stop_timeout': 'StopTimeout', + 'subgidname': 'SubGIDMap', + 'subuidname': 'SubUIDMap', + 'sysctl': 'Sysctl', + 'timezone': 'Timezone', + 'tmpfs': 'Tmpfs', + 'uidmap': 'UIDMap', + 'ulimit': 'Ulimit', + 'Unmask': 'Unmask', # --security-opt unmask=ALL + 'user': 'User', + 'userns': 'UserNS', + 'volume': 'Volume', + 'workdir': 'WorkingDir', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Container", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for container-specific parameters. + """ + # Work on params in params_map and convert them to a right form + if params["annotation"]: + params['annotation'] = ["%s=%s" % + (k, v) for k, v in params['annotation'].items()] + if params["cap_add"]: + params["cap_add"] = " ".join(params["cap_add"]) + if params["cap_drop"]: + params["cap_drop"] = " ".join(params["cap_drop"]) + if params["command"]: + params["command"] = (" ".join(params["command"]) + if isinstance(params["command"], list) + else params["command"]) + if params["label"]: + params["label"] = ["%s=%s" % (k, v) for k, v in params["label"].items()] + if params["env"]: + params["env"] = ["%s=%s" % (k, v) for k, v in params["env"].items()] + if params["sysctl"]: + params["sysctl"] = ["%s=%s" % (k, v) for k, v in params["sysctl"].items()] + if params["tmpfs"]: + params["tmpfs"] = ["%s:%s" % (k, v) if v else k for k, v in params["tmpfs"].items()] + + # Work on params which are not in the param_map but can be calculated + params["global_args"] = [] + if params["user"] and len(str(params["user"]).split(":")) > 1: + user, group = params["user"].split(":") + params["user"] = user + params["group"] = group + if params["security_opt"]: + if "no-new-privileges" in params["security_opt"]: + params["no_new_privileges"] = True + params["security_opt"].remove("no-new-privileges") + if params["log_level"]: + params["global_args"].append(f"--log-level {params['log_level']}") + if params["debug"]: + params["global_args"].append("--log-level debug") + + # Work on params which are not in the param_map and add them to PodmanArgs + params["podman_args"] = [] + if params["authfile"]: + params["podman_args"].append(f"--authfile {params['authfile']}") + if params["attach"]: + for attach in params["attach"]: + params["podman_args"].append(f"--attach {attach}") + if params["blkio_weight"]: + params["podman_args"].append(f"--blkio-weight {params['blkio_weight']}") + if params["blkio_weight_device"]: + params["podman_args"].append(" ".join([ + f"--blkio-weight-device {':'.join(blkio)}" for blkio in params["blkio_weight_device"].items()])) + if params["cgroupns"]: + params["podman_args"].append(f"--cgroupns {params['cgroupns']}") + if params["cgroup_parent"]: + params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["cidfile"]: + params["podman_args"].append(f"--cidfile {params['cidfile']}") + if params["conmon_pidfile"]: + params["podman_args"].append(f"--conmon-pidfile {params['conmon_pidfile']}") + if params["cpuset_cpus"]: + params["podman_args"].append(f"--cpuset-cpus {params['cpuset_cpus']}") + if params["cpuset_mems"]: + params["podman_args"].append(f"--cpuset-mems {params['cpuset_mems']}") + if params["cpu_period"]: + params["podman_args"].append(f"--cpu-period {params['cpu_period']}") + if params["cpu_quota"]: + params["podman_args"].append(f"--cpu-quota {params['cpu_quota']}") + if params["cpu_rt_period"]: + params["podman_args"].append(f"--cpu-rt-period {params['cpu_rt_period']}") + if params["cpu_rt_runtime"]: + params["podman_args"].append(f"--cpu-rt-runtime {params['cpu_rt_runtime']}") + if params["cpu_shares"]: + params["podman_args"].append(f"--cpu-shares {params['cpu_shares']}") + if params["device_read_bps"]: + for i in params["device_read_bps"]: + params["podman_args"].append(f"--device-read-bps {i}") + if params["device_read_iops"]: + for i in params["device_read_iops"]: + params["podman_args"].append(f"--device-read-iops {i}") + if params["device_write_bps"]: + for i in params["device_write_bps"]: + params["podman_args"].append(f"--device-write-bps {i}") + if params["device_write_iops"]: + for i in params["device_write_iops"]: + params["podman_args"].append(f"--device-write-iops {i}") + if params["etc_hosts"]: + for host_ip in params['etc_hosts'].items(): + params["podman_args"].append(f"--add-host {':'.join(host_ip)}") + if params["hooks_dir"]: + for hook in params["hooks_dir"]: + params["podman_args"].append(f"--hooks-dir {hook}") + if params["http_proxy"]: + params["podman_args"].append(f"--http-proxy {params['http_proxy']}") + if params["image_volume"]: + params["podman_args"].append(f"--image-volume {params['image_volume']}") + if params["init_path"]: + params["podman_args"].append(f"--init-path {params['init_path']}") + if params["interactive"]: + params["podman_args"].append("--interactive") + if params["ipc"]: + params["podman_args"].append(f"--ipc {params['ipc']}") + if params["kernel_memory"]: + params["podman_args"].append(f"--kernel-memory {params['kernel_memory']}") + if params["label_file"]: + params["podman_args"].append(f"--label-file {params['label_file']}") + if params["log_opt"]: + for k, v in params['log_opt'].items(): + params["podman_args"].append(f"--log-opt {k.replace('max_size', 'max-size')}={v}") + if params["mac_address"]: + params["podman_args"].append(f"--mac-address {params['mac_address']}") + if params["memory"]: + params["podman_args"].append(f"--memory {params['memory']}") + if params["memory_reservation"]: + params["podman_args"].append(f"--memory-reservation {params['memory_reservation']}") + if params["memory_swap"]: + params["podman_args"].append(f"--memory-swap {params['memory_swap']}") + if params["memory_swappiness"]: + params["podman_args"].append(f"--memory-swappiness {params['memory_swappiness']}") + if params["network_aliases"]: + for alias in params["network_aliases"]: + params["podman_args"].append(f"--network-alias {alias}") + if params["no_hosts"] is not None: + params["podman_args"].append(f"--no-hosts={params['no_hosts']}") + if params["oom_kill_disable"]: + params["podman_args"].append(f"--oom-kill-disable={params['oom_kill_disable']}") + if params["oom_score_adj"]: + params["podman_args"].append(f"--oom-score-adj {params['oom_score_adj']}") + if params["pid"]: + params["podman_args"].append(f"--pid {params['pid']}") + if params["privileged"]: + params["podman_args"].append("--privileged") + if params["publish_all"]: + params["podman_args"].append("--publish-all") + if params["requires"]: + params["podman_args"].append(f"--requires {','.join(params['requires'])}") + if params["restart_policy"]: + params["podman_args"].append(f"--restart-policy {params['restart_policy']}") + if params["rm"]: + params["podman_args"].append("--rm") + if params["security_opt"]: + for security_opt in params["security_opt"]: + params["podman_args"].append(f"--security-opt {security_opt}") + if params["sig_proxy"]: + params["podman_args"].append(f"--sig-proxy {params['sig_proxy']}") + if params["stop_signal"]: + params["podman_args"].append(f"--stop-signal {params['stop_signal']}") + if params["systemd"]: + params["podman_args"].append(f"--systemd={str(params['systemd']).lower()}") + if params["tty"]: + params["podman_args"].append("--tty") + if params["uts"]: + params["podman_args"].append(f"--uts {params['uts']}") + if params["volumes_from"]: + for volume in params["volumes_from"]: + params["podman_args"].append(f"--volumes-from {volume}") + if params["cmd_args"]: + params["podman_args"].append(params["cmd_args"]) + + # Return params with custom processing applied + return params + + +class NetworkQuadlet(Quadlet): + param_map = { + 'name': 'NetworkName', + 'internal': 'Internal', + 'driver': 'Driver', + 'gateway': 'Gateway', + 'disable_dns': 'DisableDNS', + 'subnet': 'Subnet', + 'ip_range': 'IPRange', + 'ipv6': 'IPv6', + "opt": "Options", + # Add more parameter mappings specific to networks + 'ContainersConfModule': 'ContainersConfModule', + "DNS": "DNS", + "IPAMDriver": "IPAMDriver", + "Label": "Label", + "global_args": "GlobalArgs", + "podman_args": "PodmanArgs", + } + + def __init__(self, params: dict): + super().__init__("Network", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for network-specific parameters. + """ + # Work on params in params_map and convert them to a right form + if params["debug"]: + params["global_args"].append("--log-level debug") + if params["opt"]: + new_opt = [] + for k, v in params["opt"].items(): + if v is not None: + new_opt.append(f"{k}={v}") + params["opt"] = new_opt + return params + + +# This is a inherited class that represents a Quadlet file for the Podman pod +class PodQuadlet(Quadlet): + param_map = { + 'name': 'PodName', + "network": "Network", + "publish": "PublishPort", + "volume": "Volume", + 'ContainersConfModule': 'ContainersConfModule', + "global_args": "GlobalArgs", + "podman_args": "PodmanArgs", + } + + def __init__(self, params: dict): + super().__init__("Pod", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for pod-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["add_host"]: + for host in params['add_host']: + params["podman_args"].append(f"--add-host {host}") + if params["cgroup_parent"]: + params["podman_args"].append(f"--cgroup-parent {params['cgroup_parent']}") + if params["blkio_weight"]: + params["podman_args"].append(f"--blkio-weight {params['blkio_weight']}") + if params["blkio_weight_device"]: + params["podman_args"].append(" ".join([ + f"--blkio-weight-device {':'.join(blkio)}" for blkio in params["blkio_weight_device"].items()])) + if params["cpuset_cpus"]: + params["podman_args"].append(f"--cpuset-cpus {params['cpuset_cpus']}") + if params["cpuset_mems"]: + params["podman_args"].append(f"--cpuset-mems {params['cpuset_mems']}") + if params["cpu_shares"]: + params["podman_args"].append(f"--cpu-shares {params['cpu_shares']}") + if params["cpus"]: + params["podman_args"].append(f"--cpus {params['cpus']}") + if params["device"]: + for device in params["device"]: + params["podman_args"].append(f"--device {device}") + if params["device_read_bps"]: + for i in params["device_read_bps"]: + params["podman_args"].append(f"--device-read-bps {i}") + if params["device_write_bps"]: + for i in params["device_write_bps"]: + params["podman_args"].append(f"--device-write-bps {i}") + if params["dns"]: + for dns in params["dns"]: + params["podman_args"].append(f"--dns {dns}") + if params["dns_opt"]: + for dns_option in params["dns_opt"]: + params["podman_args"].append(f"--dns-option {dns_option}") + if params["dns_search"]: + for dns_search in params["dns_search"]: + params["podman_args"].append(f"--dns-search {dns_search}") + if params["gidmap"]: + for gidmap in params["gidmap"]: + params["podman_args"].append(f"--gidmap {gidmap}") + if params["hostname"]: + params["podman_args"].append(f"--hostname {params['hostname']}") + if params["infra"]: + params["podman_args"].append(f"--infra {params['infra']}") + if params["infra_command"]: + params["podman_args"].append(f"--infra-command {params['infra_command']}") + if params["infra_conmon_pidfile"]: + params["podman_args"].append(f"--infra-conmon-pidfile {params['infra_conmon_pidfile']}") + if params["infra_image"]: + params["podman_args"].append(f"--infra-image {params['infra_image']}") + if params["infra_name"]: + params["podman_args"].append(f"--infra-name {params['infra_name']}") + if params["ip"]: + params["podman_args"].append(f"--ip {params['ip']}") + if params["label"]: + for label, label_v in params["label"].items(): + params["podman_args"].append(f"--label {label}={label_v}") + if params["label_file"]: + params["podman_args"].append(f"--label-file {params['label_file']}") + if params["mac_address"]: + params["podman_args"].append(f"--mac-address {params['mac_address']}") + if params["memory"]: + params["podman_args"].append(f"--memory {params['memory']}") + if params["memory_swap"]: + params["podman_args"].append(f"--memory-swap {params['memory_swap']}") + if params["no_hosts"]: + params["podman_args"].append(f"--no-hosts {params['no_hosts']}") + if params["pid"]: + params["podman_args"].append(f"--pid {params['pid']}") + if params["pod_id_file"]: + params["podman_args"].append(f"--pod-id-file {params['pod_id_file']}") + if params["share"]: + params["podman_args"].append(f"--share {params['share']}") + if params["subgidname"]: + params["podman_args"].append(f"--subgidname {params['subgidname']}") + if params["subuidname"]: + params["podman_args"].append(f"--subuidname {params['subuidname']}") + if params["uidmap"]: + for uidmap in params["uidmap"]: + params["podman_args"].append(f"--uidmap {uidmap}") + if params["userns"]: + params["podman_args"].append(f"--userns {params['userns']}") + if params["debug"]: + params["global_args"].append("--log-level debug") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman volume +class VolumeQuadlet(Quadlet): + param_map = { + 'name': 'VolumeName', + 'driver': 'Driver', + 'label': 'Label', + # 'opt': 'Options', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Volume", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for volume-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["debug"]: + params["global_args"].append("--log-level debug") + if params["label"]: + params["label"] = ["%s=%s" % (k, v) for k, v in params["label"].items()] + if params["options"]: + for opt in params["options"]: + params["podman_args"].append(f"--opt {opt}") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman kube +class KubeQuadlet(Quadlet): + param_map = { + 'configmap': 'ConfigMap', + 'log_driver': 'LogDriver', + 'network': 'Network', + 'kube_file': 'Yaml', + 'userns': 'UserNS', + 'AutoUpdate': 'AutoUpdate', + 'ExitCodePropagation': 'ExitCodePropagation', + 'KubeDownForce': 'KubeDownForce', + 'PublishPort': 'PublishPort', + 'SetWorkingDirectory': 'SetWorkingDirectory', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Kube", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for kube-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["debug"]: + params["global_args"].append("--log-level debug") + + return params + + +# This is a inherited class that represents a Quadlet file for the Podman image +class ImageQuadlet(Quadlet): + param_map = { + 'AllTags': 'AllTags', + 'arch': 'Arch', + 'authfile': 'AuthFile', + 'ca_cert_dir': 'CertDir', + 'creds': 'Creds', + 'DecryptionKey': 'DecryptionKey', + 'name': 'Image', + 'ImageTag': 'ImageTag', + 'OS': 'OS', + 'validate_certs': 'TLSVerify', + 'Variant': 'Variant', + 'ContainersConfModule': 'ContainersConfModule', + 'global_args': 'GlobalArgs', + 'podman_args': 'PodmanArgs', + } + + def __init__(self, params: dict): + super().__init__("Image", params) + + def custom_prepare_params(self, params: dict) -> dict: + """ + Custom parameter processing for image-specific parameters. + """ + # Work on params in params_map and convert them to a right form + params["global_args"] = [] + params["podman_args"] = [] + + if params["username"] and params["password"]: + params["creds"] = f"{params['username']}:{params['password']}" + # if params['validate_certs'] is not None: + # params['validate_certs'] = str(params['validate_certs']).lower() + + return params + + +def check_quadlet_directory(module, quadlet_dir): + '''Check if the directory exists and is writable. If not, fail the module.''' + if not os.path.exists(quadlet_dir): + try: + os.makedirs(quadlet_dir) + except Exception as e: + module.fail_json( + msg="Directory for quadlet_file can't be created: %s" % e) + if not os.access(quadlet_dir, os.W_OK): + module.fail_json( + msg="Directory for quadlet_file is not writable: %s" % quadlet_dir) + + +def create_quadlet_state(module, issuer): + '''Create a quadlet file for the specified issuer.''' + class_map = { + "container": ContainerQuadlet, + "network": NetworkQuadlet, + "pod": PodQuadlet, + "volume": VolumeQuadlet, + "kube": KubeQuadlet, + "image": ImageQuadlet, + } + # Let's detect which user is running + user = "root" if os.geteuid() == 0 else "user" + quadlet_dir = module.params.get('quadlet_dir') + if not quadlet_dir: + if user == "root": + quadlet_dir = QUADLET_ROOT_PATH + else: + quadlet_dir = os.path.expanduser(QUADLET_NON_ROOT_PATH) + # Create a filename based on the issuer + if not module.params.get('name') and not module.params.get('quadlet_filename'): + module.fail_json(msg=f"Filename for {issuer} is required for creating a quadlet file.") + if issuer == "image": + name = module.params['name'].split("/")[-1].split(":")[0] + else: + name = module.params.get('name') + quad_file_name = module.params['quadlet_filename'] + if quad_file_name and not quad_file_name.endswith(f".{issuer}"): + quad_file_name = f"{quad_file_name}.{issuer}" + filename = quad_file_name or f"{name}.{issuer}" + quadlet_file_path = os.path.join(quadlet_dir, filename) + # Check if the directory exists and is writable + check_quadlet_directory(module, quadlet_dir) + # Check if file already exists and if it's different + quadlet = class_map[issuer](module.params) + quadlet_content = quadlet.create_quadlet_content() + file_diff = compare_systemd_file_content(quadlet_file_path, quadlet_content) + if bool(file_diff): + quadlet.write_to_file(quadlet_file_path) + results_update = { + 'changed': True, + "diff": { + "before": "\n".join(file_diff[0]) if isinstance(file_diff[0], list) else file_diff[0] + "\n", + "after": "\n".join(file_diff[1]) if isinstance(file_diff[1], list) else file_diff[1] + "\n", + }} + else: + results_update = {} + return results_update + +# Check with following command: +# QUADLET_UNIT_DIRS= /usr/lib/systemd/system-generators/podman-system-generator {--user} --dryrun diff --git a/plugins/modules/podman_container.py b/plugins/modules/podman_container.py index 51cb57a..7513e30 100644 --- a/plugins/modules/podman_container.py +++ b/plugins/modules/podman_container.py @@ -55,6 +55,8 @@ options: If container doesn't exist, the module creates it and leaves it in 'created' state. If configuration doesn't match or 'recreate' option is set, the container will be recreated + - I(quadlet) - Write a quadlet file with the specified configuration. + Requires the C(quadlet_dir) option to be set. type: str default: started choices: @@ -63,6 +65,7 @@ options: - stopped - started - created + - quadlet image: description: - Repository path (or image name) and tag used to create the container. @@ -721,6 +724,22 @@ options: - Publish all exposed ports to random ports on the host interfaces. The default is false. type: bool + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual container args + options as a list of lines to add. + type: list + elements: str read_only: description: - Mount the container's root filesystem as read only. Default is false @@ -994,6 +1013,23 @@ EXAMPLES = r""" - --deploy-hook - "echo 1 > /var/lib/letsencrypt/complete" +- name: Create a Quadlet file + containers.podman.podman_container: + name: quadlet-container + image: nginx + state: quadlet + quadlet_dir: ~/.config/containers/systemd/nginx.container + device: "/dev/sda:/dev/xvda:rwm" + ports: + - "8080:80" + volumes: + - "/var/www:/usr/share/nginx/html" + quadlet_options: + - "AutoUpdate=registry" + - "Pull=true" + - | + [Install] + WantedBy=default.target """ RETURN = r""" diff --git a/plugins/modules/podman_image.py b/plugins/modules/podman_image.py index 6305a5d..d78e6d7 100644 --- a/plugins/modules/podman_image.py +++ b/plugins/modules/podman_image.py @@ -64,6 +64,7 @@ DOCUMENTATION = r''' - present - absent - build + - quadlet validate_certs: description: - Require HTTPS and validate certificates when pulling or pushing. Also used during build if a pull or push is necessary. @@ -175,6 +176,24 @@ DOCUMENTATION = r''' - docker-daemon - oci-archive - ostree + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes image name without prefixes and tags. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false ''' EXAMPLES = r""" @@ -410,6 +429,7 @@ import shlex from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule from ansible_collections.containers.podman.plugins.module_utils.podman.common import run_podman_command +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state class PodmanImageManager(object): @@ -451,6 +471,9 @@ class PodmanImageManager(object): if self.state in ['absent']: self.absent() + if self.state == 'quadlet': + self.make_quadlet() + def _run(self, args, expected_rc=0, ignore_errors=False): cmd = " ".join([self.executable] + [to_native(i) for i in args]) @@ -537,6 +560,11 @@ class PodmanImageManager(object): if not self.module.check_mode: self.remove_image_id() + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "image") + self.results.update(results_update) + self.module.exit_json(**self.results) + def find_image(self, image_name=None): if image_name is None: image_name = self.image_name @@ -810,13 +838,16 @@ def main(): push=dict(type='bool', default=False), path=dict(type='str'), force=dict(type='bool', default=False), - state=dict(type='str', default='present', choices=['absent', 'present', 'build']), + state=dict(type='str', default='present', choices=['absent', 'present', 'build', 'quadlet']), validate_certs=dict(type='bool', aliases=['tlsverify', 'tls_verify']), executable=dict(type='str', default='podman'), auth_file=dict(type='path', aliases=['authfile']), username=dict(type='str'), password=dict(type='str', no_log=True), ca_cert_dir=dict(type='path'), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str'), + quadlet_options=dict(type='list', elements='str', required=False), build=dict( type='dict', aliases=['build_args', 'buildargs'], diff --git a/plugins/modules/podman_network.py b/plugins/modules/podman_network.py index 3f52af4..5ae4048 100644 --- a/plugins/modules/podman_network.py +++ b/plugins/modules/podman_network.py @@ -127,11 +127,30 @@ options: choices: - present - absent + - quadlet recreate: description: - Recreate network even if exists. type: bool default: false + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false """ EXAMPLES = r""" @@ -197,7 +216,7 @@ network: ] """ -import json # noqa: F402 +import json try: import ipaddress HAS_IP_ADDRESS_MODULE = True @@ -208,6 +227,7 @@ from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible.module_utils._text import to_bytes, to_native # noqa: F402 from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state class PodmanNetworkModuleParams: @@ -620,6 +640,7 @@ class PodmanNetworkManager: states_map = { 'present': self.make_present, 'absent': self.make_absent, + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() @@ -652,12 +673,17 @@ class PodmanNetworkManager: 'podman_actions': self.network.actions}) self.module.exit_json(**self.results) + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "network") + self.results.update(results_update) + self.module.exit_json(**self.results) + def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default="present", - choices=['present', 'absent']), + choices=['present', 'absent', 'quadlet']), name=dict(type='str', required=True), disable_dns=dict(type='bool', required=False), driver=dict(type='str', required=False), @@ -681,6 +707,9 @@ def main(): executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), recreate=dict(type='bool', default=False), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), ), required_by=dict( # for IP range and GW to set 'subnet' is required ip_range=('subnet'), diff --git a/plugins/modules/podman_play.py b/plugins/modules/podman_play.py index 390aab7..8e8b1d9 100644 --- a/plugins/modules/podman_play.py +++ b/plugins/modules/podman_play.py @@ -103,7 +103,8 @@ options: 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. + - 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: @@ -131,6 +132,7 @@ options: - created - started - absent + - quadlet required: True tls_verify: description: @@ -158,6 +160,24 @@ options: An empty value ("") means user namespaces are disabled. required: false type: str + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. Must be specified if state is quadlet. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false ''' EXAMPLES = ''' @@ -188,6 +208,7 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion, get_podman_version +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state # noqa: F402 class PodmanKubeManagement: @@ -338,6 +359,12 @@ class PodmanKubeManagement: changed = True return changed, out, err + def make_quadlet(self): + results = {"changed": False} + results_update = create_quadlet_state(self.module, "kube") + results.update(results_update) + self.module.exit_json(**results) + def main(): module = AnsibleModule( @@ -361,7 +388,7 @@ def main(): network=dict(type='list', elements='str'), state=dict( type='str', - choices=['started', 'created', 'absent'], + choices=['started', 'created', 'absent', 'quadlet'], required=True), tls_verify=dict(type='bool'), debug=dict(type='bool'), @@ -371,8 +398,14 @@ def main(): log_level=dict( type='str', choices=["debug", "info", "warn", "error", "fatal", "panic"]), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), ), supports_check_mode=True, + required_if=[ + ('state', 'quadlet', ['quadlet_filename']), + ], ) executable = module.get_bin_path( @@ -385,6 +418,8 @@ def main(): else: pods = manage.discover_pods() changed, out, err = manage.remove_associated_pods(pods) + elif module.params['state'] == 'quadlet': + manage.make_quadlet() else: changed, out, err = manage.play() results = { diff --git a/plugins/modules/podman_pod.py b/plugins/modules/podman_pod.py index 7b57fd3..0d3adf7 100644 --- a/plugins/modules/podman_pod.py +++ b/plugins/modules/podman_pod.py @@ -30,6 +30,7 @@ options: - stopped - paused - unpaused + - quadlet recreate: description: - Use with present and started states to force the re-creation of an @@ -340,6 +341,22 @@ options: required: false aliases: - ports + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual container args + options as a list of lines to add. + type: list + elements: str share: description: - A comma delimited list of kernel namespaces to share. If none or "" is specified, @@ -454,9 +471,7 @@ from ..module_utils.podman.podman_pod_lib import ARGUMENTS_SPEC_POD # noqa: F40 def main(): - module = AnsibleModule( - argument_spec=ARGUMENTS_SPEC_POD - ) + module = AnsibleModule(argument_spec=ARGUMENTS_SPEC_POD) results = PodmanPodManager(module, module.params).execute() module.exit_json(**results) diff --git a/plugins/modules/podman_volume.py b/plugins/modules/podman_volume.py index b4d5062..d7d9430 100644 --- a/plugins/modules/podman_volume.py +++ b/plugins/modules/podman_volume.py @@ -24,6 +24,7 @@ options: choices: - present - absent + - quadlet recreate: description: - Recreate volume even if exists. @@ -62,6 +63,24 @@ options: - Return additional information which can be helpful for investigations. type: bool default: False + quadlet_dir: + description: + - Path to the directory to write quadlet file in. + By default, it will be set as C(/etc/containers/systemd/) for root user, + C(~/.config/containers/systemd/) for non-root users. + type: path + required: false + quadlet_filename: + description: + - Name of quadlet file to write. By default it takes I(name) value. + type: str + quadlet_options: + description: + - Options for the quadlet file. Provide missing in usual network args + options as a list of lines to add. + type: list + elements: str + required: false requirements: - "podman" @@ -105,6 +124,7 @@ from ansible.module_utils.basic import AnsibleModule # noqa: F402 from ansible.module_utils._text import to_bytes, to_native # noqa: F402 from ansible_collections.containers.podman.plugins.module_utils.podman.common import LooseVersion from ansible_collections.containers.podman.plugins.module_utils.podman.common import lower_keys +from ansible_collections.containers.podman.plugins.module_utils.podman.quadlet import create_quadlet_state class PodmanVolumeModuleParams: @@ -436,6 +456,7 @@ class PodmanVolumeManager: states_map = { 'present': self.make_present, 'absent': self.make_absent, + 'quadlet': self.make_quadlet, } process_action = states_map[self.state] process_action() @@ -468,12 +489,17 @@ class PodmanVolumeManager: 'podman_actions': self.volume.actions}) self.module.exit_json(**self.results) + def make_quadlet(self): + results_update = create_quadlet_state(self.module, "volume") + self.results.update(results_update) + self.module.exit_json(**self.results) + def main(): module = AnsibleModule( argument_spec=dict( state=dict(type='str', default="present", - choices=['present', 'absent']), + choices=['present', 'absent', 'quadlet']), name=dict(type='str', required=True), label=dict(type='dict', required=False), driver=dict(type='str', required=False), @@ -481,6 +507,9 @@ def main(): recreate=dict(type='bool', default=False), executable=dict(type='str', required=False, default='podman'), debug=dict(type='bool', default=False), + quadlet_dir=dict(type='path', required=False), + quadlet_filename=dict(type='str', required=False), + quadlet_options=dict(type='list', elements='str', required=False), )) PodmanVolumeManager(module).execute() diff --git a/tests/integration/targets/podman_container/tasks/main.yml b/tests/integration/targets/podman_container/tasks/main.yml index 5a05494..02d664a 100644 --- a/tests/integration/targets/podman_container/tasks/main.yml +++ b/tests/integration/targets/podman_container/tasks/main.yml @@ -707,7 +707,7 @@ path: /tmp/containzzzzcontainer1.service register: service_file - - name: Check that container has correct systemd output v4 + - name: Check that container has correct systemd output v4 and quadlet assert: that: - system14.podman_systemd.keys() | list | first == 'containzzzzcontainer1' @@ -717,6 +717,11 @@ - "'autogenerated by Podman' not in system14.podman_systemd.values() | list | first" - "'RestartSec=10' in system14.podman_systemd.values() | list | first" - "'TimeoutStartSec=20' in system14.podman_systemd.values() | list | first" + - system14.podman_quadlet | length > 0 + - system14.podman_quadlet | length > 0 + - "'ContainerName=container1' in system14.podman_quadlet" + - "'Image=alpine' in system14.podman_quadlet" + when: podman_version == 4 - name: Check that container has correct systemd output v3 @@ -1085,6 +1090,264 @@ - attach3 is failed - "'No such file or directory' in attach3.stderr" + - name: Create a Quadlet for container with filename + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: container-quadlet + image: alpine + state: quadlet + quadlet_dir: /tmp + quadlet_filename: customfile + + - name: Check if files exists + stat: + path: /tmp/customfile.container + register: quadlet_file_custom + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom.stat.exists + + - name: Create a Quadlet for container with filename w/o dir + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: container-quadlet + image: alpine + state: quadlet + quadlet_filename: container11.container + + - name: Check if files exists + stat: + path: ~/.config/containers/systemd/container11.container + register: quadlet_file_custom2 + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom2.stat.exists + + - name: Create a Quadlet for container with filename w/o dir + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: container-quadlet + image: alpine + state: quadlet + + - name: Check if files exists + stat: + path: ~/.config/containers/systemd/container-quadlet.container + register: quadlet_file_custom3 + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom3.stat.exists + + - name: Create a Quadlet for container + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: container-quadlet + image: alpine:3.12 + state: quadlet + quadlet_dir: /tmp + command: sleep 1d + recreate: true + etc_hosts: + host1: 127.0.0.1 + host2: 127.0.0.1 + annotation: + this: "annotation_value" + dns: + - 1.1.1.1 + - 8.8.4.4 + dns_search: example.com + cap_add: + - SYS_TIME + - NET_ADMIN + publish: + - "9000:80" + - "9001:8000" + workdir: "/bin" + env: + FOO: bar=1 + BAR: foo + TEST: 1 + BOOL: false + label: + somelabel: labelvalue + otheralbe: othervalue + volumes: + - /tmp:/data + mounts: + - type=devpts,destination=/dev/pts + quadlet_options: + - AutoUpdate=registry + - Unmask=ALL + - SecurityLabelFileType=usr_t + - Annotation=key1=annotation_value1 + - Annotation=key2=annotation_value2 + - | + [Install] + WantedBy=default.target + + - name: Check if files exists + stat: + path: /tmp/container-quadlet.container + register: quadlet_file + + - name: Check output is correct for Quadlet container in /tmp/container-quadlet.container file + assert: + that: + - quadlet_file.stat.exists + + - name: Check for the existence of lines in /tmp/container-quadlet.container + lineinfile: + path: /tmp/container-quadlet.container + line: "{{ item }}" + state: present + check_mode: yes + register: line_check + loop: + - "[Container]" + - "Annotation=this=annotation_value" + - "Annotation=key1=annotation_value1" + - "Annotation=key2=annotation_value2" + - "ContainerName=container-quadlet" + - "Image=alpine:3.12" + - "Exec=sleep 1d" + - "Volume=/tmp:/data" + - "Mount=type=devpts,destination=/dev/pts" + - "WorkingDir=/bin" + - "Unmask=ALL" + - "SecurityLabelFileType=usr_t" + - "Environment=BOOL=False" + - "PublishPort=9001:8000" + - "PodmanArgs=--add-host host2:127.0.0.1" + - "Label=somelabel=labelvalue" + - "WantedBy=default.target" + loop_control: + label: "{{ item }}" + + - name: Fail the task if any line is not present + fail: + msg: "The following line is not present in /tmp/container-quadlet.container: {{ item.item }}" + when: item.changed + loop: "{{ line_check.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Create a Quadlet for container - same + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: container-quadlet + image: alpine:3.12 + state: quadlet + quadlet_dir: /tmp + command: sleep 1d + recreate: true + etc_hosts: + host1: 127.0.0.1 + host2: 127.0.0.1 + annotation: + this: "annotation_value" + dns: + - 1.1.1.1 + - 8.8.4.4 + dns_search: example.com + cap_add: + - SYS_TIME + - NET_ADMIN + publish: + - "9000:80" + - "9001:8000" + workdir: "/bin" + env: + FOO: bar=1 + BAR: foo + TEST: 1 + BOOL: false + label: + somelabel: labelvalue + otheralbe: othervalue + volumes: + - /tmp:/data + mounts: + - type=devpts,destination=/dev/pts + quadlet_options: + - AutoUpdate=registry + - Unmask=ALL + - SecurityLabelFileType=usr_t + - Annotation=key1=annotation_value1 + - Annotation=key2=annotation_value2 + - | + [Install] + WantedBy=default.target + register: quad2 + + - name: Check if quadlet changed + assert: + that: + - quad2 is not changed + + - name: Create a Quadlet for container - different + containers.podman.podman_container: + executable: "{{ test_executable | default('podman') }}" + name: container-quadlet + image: alpine:3.12 + state: quadlet + quadlet_dir: /tmp + command: sleep 1d + recreate: true + etc_hosts: + host1: 127.0.0.45 + host2: 127.0.0.1 + annotation: + this: "annotation_value" + dns: + - 1.1.1.1 + - 8.8.4.4 + dns_search: example.com + cap_add: + - SYS_TIME + - NET_ADMIN + publish: + - "9000:80" + - "9001:8000" + workdir: "/bin" + env: + FOO: bar=1 + BAR: foo + TEST: 1 + BOOL: false + label: + somelabel: labelvalue + otheralbe: othervalue + volumes: + - /tmp:/data + mounts: + - type=devpts,destination=/dev/pts + quadlet_options: + - AutoUpdate=registry + - Unmask=ALL + - SecurityLabelFileType=usr_t + - Annotation=key1=annotation_value1 + - Annotation=key2=annotation_value2 + - | + [Install] + WantedBy=default.target + register: quad3 + + - name: Print diff + debug: + var: quad3.diff + + - name: Check if changed and diff + assert: + that: + - quad3 is changed + - "'127.0.0.45' in quad3.diff.after" + always: - name: Remove container diff --git a/tests/integration/targets/podman_image/tasks/main.yml b/tests/integration/targets/podman_image/tasks/main.yml index 0db4c05..96133b9 100644 --- a/tests/integration/targets/podman_image/tasks/main.yml +++ b/tests/integration/targets/podman_image/tasks/main.yml @@ -329,6 +329,134 @@ - item.Architecture == "arm" loop: "{{ imageinfo_arch.images }}" + - name: Create a Quadlet for image with filename + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: quay.io/coreos/coreos-installer:latest + state: quadlet + arch: x86_64 + quadlet_dir: /tmp + quadlet_filename: customfile + + - name: Check if files exists + stat: + path: /tmp/customfile.image + register: quadlet_file_custom + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom.stat.exists + + - name: Create quadlet image file + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: quay.io/coreos/coreos-installer:latest + state: quadlet + arch: x86_64 + ca_cert_dir: /etc/docker/certs.d + username: user + password: pass + validate_certs: false + quadlet_dir: /tmp/ + quadlet_options: + - "ImageTag=quay.io/coreos/coreos-installer:12345" + - "AllTags=true" + - |- + [Install] + WantedBy=default.target + + - name: Check if files exists + stat: + path: /tmp/coreos-installer.image + register: quadlet_file + + - name: Check output is correct for Quadlet image in /tmp/coreos-installer.image file + assert: + that: + - quadlet_file.stat.exists + + - name: Check for the existence of lines in /tmp/coreos-installer.image + lineinfile: + path: /tmp/coreos-installer.image + line: "{{ item }}" + state: present + check_mode: yes + register: line_check + loop: + - "[Image]" + - "Image=quay.io/coreos/coreos-installer:latest" + - "ImageTag=quay.io/coreos/coreos-installer:12345" + - "AllTags=true" + - "WantedBy=default.target" + - "Arch=x86_64" + - "CertDir=/etc/docker/certs.d" + - "Creds=user:pass" + - "TLSVerify=false" + loop_control: + label: "{{ item }}" + + - name: Fail the task if any line is not present + fail: + msg: "The following line is not present in /tmp/coreos-installer.image: {{ item.item }}" + when: item.changed + loop: "{{ line_check.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Create quadlet image file - same + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: quay.io/coreos/coreos-installer:latest + state: quadlet + arch: x86_64 + ca_cert_dir: /etc/docker/certs.d + username: user + password: pass + validate_certs: false + quadlet_dir: /tmp + quadlet_options: + - "ImageTag=quay.io/coreos/coreos-installer:12345" + - "AllTags=true" + - |- + [Install] + WantedBy=default.target + register: quad2 + + - name: Check if quadlet changed + assert: + that: + - quad2 is not changed + + - name: Create quadlet image file - different + containers.podman.podman_image: + executable: "{{ test_executable | default('podman') }}" + name: quay.io/coreos/coreos-installer:latest + state: quadlet + arch: arm64 + ca_cert_dir: /etc/docker/certs.d + username: user + password: pass + validate_certs: false + quadlet_dir: /tmp/ + quadlet_options: + - "ImageTag=quay.io/coreos/coreos-installer:12345" + - "AllTags=true" + - |- + [Install] + WantedBy=default.target + register: quad3 + + - name: Print diff + debug: + var: quad3.diff + + - name: Check if changed and diff + assert: + that: + - quad3 is changed + - "'arm64' in quad3.diff.after" + always: - name: Cleanup images containers.podman.podman_image: diff --git a/tests/integration/targets/podman_network/tasks/main.yml b/tests/integration/targets/podman_network/tasks/main.yml index d207e4c..272ac6b 100644 --- a/tests/integration/targets/podman_network/tasks/main.yml +++ b/tests/integration/targets/podman_network/tasks/main.yml @@ -350,6 +350,131 @@ that: - info17 is not changed + - name: Create a Quadlet for network with filename + containers.podman.podman_network: + executable: "{{ test_executable | default('podman') }}" + name: testnet + state: quadlet + quadlet_dir: /tmp + quadlet_filename: customfile + + - name: Check if files exists + stat: + path: /tmp/customfile.network + register: quadlet_file_custom + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom.stat.exists + + - name: Create quadlet network file + containers.podman.podman_network: + executable: "{{ test_executable | default('podman') }}" + name: testnet + state: quadlet + disable_dns: true + subnet: "10.123.12.0" + internal: false + opt: + isolate: true + mtu: 1511 + vlan: 111 + quadlet_dir: /tmp + quadlet_options: + - "Label=Test=network" + - "Label=foo=bar" + + - name: Check if files exists + stat: + path: /tmp/testnet.network + register: quadlet_file + + - name: Check output is correct for Quadlet network in /tmp/testnet.network file + assert: + that: + - quadlet_file.stat.exists + + - name: Check for the existence of lines in /tmp/testnet.network + lineinfile: + path: /tmp/testnet.network + line: "{{ item }}" + state: present + check_mode: yes + register: line_check + loop: + - "[Network]" + - "NetworkName=testnet" + - "Subnet=10.123.12.0" + - "DisableDNS=true" + - "Internal=false" + - "Options=isolate=True" + - "Options=mtu=1511" + - "Options=vlan=111" + - "Label=Test=network" + - "Label=foo=bar" + loop_control: + label: "{{ item }}" + + - name: Fail the task if any line is not present + fail: + msg: "The following line is not present in /tmp/testnet.network: {{ item.item }}" + when: item.changed + loop: "{{ line_check.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Create quadlet network file - same + containers.podman.podman_network: + executable: "{{ test_executable | default('podman') }}" + name: testnet + state: quadlet + disable_dns: true + subnet: "10.123.12.0" + internal: false + opt: + isolate: true + mtu: 1511 + vlan: 111 + quadlet_dir: /tmp + quadlet_options: + - "Label=Test=network" + - "Label=foo=bar" + register: quad2 + + - name: Check if quadlet changed + assert: + that: + - quad2 is not changed + + - name: Create quadlet network file - different + containers.podman.podman_network: + executable: "{{ test_executable | default('podman') }}" + name: testnet + state: quadlet + disable_dns: true + subnet: "10.123.15.0" + internal: false + opt: + isolate: true + mtu: 1511 + vlan: 111 + quadlet_dir: /tmp + quadlet_options: + - "Label=Test=network" + - "Label=foo=bar" + register: quad3 + + - name: Print diff + debug: + var: quad3.diff + + - name: Check if changed and diff + assert: + that: + - quad3 is changed + - "'10.123.15.0' in quad3.diff.after" + always: - name: Cleanup diff --git a/tests/integration/targets/podman_play/tasks/main.yml b/tests/integration/targets/podman_play/tasks/main.yml index a97f3cd..d22615e 100644 --- a/tests/integration/targets/podman_play/tasks/main.yml +++ b/tests/integration/targets/podman_play/tasks/main.yml @@ -130,6 +130,157 @@ that: - nonexist.pods == [] + + - name: Create a Quadlet for kube with filename + containers.podman.podman_play: + executable: "{{ test_executable | default('podman') }}" + kube_file: /home/kubeuser/tmp/multipod.yaml + state: quadlet + quadlet_dir: /tmp + quadlet_filename: customfile + + - name: Check if files exists + stat: + path: /tmp/customfile.kube + register: quadlet_file_custom + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom.stat.exists + + - name: Create a kube quadlet without filename + containers.podman.podman_play: + executable: "{{ test_executable | default('podman') }}" + kube_file: /home/kubeuser/tmp/multipod.yaml + state: quadlet + quadlet_dir: /tmp + register: quadlet_file_no_name + ignore_errors: true + + - name: Check that task failed + assert: + that: + - quadlet_file_no_name is failed + + - name: Create a kube quadlet + containers.podman.podman_play: + executable: "{{ test_executable | default('podman') }}" + kube_file: /home/kubeuser/tmp/multipod.yaml + state: quadlet + userns: keep-id:uid=200,gid=210 + log_driver: journald + network: host + configmap: + - /tmp/configmap1 + - /tmp/configmap2 + debug: true + quadlet_dir: /tmp + quadlet_filename: quadlet + quadlet_options: + - "PodmanArgs=--annotation=key1=value1" + - "PodmanArgs=--context-dir /my/path" + - | + [Install] + WantedBy=default.target + + - name: Check if files exists + stat: + path: /tmp/quadlet.kube + register: quadlet_file + + - name: Check output is correct for Quadlet container in /tmp/quadlet.kube file + assert: + that: + - quadlet_file.stat.exists + + - name: Check for the existence of lines in /tmp/quadlet.kube + lineinfile: + path: /tmp/quadlet.kube + line: "{{ item }}" + state: present + check_mode: yes + register: line_check + loop: + - "[Kube]" + - "ConfigMap=/tmp/configmap1" + - "ConfigMap=/tmp/configmap2" + - "LogDriver=journald" + - "Network=host" + - "Yaml=/home/kubeuser/tmp/multipod.yaml" + - "UserNS=keep-id:uid=200,gid=210" + - "GlobalArgs=--log-level debug" + - "WantedBy=default.target" + loop_control: + label: "{{ item }}" + + - name: Fail the task if any line is not present + fail: + msg: "The following line is not present in /tmp/quadlet.kube: {{ item.item }}" + when: item.changed + loop: "{{ line_check.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Create a kube quadlet - same + containers.podman.podman_play: + executable: "{{ test_executable | default('podman') }}" + kube_file: /home/kubeuser/tmp/multipod.yaml + state: quadlet + userns: keep-id:uid=200,gid=210 + log_driver: journald + network: host + configmap: + - /tmp/configmap1 + - /tmp/configmap2 + debug: true + quadlet_dir: /tmp + quadlet_filename: quadlet.kube + quadlet_options: + - "PodmanArgs=--annotation=key1=value1" + - "PodmanArgs=--context-dir /my/path" + - | + [Install] + WantedBy=default.target + register: quad2 + + - name: Check if quadlet changed + assert: + that: + - quad2 is not changed + + - name: Create a kube quadlet - different + containers.podman.podman_play: + executable: "{{ test_executable | default('podman') }}" + kube_file: /home/kubeuser/tmp/multipod.yaml + state: quadlet + userns: keep-id:uid=200,gid=210 + log_driver: journald + network: host + configmap: + - /tmp/configmap55 + - /tmp/configmap2 + debug: true + quadlet_dir: /tmp + quadlet_filename: quadlet.kube + quadlet_options: + - "PodmanArgs=--annotation=key1=value1" + - "PodmanArgs=--context-dir /my/path" + - | + [Install] + WantedBy=default.target + register: quad3 + + - name: Print diff + debug: + var: quad3.diff + + - name: Check if changed and diff + assert: + that: + - quad3 is changed + - "'configmap55' in quad3.diff.after" + always: - name: Delete all pods leftovers from tests diff --git a/tests/integration/targets/podman_pod/tasks/main.yml b/tests/integration/targets/podman_pod/tasks/main.yml index d33e949..cb455aa 100644 --- a/tests/integration/targets/podman_pod/tasks/main.yml +++ b/tests/integration/targets/podman_pod/tasks/main.yml @@ -981,6 +981,151 @@ - pasta_pod2 is changed - pasta_cont2 is changed + - name: Create a Quadlet for pod with filename + containers.podman.podman_pod: + executable: "{{ test_executable | default('podman') }}" + name: podq + state: quadlet + network: examplenet + quadlet_dir: /tmp + quadlet_filename: customfile + + - name: Check if files exists + stat: + path: /tmp/customfile.pod + register: quadlet_file_custom + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom.stat.exists + + - name: Create a Quadlet pod file + containers.podman.podman_pod: + executable: "{{ test_executable | default('podman') }}" + name: podq + state: quadlet + network: examplenet + share: net + subuidname: username1 + userns: auto + publish: 8000:8001 + add_host: + - host1 + volume: + - /tmp:/data + - /whocares:/data2:ro + quadlet_dir: /tmp + quadlet_options: + - "Label=somelabel=labelvalue" + - | + [Install] + WantedBy=default.target + register: quadlet_pod + + - name: Check if files exists + stat: + path: /tmp/podq.pod + register: quadlet_file + + - name: Check output is correct for Quadlet container in /tmp/podq.pod file + assert: + that: + - quadlet_file.stat.exists + + - name: Check for the existence of lines in /tmp/podq.pod + lineinfile: + path: /tmp/podq.pod + line: "{{ item }}" + state: present + check_mode: yes + register: line_check + loop: + - "[Pod]" + - "Network=examplenet" + - "PodName=podq" + - "PublishPort=8000:8001" + - "Volume=/tmp:/data" + - "Volume=/whocares:/data2:ro" + - "PodmanArgs=--add-host host1" + - "PodmanArgs=--share net" + - "PodmanArgs=--subuidname username1" + - "PodmanArgs=--userns auto" + - "PodmanArgs=--add-host host1" + - "Label=somelabel=labelvalue" + - "WantedBy=default.target" + loop_control: + label: "{{ item }}" + + - name: Fail the task if any line is not present + fail: + msg: "The following line is not present in /tmp/podq.pod: {{ item.item }}" + when: item.changed + loop: "{{ line_check.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Create a Quadlet pod file - same + containers.podman.podman_pod: + executable: "{{ test_executable | default('podman') }}" + name: podq + state: quadlet + network: examplenet + share: net + subuidname: username1 + userns: auto + publish: 8000:8001 + add_host: + - host1 + volume: + - /tmp:/data + - /whocares:/data2:ro + quadlet_dir: /tmp + quadlet_options: + - "Label=somelabel=labelvalue" + - | + [Install] + WantedBy=default.target + register: quad2 + + - name: Check if quadlet changed + assert: + that: + - quad2 is not changed + + - name: Create a Quadlet pod file - different + containers.podman.podman_pod: + executable: "{{ test_executable | default('podman') }}" + name: podq + state: quadlet + network: examplenet + share: net + subuidname: username1 + userns: auto + publish: 8000:8001 + add_host: + - host1 + volume: + - /tmp:/newdata + - /whocares:/data2:ro + quadlet_dir: /tmp + quadlet_options: + - "Label=somelabel=labelvalue" + - | + [Install] + WantedBy=default.target + register: quad3 + + - name: Print diff + debug: + var: quad3.diff + + - name: Check if changed and diff + assert: + that: + - quad3 is changed + - "'newdata' in quad3.diff.after" + always: - name: Delete all pods leftovers from tests diff --git a/tests/integration/targets/podman_volume/tasks/main.yml b/tests/integration/targets/podman_volume/tasks/main.yml index 144a39f..9d43f3f 100644 --- a/tests/integration/targets/podman_volume/tasks/main.yml +++ b/tests/integration/targets/podman_volume/tasks/main.yml @@ -161,6 +161,134 @@ - info10 is failed - delete.volume == {} + - name: Create a Quadlet for volume with filename + containers.podman.podman_volume: + executable: "{{ test_executable | default('podman') }}" + name: testvol + state: quadlet + quadlet_dir: /tmp + quadlet_filename: customfile + + - name: Check if files exists + stat: + path: /tmp/customfile.volume + register: quadlet_file_custom + + - name: Fail if no file is present + assert: + that: + - quadlet_file_custom.stat.exists + + - name: Create quadlet volume file + containers.podman.podman_volume: + executable: "{{ test_executable | default('podman') }}" + name: testvol + state: quadlet + driver: local + label: + namelabel: value + foo: bar + debug: true + options: + - "device=/dev/loop1" + - "type=ext4" + quadlet_dir: /tmp + quadlet_options: + - "Label=Test=volume" + - "Label=test1=value1" + + - name: Check if files exists + stat: + path: /tmp/testvol.volume + register: quadlet_file + + - name: Check output is correct for Quadlet volume in /tmp/testvol.volume file + assert: + that: + - quadlet_file.stat.exists + + - name: Check for the existence of lines in /tmp/testvol.volume + lineinfile: + path: /tmp/testvol.volume + line: "{{ item }}" + state: present + check_mode: yes + register: line_check + loop: + - "[Volume]" + - "VolumeName=testvol" + - "Driver=local" + - "Label=namelabel=value" + - "Label=foo=bar" + - "Label=Test=volume" + - "Label=test1=value1" + - "PodmanArgs=--opt device=/dev/loop1" + - "PodmanArgs=--opt type=ext4" + - "GlobalArgs=--log-level debug" + loop_control: + label: "{{ item }}" + + - name: Fail the task if any line is not present + fail: + msg: "The following line is not present in /tmp/testvol.volume: {{ item.item }}" + when: item.changed + loop: "{{ line_check.results }}" + loop_control: + label: "{{ item.item }}" + + - name: Create quadlet volume file - same + containers.podman.podman_volume: + executable: "{{ test_executable | default('podman') }}" + name: testvol + state: quadlet + driver: local + label: + namelabel: value + foo: bar + debug: true + options: + - "device=/dev/loop1" + - "type=ext4" + quadlet_dir: /tmp + quadlet_options: + - "Label=Test=volume" + - "Label=test1=value1" + register: quad2 + + - name: Check if quadlet changed + assert: + that: + - quad2 is not changed + + - name: Create quadlet volume file - different + containers.podman.podman_volume: + executable: "{{ test_executable | default('podman') }}" + name: testvol + state: quadlet + driver: local + label: + namelabel: value + foo: bar + debug: true + options: + - "device=/dev/loop5" + - "type=ext4" + quadlet_dir: /tmp + quadlet_options: + - "Label=Test=volume" + - "Label=test1=value1" + register: quad3 + + - name: Print diff + debug: + var: quad3.diff + + - name: Check if changed and diff + assert: + that: + - quad3 is changed + - "'loop5' in quad3.diff.after" + always: - name: Make sure volume doesn't exist