From fe822b3352214d4d148e21f6bbe4af15df112f55 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Mon, 28 Apr 2025 10:26:53 +0200 Subject: [PATCH 1/3] chore(deps): Bump kubernetes.core to >= 5.2.0 In order to make use of new features in kubernetes.core bump its minimum version to 5.2.0. Signed-off-by: Felix Matouschek --- README.md | 2 +- galaxy.yml | 2 +- requirements.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0247c9f..e1e8ad3 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ ansible-galaxy collection install kubevirt-kubevirt.core-*.tar.gz #### Ansible collections -* [kubernetes.core](https://galaxy.ansible.com/ui/repo/published/kubernetes/core)>=3.1.0,<6.0.0 +* [kubernetes.core](https://galaxy.ansible.com/ui/repo/published/kubernetes/core)>=5.2.0,<6.0.0 To install all the dependencies: ```bash diff --git a/galaxy.yml b/galaxy.yml index 7ddface..fb59ed8 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -6,7 +6,7 @@ readme: README.md authors: - KubeVirt Project (kubevirt.io) dependencies: - kubernetes.core: '>=3.1.0,<6.0.0' + kubernetes.core: '>=5.2.0,<6.0.0' description: Lean Ansible bindings for KubeVirt license_file: LICENSE tags: diff --git a/requirements.yml b/requirements.yml index 169acb2..f0e367d 100644 --- a/requirements.yml +++ b/requirements.yml @@ -1,4 +1,4 @@ --- collections: - name: kubernetes.core - version: '>=3.1.0,<6.0.0' + version: '>=5.2.0,<6.0.0' From a9d7fa22aa69b2386eb8cc888cff397b35bd1687 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Mon, 28 Apr 2025 10:27:50 +0200 Subject: [PATCH 2/3] feat(modules): Add hidden_fields argument Add the hidden_fields argument to kubevirt_vm and kubevirt_{vm,vmi}_info which allows to hide and ignore certain fields in the returned definition of a VM or VMI. By default this argument is set to ignore changes to the kubemacpool.io/transaction-timestamp annotation and managed fields, which may change at any time and cause the modules to return a changed status although nothing has changed other than their values. Signed-off-by: Felix Matouschek --- plugins/module_utils/info.py | 1 + plugins/modules/kubevirt_vm.py | 17 ++++++++ plugins/modules/kubevirt_vm_info.py | 17 ++++++++ plugins/modules/kubevirt_vmi_info.py | 20 ++++++++- .../unit/plugins/modules/test_kubevirt_vm.py | 43 +++++++++++++++++++ .../plugins/modules/test_kubevirt_vm_info.py | 20 +++++++++ .../plugins/modules/test_kubevirt_vmi_info.py | 20 +++++++++ 7 files changed, 137 insertions(+), 1 deletion(-) diff --git a/plugins/module_utils/info.py b/plugins/module_utils/info.py index 8663ee9..ecd571e 100644 --- a/plugins/module_utils/info.py +++ b/plugins/module_utils/info.py @@ -43,6 +43,7 @@ def execute_info_module(module, kind, wait_condition): wait=module.params["wait"], wait_sleep=module.params["wait_sleep"], wait_timeout=module.params["wait_timeout"], + hidden_fields=module.params["hidden_fields"], condition=wait_condition, ) module.exit_json(changed=False, **facts) diff --git a/plugins/modules/kubevirt_vm.py b/plugins/modules/kubevirt_vm.py index 112993a..abfa35b 100644 --- a/plugins/modules/kubevirt_vm.py +++ b/plugins/modules/kubevirt_vm.py @@ -158,6 +158,15 @@ options: - If set to O(force=yes), and O(state=present) is set, an existing object will be replaced. type: bool default: no + hidden_fields: + description: + - Hide fields matching this option in the result. + - An example might be O(hidden_fields=[metadata.managedFields]) + or O(hidden_fields=[metadata.annotations[kubemacpool.io/transaction-timestamp]]). + type: list + elements: str + default: ['metadata.annotations[kubemacpool.io/transaction-timestamp]', metadata.managedFields] + version_added: 2.2.0 requirements: - "python >= 3.9" @@ -402,6 +411,14 @@ def arg_spec() -> Dict: }, }, }, + "hidden_fields": { + "type": "list", + "elements": "str", + "default": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], + }, } spec.update(deepcopy(AUTH_ARG_SPEC)) spec.update(deepcopy(COMMON_ARG_SPEC)) diff --git a/plugins/modules/kubevirt_vm_info.py b/plugins/modules/kubevirt_vm_info.py index 62cd27f..fb0c634 100644 --- a/plugins/modules/kubevirt_vm_info.py +++ b/plugins/modules/kubevirt_vm_info.py @@ -74,6 +74,15 @@ options: - Ignored if O(wait) is not set. default: 120 type: int + hidden_fields: + description: + - Hide fields matching this option in the result. + - An example might be O(hidden_fields=[metadata.managedFields]) + or O(hidden_fields=[metadata.annotations[kubemacpool.io/transaction-timestamp]]). + type: list + elements: str + default: ['metadata.annotations[kubemacpool.io/transaction-timestamp]', metadata.managedFields] + version_added: 2.2.0 requirements: - "python >= 3.9" @@ -174,6 +183,14 @@ def arg_spec(): """ spec = { "running": {"type": "bool"}, + "hidden_fields": { + "type": "list", + "elements": "str", + "default": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], + }, } spec.update(deepcopy(INFO_ARG_SPEC)) spec.update(deepcopy(AUTH_ARG_SPEC)) diff --git a/plugins/modules/kubevirt_vmi_info.py b/plugins/modules/kubevirt_vmi_info.py index 3b3d3ca..d857fae 100644 --- a/plugins/modules/kubevirt_vmi_info.py +++ b/plugins/modules/kubevirt_vmi_info.py @@ -66,6 +66,15 @@ options: - Ignored if O(wait) is not set. default: 120 type: int + hidden_fields: + description: + - Hide fields matching this option in the result. + - An example might be O(hidden_fields=[metadata.managedFields]) + or O(hidden_fields=[metadata.annotations[kubemacpool.io/transaction-timestamp]]). + type: list + elements: str + default: ['metadata.annotations[kubemacpool.io/transaction-timestamp]', metadata.managedFields] + version_added: 2.2.0 requirements: - "python >= 3.9" @@ -157,7 +166,16 @@ def arg_spec(): """ arg_spec defines the argument spec of this module. """ - spec = {} + spec = { + "hidden_fields": { + "type": "list", + "elements": "str", + "default": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], + }, + } spec.update(deepcopy(INFO_ARG_SPEC)) spec.update(deepcopy(AUTH_ARG_SPEC)) diff --git a/tests/unit/plugins/modules/test_kubevirt_vm.py b/tests/unit/plugins/modules/test_kubevirt_vm.py index f199295..907d549 100644 --- a/tests/unit/plugins/modules/test_kubevirt_vm.py +++ b/tests/unit/plugins/modules/test_kubevirt_vm.py @@ -210,12 +210,26 @@ MODULE_PARAMS_DELETE = MODULE_PARAMS_DEFAULT | { "wait": True, } +MODULE_PARAMS_HIDDEN_FIELDS = MODULE_PARAMS_DEFAULT | { + "name": "testvm", + "namespace": "default", + "running": False, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.annotations[kubectl.kubernetes.io/last-applied-configuration]", + ], +} + K8S_MODULE_PARAMS_CREATE = MODULE_PARAMS_CREATE | { "generate_name": None, "running": None, "run_strategy": None, "resource_definition": VM_DEFINITION_CREATE, "wait_condition": {"type": "Ready", "status": True}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], } K8S_MODULE_PARAMS_RUNNING = MODULE_PARAMS_RUNNING | { @@ -223,6 +237,10 @@ K8S_MODULE_PARAMS_RUNNING = MODULE_PARAMS_RUNNING | { "run_strategy": None, "resource_definition": VM_DEFINITION_RUNNING, "wait_condition": {"type": "Ready", "status": True}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], } K8S_MODULE_PARAMS_STOPPED = MODULE_PARAMS_STOPPED | { @@ -230,6 +248,10 @@ K8S_MODULE_PARAMS_STOPPED = MODULE_PARAMS_STOPPED | { "run_strategy": None, "resource_definition": VM_DEFINITION_STOPPED, "wait_condition": {"type": "Ready", "status": False, "reason": "VMINotExists"}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], } K8S_MODULE_PARAMS_HALTED = MODULE_PARAMS_HALTED | { @@ -237,6 +259,10 @@ K8S_MODULE_PARAMS_HALTED = MODULE_PARAMS_HALTED | { "running": None, "resource_definition": VM_DEFINITION_HALTED, "wait_condition": {"type": "Ready", "status": False, "reason": "VMINotExists"}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], } K8S_MODULE_PARAMS_DELETE = MODULE_PARAMS_DELETE | { @@ -245,6 +271,17 @@ K8S_MODULE_PARAMS_DELETE = MODULE_PARAMS_DELETE | { "run_strategy": None, "resource_definition": VM_DEFINITION_RUNNING, "wait_condition": {"type": "Ready", "status": True}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], +} + +K8S_MODULE_PARAMS_HIDDEN_FIELDS = MODULE_PARAMS_HIDDEN_FIELDS | { + "generate_name": None, + "run_strategy": None, + "resource_definition": VM_DEFINITION_STOPPED, + "wait_condition": {"type": "Ready", "status": False, "reason": "VMINotExists"}, } @@ -281,6 +318,12 @@ K8S_MODULE_PARAMS_DELETE = MODULE_PARAMS_DELETE | { VM_DEFINITION_RUNNING, "delete", ), + ( + MODULE_PARAMS_HIDDEN_FIELDS, + K8S_MODULE_PARAMS_HIDDEN_FIELDS, + VM_DEFINITION_STOPPED, + "update", + ), ], ) def test_module(mocker, module_params, k8s_module_params, vm_definition, method): diff --git a/tests/unit/plugins/modules/test_kubevirt_vm_info.py b/tests/unit/plugins/modules/test_kubevirt_vm_info.py index 5804b0a..cbd5417 100644 --- a/tests/unit/plugins/modules/test_kubevirt_vm_info.py +++ b/tests/unit/plugins/modules/test_kubevirt_vm_info.py @@ -52,6 +52,10 @@ FIND_ARGS_DEFAULT = { "wait_sleep": 5, "wait_timeout": 120, "condition": {"type": "Ready", "status": True}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], } FIND_ARGS_NAME_NAMESPACE = FIND_ARGS_DEFAULT | { @@ -77,6 +81,13 @@ FIND_ARGS_STOPPED = FIND_ARGS_DEFAULT | { "condition": {"type": "Ready", "status": False, "reason": "VMINotExists"}, } +FIND_ARGS_HIDDEN_FIELDS = FIND_ARGS_DEFAULT | { + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.annotations[kubectl.kubernetes.io/last-applied-configuration]", + ], +} + @pytest.mark.parametrize( "module_args,find_args", @@ -87,6 +98,15 @@ FIND_ARGS_STOPPED = FIND_ARGS_DEFAULT | { ({"field_selectors": "app=test"}, FIND_ARGS_FIELD_SELECTOR), ({"wait": True, "running": True}, FIND_ARGS_RUNNING), ({"wait": True, "running": False}, FIND_ARGS_STOPPED), + ( + { + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.annotations[kubectl.kubernetes.io/last-applied-configuration]", + ] + }, + FIND_ARGS_HIDDEN_FIELDS, + ), ], ) def test_module(mocker, module_args, find_args): diff --git a/tests/unit/plugins/modules/test_kubevirt_vmi_info.py b/tests/unit/plugins/modules/test_kubevirt_vmi_info.py index bc7de6a..772e4eb 100644 --- a/tests/unit/plugins/modules/test_kubevirt_vmi_info.py +++ b/tests/unit/plugins/modules/test_kubevirt_vmi_info.py @@ -43,6 +43,10 @@ FIND_ARGS_DEFAULT = { "wait_sleep": 5, "wait_timeout": 120, "condition": {"type": "Ready", "status": True}, + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.managedFields", + ], } FIND_ARGS_NAME_NAMESPACE = FIND_ARGS_DEFAULT | { @@ -58,6 +62,13 @@ FIND_ARGS_FIELD_SELECTOR = FIND_ARGS_DEFAULT | { "field_selectors": ["app=test"], } +FIND_ARGS_HIDDEN_FIELDS = FIND_ARGS_DEFAULT | { + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.annotations[kubectl.kubernetes.io/last-applied-configuration]", + ], +} + @pytest.mark.parametrize( "module_args,find_args", @@ -66,6 +77,15 @@ FIND_ARGS_FIELD_SELECTOR = FIND_ARGS_DEFAULT | { ({"name": "testvm", "namespace": "default"}, FIND_ARGS_NAME_NAMESPACE), ({"label_selectors": "app=test"}, FIND_ARGS_LABEL_SELECTOR), ({"field_selectors": "app=test"}, FIND_ARGS_FIELD_SELECTOR), + ( + { + "hidden_fields": [ + "metadata.annotations[kubemacpool.io/transaction-timestamp]", + "metadata.annotations[kubectl.kubernetes.io/last-applied-configuration]", + ] + }, + FIND_ARGS_HIDDEN_FIELDS, + ), ], ) def test_module(mocker, module_args, find_args): From 117694ab1e3a9e2a05581db56a08d83ad1fbe943 Mon Sep 17 00:00:00 2001 From: Felix Matouschek Date: Mon, 28 Apr 2025 14:15:01 +0200 Subject: [PATCH 3/3] fix(changed): Fix change detection temporarily Fix the change detection of kubernetes.core temporarily by monkey patching the service.diff_objects function. This fix should be removed once it was merged into kubernetes.core. A dummy _patch_diff_objects function is introduced to satisfy ansible linters. Signed-off-by: Felix Matouschek --- plugins/module_utils/diff.py | 43 ++++++++++++++++++++++++++++ plugins/modules/kubevirt_vm.py | 6 ++++ plugins/modules/kubevirt_vm_info.py | 6 ++++ plugins/modules/kubevirt_vmi_info.py | 6 ++++ 4 files changed, 61 insertions(+) create mode 100644 plugins/module_utils/diff.py diff --git a/plugins/module_utils/diff.py b/plugins/module_utils/diff.py new file mode 100644 index 0000000..6ba4aa1 --- /dev/null +++ b/plugins/module_utils/diff.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2025 Red Hat, Inc. +# Apache License 2.0 (see LICENSE or http://www.apache.org/licenses/LICENSE-2.0) + +from typing import Dict, Tuple, Optional + +from ansible_collections.kubernetes.core.plugins.module_utils.k8s import service + + +# Copied from +# https://github.com/ansible-collections/kubernetes.core/blob/d329e7ee42799ae9d86b54cf2c7dfc8059103504/plugins/module_utils/k8s/service.py#L493 +# Removed this once this fix was merged into kubernetes.core. +def _diff_objects( + existing: Dict, new: Dict, hidden_fields: Optional[list] = None +) -> Tuple[bool, Dict]: + result = {} + diff = service.recursive_diff(existing, new) + if not diff: + return True, result + + result["before"] = service.hide_fields(diff[0], hidden_fields) + result["after"] = service.hide_fields(diff[1], hidden_fields) + + if list(result["after"].keys()) == ["metadata"] and list( + result["before"].keys() + ) == ["metadata"]: + # If only metadata.generation and metadata.resourceVersion changed, ignore it + ignored_keys = set(["generation", "resourceVersion"]) + + if set(result["after"]["metadata"].keys()).issubset(ignored_keys) and set( + result["before"]["metadata"].keys() + ).issubset(ignored_keys): + return True, result + + return False, result + + +service.diff_objects = _diff_objects + + +def _patch_diff_objects(): + """_dummy is required to satisfy the unused import linter and the ansible-doc sanity check.""" + pass diff --git a/plugins/modules/kubevirt_vm.py b/plugins/modules/kubevirt_vm.py index abfa35b..73d0c6f 100644 --- a/plugins/modules/kubevirt_vm.py +++ b/plugins/modules/kubevirt_vm.py @@ -282,6 +282,11 @@ result: type: str """ +# Monkey patch service.diff_objects to temporarily fix the changed logic +from ansible_collections.kubevirt.core.plugins.module_utils.diff import ( + _patch_diff_objects, +) + from copy import deepcopy from typing import Dict @@ -457,4 +462,5 @@ def main() -> None: if __name__ == "__main__": + _patch_diff_objects() main() diff --git a/plugins/modules/kubevirt_vm_info.py b/plugins/modules/kubevirt_vm_info.py index fb0c634..a6d0668 100644 --- a/plugins/modules/kubevirt_vm_info.py +++ b/plugins/modules/kubevirt_vm_info.py @@ -159,6 +159,11 @@ resources: type: dict """ +# Monkey patch service.diff_objects to temporarily fix the changed logic +from ansible_collections.kubevirt.core.plugins.module_utils.diff import ( + _patch_diff_objects, +) + from copy import deepcopy from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( @@ -223,4 +228,5 @@ def main(): if __name__ == "__main__": + _patch_diff_objects() main() diff --git a/plugins/modules/kubevirt_vmi_info.py b/plugins/modules/kubevirt_vmi_info.py index d857fae..1c2acae 100644 --- a/plugins/modules/kubevirt_vmi_info.py +++ b/plugins/modules/kubevirt_vmi_info.py @@ -144,6 +144,11 @@ resources: type: dict """ +# Monkey patch service.diff_objects to temporarily fix the changed logic +from ansible_collections.kubevirt.core.plugins.module_utils.diff import ( + _patch_diff_objects, +) + from copy import deepcopy from ansible_collections.kubernetes.core.plugins.module_utils.ansiblemodule import ( @@ -202,4 +207,5 @@ def main(): if __name__ == "__main__": + _patch_diff_objects() main()