diff --git a/molecule/default/roles/helm/tasks/run_test.yml b/molecule/default/roles/helm/tasks/run_test.yml index a67a1e8f..42c54d09 100644 --- a/molecule/default/roles/helm/tasks/run_test.yml +++ b/molecule/default/roles/helm/tasks/run_test.yml @@ -12,6 +12,9 @@ - name: "Install {{ helm_version }}" include_tasks: install.yml +- name: tests_repository + include_tasks: tests_repository.yml + - name: Deploy charts include_tasks: "tests_chart/{{ test_chart_type }}.yml" loop_control: diff --git a/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml b/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml index 73755167..067b216b 100644 --- a/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml +++ b/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml @@ -1,11 +1,19 @@ --- - name: Add chart repo - shell: "helm repo add stable {{ chart_test_repo }}" + helm_repository: + name: test_helm + repo_url: "{{ chart_test_repo }}" - name: Install Chart from repository include_tasks: "../tests_chart.yml" vars: source: repository - chart_source: "stable/{{ chart_test }}" + chart_source: "test_helm/{{ chart_test }}" chart_source_version: "{{ chart_test_version }}" chart_source_version_upgrade: "{{ chart_test_version_upgrade }}" + +- name: Add chart repo + helm_repository: + name: test_helm + repo_url: "{{ chart_test_repo }}" + state: absent diff --git a/molecule/default/roles/helm/tasks/tests_repository.yml b/molecule/default/roles/helm/tasks/tests_repository.yml new file mode 100644 index 00000000..9d274819 --- /dev/null +++ b/molecule/default/roles/helm/tasks/tests_repository.yml @@ -0,0 +1,61 @@ +--- +- name: "Ensure test_helm_repo doesn't exist" + helm_repository: + name: test_helm_repo + state: absent + +- name: Add test_helm_repo chart repository + helm_repository: + name: test_helm_repo + repo_url: "{{ chart_test_repo }}" + register: repository + +- name: Assert that test_helm_repo repository is added + assert: + that: + - repository is changed + +- name: Check idempotency + helm_repository: + name: test_helm_repo + repo_url: "{{ chart_test_repo }}" + register: repository + +- name: Assert idempotency + assert: + that: + - repository is not changed + +- name: Failed to add repository with the same name + helm_repository: + name: test_helm_repo + repo_url: "https://other-charts.url" + register: repository_errors + ignore_errors: yes + +- name: Assert that adding repository with the same name failed + assert: + that: + - repository_errors is failed + +- name: Remove test_helm_repo chart repository + helm_repository: + name: test_helm_repo + state: absent + register: repository + +- name: Assert that test_helm_repo repository is removed + assert: + that: + - repository is changed + +- name: Check idempotency after remove + helm_repository: + name: test_helm_repo + state: absent + register: repository + +- name: Assert idempotency + assert: + that: + - repository is not changed diff --git a/plugins/modules/helm.py b/plugins/modules/helm.py index 7f5b125d..2cdd9672 100644 --- a/plugins/modules/helm.py +++ b/plugins/modules/helm.py @@ -27,10 +27,6 @@ requirements: description: - Install, upgrade, delete packages with the Helm package manager. -notes: - - For chart backed by HTTP basic authentication, you need to run `helm repo add` command - with ` --username` and `--password` before calling the module - options: binary_path: description: @@ -126,7 +122,7 @@ options: ''' EXAMPLES = ''' -- name: Create helm namespace HELM 3 doesn't create it automatically +- name: Create helm namespace as HELM 3 doesn't create it automatically k8s: api_version: v1 kind: Namespace @@ -135,7 +131,9 @@ EXAMPLES = ''' # From repository - name: Add stable chart repo - shell: "helm repo add stable https://kubernetes-charts.storage.googleapis.com" + helm_repository: + name: stable + repo_url: "https://kubernetes-charts.storage.googleapis.com" - name: Deploy latest version of Grafana chart inside monitoring namespace with values helm: @@ -398,7 +396,6 @@ def main(): required_if=[ ('release_state', 'present', ['release_name', 'chart_ref']), ('release_state', 'absent', ['release_name']) - ], supports_check_mode=True, ) diff --git a/plugins/modules/helm_info.py b/plugins/modules/helm_info.py index 07697aa0..5d773c21 100644 --- a/plugins/modules/helm_info.py +++ b/plugins/modules/helm_info.py @@ -13,14 +13,19 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: helm_info + short_description: Get informations from Helm package deployed inside the cluster -description: - - Get informations (values, states, ...) from Helm package deployed inside the cluster + author: - Lucas Boisserie (@LucasBoisserie) + requirements: - "helm (https://github.com/helm/helm/releases)" - "yaml (https://pypi.org/project/PyYAML/)" + +description: + - Get informations (values, states, ...) from Helm package deployed inside the cluster + options: binary_path: description: @@ -36,7 +41,6 @@ options: release_namespace: description: - Kubernetes namespace where the chart should be installed. - - Can't be changed with helm 2. required: true type: str aliases: [ namespace ] diff --git a/plugins/modules/helm_repository.py b/plugins/modules/helm_repository.py new file mode 100644 index 00000000..908c0418 --- /dev/null +++ b/plugins/modules/helm_repository.py @@ -0,0 +1,208 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: helm_repository + +short_description: Add and remove Helm repository + +author: + - Lucas Boisserie (@LucasBoisserie) + +requirements: + - "helm (https://github.com/helm/helm/releases)" + - "yaml (https://pypi.org/project/PyYAML/)" + +description: + - Manage Helm repositories + +options: + binary_path: + description: + - The path of a helm binary to use. + required: false + type: path + repo_name: + description: + - Chart repository name. + required: true + type: str + aliases: [ name ] + repo_url: + description: + - Chart repository url + type: str + aliases: [ url ] + repo_username: + description: + - Chart repository username for repository with basic auth. + - Required if chart_repo_password is specified. + required: false + type: str + aliases: [ username ] + repo_password: + description: + - Chart repository password for repository with basic auth. + - Required if chart_repo_username is specified. + required: false + type: str + aliases: [ password ] + repo_state: + choices: ['present', 'absent'] + description: + - Desirated state of repositoriy. + required: false + default: present + aliases: [ state ] + type: str +''' + +EXAMPLES = ''' +- name: Add default repository + helm_repository: + name: stable + repo_url: https://kubernetes-charts.storage.googleapis.com +''' + +RETURN = ''' # ''' + +import traceback + +try: + import yaml + IMP_YAML = True +except ImportError: + IMP_YAML_ERR = traceback.format_exc() + IMP_YAML = False + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +module = None + + +# Get repository from all repositories added +def get_repository(state, repo_name): + if state is not None: + for repository in state: + if repository['name'] == repo_name: + return repository + return None + + +# Get repository status +def get_repository_status(command, repository_name): + list_command = command + " repo list --output=yaml" + + rc, out, err = module.run_command(list_command) + + # no repo => rc=1 and 'no repositories to show' in output + if rc == 1 and "no repositories to show" in err: + return None + elif rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=list_command + ) + + return get_repository(yaml.safe_load(out), repository_name) + + +# Install repository +def install_repository(command, repository_name, repository_url, repository_username, repository_password): + install_command = command + " repo add " + repository_name + " " + repository_url + + if repository_username is not None and repository_password is not None: + install_command += " --username=" + repository_username + install_command += " --password=" + repository_password + + return install_command + + +# Delete repository +def delete_repository(command, repository_name): + remove_command = command + " repo rm " + repository_name + + return remove_command + + +def main(): + global module + + module = AnsibleModule( + argument_spec=dict( + binary_path=dict(type='path'), + repo_name=dict(type='str', aliases=['name'], required=True), + repo_url=dict(type='str', aliases=['url']), + repo_username=dict(type='str', aliases=['username']), + repo_password=dict(type='str', aliases=['password'], no_log=True), + repo_state=dict(default='present', choices=['present', 'absent'], aliases=['state']), + ), + required_together=[ + ['repo_username', 'repo_password'] + ], + required_if=[ + ('repo_state', 'present', ['repo_url']), + ], + supports_check_mode=True, + ) + + if not IMP_YAML: + module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) + + changed = False + + bin_path = module.params.get('binary_path') + repo_name = module.params.get('repo_name') + repo_url = module.params.get('repo_url') + repo_username = module.params.get('repo_username') + repo_password = module.params.get('repo_password') + repo_state = module.params.get('repo_state') + + if bin_path is not None: + helm_cmd = bin_path + else: + helm_cmd = module.get_bin_path('helm', required=True) + + repository_status = get_repository_status(helm_cmd, repo_name) + + if repo_state == "absent" and repository_status is not None: + helm_cmd = delete_repository(helm_cmd, repo_name) + changed = True + elif repo_state == "present": + if repository_status is None: + helm_cmd = install_repository(helm_cmd, repo_name, repo_url, repo_username, repo_password) + changed = True + elif repository_status['url'] != repo_url: + module.fail_json(msg="Repository already have a repository named {0}".format(repo_name)) + + if module.check_mode: + module.exit_json(changed=changed) + elif not changed: + module.exit_json(changed=False, repo_name=repo_name, repo_url=repo_url) + + rc, out, err = module.run_command(helm_cmd) + + if repo_password is not None: + helm_cmd = helm_cmd.replace(repo_password, '******') + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=helm_cmd + ) + + module.exit_json(changed=changed, stdout=out, stderr=err, command=helm_cmd) + + +if __name__ == '__main__': + main()