From 13791ec7bf0beaa81c6acf6eeee92e3a9d17463e Mon Sep 17 00:00:00 2001 From: Yuriy Novostavskiy Date: Mon, 26 Jan 2026 19:39:07 +0100 Subject: [PATCH] Limit compatibility to Helm =>v3.0.0,<4.0.0 (#1039) SUMMARY Helm v4 is a major version with backward-incompatible changes, including to the flags and output of the Helm CLI and to the SDK. This version is currently not supported in the kubernetes.core. This PR is related to #1038 and is a short-term solution to mark compatibility explicitly ISSUE TYPE Bugfix Pull Request Docs Pull Request COMPONENT NAME helm helm_template helm_info helm_repository helm_pull helm_registry_auth helm_plugin helm_plugin_info ADDITIONAL INFORMATION Added `validate_helm_version()`` method to AnsibleHelmModule that enforces version constraint >=3.0.0,<4.0.0. Fails fast with clear error message: "Helm version must be >=3.0.0,<4.0.0, current version is {version}" Some modules (i.e. helm_registry_auth) technically is compatible with Helm v4, but validation was added to all helm modules. Partially coauthored by GitHub Copilot with Claude Sonnet 4 model. Addresses issue #1038 Reviewed-by: GomathiselviS Reviewed-by: Yuriy Novostavskiy Reviewed-by: Mike Graves Reviewed-by: Alina Buzachis Reviewed-by: Bianca Henderson --- README.md | 4 + .../20251115-limit-versions-of-helm.yaml | 2 + docs/kubernetes.core.helm_pull_module.rst | 4 +- ...ernetes.core.helm_registry_auth_module.rst | 4 +- plugins/module_utils/helm.py | 18 ++ plugins/modules/helm.py | 3 + plugins/modules/helm_info.py | 3 + plugins/modules/helm_plugin.py | 3 + plugins/modules/helm_plugin_info.py | 3 + plugins/modules/helm_pull.py | 11 +- plugins/modules/helm_registry_auth.py | 5 +- plugins/modules/helm_repository.py | 3 + plugins/modules/helm_template.py | 3 + tests/integration/targets/helm/tasks/main.yml | 1 + .../targets/helm/tasks/run_test.yml | 66 +++-- .../targets/helm/tasks/test_helm_version.yml | 47 ++++ tests/unit/module_utils/test_helm.py | 43 +++ .../unit/modules/test_helm_template_module.py | 64 +++-- tests/unit/modules/test_module_helm.py | 254 ++++++++++-------- 19 files changed, 370 insertions(+), 171 deletions(-) create mode 100644 changelogs/fragments/20251115-limit-versions-of-helm.yaml create mode 100644 tests/integration/targets/helm/tasks/test_helm_version.yml diff --git a/README.md b/README.md index 2b6072fa..2f4b6940 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ A collection may contain metadata that identifies these versions. PEP440 is the schema used to describe the versions of Ansible. +### Helm Version Compatibility + +Helm modules in this collection are compatible with Helm v3.x and are not yet compatible with Helm v4. Individual modules and their parameters may support a more specific range of Helm versions. + ### Python Support * Collection supports 3.9+ diff --git a/changelogs/fragments/20251115-limit-versions-of-helm.yaml b/changelogs/fragments/20251115-limit-versions-of-helm.yaml new file mode 100644 index 00000000..0677f823 --- /dev/null +++ b/changelogs/fragments/20251115-limit-versions-of-helm.yaml @@ -0,0 +1,2 @@ +bugfixes: + - Limit supported versions of Helm to <4.0.0 (https://github.com/ansible-collections/kubernetes.core/pull/1039). diff --git a/docs/kubernetes.core.helm_pull_module.rst b/docs/kubernetes.core.helm_pull_module.rst index bab8b6da..c89978e7 100644 --- a/docs/kubernetes.core.helm_pull_module.rst +++ b/docs/kubernetes.core.helm_pull_module.rst @@ -27,7 +27,7 @@ Requirements ------------ The below requirements are needed on the host that executes this module. -- helm >= 3.0 (https://github.com/helm/helm/releases) +- helm >= 3.0, <4.0.0 (https://github.com/helm/helm/releases) Parameters @@ -401,7 +401,7 @@ Examples Return Values ------------- -Common return values are documented `here `_, the following are the fields unique to this module: +Common return values are documented `here `_, the following are the fields unique to this module: .. raw:: html diff --git a/docs/kubernetes.core.helm_registry_auth_module.rst b/docs/kubernetes.core.helm_registry_auth_module.rst index 83a9b217..9f33ea4a 100644 --- a/docs/kubernetes.core.helm_registry_auth_module.rst +++ b/docs/kubernetes.core.helm_registry_auth_module.rst @@ -25,7 +25,7 @@ Requirements ------------ The below requirements are needed on the host that executes this module. -- helm (https://github.com/helm/helm/releases) => 3.8.0 +- helm (https://github.com/helm/helm/releases) >= 3.8.0, <4.0.0 Parameters @@ -215,7 +215,7 @@ Examples Return Values ------------- -Common return values are documented `here `_, the following are the fields unique to this module: +Common return values are documented `here `_, the following are the fields unique to this module: .. raw:: html diff --git a/plugins/module_utils/helm.py b/plugins/module_utils/helm.py index ac8a23c6..3291e45a 100644 --- a/plugins/module_utils/helm.py +++ b/plugins/module_utils/helm.py @@ -202,6 +202,24 @@ class AnsibleHelmModule(object): return m.group(1) return None + def validate_helm_version(self): + """ + Validate that Helm version is >=3.0.0 and <4.0.0. + Helm 4 is not yet supported. + """ + helm_version = self.get_helm_version() + if helm_version is None: + self.fail_json(msg="Unable to determine Helm version") + + if (LooseVersion(helm_version) < LooseVersion("3.0.0")) or ( + LooseVersion(helm_version) >= LooseVersion("4.0.0") + ): + self.fail_json( + msg="Helm version must be >=3.0.0,<4.0.0, current version is {0}".format( + helm_version + ) + ) + def get_values(self, release_name, get_all=False): """ Get Values from deployed release diff --git a/plugins/modules/helm.py b/plugins/modules/helm.py index b4602847..c81bc6a8 100644 --- a/plugins/modules/helm.py +++ b/plugins/modules/helm.py @@ -928,6 +928,9 @@ def main(): if not IMP_YAML: module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + changed = False chart_ref = module.params.get("chart_ref") diff --git a/plugins/modules/helm_info.py b/plugins/modules/helm_info.py index a0b94e32..17d3ebb8 100644 --- a/plugins/modules/helm_info.py +++ b/plugins/modules/helm_info.py @@ -245,6 +245,9 @@ def main(): if not IMP_YAML: module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + release_name = module.params.get("release_name") release_state = module.params.get("release_state") get_all_values = module.params.get("get_all_values") diff --git a/plugins/modules/helm_plugin.py b/plugins/modules/helm_plugin.py index 69323e89..c4d05308 100644 --- a/plugins/modules/helm_plugin.py +++ b/plugins/modules/helm_plugin.py @@ -161,6 +161,9 @@ def main(): mutually_exclusive=mutually_exclusive(), ) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + state = module.params.get("state") helm_cmd_common = module.get_helm_binary() + " plugin" diff --git a/plugins/modules/helm_plugin_info.py b/plugins/modules/helm_plugin_info.py index 2d92e995..026ebfd8 100644 --- a/plugins/modules/helm_plugin_info.py +++ b/plugins/modules/helm_plugin_info.py @@ -98,6 +98,9 @@ def main(): supports_check_mode=True, ) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + plugin_name = module.params.get("plugin_name") plugin_list = [] diff --git a/plugins/modules/helm_pull.py b/plugins/modules/helm_pull.py index ebf414d3..3c374ac6 100644 --- a/plugins/modules/helm_pull.py +++ b/plugins/modules/helm_pull.py @@ -21,7 +21,7 @@ description: - There are options for unpacking the chart after download. requirements: - - "helm >= 3.0 (https://github.com/helm/helm/releases)" + - "helm >= 3.0, <4.0.0 (https://github.com/helm/helm/releases)" options: chart_ref: @@ -220,13 +220,10 @@ def main(): mutually_exclusive=[("chart_version", "chart_devel")], ) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + helm_version = module.get_helm_version() - if LooseVersion(helm_version) < LooseVersion("3.0.0"): - module.fail_json( - msg="This module requires helm >= 3.0.0, current version is {0}".format( - helm_version - ) - ) helm_pull_opt_versionning = dict( skip_tls_certs_check="3.3.0", diff --git a/plugins/modules/helm_registry_auth.py b/plugins/modules/helm_registry_auth.py index fce2abfe..107a9a7c 100644 --- a/plugins/modules/helm_registry_auth.py +++ b/plugins/modules/helm_registry_auth.py @@ -20,7 +20,7 @@ author: - Yuriy Novostavskiy (@yurnov) requirements: - - "helm (https://github.com/helm/helm/releases) => 3.8.0" + - "helm (https://github.com/helm/helm/releases) >= 3.8.0, <4.0.0" description: - Helm registry authentication module allows you to login C(helm registry login) and logout C(helm registry logout) from a Helm registry. @@ -194,6 +194,9 @@ def main(): supports_check_mode=True, ) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + changed = False host = module.params.get("host") diff --git a/plugins/modules/helm_repository.py b/plugins/modules/helm_repository.py index fabb83d4..ce0d1a36 100644 --- a/plugins/modules/helm_repository.py +++ b/plugins/modules/helm_repository.py @@ -295,6 +295,9 @@ def main(): if not IMP_YAML: module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + changed = False repo_name = module.params.get("repo_name") diff --git a/plugins/modules/helm_template.py b/plugins/modules/helm_template.py index b0f0079e..315ca694 100644 --- a/plugins/modules/helm_template.py +++ b/plugins/modules/helm_template.py @@ -347,6 +347,9 @@ def main(): if not IMP_YAML: module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) + # Validate Helm version >=3.0.0,<4.0.0 + module.validate_helm_version() + helm_cmd = module.get_helm_binary() if plain_http: diff --git a/tests/integration/targets/helm/tasks/main.yml b/tests/integration/targets/helm/tasks/main.yml index 4a2c028b..497bf575 100644 --- a/tests/integration/targets/helm/tasks/main.yml +++ b/tests/integration/targets/helm/tasks/main.yml @@ -7,3 +7,4 @@ - "v3.15.4" - "v3.16.0" - "v3.17.0" + - "v4.0.0" diff --git a/tests/integration/targets/helm/tasks/run_test.yml b/tests/integration/targets/helm/tasks/run_test.yml index d500f7e5..01580a3e 100644 --- a/tests/integration/targets/helm/tasks/run_test.yml +++ b/tests/integration/targets/helm/tasks/run_test.yml @@ -13,45 +13,53 @@ include_role: name: install_helm -- name: "Ensure we honor the environment variables" - include_tasks: test_read_envvars.yml +- name: Main helm tests with Helm v3 + when: helm_version != "v4.0.0" + block: -- name: Deploy charts - include_tasks: "tests_chart/{{ test_chart_type }}.yml" - loop_control: - loop_var: test_chart_type - with_items: - - from_local_path - - from_repository - - from_url + - name: "Ensure we honor the environment variables" + include_tasks: test_read_envvars.yml + when: helm_version != "v4.0.0" -- name: test helm upgrade with reuse_values - include_tasks: test_helm_reuse_values.yml + - name: Deploy charts + include_tasks: "tests_chart/{{ test_chart_type }}.yml" + loop_control: + loop_var: test_chart_type + with_items: + - from_local_path + - from_repository + - from_url -- name: test helm upgrade with reset_then_reuse_values - include_tasks: test_helm_reset_then_reuse_values.yml + - name: test helm upgrade with reuse_values + include_tasks: test_helm_reuse_values.yml -- name: test helm dependency update - include_tasks: test_up_dep.yml + - name: test helm upgrade with reset_then_reuse_values + include_tasks: test_helm_reset_then_reuse_values.yml -- name: Test helm uninstall - include_tasks: test_helm_uninstall.yml + - name: test helm dependency update + include_tasks: test_up_dep.yml -- name: Test helm install with chart name containing space - include_tasks: test_helm_with_space_into_chart_name.yml + - name: Test helm uninstall + include_tasks: test_helm_uninstall.yml -# https://github.com/ansible-collections/community.kubernetes/issues/296 -- name: Test Skip CRDS feature in helm chart install - include_tasks: test_crds.yml + - name: Test helm install with chart name containing space + include_tasks: test_helm_with_space_into_chart_name.yml -- name: Test insecure registry flag feature - include_tasks: test_helm_insecure.yml + # https://github.com/ansible-collections/community.kubernetes/issues/296 + - name: Test Skip CRDS feature in helm chart install + include_tasks: test_crds.yml -- name: Test take ownership flag feature - include_tasks: test_helm_take_ownership.yml + - name: Test insecure registry flag feature + include_tasks: test_helm_insecure.yml -- name: Test helm skip_schema_validation - include_tasks: test_skip_schema_validation.yml + - name: Test take ownership flag feature + include_tasks: test_helm_take_ownership.yml + + - name: Test helm skip_schema_validation + include_tasks: test_skip_schema_validation.yml + +- name: Test helm version + include_tasks: test_helm_version.yml - name: Clean helm install file: diff --git a/tests/integration/targets/helm/tasks/test_helm_version.yml b/tests/integration/targets/helm/tasks/test_helm_version.yml new file mode 100644 index 00000000..300927f9 --- /dev/null +++ b/tests/integration/targets/helm/tasks/test_helm_version.yml @@ -0,0 +1,47 @@ +--- +- name: Test helm reuse_values + vars: + helm_namespace: "{{ test_namespace[14] }}" + chart_release_values: + replica: + replicaCount: 3 + master: + count: 1 + kind: Deployment + chart_reuse_values: + replica: + replicaCount: 1 + master: + count: 3 + block: + - name: Initial chart installation + helm: + binary_path: "{{ helm_binary }}" + chart_ref: oci://registry-1.docker.io/bitnamicharts/redis + release_name: test-redis + release_namespace: "{{ helm_namespace }}" + create_namespace: true + release_values: "{{ chart_release_values }}" + register: install + ignore_errors: true + when: helm_version == "v4.0.0" + + - name: Debug install result + debug: + var: install + when: helm_version == "v4.0.0" + + - name: Ensure helm installation was failed for v4.0.0 + assert: + that: + - install is failed + - "'Helm version must be >=3.0.0,<4.0.0' in install.msg" + when: helm_version == "v4.0.0" + + always: + - name: Remove helm namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ helm_namespace }}" + state: absent diff --git a/tests/unit/module_utils/test_helm.py b/tests/unit/module_utils/test_helm.py index ec181b2d..eef323b5 100644 --- a/tests/unit/module_utils/test_helm.py +++ b/tests/unit/module_utils/test_helm.py @@ -443,3 +443,46 @@ def test_module_get_helm_set_values_args(set_values, expected): result = helm_module.get_helm_set_values_args(set_values) assert " ".join(expected) == result + + +@pytest.mark.parametrize( + "helm_version,should_fail", + [ + ("3.0.0", False), + ("3.5.0", False), + ("3.10.3", False), + ("3.15.0", False), + ("3.17.0", False), + ("2.9.0", True), + ("2.17.0", True), + ("4.0.0", True), + ("4.1.0", True), + ("5.0.0", True), + ], +) +def test_module_validate_helm_version(_ansible_helm_module, helm_version, should_fail): + _ansible_helm_module.get_helm_version = MagicMock() + _ansible_helm_module.get_helm_version.return_value = helm_version + + if should_fail: + with pytest.raises(SystemExit): + _ansible_helm_module.validate_helm_version() + _ansible_helm_module.fail_json.assert_called_once() + call_args = _ansible_helm_module.fail_json.call_args + assert "Helm version must be >=3.0.0,<4.0.0" in call_args[1]["msg"] + assert helm_version in call_args[1]["msg"] + else: + _ansible_helm_module.validate_helm_version() + _ansible_helm_module.fail_json.assert_not_called() + + +def test_module_validate_helm_version_none(_ansible_helm_module): + _ansible_helm_module.get_helm_version = MagicMock() + _ansible_helm_module.get_helm_version.return_value = None + + with pytest.raises(SystemExit): + _ansible_helm_module.validate_helm_version() + + _ansible_helm_module.fail_json.assert_called_once_with( + msg="Unable to determine Helm version" + ) diff --git a/tests/unit/modules/test_helm_template_module.py b/tests/unit/modules/test_helm_template_module.py index 499e01d9..79815ac3 100644 --- a/tests/unit/modules/test_helm_template_module.py +++ b/tests/unit/modules/test_helm_template_module.py @@ -43,15 +43,21 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): def test_dependency_update_option_not_defined(self): set_module_args({"chart_ref": "/tmp/path"}) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm_template.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm template /tmp/path", environ_update={}, data=None + # Check the last call was the actual helm template command + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm template /tmp/path" ) assert result.exception.args[0]["command"] == "/usr/bin/helm template /tmp/path" @@ -64,17 +70,21 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): } ) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm_template.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm template test --repo=https://charts.com/test", - environ_update={}, - data=None, + # Check the last call was the actual helm template command + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm template test --repo=https://charts.com/test" ) assert ( result.exception.args[0]["command"] @@ -86,17 +96,21 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): {"chart_ref": "https://charts/example.tgz", "dependency_update": True} ) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm_template.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm template https://charts/example.tgz --dependency-update", - environ_update={}, - data=None, + # Check the last call was the actual helm template command + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm template https://charts/example.tgz --dependency-update" ) assert ( result.exception.args[0]["command"] diff --git a/tests/unit/modules/test_module_helm.py b/tests/unit/modules/test_module_helm.py index c22730e1..b019aafb 100644 --- a/tests/unit/modules/test_module_helm.py +++ b/tests/unit/modules/test_module_helm.py @@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import unittest -from unittest.mock import MagicMock, call, patch +from unittest.mock import MagicMock, patch from ansible.module_utils import basic from ansible_collections.kubernetes.core.plugins.modules import helm @@ -77,18 +77,22 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) helm.run_dep_update = MagicMock() with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() helm.run_dep_update.assert_not_called() - mock_run_command.assert_called_once_with( - "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'" ) assert ( result.exception.args[0]["command"] @@ -108,18 +112,22 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) helm.run_dep_update = MagicMock() with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() helm.run_dep_update.assert_not_called() - mock_run_command.assert_called_once_with( - "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'" ) assert ( result.exception.args[0]["command"] @@ -139,19 +147,23 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = 0, "configuration updated", "" + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with patch.object(basic.AnsibleModule, "warn") as mock_warn: with self.assertRaises(AnsibleExitJson) as result: helm.main() mock_warn.assert_not_called() - mock_run_command.assert_has_calls( - [ - call( - "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, - ) - ] + # Check calls include the actual helm command (after version check) + assert any( + "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'" in str(call) + for call in mock_run_command.call_args_list ) assert ( result.exception.args[0]["command"] @@ -170,23 +182,23 @@ class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with patch.object(basic.AnsibleModule, "warn") as mock_warn: with self.assertRaises(AnsibleExitJson) as result: helm.main() mock_warn.assert_called_once() - mock_run_command.assert_has_calls( - [ - call( - "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, - ) - ] + # Check calls include the actual helm command (after version check) + assert any( + "/usr/bin/helm upgrade -i --reset-values test '/tmp/path'" in str(call) + for call in mock_run_command.call_args_list ) assert ( result.exception.args[0]["command"] @@ -245,17 +257,21 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'" ) assert ( result.exception.args[0]["command"] @@ -275,17 +291,21 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test 'chart1'" ) assert ( result.exception.args[0]["command"] @@ -305,11 +325,15 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleFailJson) as result: helm.main() # mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1', @@ -334,17 +358,21 @@ class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test 'chart1'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test 'chart1'" ) assert ( result.exception.args[0]["command"] @@ -402,17 +430,21 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'" ) assert ( result.exception.args[0]["command"] @@ -431,17 +463,21 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm upgrade -i --reset-values test 'http://repo.example/charts/application.tgz'" ) assert ( result.exception.args[0]["command"] @@ -460,11 +496,15 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_with_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleFailJson) as result: helm.main() # mock_run_command.assert_called_once_with('/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1', @@ -488,17 +528,21 @@ class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase): helm.get_release_status = MagicMock(return_value=None) helm.fetch_chart_info = MagicMock(return_value=self.chart_info_without_dep) with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: - mock_run_command.return_value = ( - 0, - "configuration updated", - "", - ) # successful execution + # Mock responses: first call is helm version, second is the actual command + mock_run_command.side_effect = [ + ( + 0, + 'version.BuildInfo{Version:"v3.10.0", GitCommit:"", GoVersion:"go1.18"}', + "", + ), + (0, "configuration updated", ""), + ] with self.assertRaises(AnsibleExitJson) as result: helm.main() - mock_run_command.assert_called_once_with( - "/usr/bin/helm install --dependency-update --replace test 'http://repo.example/charts/application.tgz'", - environ_update={"HELM_NAMESPACE": "test"}, - data=None, + # Check the last call (actual helm command, after version check) + assert ( + mock_run_command.call_args_list[-1][0][0] + == "/usr/bin/helm install --dependency-update --replace test 'http://repo.example/charts/application.tgz'" ) assert ( result.exception.args[0]["command"]