From 720326ebb4444a4d960544116e457e71ae74afa9 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Tue, 17 Mar 2020 16:16:58 +0100 Subject: [PATCH 1/7] Add helm_cli & helm_cli_info --- README.md | 2 + molecule/default/converge.yml | 4 +- molecule/default/roles/helm/defaults/main.yml | 15 + molecule/default/roles/helm/tasks/install.yml | 11 + molecule/default/roles/helm/tasks/main.yml | 8 + .../default/roles/helm/tasks/run_test.yml | 43 ++ .../helm/tasks/test_helm_not_installed.yml | 14 + .../roles/helm/tasks/test_missing_tiller.yml | 15 + .../default/roles/helm/tasks/tests_chart.yml | 185 ++++++ .../tasks/tests_chart/from_local_path.yml | 27 + .../tasks/tests_chart/from_repository.yml | 11 + .../roles/helm/tasks/tests_chart/from_url.yml | 7 + molecule/default/roles/helm/tasks/tiller.yml | 22 + .../templates/tiller-clusterRoleBinding.yml | 13 + .../roles/helm/templates/tiller-sa.yml | 6 + plugins/modules/helm_cli.py | 594 ++++++++++++++++++ plugins/modules/helm_cli_info.py | 290 +++++++++ 17 files changed, 1266 insertions(+), 1 deletion(-) create mode 100644 molecule/default/roles/helm/defaults/main.yml create mode 100644 molecule/default/roles/helm/tasks/install.yml create mode 100644 molecule/default/roles/helm/tasks/main.yml create mode 100644 molecule/default/roles/helm/tasks/run_test.yml create mode 100644 molecule/default/roles/helm/tasks/test_helm_not_installed.yml create mode 100644 molecule/default/roles/helm/tasks/test_missing_tiller.yml create mode 100644 molecule/default/roles/helm/tasks/tests_chart.yml create mode 100644 molecule/default/roles/helm/tasks/tests_chart/from_local_path.yml create mode 100644 molecule/default/roles/helm/tasks/tests_chart/from_repository.yml create mode 100644 molecule/default/roles/helm/tasks/tests_chart/from_url.yml create mode 100644 molecule/default/roles/helm/tasks/tiller.yml create mode 100644 molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml create mode 100644 molecule/default/roles/helm/templates/tiller-sa.yml create mode 100644 plugins/modules/helm_cli.py create mode 100644 plugins/modules/helm_cli_info.py diff --git a/README.md b/README.md index 965e98ef..2e5cd645 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Click on the name of a plugin or module to view that content's documentation: - [k8s_info](https://docs.ansible.com/ansible/latest/modules/k8s_info_module.html) - [k8s_scale](https://docs.ansible.com/ansible/latest/modules/k8s_scale_module.html) - [k8s_service](https://docs.ansible.com/ansible/latest/modules/k8s_service_module.html) + - [helm_cli](https://docs.ansible.com/ansible/latest/modules/helm_cli_module.html) + - [helm_cli_info](https://docs.ansible.com/ansible/latest/modules/helm_cli_info_module.html) ## Installation and Usage diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index b4402410..ebac0811 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -2,7 +2,6 @@ - name: Converge hosts: localhost connection: local - gather_facts: false collections: - community.kubernetes @@ -27,3 +26,6 @@ - include_tasks: tasks/full.yml - include_tasks: tasks/exec.yml - include_tasks: tasks/log.yml + + roles: + - helm diff --git a/molecule/default/roles/helm/defaults/main.yml b/molecule/default/roles/helm/defaults/main.yml new file mode 100644 index 00000000..b5a2a31f --- /dev/null +++ b/molecule/default/roles/helm/defaults/main.yml @@ -0,0 +1,15 @@ +--- +helm_archive_name: "helm-{{ helm_version }}-{{ ansible_system | lower }}-amd64.tar.gz" +helm_binary: "/tmp/helm/{{ ansible_system | lower }}-amd64/helm" +helm_namespace: helm + +tiller_namespace: tiller +tiller_cluster_role: cluster-admin + +chart_test: "nginx-ingress" +chart_test_version: 1.32.0 +chart_test_version_upgrade: 1.33.0 +chart_test_repo: "https://kubernetes-charts.storage.googleapis.com" +chart_test_git_repo: "http://github.com/helm/charts.git" +chart_test_values: + revisionHistoryLimit: 0 diff --git a/molecule/default/roles/helm/tasks/install.yml b/molecule/default/roles/helm/tasks/install.yml new file mode 100644 index 00000000..8030aac7 --- /dev/null +++ b/molecule/default/roles/helm/tasks/install.yml @@ -0,0 +1,11 @@ +--- +- name: Init Helm folders + file: + path: /tmp/helm/ + state: directory + +- name: Unarchive Helm binary + unarchive: + src: 'https://get.helm.sh/{{ helm_archive_name }}' + dest: /tmp/helm/ + remote_src: yes diff --git a/molecule/default/roles/helm/tasks/main.yml b/molecule/default/roles/helm/tasks/main.yml new file mode 100644 index 00000000..13c936a2 --- /dev/null +++ b/molecule/default/roles/helm/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Run tests + include_tasks: run_test.yml + loop_control: + loop_var: helm_version + with_items: + - "v2.16.0" + - "v3.1.2" diff --git a/molecule/default/roles/helm/tasks/run_test.yml b/molecule/default/roles/helm/tasks/run_test.yml new file mode 100644 index 00000000..179663b0 --- /dev/null +++ b/molecule/default/roles/helm/tasks/run_test.yml @@ -0,0 +1,43 @@ +--- +- name: Ensure helm is not install + file: + path: "{{ item }}" + state: absent + with_items: + - "/tmp/helm" + +- name: Check failed if helm is not install + include_tasks: test_helm_not_installed.yml + +- name: "Install {{ helm_version }}" + include_tasks: install.yml + +- name: Check if failed when tiller is not installed + include_tasks: test_missing_tiller.yml + when: "helm_version.startswith('v2')" + +- name: Install tiller + include_tasks: tiller.yml + when: "helm_version.startswith('v2')" + +- name: Deploy charts + include_tasks: "tests_chart/{{ test_chart_type }}.yml" + loop_control: + loop_var: test_chart_type + with_items: + - from_local_path + - from_repository + - from_url + +- name: Clean tiller + include_tasks: tiller.yml + vars: + status: absent + when: "helm_version.startswith('v2')" + +- name: Clean helm install + file: + path: "{{ item }}" + state: absent + with_items: + - "/tmp/helm/" diff --git a/molecule/default/roles/helm/tasks/test_helm_not_installed.yml b/molecule/default/roles/helm/tasks/test_helm_not_installed.yml new file mode 100644 index 00000000..6cda87d7 --- /dev/null +++ b/molecule/default/roles/helm/tasks/test_helm_not_installed.yml @@ -0,0 +1,14 @@ +--- +- name: Failed test when helm is not install + helm_cli: + binary_path: "{{ helm_binary}}_fake" + name: test + chart_ref: "{{ chart_test }}" + ignore_errors: yes + register: helm_missing_binary + +- name: Assert that helm is not installed + assert: + that: + - helm_missing_binary is failed + - "'No such file or directory' in helm_missing_binary.msg" diff --git a/molecule/default/roles/helm/tasks/test_missing_tiller.yml b/molecule/default/roles/helm/tasks/test_missing_tiller.yml new file mode 100644 index 00000000..ce1e6b85 --- /dev/null +++ b/molecule/default/roles/helm/tasks/test_missing_tiller.yml @@ -0,0 +1,15 @@ +--- +- name: Failed test when tiller is missing + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_test }}" + tiller_namespace: helm + ignore_errors: yes + register: missing_tiller + +- name: Assert that tiller is missing + assert: + that: + - missing_tiller is failed + - "'could not find tiller' in missing_tiller.msg" diff --git a/molecule/default/roles/helm/tasks/tests_chart.yml b/molecule/default/roles/helm/tasks/tests_chart.yml new file mode 100644 index 00000000..e196b3f0 --- /dev/null +++ b/molecule/default/roles/helm/tasks/tests_chart.yml @@ -0,0 +1,185 @@ +--- +- name: Create helm namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ helm_namespace }}" + wait: true + +- name: "Install {{ chart_test }} from {{ source }}" + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: "Assert that {{ chart_test }} chart is installed from {{ source }}" + assert: + that: + - install is changed + - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - install.status.status | lower == 'deployed' + +- name: Check idempotency + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: Assert idempotency + assert: + that: + - install is not changed + - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - install.status.status | lower == 'deployed' + +- name: "Add vars to {{ chart_test }} from {{ source }}" + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + values: "{{ chart_test_values }}" + register: install + +- name: "Assert that {{ chart_test }} chart is upgraded with new var from {{ source }}" + assert: + that: + - install is changed + - install.status.status | lower == 'deployed' + - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - "install.status['values'].revisionHistoryLimit == 0" + +- name: Check idempotency after add vars + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + values: "{{ chart_test_values }}" + register: install + +- name: Assert idempotency after add vars + assert: + that: + - install is not changed + - install.status.status | lower == 'deployed' + - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - "install.status['values'].revisionHistoryLimit == 0" + +- name: "Remove Vars to {{ chart_test }} from {{ source }}" + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: "Assert that {{ chart_test }} chart is upgraded with new var from {{ source }}" + assert: + that: + - install is changed + - install.status.status | lower == 'deployed' + - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - install.status['values'] == {} + +- name: Check idempotency after remove vars + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source }}" + chart_version: "{{ chart_source_version | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: Assert idempotency after remove vars + assert: + that: + - install is not changed + - install.status.status | lower == 'deployed' + - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - install.status['values'] == {} + +- name: "Upgrade {{ chart_test }} from {{ source }}" + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source_upgrade | default(chart_source) }}" + chart_version: "{{ chart_source_version_upgrade | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: "Assert that {{ chart_test }} chart is upgraded with new version from {{ source }}" + assert: + that: + - install is changed + - install.status.status | lower == 'deployed' + - install.status.chart == "{{ chart_test }}-{{ chart_test_version_upgrade }}" + +- name: Check idempotency after upgrade + helm_cli: + binary_path: "{{ helm_binary}}" + name: test + chart_ref: "{{ chart_source_upgrade | default(chart_source) }}" + chart_version: "{{ chart_source_version_upgrade | default(omit)}}" + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: Assert idempotency after upgrade + assert: + that: + - install is not changed + - install.status.status | lower == 'deployed' + - install.status.chart == "{{ chart_test }}-{{ chart_test_version_upgrade }}" + +- name: "Remove {{ chart_test }} from {{ source }}" + helm_cli: + binary_path: "{{ helm_binary}}" + state: absent + name: test + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: "Assert that {{ chart_test }} chart is remove from {{ source }}" + assert: + that: + - install is changed + +- name: Check idempotency after remove + helm_cli: + binary_path: "{{ helm_binary}}" + state: absent + name: test + namespace: "{{ helm_namespace }}" + tiller_namespace: "{{ tiller_namespace }}" + register: install + +- name: Assert idempotency + assert: + that: + - install is not changed + +- name: Remove helm namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ helm_namespace }}" + state: absent + wait: true diff --git a/molecule/default/roles/helm/tasks/tests_chart/from_local_path.yml b/molecule/default/roles/helm/tasks/tests_chart/from_local_path.yml new file mode 100644 index 00000000..8b874718 --- /dev/null +++ b/molecule/default/roles/helm/tasks/tests_chart/from_local_path.yml @@ -0,0 +1,27 @@ +--- +- name: Git clone stable repo + git: + repo: "{{ chart_test_git_repo }}" + dest: /tmp/helm_test_repo + version: 631eb8413f6728962439488f48d7d6fbb954a6db + +- name: Git clone stable repo upgrade + git: + repo: "{{ chart_test_git_repo }}" + dest: /tmp/helm_test_repo_upgrade + version: d37b5025ffc8be49699898369fbb59661e2a8ffb + +- name: Install Chart from local path + include_tasks: "../tests_chart.yml" + vars: + source: local_path + chart_source: "/tmp/helm_test_repo/stable/{{ chart_test }}/" + chart_source_upgrade: "/tmp/helm_test_repo_upgrade/stable/{{ chart_test }}/" + +- name: Remove clone repos + file: + path: "{{ item }}" + state: absent + with_items: + - /tmp/helm_test_repo + - /tmp/helm_test_repo_upgrade diff --git a/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml b/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml new file mode 100644 index 00000000..73755167 --- /dev/null +++ b/molecule/default/roles/helm/tasks/tests_chart/from_repository.yml @@ -0,0 +1,11 @@ +--- +- name: Add chart repo + shell: "helm repo add stable {{ chart_test_repo }}" + +- name: Install Chart from repository + include_tasks: "../tests_chart.yml" + vars: + source: repository + chart_source: "stable/{{ chart_test }}" + chart_source_version: "{{ chart_test_version }}" + chart_source_version_upgrade: "{{ chart_test_version_upgrade }}" diff --git a/molecule/default/roles/helm/tasks/tests_chart/from_url.yml b/molecule/default/roles/helm/tasks/tests_chart/from_url.yml new file mode 100644 index 00000000..fd3f66c3 --- /dev/null +++ b/molecule/default/roles/helm/tasks/tests_chart/from_url.yml @@ -0,0 +1,7 @@ +--- +- name: Install Chart from URL + include_tasks: "../tests_chart.yml" + vars: + source: url + chart_source: "{{ chart_test_repo }}/{{ chart_test }}-{{ chart_test_version }}.tgz" + chart_source_upgrade: "{{ chart_test_repo }}/{{ chart_test }}-{{ chart_test_version_upgrade }}.tgz" diff --git a/molecule/default/roles/helm/tasks/tiller.yml b/molecule/default/roles/helm/tasks/tiller.yml new file mode 100644 index 00000000..fc7a6991 --- /dev/null +++ b/molecule/default/roles/helm/tasks/tiller.yml @@ -0,0 +1,22 @@ +--- +- name: Tiller namespace + k8s: + api_version: v1 + kind: Namespace + name: "{{ tiller_namespace }}" + state: "{{ status | default('present') }}" + wait: true + +- name: Tiller requirements + k8s: + state: "{{ status | default('present') }}" + resource_definition: "{{ lookup('template', item) | from_yaml }}" + wait: true + with_fileglob: + - ../templates/*.yml + +- name: deploy tiller + shell: "{{ helm_binary }} init --wait --service-account tiller --tiller-namespace {{ tiller_namespace }}" + environment: + ignore_errors: yes + when: "status | default('present') == 'present'" diff --git a/molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml b/molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml new file mode 100644 index 00000000..4ac25c80 --- /dev/null +++ b/molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml @@ -0,0 +1,13 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tiller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: "{{ tiller_cluster_role }}" +subjects: + - kind: ServiceAccount + name: tiller + namespace: "{{ tiller_namespace }}" diff --git a/molecule/default/roles/helm/templates/tiller-sa.yml b/molecule/default/roles/helm/templates/tiller-sa.yml new file mode 100644 index 00000000..7adbd3c9 --- /dev/null +++ b/molecule/default/roles/helm/templates/tiller-sa.yml @@ -0,0 +1,6 @@ +--- +kind: ServiceAccount +apiVersion: v1 +metadata: + name: tiller + namespace: "{{ tiller_namespace }}" diff --git a/plugins/modules/helm_cli.py b/plugins/modules/helm_cli.py new file mode 100644 index 00000000..be077c59 --- /dev/null +++ b/plugins/modules/helm_cli.py @@ -0,0 +1,594 @@ +#!/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_cli +short_description: Manages Kubernetes packages with the Helm package manager +description: + - Install, upgrade, delete packages with the Helm package manager. +version_added: "2.10" +author: + - Lucas Boisserie (@LucasBoisserie) + - Matthieu Diehr (@d-matt) +requirements: + - "helm (https://github.com/helm/helm/releases)" + - "yaml (https://pypi.org/project/PyYAML/)" +options: + binary_path: + description: + - The path of a helm binary to use. + required: false + type: path + chart_ref: + description: + - chart_reference on chart repository + - path to a packaged chart + - path to an unpacked chart directory + - absolute URL + - Required when I(release_state) is set to C(present) + required: false + type: path + chart_repo_url: + description: + - Chart repository URL where to locate the requested chart + required: false + type: str + chart_repo_username: + description: + - Chart repository username where to locate the requested chart + - Required if I(chart_repo_password) is specified + required: false + type: str + chart_repo_password: + description: + - Chart repository password where to locate the requested chart + - Required if I(chart_repo_username) is specified + required: false + type: str + chart_version: + description: + - Chart version to install. If this is not specified, the latest version is installed + required: false + type: str + release_name: + description: + - Release name to manage + required: true + type: str + aliases: [ name ] + release_namespace: + description: + - Kubernetes namespace where the chart should be installed + - Can't be changed with helm 2 + default: "default" + required: false + type: str + aliases: [ namespace ] + release_state: + choices: ['present', 'absent'] + description: + - Desirated state of release + required: false + default: present + aliases: [ state ] + type: str + release_values: + description: + - Value to pass to chart + required: false + default: {} + aliases: [ values ] + type: dict + tiller_host: + description: + - Address of Tiller + - Ignored when is helm 3 + type: str + tiller_namespace: + description: + - Namespace of Tiller + - Ignored when is helm 3 + default: "kube-system" + type: str + update_repo_cache: + description: + - Run C(helm repo update) before the operation. Can be run as part of the package installation or as a separate step. + default: false + type: bool + +#Helm options + disable_hook: + description: + - Helm option to disable hook on install/upgrade/delete + default: False + type: bool + force: + description: + - Helm option to force reinstall, ignore on new install + default: False + type: bool + kube_context: + description: + - Helm option to specify which kubeconfig context to use + type: str + kubeconfig_path: + description: + - Helm option to specify kubeconfig path to use + type: path + aliases: [ kubeconfig ] + purge: + description: + - Remove the release from the store and make its name free for later use + default: True + 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 + default: False + type: bool + wait_timeout: + description: + - Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration) + type: str +''' + +EXAMPLES = ''' +# With Helm 2 +- name: Deploy grafana with params version + helm_cli: + name: test + chart_ref: stable/grafana + chart_version: 3.3.8 + tiller_namespace: helm + values: + replicas: 2 + +- name: Load Value from template + helm_cli: + name: test + chart_ref: stable/grafana + tiller_namespace: helm + values: "{{ lookup('template', 'somefile.yaml') | from_yaml }}" + +- name: Remove test release and waiting suppression ending + helm_cli: + name: test + state: absent + tiller_namespace: helm + wait: true + +# With Helm 3 +- name: Create helm namespace HELM 3 doesn't create it automatically + k8s: + api_version: v1 + kind: Namespace + name: "monitoring" + wait: true + +- name: Deploy latest version of Grafana chart inside monitoring namespace + helm_cli: + name: test + chart_ref: stable/grafana + release_namespace: monitoring + values: + replicas: 2 + +''' + +RETURN = """ +status: + type: complex + description: A dictionary of status output + returned: on success Creation/Upgrade/Already deploy + contains: + appversion: + type: str + returned: always + description: Version of app deployed + chart: + type: str + returned: always + description: Chart name and chart version + name: + type: str + returned: always + description: Name of the release + namespace: + type: str + returned: always + description: Namespace where the release is deployed + revision: + type: str + returned: always + description: Number of time where the release has been updated + status: + type: str + returned: always + description: Status of release (can be DEPLOYED, FAILED, ...) + updated: + type: str + returned: always + description: The Date of last update + values: + type: str + returned: always + description: Dict of Values used to deploy +stdout: + type: str + description: Full `helm` command stdout, in case you want to display it or examine the event log + returned: always + sample: '' +stderr: + type: str + description: Full `helm` command stderr, in case you want to display it or examine the event log + returned: always + sample: '' +command: + type: str + description: Full `helm` 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 upgrade ... +""" + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +from ansible.module_utils.basic import AnsibleModule + +module = None +is_helm_2 = True + + +# get Helm Version +def get_helm_client_version(command): + is_helm_2_local = True + version_command = command + " version --client --short" + rc, out, err = module.run_command(version_command) + + if not out.startswith('Client: v2'): + is_helm_2_local = False + # Fallback on Helm 3 + version_command = command + " version --short" + rc, out, err = module.run_command(version_command) + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=version_command + ) + + elif rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=version_command + ) + + return is_helm_2_local + + +# Get Values from deployed release +def get_values(command, release_name, release_namespace): + get_command = command + " get values --output=yaml " + release_name + + if not is_helm_2: + get_command += " --namespace=" + release_namespace + + rc, out, err = module.run_command(get_command) + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=get_command + ) + + # Helm 3 return "null" string when no values are set + if not is_helm_2 and out.rstrip("\n") == "null": + return yaml.safe_load('{}') + else: + return yaml.safe_load(out) + + +# Get Release from all deployed releases +def get_release(state, release_name, release_namespace): + if state is not None: + if is_helm_2: + for release in state['Releases']: + # release = {k.lower(): v for k, v in release.items()} # NOT WORKING wit python 2.6 + release_lower = dict() + for k, v in release.items(): + release_lower[k.lower()] = v + release = release_lower + if release['name'] == release_name and release['namespace'] == release_namespace: + return release + else: + for release in state: + if release['name'] == release_name and release['namespace'] == release_namespace: + return release + return None + + +# Get Release state from deployed release +def get_release_status(command, release_name, release_namespace): + list_command = command + " list --output=yaml " + + if not is_helm_2: + list_command += " --namespace=" + release_namespace + list_command += " --filter " + + list_command += release_name + + rc, out, err = module.run_command(list_command) + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=list_command + ) + + release = get_release(yaml.safe_load(out), release_name, release_namespace) + + if release is None: # not install + return None + + release['values'] = get_values(command, release_name, release_namespace) + + return release + + +# Run Repo update +def run_repo_update(command): + repo_update_command = command + " repo update" + + rc, out, err = module.run_command(repo_update_command) + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=repo_update_command + ) + + +# Get chart info +def fetch_chart_info(command, chart_ref): + if is_helm_2: + inspect_command = command + " inspect chart " + chart_ref + else: + inspect_command = command + " show chart " + chart_ref + + rc, out, err = module.run_command(inspect_command) + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=inspect_command + ) + + return yaml.safe_load(out) + + +# Install/upgrade/rollback release chart +def deploy(command, release_name, release_namespace, release_values, chart_name, wait, wait_timeout, disable_hook, force): + deploy_command = command + " upgrade -i" # install/upgrade + + # Always reset values to keep release_values equal to values released + deploy_command += " --reset-values" + + if wait: + deploy_command += " --wait" + if wait_timeout is not None: + deploy_command += " --timeout " + wait_timeout + + if force: + deploy_command += " --force" + + if disable_hook: + deploy_command += " --no-hooks" + + if release_values != {}: + try: + import tempfile + except ImportError: + module.fail_json(msg="Could not import the tempfile python module. Please install `tempfile` package.") + + fd, path = tempfile.mkstemp(suffix='.yml') + with open(path, 'w') as yaml_file: + yaml.dump(release_values, yaml_file, default_flow_style=False) + deploy_command += " -f=" + path + + deploy_command += " --namespace=" + release_namespace + deploy_command += " " + release_name + deploy_command += " " + chart_name + + return deploy_command + + +# Delete release chart for helm2 +def delete_2(command, release_name, purge, disable_hook): + delete_command = command + " delete" + + if purge: + delete_command += " --purge" + + if disable_hook: + delete_command += " --no-hooks" + + delete_command += " " + release_name + + return delete_command + + +# Delete release chart for helm3 +def delete_3(command, release_name, release_namespace, purge, disable_hook): + delete_command = command + " uninstall --namespace=" + release_namespace + + if not purge: + delete_command += " --keep-history" + + if disable_hook: + delete_command += " --no-hooks" + + delete_command += " " + release_name + + return delete_command + + +def main(): + global module, is_helm_2 + module = AnsibleModule( + argument_spec=dict( + binary_path=dict(type='path'), + chart_ref=dict(type='path'), + chart_repo_url=dict(type='str'), + chart_repo_username=dict(type='str'), + chart_repo_password=dict(type='str', no_log=True), + chart_version=dict(type='str'), + release_name=dict(type='str', required=True, aliases=['name']), + release_namespace=dict(type='str', default='default', aliases=['namespace']), + release_state=dict(default='present', choices=['present', 'absent'], aliases=['state']), + release_values=dict(type='dict', default={}, aliases=['values']), + tiller_host=dict(type='str'), + tiller_namespace=dict(type='str', default='kube-system'), + update_repo_cache=dict(type='bool', default=False), + + # Helm options + disable_hook=dict(type='bool', default=False), + force=dict(type='bool', default=False), + kube_context=dict(type='str'), + kubeconfig_path=dict(type='path', aliases=['kubeconfig']), + purge=dict(type='bool', default=True), + wait=dict(type='bool', default=False), + wait_timeout=dict(type='str'), + ), + required_if=[ + ('release_state', 'present', ['release_name', 'chart_ref']), + ('release_state', 'absent', ['release_name']) + + ], + required_together=[ + ['chart_repo_username', 'chart_repo_password'] + ], + supports_check_mode=True, + ) + + if not HAS_YAML: + module.fail_json(msg="Could not import the yaml python module. Please install `yaml` package.") + changed = False + + bin_path = module.params.get('binary_path') + chart_ref = module.params.get('chart_ref') + chart_repo_url = module.params.get('chart_repo_url') + chart_repo_username = module.params.get('chart_repo_username') + chart_repo_password = module.params.get('chart_repo_password') + chart_version = module.params.get('chart_version') + release_name = module.params.get('release_name') + release_namespace = module.params.get('release_namespace') + release_state = module.params.get('release_state') + release_values = module.params.get('release_values') + tiller_host = module.params.get('tiller_host') + tiller_namespace = module.params.get('tiller_namespace') + update_repo_cache = module.params.get('update_repo_cache') + + # Helm options + disable_hook = module.params.get('disable_hook') + force = module.params.get('force') + kube_context = module.params.get('kube_context') + kubeconfig_path = module.params.get('kubeconfig_path') + purge = module.params.get('purge') + wait = module.params.get('wait') + wait_timeout = module.params.get('wait_timeout') + + if bin_path is not None: + helm_cmd_common = bin_path + else: + helm_cmd_common = module.get_bin_path('helm', required=True) + + is_helm_2 = get_helm_client_version(helm_cmd_common) + + # Helm 2 need tiller, Helm 3 and higher doesn't + if is_helm_2: + if tiller_host is not None: + helm_cmd_common += " --host=" + tiller_namespace + else: + helm_cmd_common += " --tiller-namespace=" + tiller_namespace + + if kube_context is not None: + helm_cmd_common += " --kube-context " + kube_context + + if kubeconfig_path is not None: + helm_cmd_common += " --kubeconfig " + kubeconfig_path + + if update_repo_cache: + run_repo_update(helm_cmd_common) + + # Get real/deployed release status + release_status = get_release_status(helm_cmd_common, release_name, release_namespace) + + # keep helm_cmd_common for get_release_status in module_exit_json + helm_cmd = helm_cmd_common + if release_state == "absent" and release_status is not None: + if is_helm_2: + helm_cmd = delete_2(helm_cmd, release_name, purge, disable_hook) + else: + helm_cmd = delete_3(helm_cmd, release_name, release_namespace, purge, disable_hook) + changed = True + elif release_state == "present": + + if chart_version is not None: + helm_cmd += " --version=" + chart_version + + if chart_repo_url is not None: + helm_cmd += " --repo=" + chart_repo_url + if chart_repo_username is not None and chart_repo_password is not None: + helm_cmd += " --username=" + chart_repo_username + helm_cmd += " --password=" + chart_repo_password + + # Fetch chart info to have real version and real name for chart_ref from archive, folder or url + chart_info = fetch_chart_info(helm_cmd, chart_ref) + + if release_status is None: # Not installed + helm_cmd = deploy(helm_cmd, release_name, release_namespace, release_values, chart_ref, wait, wait_timeout, + disable_hook, False) + changed = True + + elif is_helm_2 and release_namespace != release_status['namespace']: + module.fail_json( + msg="With helm2, Target Namespace can't be changed on deployed chart ! Need to destroy the release " + "and recreate it " + ) + + elif force or release_values != release_status['values'] \ + or (chart_info['name'] + '-' + chart_info['version']) != release_status["chart"]: + helm_cmd = deploy(helm_cmd, release_name, release_namespace, release_values, chart_ref, wait, wait_timeout, + disable_hook, force) + changed = True + + if module.check_mode: + module.exit_json(changed=changed) + elif not changed: + module.exit_json(changed=False, status=release_status) + + rc, out, err = module.run_command(helm_cmd) + + 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, + status=get_release_status(helm_cmd_common, release_name, release_namespace), command=helm_cmd) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/helm_cli_info.py b/plugins/modules/helm_cli_info.py new file mode 100644 index 00000000..85389aa9 --- /dev/null +++ b/plugins/modules/helm_cli_info.py @@ -0,0 +1,290 @@ +#!/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_cli_info +short_description: Get informations from Helm package deployed inside the cluster +description: + - Get informations (values, states, ...) from Helm package deployed inside the cluster +version_added: "2.10" +author: + - Lucas Boisserie (@LucasBoisserie) +requirements: + - "helm (https://github.com/helm/helm/releases)" + - "yaml (https://pypi.org/project/PyYAML/)" +options: + binary_path: + description: + - The path of a helm binary to use. + required: false + type: path + release_name: + description: + - Release name to manage + required: true + type: str + aliases: [ name ] + release_namespace: + description: + - Kubernetes namespace where the chart should be installed + - Can't be changed with helm 2 + default: "default" + required: false + type: str + aliases: [ namespace ] + tiller_host: + description: + - Address of Tiller + - Ignored when is helm 3 + type: str + tiller_namespace: + description: + - Namespace of Tiller + - Ignored when is helm 3 + default: "kube-system" + type: str + +#Helm options + kube_context: + description: + - Helm option to specify which kubeconfig context to use + type: str + kubeconfig_path: + description: + - Helm option to specify kubeconfig path to use + type: path + aliases: [ kubeconfig ] +''' + +EXAMPLES = ''' +# With Helm 2 +- name: Get grafana deployment + helm_cli_info: + name: test + tiller_namespace: helm + +# With Helm 3 +- name: Deploy latest version of Grafana chart inside monitoring namespace + helm_cli_info: + name: test + release_namespace: monitoring +''' + +RETURN = """ +status: + type: complex + description: A dictionary of status output + returned: only when release exists + contains: + appversion: + type: str + returned: always + description: Version of app deployed + chart: + type: str + returned: always + description: Chart name and chart version + name: + type: str + returned: always + description: Name of the release + namespace: + type: str + returned: always + description: Namespace where the release is deployed + revision: + type: str + returned: always + description: Number of time where the release has been updated + status: + type: str + returned: always + description: Status of release (can be DEPLOYED, FAILED, ...) + updated: + type: str + returned: always + description: The Date of last update + values: + type: str + returned: always + description: Dict of Values used to deploy +""" + +try: + import yaml + HAS_YAML = True +except ImportError: + HAS_YAML = False + +from ansible.module_utils.basic import AnsibleModule + +module = None +is_helm_2 = True + + +# get Helm Version +def get_helm_client_version(command): + is_helm_2_local = True + version_command = command + " version --client --short" + rc, out, err = module.run_command(version_command) + + if not out.startswith('Client: v2'): + is_helm_2_local = False + # Fallback on Helm 3 + version_command = command + " version --short" + rc, out, err = module.run_command(version_command) + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=version_command + ) + + elif rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=version_command + ) + + return is_helm_2_local + + +# Get Values from deployed release +def get_values(command, release_name, release_namespace): + get_command = command + " get values --output=yaml " + release_name + + if not is_helm_2: + get_command += " --namespace=" + release_namespace + + rc, out, err = module.run_command(get_command) + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=get_command + ) + + # Helm 3 return "null" string when no values are set + if not is_helm_2 and out.rstrip("\n") == "null": + return yaml.safe_load('{}') + else: + return yaml.safe_load(out) + + +# Get Release from all deployed releases +def get_release(state, release_name, release_namespace): + if state is not None: + if is_helm_2: + for release in state['Releases']: + # release = {k.lower(): v for k, v in release.items()} # NOT WORKING wit python 2.6 + release_lower = dict() + for k, v in release.items(): + release_lower[k.lower()] = v + release = release_lower + if release['name'] == release_name and release['namespace'] == release_namespace: + return release + else: + for release in state: + if release['name'] == release_name and release['namespace'] == release_namespace: + return release + return None + + +# Get Release state from deployed release +def get_release_status(command, release_name, release_namespace): + list_command = command + " list --output=yaml " + + if not is_helm_2: + list_command += " --namespace=" + release_namespace + list_command += " --filter " + + list_command += release_name + + rc, out, err = module.run_command(list_command) + + if rc != 0: + module.fail_json( + msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), + command=list_command + ) + + release = get_release(yaml.safe_load(out), release_name, release_namespace) + + if release is None: # not install + return None + + release['values'] = get_values(command, release_name, release_namespace) + + return release + + +def main(): + global module, is_helm_2 + + module = AnsibleModule( + argument_spec=dict( + binary_path=dict(type='path'), + release_name=dict(type='str', required=True, aliases=['name']), + release_namespace=dict(type='str', default='default', aliases=['namespace']), + tiller_host=dict(type='str'), + tiller_namespace=dict(type='str', default='kube-system'), + + # Helm options + kube_context=dict(type='str'), + kubeconfig_path=dict(type='path', aliases=['kubeconfig']), + ), + supports_check_mode=True, + ) + if not HAS_YAML: + module.fail_json(msg="Could not import the yaml python module. Please install `yaml` package.") + + bin_path = module.params.get('binary_path') + release_name = module.params.get('release_name') + release_namespace = module.params.get('release_namespace') + tiller_host = module.params.get('tiller_host') + tiller_namespace = module.params.get('tiller_namespace') + + # Helm options + kube_context = module.params.get('kube_context') + kubeconfig_path = module.params.get('kubeconfig_path') + + if bin_path is not None: + helm_cmd_common = bin_path + else: + helm_cmd_common = module.get_bin_path('helm', required=True) + + is_helm_2 = get_helm_client_version(helm_cmd_common) + + # Helm 2 need tiller, Helm 3 and higher doesn't + if is_helm_2: + if tiller_host is not None: + helm_cmd_common += " --host=" + tiller_namespace + else: + helm_cmd_common += " --tiller-namespace=" + tiller_namespace + + if kube_context is not None: + helm_cmd_common += " --kube-context " + kube_context + + if kubeconfig_path is not None: + helm_cmd_common += " --kubeconfig " + kubeconfig_path + + # Get real/deployed release status + release_status = get_release_status(helm_cmd_common, release_name, release_namespace) + if release_status is not None: + module.exit_json(changed=False, status=release_status) + else: + module.exit_json(changed=False) + + +if __name__ == '__main__': + main() From c36a2d50af91d5be79c7f23e9f4c093b743e3135 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Thu, 26 Mar 2020 16:48:13 +0100 Subject: [PATCH 2/7] increase timeout for waiter --- molecule/default/roles/helm/tasks/tests_chart.yml | 1 + molecule/default/tasks/waiter.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/molecule/default/roles/helm/tasks/tests_chart.yml b/molecule/default/roles/helm/tasks/tests_chart.yml index e196b3f0..9a9249f9 100644 --- a/molecule/default/roles/helm/tasks/tests_chart.yml +++ b/molecule/default/roles/helm/tasks/tests_chart.yml @@ -183,3 +183,4 @@ name: "{{ helm_namespace }}" state: absent wait: true + wait_timeout: 180 diff --git a/molecule/default/tasks/waiter.yml b/molecule/default/tasks/waiter.yml index 1269c2e9..98ee6416 100644 --- a/molecule/default/tasks/waiter.yml +++ b/molecule/default/tasks/waiter.yml @@ -49,7 +49,7 @@ app: "{{ k8s_pod_name }}" template: "{{ k8s_pod_template }}" wait: yes - wait_sleep: 3 + wait_sleep: 5 wait_timeout: 180 vars: k8s_pod_name: wait-ds From 2e1b0f68172f9eb13bf9e0051a9952de6cf7dd60 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Fri, 27 Mar 2020 11:25:13 +0100 Subject: [PATCH 3/7] t# This is a combination of 2 commits. Add requested changes for helm --- .../helm/tasks/test_helm_not_installed.yml | 1 + .../roles/helm/tasks/test_missing_tiller.yml | 1 + plugins/modules/helm_cli.py | 78 ++++++++++--------- plugins/modules/helm_cli_info.py | 38 ++++----- 4 files changed, 62 insertions(+), 56 deletions(-) diff --git a/molecule/default/roles/helm/tasks/test_helm_not_installed.yml b/molecule/default/roles/helm/tasks/test_helm_not_installed.yml index 6cda87d7..63cfb394 100644 --- a/molecule/default/roles/helm/tasks/test_helm_not_installed.yml +++ b/molecule/default/roles/helm/tasks/test_helm_not_installed.yml @@ -4,6 +4,7 @@ binary_path: "{{ helm_binary}}_fake" name: test chart_ref: "{{ chart_test }}" + namespace: "{{ helm_namespace }}" ignore_errors: yes register: helm_missing_binary diff --git a/molecule/default/roles/helm/tasks/test_missing_tiller.yml b/molecule/default/roles/helm/tasks/test_missing_tiller.yml index ce1e6b85..47c2ebc6 100644 --- a/molecule/default/roles/helm/tasks/test_missing_tiller.yml +++ b/molecule/default/roles/helm/tasks/test_missing_tiller.yml @@ -4,6 +4,7 @@ binary_path: "{{ helm_binary}}" name: test chart_ref: "{{ chart_test }}" + namespace: "{{ helm_namespace }}" tiller_namespace: helm ignore_errors: yes register: missing_tiller diff --git a/plugins/modules/helm_cli.py b/plugins/modules/helm_cli.py index be077c59..caef9258 100644 --- a/plugins/modules/helm_cli.py +++ b/plugins/modules/helm_cli.py @@ -16,7 +16,6 @@ module: helm_cli short_description: Manages Kubernetes packages with the Helm package manager description: - Install, upgrade, delete packages with the Helm package manager. -version_added: "2.10" author: - Lucas Boisserie (@LucasBoisserie) - Matthieu Diehr (@d-matt) @@ -31,73 +30,72 @@ options: type: path chart_ref: description: - - chart_reference on chart repository - - path to a packaged chart - - path to an unpacked chart directory - - absolute URL - - Required when I(release_state) is set to C(present) + - chart_reference on chart repository. + - path to a packaged chart. + - path to an unpacked chart directory. + - absolute URL. + - Required when I(release_state) is set to C(present). required: false type: path chart_repo_url: description: - - Chart repository URL where to locate the requested chart + - Chart repository URL where to locate the requested chart. required: false type: str chart_repo_username: description: - - Chart repository username where to locate the requested chart - - Required if I(chart_repo_password) is specified + - Chart repository username where to locate the requested chart. + - Required if I(chart_repo_password) is specified. required: false type: str chart_repo_password: description: - - Chart repository password where to locate the requested chart - - Required if I(chart_repo_username) is specified + - Chart repository password where to locate the requested chart. + - Required if I(chart_repo_username) is specified. required: false type: str chart_version: description: - - Chart version to install. If this is not specified, the latest version is installed + - Chart version to install. If this is not specified, the latest version is installed. required: false type: str release_name: description: - - Release name to manage + - Release name to manage. required: true type: str aliases: [ name ] release_namespace: description: - - Kubernetes namespace where the chart should be installed - - Can't be changed with helm 2 - default: "default" - required: false + - Kubernetes namespace where the chart should be installed. + - Can't be changed with helm 2. + required: true type: str aliases: [ namespace ] release_state: choices: ['present', 'absent'] description: - - Desirated state of release + - Desirated state of release. required: false default: present aliases: [ state ] type: str release_values: description: - - Value to pass to chart + - Value to pass to chart. required: false default: {} aliases: [ values ] type: dict tiller_host: description: - - Address of Tiller - - Ignored when is helm 3 + - Address of Tiller. + - Ignored when is helm 3. type: str tiller_namespace: description: - - Namespace of Tiller - - Ignored when is helm 3 + - Namespace of Tiller. + - Ignored when is helm 3. default: "kube-system" type: str update_repo_cache: @@ -109,36 +107,36 @@ options: #Helm options disable_hook: description: - - Helm option to disable hook on install/upgrade/delete + - Helm option to disable hook on install/upgrade/delete. default: False type: bool force: description: - - Helm option to force reinstall, ignore on new install + - Helm option to force reinstall, ignore on new install. default: False type: bool kube_context: description: - - Helm option to specify which kubeconfig context to use + - Helm option to specify which kubeconfig context to use. type: str kubeconfig_path: description: - - Helm option to specify kubeconfig path to use + - Helm option to specify kubeconfig path to use. type: path aliases: [ kubeconfig ] purge: description: - - Remove the release from the store and make its name free for later use + - Remove the release from the store and make its name free for later use. default: True 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 + - 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. default: False type: bool wait_timeout: description: - - Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration) + - Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration). type: str ''' @@ -240,13 +238,16 @@ command: sample: helm upgrade ... """ +import traceback + try: import yaml - HAS_YAML = True + IMP_YAML = True except ImportError: - HAS_YAML = False + IMP_YAML_ERR = traceback.format_exc() + IMP_YAML = False -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import AnsibleModule, missing_required_lib module = None is_helm_2 = True @@ -296,7 +297,7 @@ def get_values(command, release_name, release_namespace): # Helm 3 return "null" string when no values are set if not is_helm_2 and out.rstrip("\n") == "null": - return yaml.safe_load('{}') + return {} else: return yaml.safe_load(out) @@ -399,7 +400,7 @@ def deploy(command, release_name, release_namespace, release_values, chart_name, try: import tempfile except ImportError: - module.fail_json(msg="Could not import the tempfile python module. Please install `tempfile` package.") + module.fail_json(msg=missing_required_lib("tempfile"), exception=traceback.format_exc()) fd, path = tempfile.mkstemp(suffix='.yml') with open(path, 'w') as yaml_file: @@ -454,7 +455,7 @@ def main(): chart_repo_password=dict(type='str', no_log=True), chart_version=dict(type='str'), release_name=dict(type='str', required=True, aliases=['name']), - release_namespace=dict(type='str', default='default', aliases=['namespace']), + release_namespace=dict(type='str', required=True, aliases=['namespace']), release_state=dict(default='present', choices=['present', 'absent'], aliases=['state']), release_values=dict(type='dict', default={}, aliases=['values']), tiller_host=dict(type='str'), @@ -481,8 +482,9 @@ def main(): supports_check_mode=True, ) - if not HAS_YAML: - module.fail_json(msg="Could not import the yaml python module. Please install `yaml` package.") + 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') diff --git a/plugins/modules/helm_cli_info.py b/plugins/modules/helm_cli_info.py index 85389aa9..f18926c8 100644 --- a/plugins/modules/helm_cli_info.py +++ b/plugins/modules/helm_cli_info.py @@ -16,7 +16,6 @@ module: helm_cli_info short_description: Get informations from Helm package deployed inside the cluster description: - Get informations (values, states, ...) from Helm package deployed inside the cluster -version_added: "2.10" author: - Lucas Boisserie (@LucasBoisserie) requirements: @@ -30,38 +29,37 @@ options: type: path release_name: description: - - Release name to manage + - Release name to manage. required: true type: str aliases: [ name ] release_namespace: description: - - Kubernetes namespace where the chart should be installed - - Can't be changed with helm 2 - default: "default" - required: false + - Kubernetes namespace where the chart should be installed. + - Can't be changed with helm 2. + required: true type: str aliases: [ namespace ] tiller_host: description: - - Address of Tiller - - Ignored when is helm 3 + - Address of Tiller. + - Ignored when is helm 3. type: str tiller_namespace: description: - - Namespace of Tiller - - Ignored when is helm 3 + - Namespace of Tiller. + - Ignored when is helm 3. default: "kube-system" type: str #Helm options kube_context: description: - - Helm option to specify which kubeconfig context to use + - Helm option to specify which kubeconfig context to use. type: str kubeconfig_path: description: - - Helm option to specify kubeconfig path to use + - Helm option to specify kubeconfig path to use. type: path aliases: [ kubeconfig ] ''' @@ -120,13 +118,16 @@ status: description: Dict of Values used to deploy """ +import traceback + try: import yaml - HAS_YAML = True + IMP_YAML = True except ImportError: - HAS_YAML = False + IMP_YAML_ERR = traceback.format_exc() + IMP_YAML = False -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import AnsibleModule, missing_required_lib module = None is_helm_2 = True @@ -235,7 +236,7 @@ def main(): argument_spec=dict( binary_path=dict(type='path'), release_name=dict(type='str', required=True, aliases=['name']), - release_namespace=dict(type='str', default='default', aliases=['namespace']), + release_namespace=dict(type='str', required=True, aliases=['namespace']), tiller_host=dict(type='str'), tiller_namespace=dict(type='str', default='kube-system'), @@ -245,8 +246,9 @@ def main(): ), supports_check_mode=True, ) - if not HAS_YAML: - module.fail_json(msg="Could not import the yaml python module. Please install `yaml` package.") + + if not IMP_YAML: + module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) bin_path = module.params.get('binary_path') release_name = module.params.get('release_name') From 438a30bd8eae9c190085ba7b95bcd419c275ad71 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Tue, 21 Apr 2020 11:00:56 +0200 Subject: [PATCH 4/7] Remove Helm 2 support and add tests for helm_cli_info --- molecule/default/roles/helm/tasks/main.yml | 1 - .../default/roles/helm/tasks/run_test.yml | 14 - .../roles/helm/tasks/test_missing_tiller.yml | 16 -- .../default/roles/helm/tasks/tests_chart.yml | 35 ++- molecule/default/roles/helm/tasks/tiller.yml | 22 -- plugins/modules/helm_cli.py | 244 +++++------------- plugins/modules/helm_cli_info.py | 108 ++------ 7 files changed, 112 insertions(+), 328 deletions(-) delete mode 100644 molecule/default/roles/helm/tasks/test_missing_tiller.yml delete mode 100644 molecule/default/roles/helm/tasks/tiller.yml diff --git a/molecule/default/roles/helm/tasks/main.yml b/molecule/default/roles/helm/tasks/main.yml index 13c936a2..458775de 100644 --- a/molecule/default/roles/helm/tasks/main.yml +++ b/molecule/default/roles/helm/tasks/main.yml @@ -4,5 +4,4 @@ loop_control: loop_var: helm_version with_items: - - "v2.16.0" - "v3.1.2" diff --git a/molecule/default/roles/helm/tasks/run_test.yml b/molecule/default/roles/helm/tasks/run_test.yml index 179663b0..f772ad14 100644 --- a/molecule/default/roles/helm/tasks/run_test.yml +++ b/molecule/default/roles/helm/tasks/run_test.yml @@ -12,14 +12,6 @@ - name: "Install {{ helm_version }}" include_tasks: install.yml -- name: Check if failed when tiller is not installed - include_tasks: test_missing_tiller.yml - when: "helm_version.startswith('v2')" - -- name: Install tiller - include_tasks: tiller.yml - when: "helm_version.startswith('v2')" - - name: Deploy charts include_tasks: "tests_chart/{{ test_chart_type }}.yml" loop_control: @@ -29,12 +21,6 @@ - from_repository - from_url -- name: Clean tiller - include_tasks: tiller.yml - vars: - status: absent - when: "helm_version.startswith('v2')" - - name: Clean helm install file: path: "{{ item }}" diff --git a/molecule/default/roles/helm/tasks/test_missing_tiller.yml b/molecule/default/roles/helm/tasks/test_missing_tiller.yml deleted file mode 100644 index 47c2ebc6..00000000 --- a/molecule/default/roles/helm/tasks/test_missing_tiller.yml +++ /dev/null @@ -1,16 +0,0 @@ ---- -- name: Failed test when tiller is missing - helm_cli: - binary_path: "{{ helm_binary}}" - name: test - chart_ref: "{{ chart_test }}" - namespace: "{{ helm_namespace }}" - tiller_namespace: helm - ignore_errors: yes - register: missing_tiller - -- name: Assert that tiller is missing - assert: - that: - - missing_tiller is failed - - "'could not find tiller' in missing_tiller.msg" diff --git a/molecule/default/roles/helm/tasks/tests_chart.yml b/molecule/default/roles/helm/tasks/tests_chart.yml index 9a9249f9..5ddbf0c7 100644 --- a/molecule/default/roles/helm/tasks/tests_chart.yml +++ b/molecule/default/roles/helm/tasks/tests_chart.yml @@ -6,6 +6,18 @@ name: "{{ helm_namespace }}" wait: true +- name: Check helm_cli_info empty + helm_cli_info: + binary_path: "{{ helm_binary}}" + name: test + namespace: "{{ helm_namespace }}" + register: empty_info + +- name: "Assert that no chart are installed with helm_cli_info" + assert: + that: + - empty_info.status is undefined + - name: "Install {{ chart_test }} from {{ source }}" helm_cli: binary_path: "{{ helm_binary}}" @@ -13,7 +25,6 @@ chart_ref: "{{ chart_source }}" chart_version: "{{ chart_source_version | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: "Assert that {{ chart_test }} chart is installed from {{ source }}" @@ -23,6 +34,19 @@ - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" - install.status.status | lower == 'deployed' +- name: Check helm_cli_info content + helm_cli_info: + binary_path: "{{ helm_binary}}" + name: test + namespace: "{{ helm_namespace }}" + register: content_info + +- name: "Assert that {{ chart_test }} is installed from {{ source }} with helm_cli_info" + assert: + that: + - content_info.status.chart == "{{ chart_test }}-{{ chart_test_version }}" + - content_info.status.status | lower == 'deployed' + - name: Check idempotency helm_cli: binary_path: "{{ helm_binary}}" @@ -30,7 +54,6 @@ chart_ref: "{{ chart_source }}" chart_version: "{{ chart_source_version | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: Assert idempotency @@ -47,7 +70,6 @@ chart_ref: "{{ chart_source }}" chart_version: "{{ chart_source_version | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" values: "{{ chart_test_values }}" register: install @@ -66,7 +88,6 @@ chart_ref: "{{ chart_source }}" chart_version: "{{ chart_source_version | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" values: "{{ chart_test_values }}" register: install @@ -85,7 +106,6 @@ chart_ref: "{{ chart_source }}" chart_version: "{{ chart_source_version | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: "Assert that {{ chart_test }} chart is upgraded with new var from {{ source }}" @@ -103,7 +123,6 @@ chart_ref: "{{ chart_source }}" chart_version: "{{ chart_source_version | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: Assert idempotency after remove vars @@ -121,7 +140,6 @@ chart_ref: "{{ chart_source_upgrade | default(chart_source) }}" chart_version: "{{ chart_source_version_upgrade | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: "Assert that {{ chart_test }} chart is upgraded with new version from {{ source }}" @@ -138,7 +156,6 @@ chart_ref: "{{ chart_source_upgrade | default(chart_source) }}" chart_version: "{{ chart_source_version_upgrade | default(omit)}}" namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: Assert idempotency after upgrade @@ -154,7 +171,6 @@ state: absent name: test namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: "Assert that {{ chart_test }} chart is remove from {{ source }}" @@ -168,7 +184,6 @@ state: absent name: test namespace: "{{ helm_namespace }}" - tiller_namespace: "{{ tiller_namespace }}" register: install - name: Assert idempotency diff --git a/molecule/default/roles/helm/tasks/tiller.yml b/molecule/default/roles/helm/tasks/tiller.yml deleted file mode 100644 index fc7a6991..00000000 --- a/molecule/default/roles/helm/tasks/tiller.yml +++ /dev/null @@ -1,22 +0,0 @@ ---- -- name: Tiller namespace - k8s: - api_version: v1 - kind: Namespace - name: "{{ tiller_namespace }}" - state: "{{ status | default('present') }}" - wait: true - -- name: Tiller requirements - k8s: - state: "{{ status | default('present') }}" - resource_definition: "{{ lookup('template', item) | from_yaml }}" - wait: true - with_fileglob: - - ../templates/*.yml - -- name: deploy tiller - shell: "{{ helm_binary }} init --wait --service-account tiller --tiller-namespace {{ tiller_namespace }}" - environment: - ignore_errors: yes - when: "status | default('present') == 'present'" diff --git a/plugins/modules/helm_cli.py b/plugins/modules/helm_cli.py index caef9258..a469d88d 100644 --- a/plugins/modules/helm_cli.py +++ b/plugins/modules/helm_cli.py @@ -13,15 +13,24 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- module: helm_cli + short_description: Manages Kubernetes packages with the Helm package manager -description: - - Install, upgrade, delete packages with the Helm package manager. + author: - Lucas Boisserie (@LucasBoisserie) - Matthieu Diehr (@d-matt) + requirements: - "helm (https://github.com/helm/helm/releases)" - "yaml (https://pypi.org/project/PyYAML/)" + +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: @@ -42,18 +51,6 @@ options: - Chart repository URL where to locate the requested chart. required: false type: str - chart_repo_username: - description: - - Chart repository username where to locate the requested chart. - - Required if I(chart_repo_password) is specified. - required: false - type: str - chart_repo_password: - description: - - Chart repository password where to locate the requested chart. - - Required if I(chart_repo_username) is specified. - required: false - type: str chart_version: description: - Chart version to install. If this is not specified, the latest version is installed. @@ -68,7 +65,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 ] @@ -87,17 +83,6 @@ options: default: {} aliases: [ values ] type: dict - tiller_host: - description: - - Address of Tiller. - - Ignored when is helm 3. - type: str - tiller_namespace: - description: - - Namespace of Tiller. - - Ignored when is helm 3. - default: "kube-system" - type: str update_repo_cache: description: - Run C(helm repo update) before the operation. Can be run as part of the package installation or as a separate step. @@ -141,31 +126,6 @@ options: ''' EXAMPLES = ''' -# With Helm 2 -- name: Deploy grafana with params version - helm_cli: - name: test - chart_ref: stable/grafana - chart_version: 3.3.8 - tiller_namespace: helm - values: - replicas: 2 - -- name: Load Value from template - helm_cli: - name: test - chart_ref: stable/grafana - tiller_namespace: helm - values: "{{ lookup('template', 'somefile.yaml') | from_yaml }}" - -- name: Remove test release and waiting suppression ending - helm_cli: - name: test - state: absent - tiller_namespace: helm - wait: true - -# With Helm 3 - name: Create helm namespace HELM 3 doesn't create it automatically k8s: api_version: v1 @@ -173,7 +133,11 @@ EXAMPLES = ''' name: "monitoring" wait: true -- name: Deploy latest version of Grafana chart inside monitoring namespace +# From repository +- name: Add stable chart repo + shell: "helm repo add stable https://kubernetes-charts.storage.googleapis.com" + +- name: Deploy latest version of Grafana chart inside monitoring namespace with values helm_cli: name: test chart_ref: stable/grafana @@ -181,6 +145,37 @@ EXAMPLES = ''' values: replicas: 2 +- name: Deploy Grafana chart on 5.0.12 with values loaded from template + helm_cli: + name: test + chart_ref: stable/grafana + chart_version: 5.0.12 + values: "{{ lookup('template', 'somefile.yaml') | from_yaml }}" + +- name: Remove test release and waiting suppression ending + helm_cli: + name: test + state: absent + wait: true + +# From git +- name: Git clone stable repo on HEAD + git: + repo: "http://github.com/helm/charts.git" + dest: /tmp/helm_repo + +- name: Deploy Grafana chart from local path + helm_cli: + name: test + chart_ref: /tmp/helm_repo/stable/grafana + release_namespace: monitoring + +# From url +- name: Deploy Grafana chart on 5.0.12 from url + helm_cli: + name: test + chart_ref: "https://kubernetes-charts.storage.googleapis.com/grafana-5.0.12.tgz" + release_namespace: monitoring ''' RETURN = """ @@ -250,43 +245,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule, missing_required_lib module = None -is_helm_2 = True - - -# get Helm Version -def get_helm_client_version(command): - is_helm_2_local = True - version_command = command + " version --client --short" - rc, out, err = module.run_command(version_command) - - if not out.startswith('Client: v2'): - is_helm_2_local = False - # Fallback on Helm 3 - version_command = command + " version --short" - rc, out, err = module.run_command(version_command) - - if rc != 0: - module.fail_json( - msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), - command=version_command - ) - - elif rc != 0: - module.fail_json( - msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), - command=version_command - ) - - return is_helm_2_local # Get Values from deployed release -def get_values(command, release_name, release_namespace): +def get_values(command, release_name): get_command = command + " get values --output=yaml " + release_name - if not is_helm_2: - get_command += " --namespace=" + release_namespace - rc, out, err = module.run_command(get_command) if rc != 0: @@ -296,40 +260,24 @@ def get_values(command, release_name, release_namespace): ) # Helm 3 return "null" string when no values are set - if not is_helm_2 and out.rstrip("\n") == "null": + if out.rstrip("\n") == "null": return {} else: return yaml.safe_load(out) # Get Release from all deployed releases -def get_release(state, release_name, release_namespace): +def get_release(state, release_name): if state is not None: - if is_helm_2: - for release in state['Releases']: - # release = {k.lower(): v for k, v in release.items()} # NOT WORKING wit python 2.6 - release_lower = dict() - for k, v in release.items(): - release_lower[k.lower()] = v - release = release_lower - if release['name'] == release_name and release['namespace'] == release_namespace: - return release - else: - for release in state: - if release['name'] == release_name and release['namespace'] == release_namespace: - return release + for release in state: + if release['name'] == release_name: + return release return None # Get Release state from deployed release -def get_release_status(command, release_name, release_namespace): - list_command = command + " list --output=yaml " - - if not is_helm_2: - list_command += " --namespace=" + release_namespace - list_command += " --filter " - - list_command += release_name +def get_release_status(command, release_name): + list_command = command + " list --output=yaml --filter " + release_name rc, out, err = module.run_command(list_command) @@ -339,12 +287,12 @@ def get_release_status(command, release_name, release_namespace): command=list_command ) - release = get_release(yaml.safe_load(out), release_name, release_namespace) + release = get_release(yaml.safe_load(out), release_name) if release is None: # not install return None - release['values'] = get_values(command, release_name, release_namespace) + release['values'] = get_values(command, release_name) return release @@ -363,10 +311,7 @@ def run_repo_update(command): # Get chart info def fetch_chart_info(command, chart_ref): - if is_helm_2: - inspect_command = command + " inspect chart " + chart_ref - else: - inspect_command = command + " show chart " + chart_ref + inspect_command = command + " show chart " + chart_ref rc, out, err = module.run_command(inspect_command) if rc != 0: @@ -379,7 +324,7 @@ def fetch_chart_info(command, chart_ref): # Install/upgrade/rollback release chart -def deploy(command, release_name, release_namespace, release_values, chart_name, wait, wait_timeout, disable_hook, force): +def deploy(command, release_name, release_values, chart_name, wait, wait_timeout, disable_hook, force): deploy_command = command + " upgrade -i" # install/upgrade # Always reset values to keep release_values equal to values released @@ -407,31 +352,14 @@ def deploy(command, release_name, release_namespace, release_values, chart_name, yaml.dump(release_values, yaml_file, default_flow_style=False) deploy_command += " -f=" + path - deploy_command += " --namespace=" + release_namespace - deploy_command += " " + release_name - deploy_command += " " + chart_name + deploy_command += " " + release_name + " " + chart_name return deploy_command -# Delete release chart for helm2 -def delete_2(command, release_name, purge, disable_hook): - delete_command = command + " delete" - - if purge: - delete_command += " --purge" - - if disable_hook: - delete_command += " --no-hooks" - - delete_command += " " + release_name - - return delete_command - - -# Delete release chart for helm3 -def delete_3(command, release_name, release_namespace, purge, disable_hook): - delete_command = command + " uninstall --namespace=" + release_namespace +# Delete release chart +def delete(command, release_name, purge, disable_hook): + delete_command = command + " uninstall " if not purge: delete_command += " --keep-history" @@ -445,21 +373,17 @@ def delete_3(command, release_name, release_namespace, purge, disable_hook): def main(): - global module, is_helm_2 + global module module = AnsibleModule( argument_spec=dict( binary_path=dict(type='path'), chart_ref=dict(type='path'), chart_repo_url=dict(type='str'), - chart_repo_username=dict(type='str'), - chart_repo_password=dict(type='str', no_log=True), chart_version=dict(type='str'), release_name=dict(type='str', required=True, aliases=['name']), release_namespace=dict(type='str', required=True, aliases=['namespace']), release_state=dict(default='present', choices=['present', 'absent'], aliases=['state']), release_values=dict(type='dict', default={}, aliases=['values']), - tiller_host=dict(type='str'), - tiller_namespace=dict(type='str', default='kube-system'), update_repo_cache=dict(type='bool', default=False), # Helm options @@ -476,9 +400,6 @@ def main(): ('release_state', 'absent', ['release_name']) ], - required_together=[ - ['chart_repo_username', 'chart_repo_password'] - ], supports_check_mode=True, ) @@ -490,15 +411,11 @@ def main(): bin_path = module.params.get('binary_path') chart_ref = module.params.get('chart_ref') chart_repo_url = module.params.get('chart_repo_url') - chart_repo_username = module.params.get('chart_repo_username') - chart_repo_password = module.params.get('chart_repo_password') chart_version = module.params.get('chart_version') release_name = module.params.get('release_name') release_namespace = module.params.get('release_namespace') release_state = module.params.get('release_state') release_values = module.params.get('release_values') - tiller_host = module.params.get('tiller_host') - tiller_namespace = module.params.get('tiller_namespace') update_repo_cache = module.params.get('update_repo_cache') # Helm options @@ -515,15 +432,6 @@ def main(): else: helm_cmd_common = module.get_bin_path('helm', required=True) - is_helm_2 = get_helm_client_version(helm_cmd_common) - - # Helm 2 need tiller, Helm 3 and higher doesn't - if is_helm_2: - if tiller_host is not None: - helm_cmd_common += " --host=" + tiller_namespace - else: - helm_cmd_common += " --tiller-namespace=" + tiller_namespace - if kube_context is not None: helm_cmd_common += " --kube-context " + kube_context @@ -533,16 +441,15 @@ def main(): if update_repo_cache: run_repo_update(helm_cmd_common) + helm_cmd_common += " --namespace=" + release_namespace + # Get real/deployed release status - release_status = get_release_status(helm_cmd_common, release_name, release_namespace) + release_status = get_release_status(helm_cmd_common, release_name) # keep helm_cmd_common for get_release_status in module_exit_json helm_cmd = helm_cmd_common if release_state == "absent" and release_status is not None: - if is_helm_2: - helm_cmd = delete_2(helm_cmd, release_name, purge, disable_hook) - else: - helm_cmd = delete_3(helm_cmd, release_name, release_namespace, purge, disable_hook) + helm_cmd = delete(helm_cmd, release_name, purge, disable_hook) changed = True elif release_state == "present": @@ -551,27 +458,18 @@ def main(): if chart_repo_url is not None: helm_cmd += " --repo=" + chart_repo_url - if chart_repo_username is not None and chart_repo_password is not None: - helm_cmd += " --username=" + chart_repo_username - helm_cmd += " --password=" + chart_repo_password # Fetch chart info to have real version and real name for chart_ref from archive, folder or url chart_info = fetch_chart_info(helm_cmd, chart_ref) if release_status is None: # Not installed - helm_cmd = deploy(helm_cmd, release_name, release_namespace, release_values, chart_ref, wait, wait_timeout, + helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout, disable_hook, False) changed = True - elif is_helm_2 and release_namespace != release_status['namespace']: - module.fail_json( - msg="With helm2, Target Namespace can't be changed on deployed chart ! Need to destroy the release " - "and recreate it " - ) - elif force or release_values != release_status['values'] \ or (chart_info['name'] + '-' + chart_info['version']) != release_status["chart"]: - helm_cmd = deploy(helm_cmd, release_name, release_namespace, release_values, chart_ref, wait, wait_timeout, + helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout, disable_hook, force) changed = True @@ -589,7 +487,7 @@ def main(): ) module.exit_json(changed=changed, stdout=out, stderr=err, - status=get_release_status(helm_cmd_common, release_name, release_namespace), command=helm_cmd) + status=get_release_status(helm_cmd_common, release_name), command=helm_cmd) if __name__ == '__main__': diff --git a/plugins/modules/helm_cli_info.py b/plugins/modules/helm_cli_info.py index f18926c8..2b513b02 100644 --- a/plugins/modules/helm_cli_info.py +++ b/plugins/modules/helm_cli_info.py @@ -40,17 +40,6 @@ options: required: true type: str aliases: [ namespace ] - tiller_host: - description: - - Address of Tiller. - - Ignored when is helm 3. - type: str - tiller_namespace: - description: - - Namespace of Tiller. - - Ignored when is helm 3. - default: "kube-system" - type: str #Helm options kube_context: @@ -65,13 +54,6 @@ options: ''' EXAMPLES = ''' -# With Helm 2 -- name: Get grafana deployment - helm_cli_info: - name: test - tiller_namespace: helm - -# With Helm 3 - name: Deploy latest version of Grafana chart inside monitoring namespace helm_cli_info: name: test @@ -130,43 +112,12 @@ except ImportError: from ansible.module_utils.basic import AnsibleModule, missing_required_lib module = None -is_helm_2 = True - - -# get Helm Version -def get_helm_client_version(command): - is_helm_2_local = True - version_command = command + " version --client --short" - rc, out, err = module.run_command(version_command) - - if not out.startswith('Client: v2'): - is_helm_2_local = False - # Fallback on Helm 3 - version_command = command + " version --short" - rc, out, err = module.run_command(version_command) - - if rc != 0: - module.fail_json( - msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), - command=version_command - ) - - elif rc != 0: - module.fail_json( - msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(rc, out, err), - command=version_command - ) - - return is_helm_2_local # Get Values from deployed release -def get_values(command, release_name, release_namespace): +def get_values(command, release_name): get_command = command + " get values --output=yaml " + release_name - if not is_helm_2: - get_command += " --namespace=" + release_namespace - rc, out, err = module.run_command(get_command) if rc != 0: @@ -176,40 +127,24 @@ def get_values(command, release_name, release_namespace): ) # Helm 3 return "null" string when no values are set - if not is_helm_2 and out.rstrip("\n") == "null": - return yaml.safe_load('{}') + if out.rstrip("\n") == "null": + return {} else: return yaml.safe_load(out) # Get Release from all deployed releases -def get_release(state, release_name, release_namespace): +def get_release(state, release_name): if state is not None: - if is_helm_2: - for release in state['Releases']: - # release = {k.lower(): v for k, v in release.items()} # NOT WORKING wit python 2.6 - release_lower = dict() - for k, v in release.items(): - release_lower[k.lower()] = v - release = release_lower - if release['name'] == release_name and release['namespace'] == release_namespace: - return release - else: - for release in state: - if release['name'] == release_name and release['namespace'] == release_namespace: - return release + for release in state: + if release['name'] == release_name: + return release return None # Get Release state from deployed release -def get_release_status(command, release_name, release_namespace): - list_command = command + " list --output=yaml " - - if not is_helm_2: - list_command += " --namespace=" + release_namespace - list_command += " --filter " - - list_command += release_name +def get_release_status(command, release_name): + list_command = command + " list --output=yaml --filter " + release_name rc, out, err = module.run_command(list_command) @@ -219,26 +154,24 @@ def get_release_status(command, release_name, release_namespace): command=list_command ) - release = get_release(yaml.safe_load(out), release_name, release_namespace) + release = get_release(yaml.safe_load(out), release_name) if release is None: # not install return None - release['values'] = get_values(command, release_name, release_namespace) + release['values'] = get_values(command, release_name) return release def main(): - global module, is_helm_2 + global module module = AnsibleModule( argument_spec=dict( binary_path=dict(type='path'), release_name=dict(type='str', required=True, aliases=['name']), release_namespace=dict(type='str', required=True, aliases=['namespace']), - tiller_host=dict(type='str'), - tiller_namespace=dict(type='str', default='kube-system'), # Helm options kube_context=dict(type='str'), @@ -253,8 +186,6 @@ def main(): bin_path = module.params.get('binary_path') release_name = module.params.get('release_name') release_namespace = module.params.get('release_namespace') - tiller_host = module.params.get('tiller_host') - tiller_namespace = module.params.get('tiller_namespace') # Helm options kube_context = module.params.get('kube_context') @@ -265,23 +196,16 @@ def main(): else: helm_cmd_common = module.get_bin_path('helm', required=True) - is_helm_2 = get_helm_client_version(helm_cmd_common) - - # Helm 2 need tiller, Helm 3 and higher doesn't - if is_helm_2: - if tiller_host is not None: - helm_cmd_common += " --host=" + tiller_namespace - else: - helm_cmd_common += " --tiller-namespace=" + tiller_namespace - if kube_context is not None: helm_cmd_common += " --kube-context " + kube_context if kubeconfig_path is not None: helm_cmd_common += " --kubeconfig " + kubeconfig_path - # Get real/deployed release status - release_status = get_release_status(helm_cmd_common, release_name, release_namespace) + helm_cmd_common += " --namespace=" + release_namespace + + release_status = get_release_status(helm_cmd_common, release_name) + if release_status is not None: module.exit_json(changed=False, status=release_status) else: From e89aa1c69213479d9bd94072d5e8e7766bf0dda6 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Thu, 23 Apr 2020 15:50:58 +0200 Subject: [PATCH 5/7] Fix englist grammar errors and rename modules to helm and helm_info --- README.md | 4 +- .../default/roles/helm/tasks/run_test.yml | 4 +- .../helm/tasks/test_helm_not_installed.yml | 4 +- .../default/roles/helm/tasks/tests_chart.yml | 80 +++++++++---------- .../templates/tiller-clusterRoleBinding.yml | 13 --- .../roles/helm/templates/tiller-sa.yml | 6 -- plugins/modules/{helm_cli.py => helm.py} | 12 +-- .../{helm_cli_info.py => helm_info.py} | 4 +- 8 files changed, 54 insertions(+), 73 deletions(-) delete mode 100644 molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml delete mode 100644 molecule/default/roles/helm/templates/tiller-sa.yml rename plugins/modules/{helm_cli.py => helm.py} (99%) rename plugins/modules/{helm_cli_info.py => helm_info.py} (99%) diff --git a/README.md b/README.md index 2e5cd645..fd8ea067 100644 --- a/README.md +++ b/README.md @@ -27,8 +27,8 @@ Click on the name of a plugin or module to view that content's documentation: - [k8s_info](https://docs.ansible.com/ansible/latest/modules/k8s_info_module.html) - [k8s_scale](https://docs.ansible.com/ansible/latest/modules/k8s_scale_module.html) - [k8s_service](https://docs.ansible.com/ansible/latest/modules/k8s_service_module.html) - - [helm_cli](https://docs.ansible.com/ansible/latest/modules/helm_cli_module.html) - - [helm_cli_info](https://docs.ansible.com/ansible/latest/modules/helm_cli_info_module.html) + - [helm](https://docs.ansible.com/ansible/latest/modules/helm_module.html) + - [helm_info](https://docs.ansible.com/ansible/latest/modules/helm_info_module.html) ## Installation and Usage diff --git a/molecule/default/roles/helm/tasks/run_test.yml b/molecule/default/roles/helm/tasks/run_test.yml index f772ad14..a67a1e8f 100644 --- a/molecule/default/roles/helm/tasks/run_test.yml +++ b/molecule/default/roles/helm/tasks/run_test.yml @@ -1,12 +1,12 @@ --- -- name: Ensure helm is not install +- name: Ensure helm is not installed file: path: "{{ item }}" state: absent with_items: - "/tmp/helm" -- name: Check failed if helm is not install +- name: Check failed if helm is not installed include_tasks: test_helm_not_installed.yml - name: "Install {{ helm_version }}" diff --git a/molecule/default/roles/helm/tasks/test_helm_not_installed.yml b/molecule/default/roles/helm/tasks/test_helm_not_installed.yml index 63cfb394..0832dcb0 100644 --- a/molecule/default/roles/helm/tasks/test_helm_not_installed.yml +++ b/molecule/default/roles/helm/tasks/test_helm_not_installed.yml @@ -1,6 +1,6 @@ --- -- name: Failed test when helm is not install - helm_cli: +- name: Failed test when helm is not installed + helm: binary_path: "{{ helm_binary}}_fake" name: test chart_ref: "{{ chart_test }}" diff --git a/molecule/default/roles/helm/tasks/tests_chart.yml b/molecule/default/roles/helm/tasks/tests_chart.yml index 5ddbf0c7..eb6d518f 100644 --- a/molecule/default/roles/helm/tasks/tests_chart.yml +++ b/molecule/default/roles/helm/tasks/tests_chart.yml @@ -6,24 +6,24 @@ name: "{{ helm_namespace }}" wait: true -- name: Check helm_cli_info empty - helm_cli_info: - binary_path: "{{ helm_binary}}" +- name: Check helm_info empty + helm_info: + binary_path: "{{ helm_binary }}" name: test namespace: "{{ helm_namespace }}" register: empty_info -- name: "Assert that no chart are installed with helm_cli_info" +- name: "Assert that no charts are installed with helm_info" assert: that: - empty_info.status is undefined - name: "Install {{ chart_test }} from {{ source }}" - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source }}" - chart_version: "{{ chart_source_version | default(omit)}}" + chart_version: "{{ chart_source_version | default(omit) }}" namespace: "{{ helm_namespace }}" register: install @@ -34,25 +34,25 @@ - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" - install.status.status | lower == 'deployed' -- name: Check helm_cli_info content - helm_cli_info: - binary_path: "{{ helm_binary}}" +- name: Check helm_info content + helm_info: + binary_path: "{{ helm_binary }}" name: test namespace: "{{ helm_namespace }}" register: content_info -- name: "Assert that {{ chart_test }} is installed from {{ source }} with helm_cli_info" +- name: "Assert that {{ chart_test }} is installed from {{ source }} with helm_info" assert: that: - content_info.status.chart == "{{ chart_test }}-{{ chart_test_version }}" - content_info.status.status | lower == 'deployed' - name: Check idempotency - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source }}" - chart_version: "{{ chart_source_version | default(omit)}}" + chart_version: "{{ chart_source_version | default(omit) }}" namespace: "{{ helm_namespace }}" register: install @@ -64,11 +64,11 @@ - install.status.status | lower == 'deployed' - name: "Add vars to {{ chart_test }} from {{ source }}" - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source }}" - chart_version: "{{ chart_source_version | default(omit)}}" + chart_version: "{{ chart_source_version | default(omit) }}" namespace: "{{ helm_namespace }}" values: "{{ chart_test_values }}" register: install @@ -81,12 +81,12 @@ - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" - "install.status['values'].revisionHistoryLimit == 0" -- name: Check idempotency after add vars - helm_cli: - binary_path: "{{ helm_binary}}" +- name: Check idempotency after adding vars + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source }}" - chart_version: "{{ chart_source_version | default(omit)}}" + chart_version: "{{ chart_source_version | default(omit) }}" namespace: "{{ helm_namespace }}" values: "{{ chart_test_values }}" register: install @@ -100,11 +100,11 @@ - "install.status['values'].revisionHistoryLimit == 0" - name: "Remove Vars to {{ chart_test }} from {{ source }}" - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source }}" - chart_version: "{{ chart_source_version | default(omit)}}" + chart_version: "{{ chart_source_version | default(omit) }}" namespace: "{{ helm_namespace }}" register: install @@ -116,16 +116,16 @@ - install.status.chart == "{{ chart_test }}-{{ chart_test_version }}" - install.status['values'] == {} -- name: Check idempotency after remove vars - helm_cli: - binary_path: "{{ helm_binary}}" +- name: Check idempotency after removing vars + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source }}" - chart_version: "{{ chart_source_version | default(omit)}}" + chart_version: "{{ chart_source_version | default(omit) }}" namespace: "{{ helm_namespace }}" register: install -- name: Assert idempotency after remove vars +- name: Assert idempotency after removing vars assert: that: - install is not changed @@ -134,11 +134,11 @@ - install.status['values'] == {} - name: "Upgrade {{ chart_test }} from {{ source }}" - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source_upgrade | default(chart_source) }}" - chart_version: "{{ chart_source_version_upgrade | default(omit)}}" + chart_version: "{{ chart_source_version_upgrade | default(omit) }}" namespace: "{{ helm_namespace }}" register: install @@ -150,11 +150,11 @@ - install.status.chart == "{{ chart_test }}-{{ chart_test_version_upgrade }}" - name: Check idempotency after upgrade - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" name: test chart_ref: "{{ chart_source_upgrade | default(chart_source) }}" - chart_version: "{{ chart_source_version_upgrade | default(omit)}}" + chart_version: "{{ chart_source_version_upgrade | default(omit) }}" namespace: "{{ helm_namespace }}" register: install @@ -166,21 +166,21 @@ - install.status.chart == "{{ chart_test }}-{{ chart_test_version_upgrade }}" - name: "Remove {{ chart_test }} from {{ source }}" - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" state: absent name: test namespace: "{{ helm_namespace }}" register: install -- name: "Assert that {{ chart_test }} chart is remove from {{ source }}" +- name: "Assert that {{ chart_test }} chart is removed from {{ source }}" assert: that: - install is changed - name: Check idempotency after remove - helm_cli: - binary_path: "{{ helm_binary}}" + helm: + binary_path: "{{ helm_binary }}" state: absent name: test namespace: "{{ helm_namespace }}" diff --git a/molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml b/molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml deleted file mode 100644 index 4ac25c80..00000000 --- a/molecule/default/roles/helm/templates/tiller-clusterRoleBinding.yml +++ /dev/null @@ -1,13 +0,0 @@ ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: tiller -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: "{{ tiller_cluster_role }}" -subjects: - - kind: ServiceAccount - name: tiller - namespace: "{{ tiller_namespace }}" diff --git a/molecule/default/roles/helm/templates/tiller-sa.yml b/molecule/default/roles/helm/templates/tiller-sa.yml deleted file mode 100644 index 7adbd3c9..00000000 --- a/molecule/default/roles/helm/templates/tiller-sa.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -kind: ServiceAccount -apiVersion: v1 -metadata: - name: tiller - namespace: "{{ tiller_namespace }}" diff --git a/plugins/modules/helm_cli.py b/plugins/modules/helm.py similarity index 99% rename from plugins/modules/helm_cli.py rename to plugins/modules/helm.py index a469d88d..7f5b125d 100644 --- a/plugins/modules/helm_cli.py +++ b/plugins/modules/helm.py @@ -12,7 +12,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- -module: helm_cli +module: helm short_description: Manages Kubernetes packages with the Helm package manager @@ -138,7 +138,7 @@ EXAMPLES = ''' shell: "helm repo add stable https://kubernetes-charts.storage.googleapis.com" - name: Deploy latest version of Grafana chart inside monitoring namespace with values - helm_cli: + helm: name: test chart_ref: stable/grafana release_namespace: monitoring @@ -146,14 +146,14 @@ EXAMPLES = ''' replicas: 2 - name: Deploy Grafana chart on 5.0.12 with values loaded from template - helm_cli: + helm: name: test chart_ref: stable/grafana chart_version: 5.0.12 values: "{{ lookup('template', 'somefile.yaml') | from_yaml }}" - name: Remove test release and waiting suppression ending - helm_cli: + helm: name: test state: absent wait: true @@ -165,14 +165,14 @@ EXAMPLES = ''' dest: /tmp/helm_repo - name: Deploy Grafana chart from local path - helm_cli: + helm: name: test chart_ref: /tmp/helm_repo/stable/grafana release_namespace: monitoring # From url - name: Deploy Grafana chart on 5.0.12 from url - helm_cli: + helm: name: test chart_ref: "https://kubernetes-charts.storage.googleapis.com/grafana-5.0.12.tgz" release_namespace: monitoring diff --git a/plugins/modules/helm_cli_info.py b/plugins/modules/helm_info.py similarity index 99% rename from plugins/modules/helm_cli_info.py rename to plugins/modules/helm_info.py index 2b513b02..07697aa0 100644 --- a/plugins/modules/helm_cli_info.py +++ b/plugins/modules/helm_info.py @@ -12,7 +12,7 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', DOCUMENTATION = ''' --- -module: helm_cli_info +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 @@ -55,7 +55,7 @@ options: EXAMPLES = ''' - name: Deploy latest version of Grafana chart inside monitoring namespace - helm_cli_info: + helm_info: name: test release_namespace: monitoring ''' From 1a2399444877ea1f58ba2b138fd1f425ce4a3366 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Sat, 25 Apr 2020 12:20:12 +0200 Subject: [PATCH 6/7] Add helm_repository module and fix review suggestion --- .../default/roles/helm/tasks/run_test.yml | 3 + .../tasks/tests_chart/from_repository.yml | 12 +- .../roles/helm/tasks/tests_repository.yml | 61 +++++ plugins/modules/helm.py | 11 +- plugins/modules/helm_info.py | 10 +- plugins/modules/helm_repository.py | 208 ++++++++++++++++++ 6 files changed, 293 insertions(+), 12 deletions(-) create mode 100644 molecule/default/roles/helm/tasks/tests_repository.yml create mode 100644 plugins/modules/helm_repository.py 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() From 9f788c63e7c57b432d14d41caeecd56d7542e377 Mon Sep 17 00:00:00 2001 From: LucasBoisserie Date: Mon, 27 Apr 2020 18:03:39 +0200 Subject: [PATCH 7/7] Missing helm_repository --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fd8ea067..ddee734b 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Click on the name of a plugin or module to view that content's documentation: - [k8s_service](https://docs.ansible.com/ansible/latest/modules/k8s_service_module.html) - [helm](https://docs.ansible.com/ansible/latest/modules/helm_module.html) - [helm_info](https://docs.ansible.com/ansible/latest/modules/helm_info_module.html) + - [helm_repository](https://docs.ansible.com/ansible/latest/modules/helm_repository_module.html) ## Installation and Usage