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.
+
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
|