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 Mar 1, 2024
1 parent f2dcda6 commit 197b523
Show file tree
Hide file tree
Showing 6 changed files with 344 additions and 2 deletions.
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='path'),
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 = 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]) 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",
}})

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: path
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
34 changes: 33 additions & 1 deletion plugins/modules/podman_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,17 @@
choices:
- present
- absent
- quadlet
recreate:
description:
- Recreate network even if exists.
type: bool
default: false
quadlet_file_path:
description:
- Path to the quadlet file to be created.
type: path
required: false
"""

EXAMPLES = r"""
Expand Down Expand Up @@ -203,11 +209,14 @@
HAS_IP_ADDRESS_MODULE = True
except ImportError:
HAS_IP_ADDRESS_MODULE = False
import os # noqa: F402

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 ..module_utils.podman.quadlet import NetworkQuadlet
from ..module_utils.podman.common import compare_systemd_file_content


class PodmanNetworkModuleParams:
Expand Down Expand Up @@ -620,6 +629,7 @@ def execute(self):
states_map = {
'present': self.make_present,
'absent': self.make_absent,
'quadlet': self.make_quadlet,
}
process_action = states_map[self.state]
process_action()
Expand Down Expand Up @@ -652,12 +662,33 @@ def make_absent(self):
'podman_actions': self.network.actions})
self.module.exit_json(**self.results)

def make_quadlet(self):
"""Create a quadlet file for the network."""
quadlet_file_path = 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 = NetworkQuadlet(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]) 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",
}})
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),
Expand All @@ -681,6 +712,7 @@ def main():
executable=dict(type='str', required=False, default='podman'),
debug=dict(type='bool', default=False),
recreate=dict(type='bool', default=False),
quadlet_file_path=dict(type='path', required=False),
),
required_by=dict( # for IP range and GW to set 'subnet' is required
ip_range=('subnet'),
Expand Down
Loading

0 comments on commit 197b523

Please sign in to comment.