From eaa140fe4e1979b8d5164e5e49d4b3b68691209e Mon Sep 17 00:00:00 2001 From: Wissem BEN CHAABANE Date: Tue, 17 Aug 2021 22:25:52 +0200 Subject: [PATCH] Add helm dependency update --- .../roles/helm/files/dep-up/Chart.yaml | 10 ++ .../roles/helm/files/dep-up/values.yaml | 2 + molecule/default/roles/helm/tasks/install.yml | 3 +- .../default/roles/helm/tasks/run_test.yml | 3 + .../default/roles/helm/tasks/test_up_dep.yml | 127 ++++++++++++++++++ .../default/roles/helm/tasks/tests_chart.yml | 7 +- plugins/modules/helm.py | 45 ++++++- tests/unit/modules/test_module_helm.py | 53 ++++++++ 8 files changed, 239 insertions(+), 11 deletions(-) create mode 100644 molecule/default/roles/helm/files/dep-up/Chart.yaml create mode 100644 molecule/default/roles/helm/files/dep-up/values.yaml create mode 100644 molecule/default/roles/helm/tasks/test_up_dep.yml create mode 100644 tests/unit/modules/test_module_helm.py diff --git a/molecule/default/roles/helm/files/dep-up/Chart.yaml b/molecule/default/roles/helm/files/dep-up/Chart.yaml new file mode 100644 index 00000000000..663f0ec8d83 --- /dev/null +++ b/molecule/default/roles/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/molecule/default/roles/helm/files/dep-up/values.yaml b/molecule/default/roles/helm/files/dep-up/values.yaml new file mode 100644 index 00000000000..36072811495 --- /dev/null +++ b/molecule/default/roles/helm/files/dep-up/values.yaml @@ -0,0 +1,2 @@ +chart-test: + myValue: helm update dependency test diff --git a/molecule/default/roles/helm/tasks/install.yml b/molecule/default/roles/helm/tasks/install.yml index 8030aac7054..9b42590dadb 100644 --- a/molecule/default/roles/helm/tasks/install.yml +++ b/molecule/default/roles/helm/tasks/install.yml @@ -6,6 +6,7 @@ - name: Unarchive Helm binary unarchive: - src: 'https://get.helm.sh/{{ helm_archive_name }}' + src: "https://get.helm.sh/{{ helm_archive_name }}" dest: /tmp/helm/ remote_src: yes + validate_certs: no diff --git a/molecule/default/roles/helm/tasks/run_test.yml b/molecule/default/roles/helm/tasks/run_test.yml index 108bd24bd87..0a8aa9743b7 100644 --- a/molecule/default/roles/helm/tasks/run_test.yml +++ b/molecule/default/roles/helm/tasks/run_test.yml @@ -18,6 +18,9 @@ - name: tests_repository include_tasks: tests_repository.yml +- name: test helm dependency update + include_tasks: test_up_dep.yml + - name: Deploy charts include_tasks: "tests_chart/{{ test_chart_type }}.yml" loop_control: diff --git a/molecule/default/roles/helm/tasks/test_up_dep.yml b/molecule/default/roles/helm/tasks/test_up_dep.yml new file mode 100644 index 00000000000..7a08ba18efe --- /dev/null +++ b/molecule/default/roles/helm/tasks/test_up_dep.yml @@ -0,0 +1,127 @@ +- name: copy chart + copy: + src: "{{ item }}" + dest: /tmp + loop: + - test-chart + - dep-up + +- name: "Test chart without dependencies block" + block: + - name: "Test chart without dependencies block" + 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 + register: release + + - debug: var=release + + - name: "Check if the subchart exist in chart" + stat: + path: "/tmp/test-chart/Chart.lock" + register: stat_result + + - assert: + that: + - not stat_result.stat.exists + success_msg: "There is no Subchart pulled" + fail_msg: "subchart exist in the charts directory" + +- name: "Test chart with dependencies block" + block: + - name: "Test chart with dependencies block" + helm: + binary_path: "{{ helm_binary }}" + name: test + chart_ref: "/tmp/dep-up" + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + create_namespace: yes + register: release + + - name: "Check if the subchart exist in chart" + stat: + path: "/tmp/dep-up/Chart.lock" + register: stat_result + + - assert: + that: + - stat_result.stat.exists + success_msg: "subchart exist in the chart directory" + fail_msg: "subchart not exist in the charts directory" + +# Test The update dependency with chart_repo_url +- name: "Test chart without dependencies block and chart_repo_url defined" + block: + - name: "Test chart without dependencies block and chart_repo_url defined" + helm: + binary_path: "{{ helm_binary }}" + name: test + chart_ref: "ingress-nginx" + chart_repo_url: https://kubernetes.github.io/ingress-nginx + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + create_namespace: yes + register: release + + - assert: + that: + - "'--dependency-update' not in release.command" + - "'upgrade' in release.command" + success_msg: "Command does not contains '--dependency-update' options" + fail_msg: "Command contains '--dependency-update' options" + +- name: "Test chart with dependencies block and chart_repo_url defined and replace True" + block: + - name: "Test chart with dependencies block and chart_repo_url defined and replace True" + helm: + binary_path: "{{ helm_binary }}" + name: test1 + chart_ref: "dep_up" + chart_repo_url: http://repo:8080/charts + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + create_namespace: yes + replace: true + register: release + - debug: var=release + - assert: + that: + - "'--dependency-update' in release.command" + - "'install' in release.command" + success_msg: "Command contains '--dependency-update' options with helm install command" + fail_msg: "Command not contains '--dependency-update' with helm install command" + +- name: "Test chart with dependencies block and chart_repo_url defined and replace False fails" + block: + - name: "Test chart with dependencies block and chart_repo_url defined and replace False fails" + helm: + binary_path: "{{ helm_binary }}" + name: test2 + chart_ref: "dep_up" + chart_repo_url: http://repo:8080/charts + chart_version: "{{ chart_source_version | default(omit) }}" + namespace: "{{ helm_namespace }}" + create_namespace: yes + replace: false + register: release + ignore_errors: true + + - assert: + that: + - release.failed + - release.msg == "'--dependency-update' hasn't been supported yet with 'helm upgrade'. Please use 'helm install' instead by adding 'replace' option" + success_msg: "Command build fail when adding '--dependency-update' with the helm upgrade command" + +- name: Remove helm namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ helm_namespace }}" + state: absent + wait: true + wait_timeout: 180 diff --git a/molecule/default/roles/helm/tasks/tests_chart.yml b/molecule/default/roles/helm/tasks/tests_chart.yml index 2199991cf72..b1b4e7f161c 100644 --- a/molecule/default/roles/helm/tasks/tests_chart.yml +++ b/molecule/default/roles/helm/tasks/tests_chart.yml @@ -36,7 +36,7 @@ assert: that: - install_fail is failed - - "'Error: create: failed to create: namespaces \"' + helm_namespace + '\" not found' in install_fail.stderr" + - '''Error: create: failed to create: namespaces "'' + helm_namespace + ''" not found'' in install_fail.stderr' - name: "Install {{ chart_test }} from {{ source }} in check mode" helm: @@ -345,8 +345,7 @@ register: result - assert: - that: - result.stat.exists + that: result.stat.exists - name: Release using non-existent context helm: @@ -364,7 +363,7 @@ assert: that: - result is failed - - "'context \"does-not-exist\" does not exist' in result.stderr" + - '''context "does-not-exist" does not exist'' in result.stderr' always: - name: Clean up temp dir diff --git a/plugins/modules/helm.py b/plugins/modules/helm.py index 67f25111e66..3c784c14322 100644 --- a/plugins/modules/helm.py +++ b/plugins/modules/helm.py @@ -26,6 +26,9 @@ description: - Install, upgrade, delete packages with the Helm package manager. +notes: + - The module perform the helm dependency update if we specify the C(dependencies) block in the I(Chart.yaml/requirements.yaml) file. + options: chart_ref: description: @@ -352,6 +355,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 + """ + repo_dep_update = command + " dependency update " + chart_ref + rc, out, err = run_helm(module, repo_dep_update) + + def fetch_chart_info(module, command, chart_ref): """ Get chart info @@ -364,14 +375,16 @@ def fetch_chart_info(module, command, chart_ref): def deploy(command, release_name, release_values, chart_name, wait, - wait_timeout, disable_hook, force, values_files, history_max, atomic=False, - create_namespace=False, replace=False, skip_crds=False): + wait_timeout, disable_hook, force, values_files, history_max, + dependency_update=None, atomic=False, create_namespace=False, replace=False, skip_crds=False): """ Install/upgrade/rollback release chart """ 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 @@ -588,6 +601,8 @@ def main(): skip_crds = module.params.get('skip_crds') history_max = module.params.get('history_max') + dependency_update = False + if bin_path is not None: helm_cmd_common = bin_path else: @@ -612,16 +627,34 @@ def main(): if chart_version is not None: helm_cmd += " --version=" + chart_version - if chart_repo_url is not None: + if chart_repo_url: helm_cmd += " --repo=" + chart_repo_url # 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 chart_info.get('dependencies'): + # '--update-dependency' not supported with 'helm upgrade' + # Maybe in the near future https://github.com/helm/helm/pull/8810 + if chart_repo_url: + module.warn("This is a not stable feature with 'chart_repo_url'. Please consider to use dependency update with on-disk charts") + if replace: + dependency_update = True + else: + 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: + # 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 + run_dep_update(module, helm_cmd_common, chart_ref) if release_status is None: # Not installed helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout, disable_hook, False, values_files=values_files, atomic=atomic, - create_namespace=create_namespace, replace=replace, + create_namespace=create_namespace, dependency_update=dependency_update, replace=replace, skip_crds=skip_crds, history_max=history_max) changed = True @@ -637,8 +670,8 @@ def main(): if force or would_change: helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout, - disable_hook, force, values_files=values_files, atomic=atomic, - create_namespace=create_namespace, replace=replace, + disable_hook, False, values_files=values_files, atomic=atomic, + create_namespace=create_namespace, dependency_update=dependency_update, replace=replace, skip_crds=skip_crds, history_max=history_max) changed = True diff --git a/tests/unit/modules/test_module_helm.py b/tests/unit/modules/test_module_helm.py new file mode 100644 index 00000000000..e88ff253140 --- /dev/null +++ b/tests/unit/modules/test_module_helm.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2020, 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 +from unittest.mock import MagicMock, patch + +__metaclass__ = type + +from ansible_collections.kubernetes.core.plugins.modules.helm import ( + run_dep_update +) + + +class MockedModule: + def __init__(self): + self.params = { + "api_key": None, + "ca_cert": None, + "host": None, + "kube_context": None, + "kubeconfig": None, + "release_namespace": None, + "validate_certs": None, + } + + self.r = {} + + def run_command(self, command, environ_update=None): + self.r = {"command": command, "environ_update": environ_update} + return 0, "", "" + + +def chart_info(): + return { + 'apiVersion': 'v2', + 'appVersion': 'default', + 'dependencies': [{'name': 'test-chart', 'repository': 'file://../test-chart', 'version': '0.1.0'}], + 'description': 'A Helm chart for molecule test', + 'name': 'dep_up', + 'type': 'application', + 'version': '0.1.0' + } + + +def test_dependency_update_naked(): + module = MockedModule() + module.params = {} + common_cmd = "helm" + chart_ref = "/tmp/path/chart" + run_dep_update(module, common_cmd, chart_ref) + assert module.r["command"] == "helm dependency update /tmp/path/chart" + assert module.r["environ_update"] == {}