1
0
Fork 0
mirror of https://github.com/containers/ansible-podman-collections.git synced 2026-02-04 07:11:49 +00:00

WIP: add inventory plugins

Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
This commit is contained in:
Sagi Shnaidman 2025-08-09 19:44:17 +03:00
parent 6e8c88068f
commit 6a713de37a
4 changed files with 395 additions and 1 deletions

View file

@ -0,0 +1,110 @@
name: Test inventory and example playbooks
on:
pull_request:
push:
branches: [ main, connections ]
jobs:
examples:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Ansible 2.18
run: python3 -m pip install --user --force-reinstall --upgrade ansible-core==2.18
- name: Build and install the collection tarball
run: |
rm -rf /tmp/just_new_collection
~/.local/bin/ansible-galaxy collection build --output-path /tmp/just_new_collection --force
~/.local/bin/ansible-galaxy collection install -vvv --force /tmp/just_new_collection/*.tar.gz
- name: Install system deps (podman, buildah)
run: |
sudo apt-get update
sudo apt-get install -y podman buildah jq
podman --version
buildah --version
- name: Configure rootless storage
run: |
mkdir -p ~/.config/containers
printf '[storage]\ndriver = "overlay"\n' > ~/.config/containers/storage.conf
printf '[engine]\ncgroup_manager = "cgroupfs"\nevents_logger = "file"\n' > ~/.config/containers/engine.conf
- name: Install Ansible
run: |
python -m pip install --upgrade pip
python -m pip install ansible-core
ansible --version
- name: Build basic containers for discovery
run: |
podman pull alpine:latest
podman run -d --name podman-inventory-test alpine:latest sleep 3600
buildah from --name hello-buildah alpine:latest
echo 'Podman ps output:'
podman ps -a --format json | jq '.'
echo 'Buildah containers output:'
buildah containers -a --json | jq '.'
- name: Write podman inventory source
run: |
mkdir -p ci/tmpinv
cat > ci/tmpinv/podman.yml <<'EOF'
plugin: containers.podman.podman_containers
include_stopped: false
connection_plugin: containers.podman.podman
EOF
- name: Write buildah inventory source
run: |
cat > ci/tmpinv/buildah.yml <<'EOF'
plugin: containers.podman.buildah_containers
connection_plugin: containers.podman.buildah
EOF
- name: Sanity check podman inventory
run: |
out=$(ANSIBLE_INVENTORY_ENABLED=containers.podman.podman_containers,yaml,ini ansible-inventory -i ci/tmpinv/podman.yml --list)
echo "$out" | jq '.'
echo "$out" | jq -e '. | to_entries | any(.value.hosts != null and (.value.hosts | length) > 0)'
- name: Sanity check buildah inventory
run: |
out=$(ANSIBLE_INVENTORY_ENABLED=containers.podman.buildah_containers,yaml,ini ansible-inventory -i ci/tmpinv/buildah.yml --list)
echo "$out" | jq '.'
echo "$out" | jq -e '. | to_entries | any(.value.hosts != null and (.value.hosts | length) > 0)'
- name: Run example Node build
working-directory: playbook/examples
run: |
ansible-playbook -i localhost, -c local build_node_ai_api.yml -e image_name=ci-ai-node:latest
buildah images --format '{{.Name}}:{{.Tag}}' | grep -q '^ci-ai-node:latest$'
- name: Run example Go multistage build
working-directory: playbook/examples
run: |
ansible-playbook -i localhost, -c local build_go_ai_multistage.yml -e image_name=ci-ai-go:latest
buildah images --format '{{.Name}}:{{.Tag}}' | grep -q '^ci-ai-go:latest$'
- name: Run AI env build
working-directory: playbook/examples
run: |
ansible-playbook -i localhost, -c local build_ai_env_with_ansible.yml -e image_name=ci-ai-env:latest
buildah images --format '{{.Name}}:{{.Tag}}' | grep -q '^ci-ai-env:latest$'
- name: Show resulting images
run: |
buildah images

View file

@ -6,6 +6,6 @@ ansible-galaxy collection build --output-path /tmp/ansible-lint-collection --for
pushd /tmp/ansible-lint-collection/
ansible-galaxy collection install -vvv --force $(ls /tmp/ansible-lint-collection/) -p /tmp/ansible-lint-installs
pushd /tmp/ansible-lint-installs/ansible_collections/containers/podman
ansible-test units --python 3.12 -vvv
ansible-test units --python $(python -V | sed "s/Python //g" | awk -F"." {'print $1"."$2'}) -vvv
popd
popd

View file

@ -0,0 +1,114 @@
# Copyright (c) 2025
# GNU General Public License v3.0+
from __future__ import annotations
__metaclass__ = type
DOCUMENTATION = r"""
name: buildah_containers
short_description: Inventory plugin that discovers Buildah working containers as hosts
version_added: '1.18.0'
description:
- Discover Buildah working containers on the local host and add them as inventory hosts.
- Each discovered host is assigned the Buildah connection plugin so tasks execute inside the working container.
options:
plugin:
description: Token that ensures this is a source file for the 'containers.podman.buildah_containers' inventory plugin.
required: true
type: str
choices: ['containers.podman.buildah_containers']
executable:
description: Path to the C(buildah) executable.
type: str
default: buildah
env:
- name: ANSIBLE_BUILDAH_EXECUTABLE
name_patterns:
description: Glob patterns to match working container names or IDs; empty means include all.
type: list
elements: str
default: []
connection_plugin:
description: Fully-qualified connection plugin to use for discovered hosts.
type: str
default: containers.podman.buildah
"""
EXAMPLES = r"""
plugin: containers.podman.buildah_containers
connection_plugin: containers.podman.buildah
name_patterns:
- my-build-*
"""
import json
import fnmatch
import shutil
import subprocess
import os
from ansible.errors import AnsibleParserError
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
class InventoryModule(BaseInventoryPlugin, Cacheable):
NAME = "containers.podman.buildah_containers"
def verify_file(self, path: str) -> bool:
if not super(InventoryModule, self).verify_file(path):
return False
unused, ext = os.path.splitext(path)
if ext not in (".yml", ".yaml"):
return False
try:
with open(path, "r", encoding="utf-8") as f:
header = f.read(2048)
return (
(f"plugin: {self.NAME}\n" in header)
or (f"plugin: '{self.NAME}'" in header)
or (f'plugin: "{self.NAME}"' in header)
)
except Exception:
return False
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
config = self._read_config_data(path)
executable = config.get("executable", "buildah")
name_patterns = list(config.get("name_patterns", []) or [])
connection_plugin = config.get("connection_plugin", "containers.podman.buildah")
buildah_path = shutil.which(executable) or executable
# 'buildah containers -a --format json' lists working containers
args = [buildah_path, "containers", "-a", "--json"]
try:
output = subprocess.check_output(args, stderr=subprocess.STDOUT)
containers = json.loads(output.decode("utf-8"))
except Exception as exc:
raise AnsibleParserError(f"Failed to list buildah containers: {exc}")
for c in containers or []:
name = c.get("name") or c.get("containername") or c.get("id")
cid = c.get("id") or c.get("containerid")
if not name and cid:
name = cid[:12]
# name filtering
if name_patterns:
if not any(fnmatch.fnmatch(name, pat) or (cid and fnmatch.fnmatch(cid, pat)) for pat in name_patterns):
continue
host = name or cid
if not host:
continue
self.inventory.add_host(host)
self.inventory.set_variable(host, "ansible_connection", connection_plugin)
self.inventory.set_variable(host, "ansible_host", name or cid)
if cid:
self.inventory.set_variable(host, "buildah_container_id", cid)
if name:
self.inventory.set_variable(host, "buildah_container_name", name)

View file

@ -0,0 +1,170 @@
# Copyright (c) 2025
# GNU General Public License v3.0+
from __future__ import annotations
__metaclass__ = type
DOCUMENTATION = r"""
name: podman_containers
short_description: Inventory plugin that discovers Podman containers as hosts
version_added: '1.18.0'
description:
- Discover running (and optionally stopped) Podman containers on the local host and add them as inventory hosts.
- Each discovered host is assigned an Ansible connection plugin so tasks execute inside the container without SSH.
options:
plugin:
description: Token that ensures this is a source file for the 'containers.podman.podman_containers' inventory plugin.
required: true
type: str
choices: ['containers.podman.podman_containers']
executable:
description: Path to the C(podman) executable.
type: str
default: podman
env:
- name: ANSIBLE_PODMAN_EXECUTABLE
include_stopped:
description: Whether to include stopped/exited containers.
type: bool
default: false
name_patterns:
description: Glob patterns to match container names or IDs; empty means include all.
type: list
elements: str
default: []
label_selectors:
description: Key/value labels that must match (all) for a container to be included.
type: dict
default: {}
connection_plugin:
description: Fully-qualified connection plugin to use for discovered hosts.
type: str
default: containers.podman.podman
group_by_image:
description: Add containers to a group derived from image name (e.g., C(image_node_14)).
type: bool
default: true
group_by_label:
description: Label keys to group containers by (C(label_<key>_<value>)).
type: list
elements: str
default: []
"""
EXAMPLES = r"""
plugin: containers.podman.podman_containers
include_stopped: false
label_selectors:
role: api
connection_plugin: containers.podman.podman
"""
import json
import fnmatch
import shutil
import subprocess
import os
from typing import Dict, List
from ansible.errors import AnsibleParserError
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
class InventoryModule(BaseInventoryPlugin, Cacheable):
NAME = "containers.podman.podman_containers"
def verify_file(self, path: str) -> bool:
if not super(InventoryModule, self).verify_file(path):
return False
unused, ext = os.path.splitext(path)
if ext not in (".yml", ".yaml"):
return False
try:
with open(path, "r", encoding="utf-8") as f:
header = f.read(2048)
return (
(f"plugin: {self.NAME}\n" in header)
or (f"plugin: '{self.NAME}'" in header)
or (f'plugin: "{self.NAME}"' in header)
)
except Exception:
return False
def parse(self, inventory, loader, path, cache=True):
super(InventoryModule, self).parse(inventory, loader, path)
config = self._read_config_data(path)
executable = config.get("executable", "podman")
include_stopped = bool(config.get("include_stopped", False))
name_patterns = list(config.get("name_patterns", []) or [])
label_selectors: Dict[str, str] = dict(config.get("label_selectors", {}) or {})
connection_plugin = config.get("connection_plugin", "containers.podman.podman")
group_by_image = bool(config.get("group_by_image", True))
group_by_label: List[str] = list(config.get("group_by_label", []) or [])
podman_path = shutil.which(executable) or executable
args = [podman_path, "ps", "--format", "json"]
if include_stopped:
args.insert(2, "-a")
try:
output = subprocess.check_output(args, stderr=subprocess.STDOUT)
containers = json.loads(output.decode("utf-8"))
except Exception as exc:
raise AnsibleParserError(f"Failed to list podman containers: {exc}")
for c in containers or []:
name = (
(c.get("Names") or [c.get("Names", "")])[0]
if isinstance(c.get("Names"), list)
else c.get("Names") or c.get("Names", "")
)
cid = c.get("Id") or c.get("ID") or c.get("Id")
if not name and cid:
name = cid[:12]
# name filtering
if name_patterns:
if not any(fnmatch.fnmatch(name, pat) or (cid and fnmatch.fnmatch(cid, pat)) for pat in name_patterns):
continue
# label filtering
labels = c.get("Labels") or {}
if any(labels.get(k) != v for k, v in label_selectors.items()):
continue
host = name or cid
if not host:
continue
self.inventory.add_host(host)
# Set connection plugin and remote_addr (container id or name works)
self.inventory.set_variable(host, "ansible_connection", connection_plugin)
self.inventory.set_variable(host, "ansible_host", name or cid)
# Common vars
image = c.get("Image") or c.get("ImageName")
status = c.get("Status") or c.get("State")
self.inventory.set_variable(host, "podman_container_id", cid)
self.inventory.set_variable(host, "podman_container_name", name)
if image:
self.inventory.set_variable(host, "podman_image", image)
if status:
self.inventory.set_variable(host, "podman_status", status)
if labels:
self.inventory.set_variable(host, "podman_labels", labels)
# Grouping
if group_by_image and image:
safe_image = image.replace(":", "_").replace("/", "_").replace("-", "_")
self.inventory.add_group(f"image_{safe_image}")
self.inventory.add_host(host, group=f"image_{safe_image}")
for key in group_by_label:
if key in labels:
val = str(labels.get(key)).replace("/", "_").replace(":", "_").replace("-", "_")
group = f"label_{key}_{val}"
self.inventory.add_group(group)
self.inventory.add_host(host, group=group)