Skip to content

Commit

Permalink
WIP add Quadlet support for Podman
Browse files Browse the repository at this point in the history
Fix containers#671
Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
  • Loading branch information
sshnaidm committed Feb 29, 2024
1 parent f2dcda6 commit e853e90
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 1 deletion.
25 changes: 24 additions & 1 deletion plugins/module_utils/podman/podman_container_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
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 .quadlet import ContainerQuadlet
from .common import compare_systemd_file_content

__metaclass__ = type

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']),
Expand Down Expand Up @@ -116,6 +118,7 @@
publish=dict(type='list', elements='str', aliases=[
'ports', 'published', 'published_ports']),
publish_all=dict(type='bool'),
quadlet_file_path=dict(type='str'),
read_only=dict(type='bool'),
read_only_tmpfs=dict(type='bool'),
recreate=dict(type='bool', default=False),
Expand Down Expand Up @@ -1800,6 +1803,25 @@ def make_absent(self):
self.results.update({'container': {},
'podman_actions': self.container.actions})

def make_quadlet(self):
quadlet_file_path = os.path.expanduser(self.module_params['quadlet_file_path'])
if not quadlet_file_path:
self.module.fail_json(msg="quadlet_file_path is required for quadlet state")
if not os.path.exists(os.path.dirname(quadlet_file_path)):
self.module.fail_json(msg="Directory for quadlet_file_path doesn't exist")
# Check if file already exists and if it's different
quadlet = ContainerQuadlet(self.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)
self.results.update({
'changed': True,
"diff": {
"before": "\n".join(file_diff[0]) + "\n",
"after": "\n".join(file_diff[1]) + "\n",
}})

def execute(self):
"""Execute the desired action according to map of actions & states."""
states_map = {
Expand All @@ -1808,6 +1830,7 @@ def execute(self):
'absent': self.make_absent,
'stopped': self.make_stopped,
'created': self.make_created,
'quadlet': self.make_quadlet,
}
process_action = states_map[self.state]
process_action()
Expand Down
153 changes: 153 additions & 0 deletions plugins/module_utils/podman/quadlet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
# Copyright (c) 2024 Sagi Shnaidman
# 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


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 using a dictionary comprehension.
"""
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.
"""
return f"[{self.section}]\n" + "\n".join(
f"{key}={value}" for key, value in self.dict_params
) + "\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 = {
'annotation': 'Annotation',
'name': 'ContainerName',
'image': 'Image',
'volume': 'Volume',
'network': 'Network',
'command': 'Exec',
'cap_add': 'AddCapability',
# Add more parameter mappings specific to containers
}

def __init__(self, params: dict):
super().__init__("Container", params)

def custom_prepare_params(self, params: dict) -> dict:
"""
Custom parameter processing for container-specific parameters.
"""
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["command"]:
params["command"] = (" ".join(params["command"])
if isinstance(params["command"], list)
else params["command"])
# 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',
# Add more parameter mappings specific to networks
}

def __init__(self, params: dict):
super().__init__("Network", params)


# This is a inherited class that represents a Quadlet file for the Podman image
class Image(Quadlet):
pass


# This is a inherited class that represents a Quadlet file for the Podman pod
class Pod(Quadlet):
pass


# This is a inherited class that represents a Quadlet file for the Podman volume
class Volume(Quadlet):
pass


# This is a inherited class that represents a Quadlet file for the Podman kube
class Kube(Quadlet):
pass


if __name__ == "__main__":
container_params = {
"annotation": {"key1": "value1", "key2": "value2"},
"name": "my_container",
"command": ["sleep", "10"],
"image": "quay.io/aaa/alpine",
"volume": ["/my/host/path:/my/container/path:ro", "/another/host/path:/another/container/path:rw"],
"cap_add": ["CAP_DAC_OVERRIDE", "CAP_IPC_OWNER"],
"network": None,
"foo": "bar",

# ... other container-specific parameters
}

container_quadlet = ContainerQuadlet(container_params)
# print(container_quadlet.create_quadlet_content())

network_params = {
"name": "my_network",
"internal": True,
"driver": "bridge",
"gateway": "1.2.3.4",
"disable_dns": False,
"subnet": None,
# ... other network-specific parameters
}
network_quadlet = NetworkQuadlet(network_params)
# print(network_quadlet.create_quadlet_content())
19 changes: 19 additions & 0 deletions plugins/modules/podman_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
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_file_path) option to be set.
type: str
default: started
choices:
Expand All @@ -63,6 +65,7 @@
- stopped
- started
- created
- quadlet
image:
description:
- Repository path (or image name) and tag used to create the container.
Expand Down Expand Up @@ -721,6 +724,10 @@
- Publish all exposed ports to random ports on the host interfaces. The
default is false.
type: bool
quadlet_file_path:
description:
- Path to the quadlet file to write.
type: str
read_only:
description:
- Mount the container's root filesystem as read only. Default is false
Expand Down Expand Up @@ -994,6 +1001,18 @@
- --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_file_path: ~/.config/containers/systemd/nginx.container
device: "/dev/sda:/dev/xvda:rwm"
ports:
- "8080:80"
volumes:
- "/var/www:/usr/share/nginx/html"
"""

RETURN = r"""
Expand Down
72 changes: 72 additions & 0 deletions tests/integration/targets/podman_container/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,78 @@
- attach3 is failed
- "'No such file or directory' in attach3.stderr"

- 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_file_path: /tmp/quadlet.container
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

- name: Check if files exists
stat:
path: /tmp/quadlet.container
register: quadlet_file

- name: Check output is correct for Quadlet container in /tmp/quadlet.container file
assert:
that:
- quadlet_file.stat.exists

- name: Check for the existence of lines in /tmp/quadlet.container
lineinfile:
path: /tmp/quadlet.container
line: "{{ item }}"
state: present
check_mode: yes
register: line_check
loop:
- "Annotation=this=annotation_value"
- "ContainerName=container-quadlet"
- "Image=alpine:3.12"
- "Exec=sleep 1d"
- "Volume=/tmp:/data"
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.container: {{ item.item }}"
when: item.changed
loop: "{{ line_check.results }}"
loop_control:
label: "{{ item.item }}"

always:

- name: Remove container
Expand Down

0 comments on commit e853e90

Please sign in to comment.