diff --git a/changelogs/fragments/208-add-dependency-update.yaml b/changelogs/fragments/208-add-dependency-update.yaml new file mode 100644 index 0000000000..05cc10131e --- /dev/null +++ b/changelogs/fragments/208-add-dependency-update.yaml @@ -0,0 +1,2 @@ +minor_changes: + - helm - add support for helm dependency update (https://github.com/ansible-collections/kubernetes.core/pull/208). diff --git a/docs/kubernetes.core.helm_module.rst b/docs/kubernetes.core.helm_module.rst index a389408a45..77b2c27bf8 100644 --- a/docs/kubernetes.core.helm_module.rst +++ b/docs/kubernetes.core.helm_module.rst @@ -193,6 +193,30 @@ Parameters
Create the release namespace if not present.
+ + +
+ dependency_update + +
+ boolean +
+ + + + + +
Run standelone helm dependency update CHART before the operation.
+
Run inline --dependency-update with helm install command. This feature is not supported yet with the helm upgrade command.
+
So we should consider to use dependency_update options with replace option enabled when specifying chart_repo_url.
+
The dependency_update option require the add of dependencies block in Chart.yaml/requirements.yaml file.
+
For more information please visit https://helm.sh/docs/helm/helm_dependency/
+

aliases: dep_up
+ +
diff --git a/docs/kubernetes.core.helm_template_module.rst b/docs/kubernetes.core.helm_template_module.rst index 9a8ea5c88d..67e53716b7 100644 --- a/docs/kubernetes.core.helm_template_module.rst +++ b/docs/kubernetes.core.helm_template_module.rst @@ -96,6 +96,28 @@ Parameters
Chart version to use. If this is not specified, the latest version is installed.
+ + +
+ dependency_update + +
+ boolean +
+ + + + + +
Run helm dependency update before the operation.
+
The dependency_update option require the add of dependencies block in Chart.yaml/requirements.yaml file.
+
For more information please visit https://helm.sh/docs/helm/helm_dependency/
+

aliases: dep_up
+ +
diff --git a/plugins/modules/helm.py b/plugins/modules/helm.py index 5bc999b9ae..e13bc8468c 100644 --- a/plugins/modules/helm.py +++ b/plugins/modules/helm.py @@ -51,6 +51,17 @@ - Chart version to install. If this is not specified, the latest version is installed. required: false type: str + dependency_update: + description: + - Run standalone C(helm dependency update CHART) before the operation. + - Run inline C(--dependency-update) with C(helm install) command. This feature is not supported yet with the C(helm upgrade) command. + - So we should consider to use I(dependency_update) options with I(replace) option enabled when specifying I(chart_repo_url). + - The I(dependency_update) option require the add of C(dependencies) block in C(Chart.yaml/requirements.yaml) file. + - For more information please visit U(https://helm.sh/docs/helm/helm_dependency/) + default: false + type: bool + aliases: [ dep_up ] + version_added: "2.4.0" release_name: description: - Release name to manage. @@ -322,6 +333,7 @@ sample: helm upgrade ... """ +import re import tempfile import traceback from ansible_collections.kubernetes.core.plugins.module_utils.version import ( @@ -385,6 +397,14 @@ def run_repo_update(module, command): rc, out, err = run_helm(module, repo_update_command) +def run_dep_update(module, command, chart_ref): + """ + Run dependency update + """ + dep_update = command + " dependency update " + chart_ref + rc, out, err = run_helm(module, dep_update) + + def fetch_chart_info(module, command, chart_ref): """ Get chart info @@ -413,6 +433,7 @@ def deploy( post_renderer=None, skip_crds=False, timeout=None, + dependency_update=None, ): """ Install/upgrade/rollback release chart @@ -420,6 +441,8 @@ def deploy( if replace: # '--replace' is not supported by 'upgrade -i' deploy_command = command + " install" + if dependency_update: + deploy_command += " --dependency-update" else: deploy_command = command + " upgrade -i" # install/upgrade @@ -597,6 +620,7 @@ def main(): chart_ref=dict(type="path"), chart_repo_url=dict(type="str"), chart_version=dict(type="str"), + dependency_update=dict(type="bool", default=False, aliases=["dep_up"]), release_name=dict(type="str", required=True, aliases=["name"]), release_namespace=dict(type="str", required=True, aliases=["namespace"]), release_state=dict( @@ -667,6 +691,7 @@ def main(): chart_ref = module.params.get("chart_ref") chart_repo_url = module.params.get("chart_repo_url") chart_version = module.params.get("chart_version") + dependency_update = module.params.get("dependency_update") release_name = module.params.get("release_name") release_state = module.params.get("release_state") release_values = module.params.get("release_values") @@ -729,6 +754,36 @@ def main(): # Fetch chart info to have real version and real name for chart_ref from archive, folder or url chart_info = fetch_chart_info(module, helm_cmd, chart_ref) + if dependency_update: + if chart_info.get("dependencies"): + # Can't use '--dependency-update' with 'helm upgrade' that is the + # default chart install method, so if chart_repo_url is defined + # we can't use the dependency update command. But, in the near future + # we can get rid of this method and use only '--dependency-update' + # option. Please see https://github.com/helm/helm/pull/8810 + if not chart_repo_url and not re.fullmatch( + r"^http[s]*://[\w.:/?&=-]+$", chart_ref + ): + run_dep_update(module, helm_cmd_common, chart_ref) + + # To not add --dependency-update option in the deploy function + dependency_update = False + else: + module.warn( + "This is a not stable feature with 'chart_repo_url'. Please consider to use dependency update with on-disk charts" + ) + if not replace: + msg_fail = ( + "'--dependency-update' hasn't been supported yet with 'helm upgrade'. " + "Please use 'helm install' instead by adding 'replace' option" + ) + module.fail_json(msg=msg_fail) + else: + module.warn( + "There is no dependencies block defined in Chart.yaml. Dependency update will not be performed. " + "Please consider add dependencies block or disable dependency_update to remove this warning." + ) + if release_status is None: # Not installed helm_cmd = deploy( helm_cmd, @@ -744,6 +799,7 @@ def main(): create_namespace=create_namespace, post_renderer=post_renderer, replace=replace, + dependency_update=dependency_update, skip_crds=skip_crds, history_max=history_max, timeout=timeout, @@ -800,6 +856,7 @@ def main(): skip_crds=skip_crds, history_max=history_max, timeout=timeout, + dependency_update=dependency_update, ) changed = True diff --git a/plugins/modules/helm_template.py b/plugins/modules/helm_template.py index 1bdf2dd12b..8c3bbd6582 100644 --- a/plugins/modules/helm_template.py +++ b/plugins/modules/helm_template.py @@ -45,6 +45,15 @@ - Chart version to use. If this is not specified, the latest version is installed. required: false type: str + dependency_update: + description: + - Run helm dependency update before the operation. + - The I(dependency_update) option require the add of C(dependencies) block in C(Chart.yaml/requirements.yaml) file. + - For more information please visit U(https://helm.sh/docs/helm/helm_dependency/) + default: false + type: bool + aliases: [ dep_up ] + version_added: "2.4.0" include_crds: description: - Include custom resource descriptions in rendered templates. @@ -167,6 +176,7 @@ def template( chart_ref, chart_repo_url=None, chart_version=None, + dependency_update=None, output_dir=None, show_only=None, release_values=None, @@ -176,6 +186,9 @@ def template( ): cmd += " template " + chart_ref + if dependency_update: + cmd += " --dependency-update" + if chart_repo_url: cmd += " --repo=" + chart_repo_url @@ -215,6 +228,7 @@ def main(): chart_ref=dict(type="path", required=True), chart_repo_url=dict(type="str"), chart_version=dict(type="str"), + dependency_update=dict(type="bool", default=False, aliases=["dep_up"]), include_crds=dict(type="bool", default=False), output_dir=dict(type="path"), release_namespace=dict(type="str"), @@ -231,6 +245,7 @@ def main(): chart_ref = module.params.get("chart_ref") chart_repo_url = module.params.get("chart_repo_url") chart_version = module.params.get("chart_version") + dependency_update = module.params.get("dependency_update") include_crds = module.params.get("include_crds") output_dir = module.params.get("output_dir") show_only = module.params.get("show_only") @@ -251,6 +266,7 @@ def main(): tmpl_cmd = template( helm_cmd, chart_ref, + dependency_update=dependency_update, chart_repo_url=chart_repo_url, chart_version=chart_version, output_dir=output_dir, diff --git a/tests/integration/targets/helm/defaults/main.yml b/tests/integration/targets/helm/defaults/main.yml index d96176de00..e43d3cbc86 100644 --- a/tests/integration/targets/helm/defaults/main.yml +++ b/tests/integration/targets/helm/defaults/main.yml @@ -25,3 +25,4 @@ test_namespace: - "helm-local-path-001" - "helm-local-path-002" - "helm-local-path-003" + - "helm-dep" diff --git a/tests/integration/targets/helm/files/dep-up/Chart.yaml b/tests/integration/targets/helm/files/dep-up/Chart.yaml new file mode 100644 index 0000000000..663f0ec8d8 --- /dev/null +++ b/tests/integration/targets/helm/files/dep-up/Chart.yaml @@ -0,0 +1,10 @@ +apiVersion: v2 +name: dep_up +description: A Helm chart for molecule test +type: application +version: 0.1.0 +appVersion: "default" +dependencies: + - name: test-chart + repository: file://../test-chart + version: "0.1.0" diff --git a/tests/integration/targets/helm/files/dep-up/values.yaml b/tests/integration/targets/helm/files/dep-up/values.yaml new file mode 100644 index 0000000000..3607281149 --- /dev/null +++ b/tests/integration/targets/helm/files/dep-up/values.yaml @@ -0,0 +1,2 @@ +chart-test: + myValue: helm update dependency test diff --git a/tests/integration/targets/helm/tasks/run_test.yml b/tests/integration/targets/helm/tasks/run_test.yml index 8232efa16c..dbe918de80 100644 --- a/tests/integration/targets/helm/tasks/run_test.yml +++ b/tests/integration/targets/helm/tasks/run_test.yml @@ -27,6 +27,9 @@ - from_repository - from_url +- name: test helm dependency update + include_tasks: test_up_dep.yml + - name: Test helm plugin include_tasks: tests_helm_plugin.yml diff --git a/tests/integration/targets/helm/tasks/test_up_dep.yml b/tests/integration/targets/helm/tasks/test_up_dep.yml new file mode 100644 index 0000000000..6ae49f3f68 --- /dev/null +++ b/tests/integration/targets/helm/tasks/test_up_dep.yml @@ -0,0 +1,171 @@ +# Helm module +- name: "Test dependency update for helm module" + block: + - name: copy chart + copy: + src: "{{ item }}" + dest: /tmp + loop: + - test-chart + - dep-up + + - set_fact: + helm_namespace: "{{ test_namespace[10] }}" + + - name: "Test chart with dependency_update false" + helm: + binary_path: "{{ helm_binary }}" + name: test + chart_ref: "/tmp/test-chart" + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + dependency_update: false + create_namespace: yes + register: release + + - name: "Get stats of the subchart" + stat: + path: "/tmp/test-chart/Chart.lock" + register: stat_result + + - name: "Check if the subchart not exist in chart" + assert: + that: + - not stat_result.stat.exists + success_msg: "subchart not exist in the chart directory" + fail_msg: "subchart exist in the charts directory" + + - name: "Test chart without dependencies block and dependency_update true" + helm: + binary_path: "{{ helm_binary }}" + name: test + chart_ref: "/tmp/test-chart" + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + create_namespace: yes + dependency_update: true + ignore_errors: true + register: release + + - assert: + that: + - release.warnings[0] == "There is no dependencies block defined in Chart.yaml. Dependency update will not be performed. Please consider add dependencies block or disable dependency_update to remove this warning." + success_msg: "warning when there is no dependencies block with dependency_update enabled" + + - name: "Test chart with dependencies block and dependency_update true" + helm: + binary_path: "{{ helm_binary }}" + name: test + chart_ref: "/tmp/dep-up" + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + dependency_update: true + create_namespace: yes + register: release + + - name: "Get stats of the subchart" + stat: + path: "/tmp/dep-up/Chart.lock" + register: stat_result + + - name: "Check if the subchart exists in chart" + assert: + that: + - stat_result.stat.exists + success_msg: "subchart exist in the chart directory" + fail_msg: "subchart not exist in the charts directory" + always: + - name: Remove helm namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ helm_namespace }}" + state: absent + wait: true + wait_timeout: 180 + + - name: "Remove charts" + file: + state: absent + path: "/tmp/{{ item }}" + loop: + - test-chart + - dep-up + +# Helm_template module +- name: "Test dependency update for helm_template module" + block: + - name: copy chart + copy: + src: "{{ item }}" + dest: /tmp + loop: + - test-chart + - dep-up + + - name: Test Helm dependency update true + helm_template: + binary_path: "{{ helm_binary }}" + chart_ref: "/tmp/dep-up" + chart_version: "{{ chart_source_version | default(omit) }}" + dependency_update: true + output_dir: "/tmp" + register: result + + - name: "Get stats of the subchart" + stat: + path: "{{ item }}" + register: stat_result + loop: + - /tmp/dep-up/Chart.lock + - /tmp/dep_up/charts/test-chart/templates/configmap.yaml + + - name: "Check if the subchart exist in chart" + assert: + that: + - stat_result.results[0].stat.exists + - stat_result.results[1].stat.exists + success_msg: "subchart exist in the charts directory" + fail_msg: "There is no Subchart pulled" + + - name: Test Helm subchart not pulled when dependency_update false for helm_template + helm_template: + binary_path: "{{ helm_binary }}" + chart_ref: "/tmp/test-chart" + chart_version: "{{ chart_source_version | default(omit) }}" + dependency_update: false + output_dir: "/tmp" + register: result + + - name: "Get stats of the subchart" + stat: + path: "{{ item }}" + register: stat_result + loop: + - /tmp/test-chart/Chart.lock + - /tmp/test-chart/templates/configmap.yaml + + - name: "Check if the subchart not exist in chart" + assert: + that: + - not stat_result.results[0].stat.exists + - stat_result.results[1].stat.exists + success_msg: "subchart not exist in the charts directory" + fail_msg: "There is no Subchart pulled" + always: + - name: Remove helm namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ helm_namespace }}" + state: absent + wait: true + wait_timeout: 180 + + - name: "Remove charts" + file: + state: absent + path: "/tmp/{{ item }}" + loop: + - test-chart + - dep-up diff --git a/tests/unit/modules/test_helm_template_module.py b/tests/unit/modules/test_helm_template_module.py new file mode 100644 index 0000000000..9fd98e4c8b --- /dev/null +++ b/tests/unit/modules/test_helm_template_module.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from unittest.mock import patch + +from ansible.module_utils import basic +from ansible_collections.kubernetes.core.plugins.modules import helm_template +from ansible_collections.kubernetes.core.tests.unit.utils.ansible_module_mock import ( + AnsibleFailJson, + AnsibleExitJson, + exit_json, + fail_json, + get_bin_path, + set_module_args, +) + + +class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): + def setUp(self): + self.mock_module_helper = patch.multiple( + basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path, + ) + self.mock_module_helper.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + helm_template.main() + + 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 + with self.assertRaises(AnsibleExitJson) as result: + helm_template.main() + mock_run_command.assert_called_once_with( + "/usr/bin/helm template /tmp/path", environ_update={} + ) + assert result.exception.args[0]["command"] == "/usr/bin/helm template /tmp/path" + + def test_dependency_update_option_false(self): + set_module_args( + { + "chart_ref": "test", + "chart_repo_url": "https://charts.com/test", + "dependency_update": False, + } + ) + with patch.object(basic.AnsibleModule, "run_command") as mock_run_command: + mock_run_command.return_value = ( + 0, + "configuration updated", + "", + ) # successful execution + 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={}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm template test --repo=https://charts.com/test" + ) + + def test_dependency_update_option_true(self): + set_module_args( + {"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 + 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={}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm template https://charts/example.tgz --dependency-update" + ) diff --git a/tests/unit/modules/test_module_helm.py b/tests/unit/modules/test_module_helm.py new file mode 100644 index 0000000000..ca61cf3ef4 --- /dev/null +++ b/tests/unit/modules/test_module_helm.py @@ -0,0 +1,497 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import unittest + +from unittest.mock import MagicMock, patch, call + +from ansible.module_utils import basic +from ansible_collections.kubernetes.core.plugins.modules import helm +from ansible_collections.kubernetes.core.tests.unit.utils.ansible_module_mock import ( + AnsibleFailJson, + AnsibleExitJson, + exit_json, + fail_json, + get_bin_path, + set_module_args, +) + + +class TestDependencyUpdateWithoutChartRepoUrlOption(unittest.TestCase): + def setUp(self): + self.mock_module_helper = patch.multiple( + basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path, + ) + self.mock_module_helper.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + + self.chart_info_without_dep = { + "apiVersion": "v2", + "appVersion": "default", + "description": "A chart used in molecule tests", + "name": "test-chart", + "type": "application", + "version": "0.1.0", + } + + self.chart_info_with_dep = { + "apiVersion": "v2", + "appVersion": "default", + "description": "A chart used in molecule tests", + "name": "test-chart", + "type": "application", + "version": "0.1.0", + "dependencies": [ + { + "name": "test", + "version": "0.1.0", + "repository": "file://../test-chart", + } + ], + } + + def test_module_fail_when_required_args_missing(self): + with self.assertRaises(AnsibleFailJson): + set_module_args({}) + helm.main() + + def test_dependency_update_option_not_defined(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "/tmp/path", + } + ) + helm.get_release_status = MagicMock(return_value=None) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm upgrade -i --reset-values test /tmp/path" + ) + + def test_dependency_update_option_false(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "/tmp/path", + "dependency_update": False, + } + ) + helm.get_release_status = MagicMock(return_value=None) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm upgrade -i --reset-values test /tmp/path" + ) + + def test_dependency_update_option_true(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "/tmp/path", + "dependency_update": True, + } + ) + 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", "" + 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"}, + ) + ] + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm upgrade -i --reset-values test /tmp/path" + ) + + def test_dependency_update_option_true_without_dependencies_block(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "/tmp/path", + "dependency_update": True, + } + ) + 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 + 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"}, + ) + ] + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm upgrade -i --reset-values test /tmp/path" + ) + + +class TestDependencyUpdateWithChartRepoUrlOption(unittest.TestCase): + def setUp(self): + self.mock_module_helper = patch.multiple( + basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path, + ) + self.mock_module_helper.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + + self.chart_info_without_dep = { + "apiVersion": "v2", + "appVersion": "default", + "description": "A chart used in molecule tests", + "name": "test-chart", + "type": "application", + "version": "0.1.0", + } + + self.chart_info_with_dep = { + "apiVersion": "v2", + "appVersion": "default", + "description": "A chart used in molecule tests", + "name": "test-chart", + "type": "application", + "version": "0.1.0", + "dependencies": [ + { + "name": "test", + "version": "0.1.0", + "repository": "file://../test-chart", + } + ], + } + + def test_dependency_update_option_not_defined(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "chart1", + "chart_repo_url": "http://repo.example/charts", + } + ) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1" + ) + + def test_dependency_update_option_False(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "chart1", + "chart_repo_url": "http://repo.example/charts", + "dependency_update": False, + } + ) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm --repo=http://repo.example/charts upgrade -i --reset-values test chart1" + ) + + def test_dependency_update_option_True_and_replace_option_disabled(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "chart1", + "chart_repo_url": "http://repo.example/charts", + "dependency_update": True, + } + ) + 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 + 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', + # environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]["msg"] == ( + "'--dependency-update' hasn't been supported yet with 'helm upgrade'. " + "Please use 'helm install' instead by adding 'replace' option" + ) + assert result.exception.args[0]["failed"] + + def test_dependency_update_option_True_and_replace_option_enabled(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "chart1", + "chart_repo_url": "http://repo.example/charts", + "dependency_update": True, + "replace": True, + } + ) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm --repo=http://repo.example/charts install --dependency-update --replace test chart1" + ) + + +class TestDependencyUpdateWithChartRefIsUrl(unittest.TestCase): + def setUp(self): + self.mock_module_helper = patch.multiple( + basic.AnsibleModule, + exit_json=exit_json, + fail_json=fail_json, + get_bin_path=get_bin_path, + ) + self.mock_module_helper.start() + + # Stop the patch after test execution + # like tearDown but executed also when the setup failed + self.addCleanup(self.mock_module_helper.stop) + + self.chart_info_without_dep = { + "apiVersion": "v2", + "appVersion": "default", + "description": "A chart used in molecule tests", + "name": "test-chart", + "type": "application", + "version": "0.1.0", + } + + self.chart_info_with_dep = { + "apiVersion": "v2", + "appVersion": "default", + "description": "A chart used in molecule tests", + "name": "test-chart", + "type": "application", + "version": "0.1.0", + "dependencies": [ + { + "name": "test", + "version": "0.1.0", + "repository": "file://../test-chart", + } + ], + } + + def test_dependency_update_option_not_defined(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "http://repo.example/charts/application.tgz", + } + ) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm upgrade -i --reset-values test http://repo.example/charts/application.tgz" + ) + + def test_dependency_update_option_False(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "http://repo.example/charts/application.tgz", + "dependency_update": False, + } + ) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm upgrade -i --reset-values test http://repo.example/charts/application.tgz" + ) + + def test_dependency_update_option_True_and_replace_option_disabled(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "http://repo.example/charts/application.tgz", + "dependency_update": True, + } + ) + 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 + 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', + # environ_update={'HELM_NAMESPACE': 'test'}) + assert result.exception.args[0]["msg"] == ( + "'--dependency-update' hasn't been supported yet with 'helm upgrade'. " + "Please use 'helm install' instead by adding 'replace' option" + ) + assert result.exception.args[0]["failed"] + + def test_dependency_update_option_True_and_replace_option_enabled(self): + set_module_args( + { + "release_name": "test", + "release_namespace": "test", + "chart_ref": "http://repo.example/charts/application.tgz", + "dependency_update": True, + "replace": True, + } + ) + 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 + 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"}, + ) + assert ( + result.exception.args[0]["command"] + == "/usr/bin/helm install --dependency-update --replace test http://repo.example/charts/application.tgz" + ) diff --git a/tests/unit/utils/ansible_module_mock.py b/tests/unit/utils/ansible_module_mock.py new file mode 100644 index 0000000000..4cb437513d --- /dev/null +++ b/tests/unit/utils/ansible_module_mock.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2021, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# This module maock the AnsibleModule class for more information please visite +# https://docs.ansible.com/ansible/latest/dev_guide/testing_units_modules.html#module-argument-processing + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import json + +from ansible.module_utils import basic +from ansible.module_utils.common.text.converters import to_bytes + + +def set_module_args(args): + """prepare arguments so that they will be picked up during module creation""" + args = json.dumps({"ANSIBLE_MODULE_ARGS": args}) + basic._ANSIBLE_ARGS = to_bytes(args) + + +class AnsibleExitJson(Exception): + """Exception class to be raised by module.exit_json and caught by the test case""" + + pass + + +class AnsibleFailJson(Exception): + """Exception class to be raised by module.fail_json and caught by the test case""" + + pass + + +def exit_json(*args, **kwargs): + """function to patch over exit_json; package return data into an exception""" + if "changed" not in kwargs: + kwargs["changed"] = False + raise AnsibleExitJson(kwargs) + + +def fail_json(*args, **kwargs): + """function to patch over fail_json; package return data into an exception""" + kwargs["failed"] = True + raise AnsibleFailJson(kwargs) + + +def get_bin_path(self, arg, required=False): + """Mock AnsibleModule.get_bin_path""" + if arg.endswith("helm"): + return "/usr/bin/helm" + else: + if required: + fail_json(msg="%r not found !" % arg) + + +# def warn(self,msg): +# return msg