fix: Ensure compatibility with ansible-core >= 2.19

ansible-core 2.19 changes the way templates are trusted and provides a
new way of patching module args in unit tests.

With this commit the following changes are made to ensure compatibility
with ansible-core >= 2.19:

- Mark inputs to composable as trusted to align with the new template
  trust model.
- Utilize the updated method for patching module arguments in unit tests
  if available.
- Replace direct access to the self._cache attribute with the inventory's
  cache property.

Signed-off-by: Felix Matouschek <fmatouschek@redhat.com>
This commit is contained in:
Felix Matouschek
2025-04-23 11:21:51 +02:00
parent 41ee9470bd
commit 93473cdd47
6 changed files with 85 additions and 26 deletions

View File

@@ -157,6 +157,14 @@ except ImportError as e:
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
# Handle import errors of trust_as_template.
# It is only available on ansible-core >=2.19.
try:
from ansible.template import trust_as_template
except ImportError:
trust_as_template = None
from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import ( from ansible_collections.kubernetes.core.plugins.module_utils.k8s.client import (
get_api_client, get_api_client,
K8SClient, K8SClient,
@@ -445,13 +453,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
results = {} results = {}
if attempt_to_read_cache: if attempt_to_read_cache:
try: try:
results = self._cache[cache_key] results = self.cache[cache_key]
except KeyError: except KeyError:
cache_needs_update = True cache_needs_update = True
if not attempt_to_read_cache or cache_needs_update: if not attempt_to_read_cache or cache_needs_update:
results = self._fetch_objects(get_api_client(**config_data), opts) results = self._fetch_objects(get_api_client(**config_data), opts)
if cache_needs_update: if cache_needs_update:
self._cache[cache_key] = results self.cache[cache_key] = results
self._populate_inventory(results, opts) self._populate_inventory(results, opts)
@@ -883,12 +891,32 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
""" """
hostvars = self.inventory.get_host(hostname).get_vars() hostvars = self.inventory.get_host(hostname).get_vars()
strict = self.get_option("strict") strict = self.get_option("strict")
def trust_compose_groups(data: Dict) -> Dict:
if trust_as_template is not None:
return {k: trust_as_template(v) for k, v in data.items()}
return data
def trust_keyed_groups(data: List) -> List:
if trust_as_template is not None:
return [{**d, "key": trust_as_template(d["key"])} for d in data]
return data
self._set_composite_vars( self._set_composite_vars(
self.get_option("compose"), hostvars, hostname, strict=True trust_compose_groups(self.get_option("compose")),
hostvars,
hostname,
strict=True,
) )
self._add_host_to_composed_groups( self._add_host_to_composed_groups(
self.get_option("groups"), hostvars, hostname, strict=strict trust_compose_groups(self.get_option("groups")),
hostvars,
hostname,
strict=strict,
) )
self._add_host_to_keyed_groups( self._add_host_to_keyed_groups(
self.get_option("keyed_groups"), hostvars, hostname, strict=strict trust_keyed_groups(self.get_option("keyed_groups")),
hostvars,
hostname,
strict=strict,
) )

View File

@@ -17,6 +17,8 @@ from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import (
KubeVirtInventoryException, KubeVirtInventoryException,
) )
from ansible.plugins.inventory import Cacheable
@pytest.mark.parametrize( @pytest.mark.parametrize(
"config_data,expected", "config_data,expected",
@@ -96,9 +98,8 @@ from ansible_collections.kubevirt.core.plugins.inventory.kubevirt import (
], ],
) )
def test_config_data_to_opts(mocker, inventory, config_data, expected): def test_config_data_to_opts(mocker, inventory, config_data, expected):
read_config_data = mocker.patch.object( mocker.patch.object(Cacheable, "cache", new_callable=mocker.PropertyMock)
inventory, "_read_config_data", return_value=config_data mocker.patch.object(inventory, "_read_config_data", return_value=config_data)
)
mocker.patch.object(inventory, "get_cache_key") mocker.patch.object(inventory, "get_cache_key")
mocker.patch.object(inventory, "get_option") mocker.patch.object(inventory, "get_option")
mocker.patch.object(kubevirt, "get_api_client") mocker.patch.object(kubevirt, "get_api_client")
@@ -129,7 +130,9 @@ def test_use_of_cache(
path = "/testpath" path = "/testpath"
config_data = {"host_format": "test-format"} config_data = {"host_format": "test-format"}
mocker.patch.dict(inventory._cache, cache_data) mocker.patch.object(
Cacheable, "cache", new_callable=mocker.PropertyMock, return_value=cache_data
)
read_config_data = mocker.patch.object( read_config_data = mocker.patch.object(
inventory, "_read_config_data", return_value=config_data inventory, "_read_config_data", return_value=config_data
@@ -168,6 +171,7 @@ def test_use_of_cache(
], ],
) )
def test_k8s_client_missing(mocker, inventory, present): def test_k8s_client_missing(mocker, inventory, present):
mocker.patch.object(Cacheable, "cache", new_callable=mocker.PropertyMock)
mocker.patch.object(kubevirt, "HAS_K8S_MODULE_HELPER", present) mocker.patch.object(kubevirt, "HAS_K8S_MODULE_HELPER", present)
mocker.patch.object(kubevirt, "get_api_client") mocker.patch.object(kubevirt, "get_api_client")
mocker.patch.object(inventory, "_read_config_data", return_value={}) mocker.patch.object(inventory, "_read_config_data", return_value={})

