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

Add inventory plugins for buildah and podman (#963)

Add inventory plugins for buildah and podman, unit tests and functional CI tests.
---------

Signed-off-by: Sagi Shnaidman <sshnaidm@redhat.com>
This commit is contained in:
Sergey 2025-08-13 16:48:50 +03:00 committed by GitHub
parent fb76891c50
commit 6ee2f3891b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
57 changed files with 3856 additions and 8899 deletions

View file

@ -0,0 +1,116 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from unittest.mock import patch
class FakeInventory:
def __init__(self):
self.hostvars = {}
self.groups = {}
def add_group(self, name):
self.groups.setdefault(name, {"hosts": [], "children": []})
def add_host(self, host, group=None):
self.hostvars.setdefault(host, {})
if group:
self.add_group(group)
if host not in self.groups[group]["hosts"]:
self.groups[group]["hosts"].append(host)
else:
self.add_group("ungrouped")
if host not in self.groups["ungrouped"]["hosts"]:
self.groups["ungrouped"]["hosts"].append(host)
def set_variable(self, host, var, value):
self.hostvars.setdefault(host, {})
self.hostvars[host][var] = value
def build_containers_json(entries):
return json.dumps(entries).encode("utf-8")
@patch(
"ansible_collections.containers.podman.plugins.inventory.buildah_containers.shutil.which", return_value="buildah"
)
def test_basic_buildah_inventory(mock_which):
from ansible_collections.containers.podman.plugins.inventory.buildah_containers import (
InventoryModule,
)
containers = [
{"name": "w1", "id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "containername": "w1"},
{"containername": "build/with/slash", "containerid": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},
{"id": "cccccccccccccccccccccccccccccccc"}, # no name
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.buildah_containers.subprocess.check_output",
return_value=build_containers_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"connection_plugin": "containers.podman.buildah"}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Names resolved
assert "w1" in inv.hostvars
assert "build/with/slash" in inv.hostvars
# Unnamed container present (either as short id or full id depending on plugin behavior)
unnamed_id = "cccccccccccccccccccccccccccccccc"
assert (unnamed_id in inv.hostvars) or (unnamed_id[:12] in inv.hostvars)
# Hostvars contain id/name
assert inv.hostvars["w1"]["buildah_container_id"].startswith("a")
assert inv.hostvars["w1"]["buildah_container_name"] == "w1"
@patch(
"ansible_collections.containers.podman.plugins.inventory.buildah_containers.shutil.which", return_value="buildah"
)
def test_name_patterns_filtering_buildah(mock_which):
from ansible_collections.containers.podman.plugins.inventory.buildah_containers import (
InventoryModule,
)
containers = [
{"name": "alpha", "id": "id1"},
{"name": "beta", "id": "id2"},
{"name": "gamma", "id": "id3"},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.buildah_containers.subprocess.check_output",
return_value=build_containers_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"name_patterns": ["b*", "id3"], "connection_plugin": "containers.podman.buildah"}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Should include beta by name pattern, and gamma via id pattern
assert set(inv.hostvars.keys()) == {"beta", "gamma"}
def test_verify_inventory_file_helper():
from ansible_collections.containers.podman.plugins.module_utils.inventory.utils import (
verify_inventory_file,
)
class Dummy:
NAME = "containers.podman.buildah_containers"
# wrong extension
assert not verify_inventory_file(Dummy(), "inv.txt")
# missing plugin header
p = "/tmp/test_inv.yml"
with open(p, "w", encoding="utf-8") as f:
f.write("foo: bar\n")
assert not verify_inventory_file(Dummy(), p)
# correct header
with open(p, "w", encoding="utf-8") as f:
f.write("plugin: containers.podman.buildah_containers\n")
assert verify_inventory_file(Dummy(), p)

View file

@ -0,0 +1,458 @@
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from unittest.mock import patch
import pytest
from ansible.errors import AnsibleParserError
class FakeInventory:
def __init__(self):
self.hostvars = {}
self.groups = {}
def add_group(self, name):
self.groups.setdefault(name, {"hosts": [], "children": []})
def add_child(self, parent, child):
self.add_group(parent)
self.add_group(child)
if child not in self.groups[parent]["children"]:
self.groups[parent]["children"].append(child)
def add_host(self, host, group=None):
self.hostvars.setdefault(host, {})
if group:
self.add_group(group)
if host not in self.groups[group]["hosts"]:
self.groups[group]["hosts"].append(host)
else:
self.add_group("ungrouped")
if host not in self.groups["ungrouped"]["hosts"]:
self.groups["ungrouped"]["hosts"].append(host)
def set_variable(self, host, var, value):
self.hostvars.setdefault(host, {})
self.hostvars[host][var] = value
def build_ps_json(entries):
return json.dumps(entries).encode("utf-8")
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_basic_discovery_and_hostvars(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{
"Names": ["app-1"],
"Id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"Image": "docker.io/library/alpine:latest",
"Status": "Up 1 second",
"Labels": {"env": "dev", "role": "api"},
},
{
"Names": ["db/primary"],
"ID": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"ImageName": "quay.io/ns/repo-name:1.0",
"State": "Exited (0) 2 seconds ago",
"Labels": {},
},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
# Feed config directly
with patch.object(
mod,
"_read_config_data",
return_value={
"executable": "podman",
"include_stopped": True,
"connection_plugin": "containers.podman.podman",
"group_by_image": True,
"group_by_label": ["env"],
"verbose_output": True,
},
):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Hosts discovered
assert "app-1" in inv.hostvars
assert "db/primary" in inv.hostvars
# Hostvars set - image, id, status keys
assert inv.hostvars["app-1"]["podman_image"] == "docker.io/library/alpine:latest"
assert inv.hostvars["db/primary"]["podman_image"] == "quay.io/ns/repo-name:1.0"
assert inv.hostvars["app-1"]["podman_container_id"].startswith("a")
assert inv.hostvars["db/primary"]["podman_container_id"].startswith("b")
assert inv.hostvars["app-1"]["podman_status"].lower().startswith("up")
assert inv.hostvars["db/primary"]["podman_status"].lower().startswith("exited")
# Verbose output included
assert "podman_ps" in inv.hostvars["app-1"]
# Image grouping sanitized
assert "image_docker.io_library_alpine_latest" in inv.groups
assert "image_quay.io_ns_repo_name_1.0" in inv.groups
# Label grouping
assert "label_env_dev" in inv.groups
assert "app-1" in inv.groups["label_env_dev"]["hosts"]
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_name_patterns_and_label_selectors(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["one"], "Id": "id1", "Image": "alpine:latest", "Status": "Up", "Labels": {}},
{"Names": ["two"], "Id": "id2", "Image": "alpine:latest", "Status": "Up", "Labels": {"role": "api"}},
{"Names": ["three"], "Id": "id3", "Image": "alpine:latest", "Status": "Up", "Labels": {"role": "db"}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {
"name_patterns": ["t*"],
"label_selectors": {"role": "api"},
}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Only 'two' matches both name pattern and label
assert list(inv.hostvars.keys()) == ["two"]
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_filters_include_exclude_and_status(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["run-a"], "Id": "r1", "Image": "quay.io/ns/a:latest", "Status": "Up", "Labels": {}},
{"Names": ["stop-b"], "Id": "s1", "Image": "quay.io/ns/b:latest", "Status": "Exited (0)", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {
"include_stopped": True,
"filters": {"include": {"image": "quay.io/*"}, "exclude": {"status": "exited*"}},
}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Stopped excluded, running included
assert "run-a" in inv.hostvars
assert "stop-b" not in inv.hostvars
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_keyed_groups_and_parent_group(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["svc"], "Id": "x1", "Image": "img", "Status": "Up", "Labels": {"role": "api"}},
{"Names": ["svc2"], "Id": "x2", "Image": "img", "Status": "Up", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {
"keyed_groups": [
{"key": "labels.role", "prefix": "k", "separator": "-", "parent_group": "keyed"},
{"key": "labels.missing", "prefix": "missing", "default_value": "unknown"},
]
}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
assert "k_api" in inv.groups # sanitized hyphen -> underscore
assert "svc" in inv.groups["k_api"]["hosts"]
assert "keyed" in inv.groups
assert "k_api" in inv.groups["keyed"]["children"]
assert "missing_unknown" in inv.groups
assert set(inv.groups["missing_unknown"]["hosts"]) == {"svc", "svc2"}
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_strict_missing_key_raises(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["h"], "Id": "id", "Image": "img", "Status": "Up", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"strict": True, "keyed_groups": [{"key": "labels.nonexistent"}]}
with patch.object(mod, "_read_config_data", return_value=cfg):
with pytest.raises(AnsibleParserError):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_include_stopped_toggles_args(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
def fake_co_with_a(args, stderr=None):
# ensure -a present when include_stopped true
assert "-a" in args
return build_ps_json([])
def fake_co_without_a(args, stderr=None):
# ensure -a absent when include_stopped false
assert "-a" not in args
return build_ps_json([])
inv = FakeInventory()
mod = InventoryModule()
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
side_effect=fake_co_without_a,
):
with patch.object(mod, "_read_config_data", return_value={"include_stopped": False}):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
inv2 = FakeInventory()
mod2 = InventoryModule()
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
side_effect=fake_co_with_a,
):
with patch.object(mod2, "_read_config_data", return_value={"include_stopped": True}):
mod2.parse(inv2, loader=None, path="dummy.yml", cache=False)
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_debug_paths_and_no_host(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
# One container with no name and no id to hit host==None path
containers = [{}]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"filters": {"include": {"name": "nomatch"}}}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Nothing added
assert inv.hostvars == {}
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_check_output_exception_path(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
side_effect=RuntimeError("boom"),
):
inv = FakeInventory()
mod = InventoryModule()
with patch.object(mod, "_read_config_data", return_value={}):
with pytest.raises(AnsibleParserError):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_filter_include_only_and_label_match(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["svc"], "Id": "x1", "Image": "reg/ns/app:1", "Status": "Up", "Labels": {"tier": "be"}},
{"Names": ["svc2"], "Id": "x2", "Image": "reg/ns/oth:1", "Status": "Up", "Labels": {"tier": "fe"}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"filters": {"include": {"label.tier": "be", "image": "reg/*"}}}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
assert list(inv.hostvars.keys()) == ["svc"]
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_group_by_image_and_label_skip_branches(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
# One without Image to skip image grouping, and one without target label for label grouping
containers = [
{"Names": ["nolbl"], "Id": "y1", "Status": "Up", "Labels": {}},
{"Names": ["haslbl"], "Id": "y2", "Image": "img", "Status": "Up", "Labels": {"other": "x"}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"group_by_image": True, "group_by_label": ["tier"]}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Image "img" should group when present; this asserts grouping executes while label grouping is skipped
assert "image_img" in inv.groups
assert "label_tier_x" not in inv.groups
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_keyed_groups_leading_trailing_separators(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["svc"], "Id": "x1", "Image": "img", "Status": "Up", "Labels": {"num": 7}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {
"keyed_groups": [
{
"key": "labels.num",
"prefix": "p",
"separator": "-",
"leading_separator": True,
"trailing_separator": True,
}
]
}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Expect group name sanitized; verify host assignment in some group containing 'p' and '7'
assert any(("p" in g and "7" in g and "svc" in inv.groups[g]["hosts"]) for g in inv.groups)
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_filters_include_by_id_only(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["first"], "Id": "idaaa", "Image": "img1", "Status": "Up", "Labels": {}},
{"Names": ["second"], "Id": "idbbb", "Image": "img2", "Status": "Up", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"filters": {"include": {"id": "ida*"}}}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
assert list(inv.hostvars.keys()) == ["first"]
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_name_falls_back_to_short_id_when_no_names(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
long_id = "1234567890abcdef1234567890abcdef"
containers = [
{"Id": long_id, "Image": "img", "Status": "Up", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
with patch.object(mod, "_read_config_data", return_value={}):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Host should be short id
assert long_id[:12] in inv.hostvars
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_filters_unknown_key_path(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["x"], "Id": "idx", "Image": "img", "Status": "Up", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"filters": {"include": {"unknown": "val"}}}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
# Include with unknown key should exclude host
assert inv.hostvars == {}
@patch("ansible_collections.containers.podman.plugins.inventory.podman_containers.shutil.which", return_value="podman")
def test_include_rules_status_only(mock_which):
from ansible_collections.containers.podman.plugins.inventory.podman_containers import (
InventoryModule,
)
containers = [
{"Names": ["run"], "Id": "r1", "Image": "img1", "Status": "Up 2s", "Labels": {}},
{"Names": ["stop"], "Id": "s1", "Image": "img2", "Status": "Exited (0)", "Labels": {}},
]
with patch(
"ansible_collections.containers.podman.plugins.inventory.podman_containers.subprocess.check_output",
return_value=build_ps_json(containers),
):
inv = FakeInventory()
mod = InventoryModule()
cfg = {"include_stopped": True, "filters": {"include": {"status": "up*"}}}
with patch.object(mod, "_read_config_data", return_value=cfg):
mod.parse(inv, loader=None, path="dummy.yml", cache=False)
assert list(inv.hostvars.keys()) == ["run"]