From 2092d921cd2cc86b8772a3a459e42a0c480665f6 Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Wed, 12 Oct 2022 15:34:19 +0200 Subject: [PATCH] helm - new module to perform helm pull (#410) helm - new module to perform helm pull Depends-On: ansible/ansible-zuul-jobs#1586 SUMMARY #355 new module to manage chart downloading helm pull ISSUE TYPE Feature Pull Request COMPONENT NAME helm_pull Reviewed-by: Mike Graves Reviewed-by: Bikouo Aubin --- plugins/module_utils/helm.py | 10 +- plugins/modules/helm_pull.py | 310 ++++++++++++++++++ tests/integration/targets/helm/aliases | 3 +- .../targets/helm/tasks/run_test.yml | 3 + .../targets/helm/tasks/tests_helm_pull.yml | 232 +++++++++++++ 5 files changed, 553 insertions(+), 5 deletions(-) create mode 100644 plugins/modules/helm_pull.py create mode 100644 tests/integration/targets/helm/tasks/tests_helm_pull.yml diff --git a/plugins/module_utils/helm.py b/plugins/module_utils/helm.py index d63d78ce0b..4e8b9565c0 100644 --- a/plugins/module_utils/helm.py +++ b/plugins/module_utils/helm.py @@ -161,10 +161,12 @@ 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) + m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out) + if m: + return m.group(1) + m = re.match(r'Client: &version.Version{SemVer:"v([0-9\.]*)", ', out) + if m: + return m.group(1) return None diff --git a/plugins/modules/helm_pull.py b/plugins/modules/helm_pull.py new file mode 100644 index 0000000000..5bd3233a2d --- /dev/null +++ b/plugins/modules/helm_pull.py @@ -0,0 +1,310 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, 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_pull +short_description: download a chart from a repository and (optionally) unpack it in local directory. +version_added: "2.4.0" +author: + - Aubin Bikouo (@abikouo) +description: + - Retrieve a package from a package repository, and download it locally. + - It can also be used to perform cryptographic verification of a chart without installing the chart. + - There are options for unpacking the chart after download. + +requirements: + - "helm >= 3.0 (https://github.com/helm/helm/releases)" + +options: + chart_ref: + description: + - chart name on chart repository. + - absolute URL. + required: true + type: str + chart_version: + description: + - Specify a version constraint for the chart version to use. + - This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). + - Mutually exclusive with C(chart_devel). + type: str + verify_chart: + description: + - Verify the package before using it. + default: False + type: bool + verify_chart_keyring: + description: + - location of public keys used for verification. + type: path + provenance: + description: + - Fetch the provenance file, but don't perform verification. + type: bool + default: False + repo_url: + description: + - chart repository url where to locate the requested chart. + type: str + aliases: [ url, chart_repo_url ] + repo_username: + description: + - Chart repository username where to locate the requested chart. + - Required if C(repo_password) is specified. + type: str + aliases: [ username, chart_repo_username ] + repo_password: + description: + - Chart repository password where to locate the requested chart. + - Required if C(repo_username) is specified. + type: str + aliases: [ password, chart_repo_password ] + pass_credentials: + description: + - Pass credentials to all domains. + default: False + type: bool + skip_tls_certs_check: + description: + - Whether or not to check tls certificate for the chart download. + - Requires helm >= 3.3.0. + type: bool + default: False + chart_devel: + description: + - Use development versions, too. Equivalent to version '>0.0.0-0'. + - Mutually exclusive with C(chart_version). + type: bool + untar_chart: + description: + - if set to true, will untar the chart after downloading it. + type: bool + default: False + destination: + description: + - location to write the chart. + type: path + required: True + chart_ca_cert: + description: + - Verify certificates of HTTPS-enabled servers using this CA bundle. + - Requires helm >= 3.1.0. + type: path + chart_ssl_cert_file: + description: + - Identify HTTPS client using this SSL certificate file. + - Requires helm >= 3.1.0. + type: path + chart_ssl_key_file: + description: + - Identify HTTPS client using this SSL key file + - Requires helm >= 3.1.0. + type: path + binary_path: + description: + - The path of a helm binary to use. + required: false + type: path +""" + +EXAMPLES = r""" +- name: Download chart using chart url + kubernetes.core.helm_pull: + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: /path/to/chart + +- name: Download Chart using chart_name and repo_url + kubernetes.core.helm_pull: + chart_ref: redis + repo_url: https://charts.bitnami.com/bitnami + untar_chart: yes + destination: /path/to/chart + +- name: Download Chart (skip tls certificate check) + kubernetes.core.helm_pull: + chart_ref: redis + repo_url: https://charts.bitnami.com/bitnami + untar_chart: yes + destination: /path/to/chart + skip_tls_certs_check: yes + +- name: Download Chart using chart registry credentials + kubernetes.core.helm_pull: + chart_ref: redis + repo_url: https://charts.bitnami.com/bitnami + untar_chart: yes + destination: /path/to/chart + username: myuser + password: mypassword123 +""" + +RETURN = r""" +stdout: + type: str + description: Full `helm pull` command stdout, in case you want to display it or examine the event log + returned: always + sample: '' +stderr: + type: str + description: Full `helm pull` command stderr, in case you want to display it or examine the event log + returned: always + sample: '' +command: + type: str + description: Full `helm pull` command built by this module, in case you want to re-run the command outside the module or debug a problem. + returned: always + sample: helm pull --repo test ... +rc: + type: int + description: Helm pull command return code + returned: always + sample: 1 +""" + +from ansible.module_utils.basic import AnsibleModule +from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( + run_helm, + get_helm_version, +) +from ansible_collections.kubernetes.core.plugins.module_utils.version import ( + LooseVersion, +) + + +def main(): + argspec = dict( + chart_ref=dict(type="str", required=True), + chart_version=dict(type="str"), + verify_chart=dict(type="bool", default=False), + verify_chart_keyring=dict(type="path"), + provenance=dict(type="bool", default=False), + repo_url=dict(type="str", aliases=["url", "chart_repo_url"]), + repo_username=dict(type="str", aliases=["username", "chart_repo_username"]), + repo_password=dict( + type="str", no_log=True, aliases=["password", "chart_repo_password"] + ), + pass_credentials=dict(type="bool", default=False), + skip_tls_certs_check=dict(type="bool", default=False), + chart_devel=dict(type="bool"), + untar_chart=dict(type="bool", default=False), + destination=dict(type="path", required=True), + chart_ca_cert=dict(type="path"), + chart_ssl_cert_file=dict(type="path"), + chart_ssl_key_file=dict(type="path"), + binary_path=dict(type="path"), + ) + module = AnsibleModule( + argument_spec=argspec, + supports_check_mode=True, + required_by=dict( + repo_username=("repo_password"), + repo_password=("repo_username"), + ), + mutually_exclusive=[("chart_version", "chart_devel")], + ) + + bin_path = module.params.get("binary_path") + 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) + + helm_version = get_helm_version(module, helm_cmd_common) + if LooseVersion(helm_version) < LooseVersion("3.0.0"): + module.fail_json( + msg="This module requires helm >= 3.0.0, current version is {0}".format( + helm_version + ) + ) + + helm_pull_opt_versionning = dict( + skip_tls_certs_check="3.3.0", + chart_ca_cert="3.1.0", + chart_ssl_cert_file="3.1.0", + chart_ssl_key_file="3.1.0", + ) + + def test_version_requirement(opt): + req_version = helm_pull_opt_versionning.get(opt) + if req_version and LooseVersion(helm_version) < LooseVersion(req_version): + module.fail_json( + msg="Parameter {0} requires helm >= {1}, current version is {2}".format( + opt, req_version, helm_version + ) + ) + + # Set `helm pull` arguments requiring values + helm_pull_opts = [] + + helm_value_args = dict( + chart_version="version", + verify_chart_keyring="keyring", + repo_url="repo", + repo_username="username", + repo_password="password", + destination="destination", + chart_ca_cert="ca-file", + chart_ssl_cert_file="cert-file", + chart_ssl_key_file="key-file", + ) + + for opt, cmdkey in helm_value_args.items(): + if module.params.get(opt): + test_version_requirement(opt) + helm_pull_opts.append("--{0} {1}".format(cmdkey, module.params.get(opt))) + + # Set `helm pull` arguments flags + helm_flag_args = dict( + verify_chart=dict(key="verify"), + provenance=dict(key="prov"), + pass_credentials=dict(key="pass-credentials"), + skip_tls_certs_check=dict(key="insecure-skip-tls-verify"), + chart_devel=dict(key="devel"), + untar_chart=dict(key="untar"), + ) + + for k, v in helm_flag_args.items(): + if module.params.get(k): + test_version_requirement(k) + helm_pull_opts.append("--{0}".format(v["key"])) + + helm_cmd_common = "{0} pull {1} {2}".format( + helm_cmd_common, module.params.get("chart_ref"), " ".join(helm_pull_opts) + ) + if not module.check_mode: + rc, out, err = run_helm(module, helm_cmd_common, fails_on_error=False) + else: + rc, out, err = (0, "", "") + + if rc == 0: + module.exit_json( + failed=False, + changed=True, + command=helm_cmd_common, + stdout=out, + stderr=err, + rc=rc, + ) + else: + module.fail_json( + msg="Failure when executing Helm command.", + command=helm_cmd_common, + changed=False, + stdout=out, + stderr=err, + rc=rc, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/helm/aliases b/tests/integration/targets/helm/aliases index 7bc4476eac..32f65d5f31 100644 --- a/tests/integration/targets/helm/aliases +++ b/tests/integration/targets/helm/aliases @@ -1,8 +1,9 @@ # slow - 11min slow -time=313 +time=334 helm_info helm_plugin helm_plugin_info helm_repository helm_template +helm_pull diff --git a/tests/integration/targets/helm/tasks/run_test.yml b/tests/integration/targets/helm/tasks/run_test.yml index b91d642aa3..52f723f506 100644 --- a/tests/integration/targets/helm/tasks/run_test.yml +++ b/tests/integration/targets/helm/tasks/run_test.yml @@ -46,6 +46,9 @@ - name: Test in-memory kubeconfig include_tasks: tests_in_memory_kubeconfig.yml +- name: Test helm pull + include_tasks: tests_helm_pull.yml + - name: Clean helm install file: path: "{{ item }}" diff --git a/tests/integration/targets/helm/tasks/tests_helm_pull.yml b/tests/integration/targets/helm/tasks/tests_helm_pull.yml new file mode 100644 index 0000000000..fde934ad64 --- /dev/null +++ b/tests/integration/targets/helm/tasks/tests_helm_pull.yml @@ -0,0 +1,232 @@ +--- +- name: Define helm versions to test + set_fact: + helm_versions: + - 3.8.0 + - 3.1.0 + - 3.0.0 + - 2.3.0 + +- block: + - name: Create temp directory for helm tests + tempfile: + state: directory + register: tmpdir + + - name: Set temp directory fact + set_fact: + temp_dir: "{{ tmpdir.path }}" + + - set_fact: + destination: "{{ temp_dir }}" + + - name: Create Helm directories + file: + state: directory + path: "{{ temp_dir }}/{{ item }}" + with_items: "{{ helm_versions }}" + + - name: Unarchive Helm binary + unarchive: + src: "https://get.helm.sh/helm-v{{ item }}-linux-amd64.tar.gz" + dest: "{{ temp_dir }}/{{ item }}" + remote_src: yes + with_items: "{{ helm_versions }}" + + # Testing helm pull with helm version == 2.3.0 + - block: + - name: Assert that helm pull failed with helm <= 3.0.0 + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + ignore_errors: true + register: _result + + - name: assert that module failed with proper message + assert: + that: + - _result is failed + - _result.msg == "This module requires helm >= 3.0.0, current version is 2.3.0" + + vars: + helm_path: "{{ temp_dir }}/2.3.0/linux-amd64/helm" + + # Testing helm pull with helm version == 3.0.0 + - block: + - name: Download chart using chart_ssl_cert_file + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + chart_ssl_cert_file: ssl_cert_file + ignore_errors: true + check_mode: true + register: _result + + - name: assert that module failed with proper message + assert: + that: + - _result is failed + - _result.msg == "Parameter chart_ssl_cert_file requires helm >= 3.1.0, current version is 3.0.0" + + - name: Download chart using chart_ssl_key_file + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + chart_ssl_key_file: ssl_key_file + ignore_errors: true + check_mode: true + register: _result + + - name: assert that module failed with proper message + assert: + that: + - _result is failed + - _result.msg == "Parameter chart_ssl_key_file requires helm >= 3.1.0, current version is 3.0.0" + + - name: Download chart using chart_ca_cert + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + chart_ca_cert: ca_cert_file + ignore_errors: true + check_mode: true + register: _result + + - name: assert that module failed with proper message + assert: + that: + - _result is failed + - _result.msg == "Parameter chart_ca_cert requires helm >= 3.1.0, current version is 3.0.0" + + vars: + helm_path: "{{ temp_dir }}/3.0.0/linux-amd64/helm" + + # Testing helm pull with helm version == 3.1.0 + - block: + - name: Download chart using chart_ssl_cert_file, chart_ca_cert, chart_ssl_key_file + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + chart_ssl_cert_file: ssl_cert_file + chart_ssl_key_file: ssl_key_file + chart_ca_cert: ca_cert_file + check_mode: true + register: _result + + - name: assert that module failed with proper message + assert: + that: + - _result is changed + - '"--ca-file ca_cert_file" in _result.command' + - '"--cert-file ssl_cert_file" in _result.command' + - '"--key-file ssl_key_file" in _result.command' + + - name: Download chart using skip_tls_certs_check + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + skip_tls_certs_check: true + ignore_errors: true + check_mode: true + register: _result + + - name: assert that module failed with proper message + assert: + that: + - _result is failed + - _result.msg == "Parameter skip_tls_certs_check requires helm >= 3.3.0, current version is 3.1.0" + + vars: + helm_path: "{{ temp_dir }}/3.1.0/linux-amd64/helm" + + # Testing helm pull with helm version == 3.8.0 + - block: + # Test options chart_version, verify, pass-credentials, provenance, untar_chart + # skip_tls_certs_check, repo_url, repo_username, repo_password + - name: Testing chart version + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: redis + destination: "{{ destination }}" + chart_version: "0.2.1" + verify_chart: true + pass_credentials: true + provenance: true + untar_chart: true + skip_tls_certs_check: true + repo_url: "https://charts.bitnami.com/bitnami" + repo_username: ansible + repo_password: testing123 + verify_chart_keyring: pubring.gpg + check_mode: true + register: _result + + - assert: + that: + - _result is changed + - '"--version 0.2.1" in _result.command' + - '"--verify" in _result.command' + - '"--pass-credentials" in _result.command' + - '"--prov" in _result.command' + - '"--untar" in _result.command' + - '"--insecure-skip-tls-verify" in _result.command' + - '"--repo https://charts.bitnami.com/bitnami" in _result.command' + - '"--username ansible" in _result.command' + - '"--password ***" in _result.command' + - '"--keyring pubring.gpg" in _result.command' + + - name: Download chart using chart_ref + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz + destination: "{{ destination }}" + register: _result + + - name: Check chart on local filesystem + stat: + path: "{{ destination }}/grafana-5.6.0.tgz" + register: _chart + + - name: Validate that chart was downloaded + assert: + that: + - _result is changed + - _chart.stat.exists + - _chart.stat.isreg + + - name: Download chart using untar_chart + kubernetes.core.helm_pull: + binary_path: "{{ helm_path }}" + chart_ref: redis + destination: "{{ destination }}" + repo_url: "https://charts.bitnami.com/bitnami" + untar_chart: true + register: _result + + - name: Check chart on local filesystem + stat: + path: "{{ destination }}/redis" + register: _chart + + - name: Validate that chart was downloaded + assert: + that: + - _result is changed + - _chart.stat.exists + - _chart.stat.isdir + + vars: + helm_path: "{{ temp_dir }}/3.8.0/linux-amd64/helm" + + + always: + - name: Delete temp directory + file: + state: absent + path: "{{ temp_dir }}"