View File

@@ -16,14 +16,21 @@ from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock impo
AnsibleExitJson, AnsibleExitJson,
exit_json, exit_json,
fail_json, fail_json,
set_module_args,
) )
# Handle import errors of patch_module_args.
# It is only available on ansible-core >=2.19.
try:
from ansible.module_utils.testing import patch_module_args
except ImportError as e:
from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import (
patch_module_args,
)
def test_module_fails_when_required_args_missing(mocker): def test_module_fails_when_required_args_missing(mocker):
mocker.patch.object(AnsibleModule, "fail_json", fail_json) mocker.patch.object(AnsibleModule, "fail_json", fail_json)
with pytest.raises(AnsibleFailJson): with pytest.raises(AnsibleFailJson), patch_module_args({}):
set_module_args({})
kubevirt_vm.main() kubevirt_vm.main()
@@ -290,8 +297,7 @@ def test_module(mocker, module_params, k8s_module_params, vm_definition, method)
}, },
) )
with pytest.raises(AnsibleExitJson): with pytest.raises(AnsibleExitJson), patch_module_args(module_params):
set_module_args(module_params)
kubevirt_vm.main() kubevirt_vm.main()
perform_action.assert_called_once_with( perform_action.assert_called_once_with(

View File

@@ -23,14 +23,21 @@ from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock impo
AnsibleFailJson, AnsibleFailJson,
exit_json, exit_json,
fail_json, fail_json,
set_module_args,
) )
# Handle import errors of patch_module_args.
# It is only available on ansible-core >=2.19.
try:
from ansible.module_utils.testing import patch_module_args
except ImportError as e:
from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import (
patch_module_args,
)
def test_module_fails_when_required_args_missing(mocker): def test_module_fails_when_required_args_missing(mocker):
mocker.patch.object(AnsibleModule, "fail_json", fail_json) mocker.patch.object(AnsibleModule, "fail_json", fail_json)
with pytest.raises(AnsibleFailJson): with pytest.raises(AnsibleFailJson), patch_module_args({"running": False}):
set_module_args({"running": False})
kubevirt_vm_info.main() kubevirt_vm_info.main()
@@ -96,8 +103,7 @@ def test_module(mocker, module_args, find_args):
}, },
) )
with pytest.raises(AnsibleExitJson): with pytest.raises(AnsibleExitJson), patch_module_args(module_args):
set_module_args(module_args)
kubevirt_vm_info.main() kubevirt_vm_info.main()
find.assert_called_once_with(**find_args) find.assert_called_once_with(**find_args)

View File

@@ -21,9 +21,17 @@ from ansible_collections.kubevirt.core.plugins.module_utils import (
from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import ( from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import (
AnsibleExitJson, AnsibleExitJson,
exit_json, exit_json,
set_module_args,
) )
# Handle import errors of patch_module_args.
# It is only available on ansible-core >=2.19.
try:
from ansible.module_utils.testing import patch_module_args
except ImportError as e:
from ansible_collections.kubevirt.core.tests.unit.utils.ansible_module_mock import (
patch_module_args,
)
FIND_ARGS_DEFAULT = { FIND_ARGS_DEFAULT = {
"kind": "VirtualMachineInstance", "kind": "VirtualMachineInstance",
"api_version": "kubevirt.io/v1", "api_version": "kubevirt.io/v1",
@@ -74,8 +82,7 @@ def test_module(mocker, module_args, find_args):
}, },
) )
with pytest.raises(AnsibleExitJson): with pytest.raises(AnsibleExitJson), patch_module_args(module_args):
set_module_args(module_args)
kubevirt_vmi_info.main() kubevirt_vmi_info.main()
find.assert_called_once_with(**find_args) find.assert_called_once_with(**find_args)

View File

@@ -10,16 +10,24 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import json from contextlib import contextmanager
from json import dumps
from typing import (
Any,
Dict,
)
from unittest import mock
from ansible.module_utils import basic from ansible.module_utils import basic
from ansible.module_utils.common.text.converters import to_bytes from ansible.module_utils.common.text.converters import to_bytes
def set_module_args(args): @contextmanager
def patch_module_args(args: Dict[str, Any] | None = None):
"""prepare arguments so that they will be picked up during module creation""" """prepare arguments so that they will be picked up during module creation"""
args = json.dumps({"ANSIBLE_MODULE_ARGS": args}) args = dumps({"ANSIBLE_MODULE_ARGS": args})
basic._ANSIBLE_ARGS = to_bytes(args) with mock.patch.object(basic, "_ANSIBLE_ARGS", to_bytes(args)):
yield
class AnsibleExitJson(Exception): class AnsibleExitJson(Exception):