diff --git a/changelogs/fragments/238-helm-add-support-for-helm-uninstall-wait.yaml b/changelogs/fragments/238-helm-add-support-for-helm-uninstall-wait.yaml new file mode 100644 index 00000000..c1e2ddd6 --- /dev/null +++ b/changelogs/fragments/238-helm-add-support-for-helm-uninstall-wait.yaml @@ -0,0 +1,2 @@ +minor_changes: + - helm - add support for wait parameter for helm uninstall command. (https://github.com/ansible-collections/kubernetes/core/issues/33). diff --git a/molecule/default/roles/helm/library/helm_test_version.py b/molecule/default/roles/helm/library/helm_test_version.py new file mode 100644 index 00000000..3068597d --- /dev/null +++ b/molecule/default/roles/helm/library/helm_test_version.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- 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 + + +DOCUMENTATION = r''' +--- +module: helm_test_version +short_description: check helm executable version +author: + - Aubin Bikouo (@abikouo) +requirements: + - "helm (https://github.com/helm/helm/releases)" +description: + - validate version of helm binary is lower than the specified version. +options: + binary_path: + description: + - The path of a helm binary to use. + required: false + type: path + version: + description: + - version to test against helm binary. + type: str + default: 3.7.0 +''' + +EXAMPLES = r''' +- name: validate helm binary version is lower than 3.5.0 + helm_test_version: + binary_path: path/to/helm + version: "3.5.0" +''' + +RETURN = r''' +message: + type: str + description: Text message describing the test result. + returned: always + sample: 'version installed: 3.4.5 is lower than version 3.5.0' +result: + type: bool + description: Test result. + returned: always + sample: 1 +''' + +import re +from distutils.version import LooseVersion + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec=dict( + binary_path=dict(type='path'), + version=dict(type='str', default='3.7.0'), + ), + ) + + bin_path = module.params.get('binary_path') + version = module.params.get('version') + + if bin_path is not None: + helm_cmd_common = bin_path + else: + helm_cmd_common = 'helm' + + helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True) + rc, out, err = module.run_command([helm_cmd_common, "version"]) + if rc != 0: + module.fail_json(msg="helm version failed.", err=err, out=out, rc=rc) + + m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out) + installed_version = m.group(1) + + message = "version installed: %s" % installed_version + if LooseVersion(installed_version) < LooseVersion(version): + message += " is lower than version %s" % version + module.exit_json(changed=False, result=True, message=message) + else: + message += " is greater than version %s" % version + module.exit_json(changed=False, result=False, message=message) + + +if __name__ == '__main__': + main() diff --git a/molecule/default/roles/helm/tasks/run_test.yml b/molecule/default/roles/helm/tasks/run_test.yml index 108bd24b..8232efa1 100644 --- a/molecule/default/roles/helm/tasks/run_test.yml +++ b/molecule/default/roles/helm/tasks/run_test.yml @@ -33,6 +33,9 @@ - name: Test helm diff include_tasks: tests_helm_diff.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 diff --git a/molecule/default/roles/helm/tasks/test_helm_uninstall.yml b/molecule/default/roles/helm/tasks/test_helm_uninstall.yml new file mode 100644 index 00000000..fb869a79 --- /dev/null +++ b/molecule/default/roles/helm/tasks/test_helm_uninstall.yml @@ -0,0 +1,81 @@ +- name: validate helm version lower than 3.7.0 + helm_test_version: + binary_path: "{{ helm_binary }}" + version: "3.7.0" + register: test_version + +- block: + - set_fact: + chart_source: "https://github.com/kubernetes/kube-state-metrics/releases/download/kube-state-metrics-helm-chart-2.13.3/kube-state-metrics-2.13.3.tgz" + chart_name: "test-wait-uninstall" + + - name: Install chart + helm: + binary_path: "{{ helm_binary }}" + name: "{{ chart_name }}" + chart_ref: "{{ chart_source }}" + namespace: "{{ helm_namespace }}" + create_namespace: true + + - name: Delete chart with wait + helm: + state: absent + binary_path: "{{ helm_binary }}" + name: "{{ chart_name }}" + namespace: "{{ helm_namespace }}" + wait: yes + register: uninstall + + - name: assert warning has been raised + assert: + that: + - uninstall.warnings + + - name: Create temp directory + tempfile: + state: directory + suffix: .test + register: _result + + - set_fact: + helm_tmp_dir: "{{ _result.path }}" + + - name: Unarchive Helm binary + unarchive: + src: 'https://get.helm.sh/helm-v3.7.0-linux-amd64.tar.gz' + dest: "{{ helm_tmp_dir }}" + remote_src: yes + + - name: Install chart + helm: + binary_path: "{{ helm_tmp_dir }}/linux-amd64/helm" + name: "{{ chart_name }}" + chart_ref: "{{ chart_source }}" + namespace: "{{ helm_namespace }}" + create_namespace: true + + - name: uninstall chart again using recent version + helm: + state: absent + binary_path: "{{ helm_tmp_dir }}/linux-amd64/helm" + name: "{{ chart_name }}" + namespace: "{{ helm_namespace }}" + wait: yes + register: uninstall + + always: + - name: Delete temp directory + file: + path: "{{ helm_tmp_dir }}" + state: absent + ignore_errors: true + + - name: Remove namespace + k8s: + kind: Namespace + name: "{{ helm_namespace }}" + state: absent + wait: true + wait_timeout: 180 + ignore_errors: true + when: test_version.result diff --git a/plugins/module_utils/helm.py b/plugins/module_utils/helm.py index 533828f2..d0c1d84a 100644 --- a/plugins/module_utils/helm.py +++ b/plugins/module_utils/helm.py @@ -11,6 +11,7 @@ from contextlib import contextmanager import os import tempfile import traceback +import re from ansible.module_utils.basic import missing_required_lib @@ -158,3 +159,14 @@ def parse_helm_plugin_list(module, output=None): ret.append((name, version, description)) return ret + + +def get_helm_version(module, helm_bin): + + helm_version_command = helm_bin + " version" + rc, out, err = module.run_command(helm_version_command) + if rc == 0: + m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out) + if m: + return m.group(1) + return None diff --git a/plugins/modules/helm.py b/plugins/modules/helm.py index 53e0072f..20de0151 100644 --- a/plugins/modules/helm.py +++ b/plugins/modules/helm.py @@ -108,7 +108,10 @@ options: type: bool wait: description: - - Wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. + - When I(release_state) is set to C(present), wait until all Pods, PVCs, Services, + and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. + - When I(release_state) is set to C(absent), will wait until all the resources are deleted before returning. + It will wait for as long as I(wait_timeout). This feature requires helm>=3.7.0. Added in version 2.3.0. default: False type: bool wait_timeout: @@ -304,6 +307,7 @@ command: import tempfile import traceback +from distutils.version import LooseVersion try: import yaml @@ -317,7 +321,8 @@ from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( run_helm, get_values, get_helm_plugin_list, - parse_helm_plugin_list + parse_helm_plugin_list, + get_helm_version, ) @@ -429,7 +434,8 @@ def deploy(command, release_name, release_values, chart_name, wait, return deploy_command -def delete(command, release_name, purge, disable_hook): +def delete(command, release_name, purge, disable_hook, + wait, wait_timeout): """ Delete release chart """ @@ -442,6 +448,12 @@ def delete(command, release_name, purge, disable_hook): if disable_hook: delete_command += " --no-hooks" + if wait: + delete_command += " --wait" + + if wait_timeout is not None: + delete_command += " --timeout " + wait_timeout + delete_command += " " + release_name return delete_command @@ -615,11 +627,19 @@ def main(): # keep helm_cmd_common for get_release_status in module_exit_json helm_cmd = helm_cmd_common + opt_result = {} if release_state == "absent" and release_status is not None: if replace: module.fail_json(msg="replace is not applicable when state is absent") - helm_cmd = delete(helm_cmd, release_name, purge, disable_hook) + if wait: + helm_version = get_helm_version(module, helm_cmd_common) + if LooseVersion(helm_version) < LooseVersion("3.7.0"): + opt_result['warnings'] = [] + opt_result['warnings'].append("helm uninstall support option --wait for helm release >= 3.7.0") + wait = False + + helm_cmd = delete(helm_cmd, release_name, purge, disable_hook, wait, wait_timeout) changed = True elif release_state == "present": @@ -673,6 +693,7 @@ def main(): status=check_status, stdout='', stderr='', + **opt_result, ) elif not changed: module.exit_json( @@ -681,6 +702,7 @@ def main(): stdout='', stderr='', command=helm_cmd, + **opt_result, ) rc, out, err = run_helm(module, helm_cmd) @@ -691,6 +713,7 @@ def main(): stderr=err, status=get_release_status(module, helm_cmd_common, release_name), command=helm_cmd, + **opt_result, ) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index c543b5de..45a04643 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -246,4 +246,7 @@ tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip plugins/module_utils/selector.py future-import-boilerplate!skip plugins/module_utils/selector.py metaclass-boilerplate!skip plugins/lookup/kustomize.py future-import-boilerplate!skip -plugins/lookup/kustomize.py metaclass-boilerplate!skip \ No newline at end of file +plugins/lookup/kustomize.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py future-import-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py shebang \ No newline at end of file diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index c543b5de..45a04643 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -246,4 +246,7 @@ tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip plugins/module_utils/selector.py future-import-boilerplate!skip plugins/module_utils/selector.py metaclass-boilerplate!skip plugins/lookup/kustomize.py future-import-boilerplate!skip -plugins/lookup/kustomize.py metaclass-boilerplate!skip \ No newline at end of file +plugins/lookup/kustomize.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py future-import-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py shebang \ No newline at end of file diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index f9aee54e..64253c03 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -240,4 +240,5 @@ plugins/modules/k8s_cp.py compile-2.7!skip plugins/modules/k8s_cp.py import-2.6!skip plugins/modules/k8s_cp.py import-2.7!skip plugins/module_utils/selector.py future-import-boilerplate!skip -plugins/module_utils/selector.py metaclass-boilerplate!skip \ No newline at end of file +plugins/module_utils/selector.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py shebang \ No newline at end of file diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 56c60dff..ba5ac517 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -240,4 +240,7 @@ tests/unit/module_utils/test_selector.py metaclass-boilerplate!skip plugins/module_utils/selector.py future-import-boilerplate!skip plugins/module_utils/selector.py metaclass-boilerplate!skip plugins/lookup/kustomize.py future-import-boilerplate!skip -plugins/lookup/kustomize.py metaclass-boilerplate!skip \ No newline at end of file +plugins/lookup/kustomize.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py metaclass-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py future-import-boilerplate!skip +molecule/default/roles/helm/library/helm_test_version.py shebang \ No newline at end of file