diff --git a/changelogs/fragments/497-helm-add-support-for-in-memory-kubeconfig.yml b/changelogs/fragments/497-helm-add-support-for-in-memory-kubeconfig.yml new file mode 100644 index 00000000..acbcd9c7 --- /dev/null +++ b/changelogs/fragments/497-helm-add-support-for-in-memory-kubeconfig.yml @@ -0,0 +1,2 @@ +minor_changes: + - helm, helm_plugin, helm_info, helm_plugin_info, kubectl - add support for in-memory kubeconfig. (https://github.com/ansible-collections/kubernetes.core/issues/492). diff --git a/plugins/connection/kubectl.py b/plugins/connection/kubectl.py index 4a4452ad..d0c3baa8 100644 --- a/plugins/connection/kubectl.py +++ b/plugins/connection/kubectl.py @@ -75,6 +75,7 @@ DOCUMENTATION = r""" kubectl_kubeconfig: description: - Path to a kubectl config file. Defaults to I(~/.kube/config) + - The configuration can be provided as dictionary. Added in version 2.4.0. default: '' vars: - name: ansible_kubectl_kubeconfig @@ -175,6 +176,8 @@ import os import os.path import shutil import subprocess +import tempfile +import json from ansible.parsing.yaml.loader import AnsibleLoader from ansible.errors import AnsibleError, AnsibleFileNotFound @@ -222,6 +225,12 @@ class Connection(ConnectionBase): self.transport_cmd = kwargs.get(cmd_arg, shutil.which(self.transport)) if not self.transport_cmd: raise AnsibleError("{0} command not found in PATH".format(self.transport)) + self._file_to_delete = None + + def delete_temporary_file(self): + if self._file_to_delete is not None: + os.remove(self._file_to_delete) + self._file_to_delete = None def _build_exec_cmd(self, cmd): """Build the local kubectl exec command to run cmd on remote_host""" @@ -244,6 +253,18 @@ class Connection(ConnectionBase): self.connection_options[key], str(skip_verify_ssl).lower() ) ) + elif key.endswith("kubeconfig") and self.get_option(key) != "": + kubeconfig_path = self.get_option(key) + if isinstance(kubeconfig_path, dict): + fd, tmpfile = tempfile.mkstemp() + with os.fdopen(fd, "w") as fp: + json.dump(kubeconfig_path, fp) + kubeconfig_path = tmpfile + self._file_to_delete = tmpfile + + cmd_arg = self.connection_options[key] + local_cmd += [cmd_arg, kubeconfig_path] + censored_local_cmd += [cmd_arg, kubeconfig_path] elif ( not key.endswith("container") and self.get_option(key) @@ -311,6 +332,7 @@ class Connection(ConnectionBase): ) stdout, stderr = p.communicate(in_data) + self.delete_temporary_file() return (p.returncode, stdout, stderr) def _prefix_login_path(self, remote_path): @@ -363,6 +385,7 @@ class Connection(ConnectionBase): "kubectl connection requires dd command in the container to put files" ) stdout, stderr = p.communicate() + self.delete_temporary_file() if p.returncode != 0: raise AnsibleError( @@ -401,6 +424,7 @@ class Connection(ConnectionBase): ) ) stdout, stderr = p.communicate() + self.delete_temporary_file() if p.returncode != 0: raise AnsibleError( diff --git a/plugins/doc_fragments/helm_common_options.py b/plugins/doc_fragments/helm_common_options.py index 7085a046..dde91db1 100644 --- a/plugins/doc_fragments/helm_common_options.py +++ b/plugins/doc_fragments/helm_common_options.py @@ -30,7 +30,8 @@ options: description: - Helm option to specify kubeconfig path to use. - If the value is not specified in the task, the value of environment variable C(K8S_AUTH_KUBECONFIG) will be used instead. - type: path + - The configuration can be provided as dictionary. Added in version 2.4.0. + type: raw aliases: [ kubeconfig_path ] host: description: diff --git a/plugins/module_utils/helm.py b/plugins/module_utils/helm.py index 685b585e..07e64589 100644 --- a/plugins/module_utils/helm.py +++ b/plugins/module_utils/helm.py @@ -7,14 +7,14 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -from contextlib import contextmanager import os import tempfile import traceback import re +import json from ansible.module_utils.basic import missing_required_lib - +from ansible.module_utils.six import string_types try: import yaml @@ -25,11 +25,18 @@ except ImportError: HAS_YAML = False -@contextmanager def prepare_helm_environ_update(module): environ_update = {} - file_to_cleam_up = None - kubeconfig_path = module.params.get("kubeconfig") + kubeconfig_path = None + if module.params.get("kubeconfig") is not None: + kubeconfig = module.params.get("kubeconfig") + if isinstance(kubeconfig, string_types): + kubeconfig_path = kubeconfig + elif isinstance(kubeconfig, dict): + fd, kubeconfig_path = tempfile.mkstemp() + with os.fdopen(fd, "w") as fp: + json.dump(kubeconfig, fp) + module.add_cleanup_file(kubeconfig_path) if module.params.get("context") is not None: environ_update["HELM_KUBECONTEXT"] = module.params.get("context") if module.params.get("release_namespace"): @@ -44,23 +51,19 @@ def prepare_helm_environ_update(module): validate_certs=module.params["validate_certs"], ca_cert=module.params["ca_cert"], ) - file_to_cleam_up = kubeconfig_path + module.add_cleanup_file(kubeconfig_path) if kubeconfig_path is not None: environ_update["KUBECONFIG"] = kubeconfig_path - try: - yield environ_update - finally: - if file_to_cleam_up: - os.remove(file_to_cleam_up) + return environ_update def run_helm(module, command, fails_on_error=True): if not HAS_YAML: module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) - with prepare_helm_environ_update(module) as environ_update: - rc, out, err = module.run_command(command, environ_update=environ_update) + environ_update = prepare_helm_environ_update(module) + rc, out, err = module.run_command(command, environ_update=environ_update) if fails_on_error and rc != 0: module.fail_json( msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format( @@ -118,7 +121,7 @@ def get_helm_plugin_list(module, helm_bin=None): """ if not helm_bin: return [] - helm_plugin_list = helm_bin + " list" + helm_plugin_list = helm_bin + " plugin list" rc, out, err = run_helm(module, helm_plugin_list) if rc != 0 or (out == "" and err == ""): module.fail_json( @@ -162,3 +165,9 @@ def get_helm_version(module, helm_bin): if m: return m.group(1) return None + + +def get_helm_binary(module): + return module.params.get("binary_path") or module.get_bin_path( + "helm", required=True + ) diff --git a/plugins/module_utils/helm_args_common.py b/plugins/module_utils/helm_args_common.py new file mode 100644 index 00000000..df4d8f33 --- /dev/null +++ b/plugins/module_utils/helm_args_common.py @@ -0,0 +1,44 @@ +from __future__ import absolute_import, division, print_function + +from ansible.module_utils.basic import env_fallback + +__metaclass__ = type + + +HELM_AUTH_ARG_SPEC = dict( + binary_path=dict(type="path"), + context=dict( + type="str", + aliases=["kube_context"], + fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), + ), + kubeconfig=dict( + type="raw", + aliases=["kubeconfig_path"], + fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]), + ), + host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), + ca_cert=dict( + type="path", + aliases=["ssl_ca_cert"], + fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), + ), + validate_certs=dict( + type="bool", + default=True, + aliases=["verify_ssl"], + fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]), + ), + api_key=dict( + type="str", + no_log=True, + fallback=(env_fallback, ["K8S_AUTH_API_KEY"]), + ), +) + +HELM_AUTH_MUTUALLY_EXCLUSIVE = [ + ("context", "ca_cert"), + ("context", "validate_certs"), + ("kubeconfig", "ca_cert"), + ("kubeconfig", "validate_certs"), +] diff --git a/plugins/modules/helm.py b/plugins/modules/helm.py index e13bc846..b1b77f63 100644 --- a/plugins/modules/helm.py +++ b/plugins/modules/helm.py @@ -336,6 +336,7 @@ command: import re import tempfile import traceback +import copy from ansible_collections.kubernetes.core.plugins.module_utils.version import ( LooseVersion, ) @@ -348,13 +349,17 @@ except ImportError: IMP_YAML_ERR = traceback.format_exc() IMP_YAML = False -from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( run_helm, get_values, get_helm_plugin_list, parse_helm_plugin_list, get_helm_version, + get_helm_binary, +) +from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import ( + HELM_AUTH_ARG_SPEC, ) @@ -531,13 +536,12 @@ def load_values_files(values_files): return values -def get_plugin_version(command, plugin): +def get_plugin_version(helm_bin, plugin): """ Check if helm plugin is installed and return corresponding version """ - cmd = command + " plugin" - rc, output, err = get_helm_plugin_list(module, helm_bin=cmd) + rc, output, err = get_helm_plugin_list(module, helm_bin=helm_bin) out = parse_helm_plugin_list(module, output=output.splitlines()) if not out: @@ -612,11 +616,10 @@ def default_check(release_status, chart_info, values=None, values_files=None): ) -def main(): - global module - module = AnsibleModule( - argument_spec=dict( - binary_path=dict(type="path"), +def argument_spec(): + arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC) + arg_spec.update( + dict( chart_ref=dict(type="path"), chart_repo_url=dict(type="str"), chart_version=dict(type="str"), @@ -629,19 +632,8 @@ def main(): release_values=dict(type="dict", default={}, aliases=["values"]), values_files=dict(type="list", default=[], elements="str"), update_repo_cache=dict(type="bool", default=False), - # Helm options disable_hook=dict(type="bool", default=False), force=dict(type="bool", default=False), - context=dict( - type="str", - aliases=["kube_context"], - fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), - ), - kubeconfig=dict( - type="path", - aliases=["kubeconfig_path"], - fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]), - ), purge=dict(type="bool", default=True), wait=dict(type="bool", default=False), wait_timeout=dict(type="str"), @@ -652,23 +644,15 @@ def main(): replace=dict(type="bool", default=False), skip_crds=dict(type="bool", default=False), history_max=dict(type="int"), - # Generic auth key - host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), - ca_cert=dict( - type="path", - aliases=["ssl_ca_cert"], - fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), - ), - validate_certs=dict( - type="bool", - default=True, - aliases=["verify_ssl"], - fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]), - ), - api_key=dict( - type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"]) - ), - ), + ) + ) + return arg_spec + + +def main(): + global module + module = AnsibleModule( + argument_spec=argument_spec(), required_if=[ ("release_state", "present", ["release_name", "chart_ref"]), ("release_state", "absent", ["release_name"]), @@ -687,7 +671,6 @@ def main(): 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_version = module.params.get("chart_version") @@ -712,10 +695,8 @@ def main(): history_max = module.params.get("history_max") timeout = module.params.get("timeout") - if bin_path is not None: - helm_cmd_common = bin_path - else: - helm_cmd_common = module.get_bin_path("helm", required=True) + helm_cmd_common = get_helm_binary(module) + helm_bin = helm_cmd_common if update_repo_cache: run_repo_update(module, helm_cmd_common) @@ -808,7 +789,7 @@ def main(): else: - helm_diff_version = get_plugin_version(helm_cmd_common, "diff") + helm_diff_version = get_plugin_version(helm_bin, "diff") if helm_diff_version and ( not chart_repo_url or ( diff --git a/plugins/modules/helm_info.py b/plugins/modules/helm_info.py index b736a311..4542450e 100644 --- a/plugins/modules/helm_info.py +++ b/plugins/modules/helm_info.py @@ -112,6 +112,7 @@ status: """ import traceback +import copy try: import yaml @@ -121,10 +122,15 @@ except ImportError: IMP_YAML_ERR = traceback.format_exc() IMP_YAML = False -from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( run_helm, get_values, + get_helm_binary, +) +from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import ( + HELM_AUTH_ARG_SPEC, + HELM_AUTH_MUTUALLY_EXCLUSIVE, ) @@ -176,63 +182,34 @@ def get_release_status(module, command, release_name, release_state): return release +def argument_spec(): + arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC) + arg_spec.update( + dict( + release_name=dict(type="str", required=True, aliases=["name"]), + release_namespace=dict(type="str", required=True, aliases=["namespace"]), + release_state=dict(type="list", default=[], elements="str"), + ) + ) + return arg_spec + + def main(): 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"]), - # Helm options - context=dict( - type="str", - aliases=["kube_context"], - fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), - ), - kubeconfig=dict( - type="path", - aliases=["kubeconfig_path"], - fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]), - ), - # Generic auth key - host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), - ca_cert=dict( - type="path", - aliases=["ssl_ca_cert"], - fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), - ), - validate_certs=dict( - type="bool", - default=True, - aliases=["verify_ssl"], - fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]), - ), - api_key=dict( - type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"]) - ), - release_state=dict(type="list", default=[], elements="str"), - ), - mutually_exclusive=[ - ("context", "ca_cert"), - ("context", "validate_certs"), - ("kubeconfig", "ca_cert"), - ("kubeconfig", "validate_certs"), - ], + argument_spec=argument_spec(), + mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE, supports_check_mode=True, ) 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") release_state = module.params.get("release_state") - if bin_path is not None: - helm_cmd_common = bin_path - else: - helm_cmd_common = module.get_bin_path("helm", required=True) + helm_cmd_common = get_helm_binary(module) release_status = get_release_status( module, helm_cmd_common, release_name, release_state diff --git a/plugins/modules/helm_plugin.py b/plugins/modules/helm_plugin.py index 0597f0ff..ad8a55ea 100644 --- a/plugins/modules/helm_plugin.py +++ b/plugins/modules/helm_plugin.py @@ -108,21 +108,24 @@ rc: sample: 1 """ -from ansible.module_utils.basic import AnsibleModule, env_fallback +import copy +from ansible.module_utils.basic import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( run_helm, get_helm_plugin_list, parse_helm_plugin_list, + get_helm_binary, +) +from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import ( + HELM_AUTH_ARG_SPEC, + HELM_AUTH_MUTUALLY_EXCLUSIVE, ) -def main(): - module = AnsibleModule( - argument_spec=dict( - binary_path=dict(type="path"), - state=dict( - type="str", default="present", choices=["present", "absent", "latest"] - ), +def argument_spec(): + arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC) + arg_spec.update( + dict( plugin_path=dict( type="str", ), @@ -132,60 +135,38 @@ def main(): plugin_version=dict( type="str", ), - # Helm options - context=dict( + state=dict( type="str", - aliases=["kube_context"], - fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), + default="present", + choices=["present", "absent", "latest"], ), - kubeconfig=dict( - type="path", - aliases=["kubeconfig_path"], - fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]), - ), - # Generic auth key - host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), - ca_cert=dict( - type="path", - aliases=["ssl_ca_cert"], - fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), - ), - validate_certs=dict( - type="bool", - default=True, - aliases=["verify_ssl"], - fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]), - ), - api_key=dict( - type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"]) - ), - ), + ) + ) + return arg_spec + + +def mutually_exclusive(): + mutually_ex = copy.deepcopy(HELM_AUTH_MUTUALLY_EXCLUSIVE) + mutually_ex.append(("plugin_name", "plugin_path")) + return mutually_ex + + +def main(): + module = AnsibleModule( + argument_spec=argument_spec(), supports_check_mode=True, required_if=[ ("state", "present", ("plugin_path",)), ("state", "absent", ("plugin_name",)), ("state", "latest", ("plugin_name",)), ], - mutually_exclusive=[ - ("plugin_name", "plugin_path"), - ("context", "ca_cert"), - ("context", "validate_certs"), - ("kubeconfig", "ca_cert"), - ("kubeconfig", "validate_certs"), - ], + mutually_exclusive=mutually_exclusive(), ) - bin_path = module.params.get("binary_path") state = module.params.get("state") + helm_bin = get_helm_binary(module) - if bin_path is not None: - helm_cmd_common = bin_path - else: - helm_cmd_common = "helm" - - helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True) - - helm_cmd_common += " plugin" + helm_cmd_common = helm_bin + " plugin" if state == "present": helm_cmd_common += " install %s" % module.params.get("plugin_path") @@ -227,7 +208,7 @@ def main(): ) elif state == "absent": plugin_name = module.params.get("plugin_name") - rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common) + rc, output, err = get_helm_plugin_list(module, helm_bin=helm_bin) out = parse_helm_plugin_list(module, output=output.splitlines()) if not out: @@ -281,7 +262,7 @@ def main(): ) elif state == "latest": plugin_name = module.params.get("plugin_name") - rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common) + rc, output, err = get_helm_plugin_list(module, helm_bin=helm_bin) out = parse_helm_plugin_list(module, output=output.splitlines()) if not out: diff --git a/plugins/modules/helm_plugin_info.py b/plugins/modules/helm_plugin_info.py index b80a7385..fefe3645 100644 --- a/plugins/modules/helm_plugin_info.py +++ b/plugins/modules/helm_plugin_info.py @@ -70,73 +70,43 @@ rc: sample: 1 """ -from ansible.module_utils.basic import AnsibleModule, env_fallback +import copy +from ansible.module_utils.basic import AnsibleModule from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( get_helm_plugin_list, parse_helm_plugin_list, + get_helm_binary, +) +from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import ( + HELM_AUTH_ARG_SPEC, + HELM_AUTH_MUTUALLY_EXCLUSIVE, ) def main(): - module = AnsibleModule( - argument_spec=dict( - binary_path=dict(type="path"), + + argument_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC) + argument_spec.update( + dict( plugin_name=dict( type="str", ), - # Helm options - context=dict( - type="str", - aliases=["kube_context"], - fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), - ), - kubeconfig=dict( - type="path", - aliases=["kubeconfig_path"], - fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]), - ), - # Generic auth key - host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), - ca_cert=dict( - type="path", - aliases=["ssl_ca_cert"], - fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), - ), - validate_certs=dict( - type="bool", - default=True, - aliases=["verify_ssl"], - fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]), - ), - api_key=dict( - type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"]) - ), - ), - mutually_exclusive=[ - ("context", "ca_cert"), - ("context", "validate_certs"), - ("kubeconfig", "ca_cert"), - ("kubeconfig", "validate_certs"), - ], + ) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE, supports_check_mode=True, ) - bin_path = module.params.get("binary_path") - - if bin_path is not None: - helm_cmd_common = bin_path - else: - helm_cmd_common = "helm" - - helm_cmd_common = module.get_bin_path(helm_cmd_common, required=True) - - helm_cmd_common += " plugin" + helm_bin = get_helm_binary(module) plugin_name = module.params.get("plugin_name") plugin_list = [] - rc, output, err = get_helm_plugin_list(module, helm_bin=helm_cmd_common) + rc, output, err = get_helm_plugin_list(module, helm_bin=helm_bin) out = parse_helm_plugin_list(module, output=output.splitlines()) @@ -155,7 +125,7 @@ def main(): module.exit_json( changed=True, - command=helm_cmd_common + " list", + command=helm_bin + " plugin list", stdout=output, stderr=err, rc=rc, diff --git a/plugins/modules/helm_repository.py b/plugins/modules/helm_repository.py index 1125e5e6..03a0e21d 100644 --- a/plugins/modules/helm_repository.py +++ b/plugins/modules/helm_repository.py @@ -97,6 +97,21 @@ options: type: path aliases: [ ssl_ca_cert ] version_added: "2.3.0" + context: + description: + - Helm option to specify which kubeconfig context to use. + - If the value is not specified in the task, the value of environment variable C(K8S_AUTH_CONTEXT) will be used instead. + type: str + aliases: [ kube_context ] + version_added: "2.4.0" + kubeconfig: + description: + - Helm option to specify kubeconfig path to use. + - If the value is not specified in the task, the value of environment variable C(K8S_AUTH_KUBECONFIG) will be used instead. + - The configuration can be provided as dictionary. + type: raw + aliases: [ kubeconfig_path ] + version_added: "2.4.0" """ EXAMPLES = r""" @@ -145,6 +160,7 @@ msg: """ import traceback +import copy try: import yaml @@ -154,8 +170,15 @@ except ImportError: IMP_YAML_ERR = traceback.format_exc() IMP_YAML = False -from ansible.module_utils.basic import AnsibleModule, env_fallback, missing_required_lib -from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( + run_helm, + get_helm_binary, +) +from ansible_collections.kubernetes.core.plugins.module_utils.helm_args_common import ( + HELM_AUTH_ARG_SPEC, + HELM_AUTH_MUTUALLY_EXCLUSIVE, +) # Get repository from all repositories added @@ -215,12 +238,10 @@ def delete_repository(command, repository_name): return remove_command -def main(): - global module - - module = AnsibleModule( - argument_spec=dict( - binary_path=dict(type="path"), +def argument_spec(): + arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC) + arg_spec.update( + dict( repo_name=dict(type="str", aliases=["name"], required=True), repo_url=dict(type="str", aliases=["url"]), repo_username=dict(type="str", aliases=["username"]), @@ -229,25 +250,19 @@ def main(): default="present", choices=["present", "absent"], aliases=["state"] ), pass_credentials=dict(type="bool", default=False, no_log=True), - # Generic auth key - host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), - ca_cert=dict( - type="path", - aliases=["ssl_ca_cert"], - fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), - ), - validate_certs=dict( - type="bool", - default=True, - aliases=["verify_ssl"], - fallback=(env_fallback, ["K8S_AUTH_VERIFY_SSL"]), - ), - api_key=dict( - type="str", no_log=True, fallback=(env_fallback, ["K8S_AUTH_API_KEY"]) - ), - ), + ) + ) + return arg_spec + + +def main(): + global module + + module = AnsibleModule( + argument_spec=argument_spec(), required_together=[["repo_username", "repo_password"]], required_if=[("repo_state", "present", ["repo_url"])], + mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE, supports_check_mode=True, ) @@ -256,7 +271,6 @@ def main(): 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") @@ -264,10 +278,7 @@ def main(): repo_state = module.params.get("repo_state") pass_credentials = module.params.get("pass_credentials") - if bin_path is not None: - helm_cmd = bin_path - else: - helm_cmd = module.get_bin_path("helm", required=True) + helm_cmd = get_helm_binary(module) repository_status = get_repository_status(module, helm_cmd, repo_name) diff --git a/plugins/modules/helm_template.py b/plugins/modules/helm_template.py index 7bd59196..9141e855 100644 --- a/plugins/modules/helm_template.py +++ b/plugins/modules/helm_template.py @@ -181,7 +181,10 @@ except ImportError: IMP_YAML = False from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible_collections.kubernetes.core.plugins.module_utils.helm import run_helm +from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( + run_helm, + get_helm_binary, +) def template( @@ -266,7 +269,6 @@ def main(): ) check_mode = module.check_mode - bin_path = module.params.get("binary_path") chart_ref = module.params.get("chart_ref") chart_repo_url = module.params.get("chart_repo_url") chart_version = module.params.get("chart_version") @@ -284,7 +286,7 @@ def main(): if not IMP_YAML: module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) - helm_cmd = bin_path or module.get_bin_path("helm", required=True) + helm_cmd = get_helm_binary(module) if update_repo_cache: update_cmd = helm_cmd + " repo update" diff --git a/tests/integration/targets/helm/defaults/main.yml b/tests/integration/targets/helm/defaults/main.yml index e43d3cbc..60a68664 100644 --- a/tests/integration/targets/helm/defaults/main.yml +++ b/tests/integration/targets/helm/defaults/main.yml @@ -26,3 +26,4 @@ test_namespace: - "helm-local-path-002" - "helm-local-path-003" - "helm-dep" + - "helm-kubeconfig" diff --git a/tests/integration/targets/helm/tasks/run_test.yml b/tests/integration/targets/helm/tasks/run_test.yml index dbe918de..b91d642a 100644 --- a/tests/integration/targets/helm/tasks/run_test.yml +++ b/tests/integration/targets/helm/tasks/run_test.yml @@ -43,6 +43,9 @@ - name: Test Skip CRDS feature in helm chart install include_tasks: test_crds.yml +- name: Test in-memory kubeconfig + include_tasks: tests_in_memory_kubeconfig.yml + - name: Clean helm install file: path: "{{ item }}" diff --git a/tests/integration/targets/helm/tasks/tests_in_memory_kubeconfig.yml b/tests/integration/targets/helm/tasks/tests_in_memory_kubeconfig.yml new file mode 100644 index 00000000..f1bd831e --- /dev/null +++ b/tests/integration/targets/helm/tasks/tests_in_memory_kubeconfig.yml @@ -0,0 +1,164 @@ +--- +- set_fact: + custom_kubeconfig_path: "~/.kube/customconfig" + default_kubeconfig_path: "~/.kube/config" + helm_in_mem_kubeconf_ns: "{{ test_namespace[11] }}" + +- block: + - name: Copy default kubeconfig + copy: + remote_src: true + src: "{{ default_kubeconfig_path }}" + dest: "{{ custom_kubeconfig_path }}" + + - name: Delete default kubeconfig + file: + path: "{{ default_kubeconfig_path }}" + state: absent + + - set_fact: + custom_kubeconfig: "{{ lookup('file', custom_kubeconfig_path) | from_yaml }}" + no_log: true + + # helm_plugin and helm_plugin_info + - name: Install subenv plugin + helm_plugin: + binary_path: "{{ helm_binary }}" + kubeconfig: "{{ custom_kubeconfig }}" + state: present + plugin_path: https://github.com/hydeenoble/helm-subenv + register: plugin + + - assert: + that: + - plugin is changed + + - name: Gather info about all plugin + helm_plugin_info: + binary_path: "{{ helm_binary }}" + kubeconfig: "{{ custom_kubeconfig }}" + register: plugin_info + + - assert: + that: + - '"plugin_list" in plugin_info' + - plugin_info.plugin_list != [] + + # helm_repository, helm, helm_info + - name: Add test_bitnami chart repository + helm_repository: + binary_path: "{{ helm_binary }}" + name: test_bitnami + kubeconfig: "{{ custom_kubeconfig }}" + repo_url: https://charts.bitnami.com/bitnami + register: repository + + - name: Assert that repository was added + assert: + that: + - repository is changed + + - name: Install chart from repository added before + helm: + binary_path: "{{ helm_binary }}" + name: rabbitmq + chart_ref: test_bitnami/rabbitmq + namespace: "{{ helm_in_mem_kubeconf_ns }}" + update_repo_cache: true + kubeconfig: "{{ custom_kubeconfig }}" + create_namespace: true + register: deploy + + - name: Assert chart was successfully deployed + assert: + that: + - deploy is changed + + - name: Get chart content + helm_info: + binary_path: "{{ helm_binary }}" + kubeconfig: "{{ custom_kubeconfig }}" + name: "rabbitmq" + namespace: "{{ helm_in_mem_kubeconf_ns }}" + register: chart_info + + - name: Assert chart was successfully deployed + assert: + that: + - '"status" in chart_info' + - chart_info.status.status is defined + - chart_info.status.status == "deployed" + + - name: Remove chart + helm: + binary_path: "{{ helm_binary }}" + name: rabbitmq + namespace: "{{ helm_in_mem_kubeconf_ns }}" + kubeconfig: "{{ custom_kubeconfig }}" + state: absent + register: remove_chart + + - name: Assert chart was successfully removed + assert: + that: + - remove_chart is changed + + - name: Get chart content + helm_info: + binary_path: "{{ helm_binary }}" + kubeconfig: "{{ custom_kubeconfig }}" + name: "rabbitmq" + namespace: "{{ helm_in_mem_kubeconf_ns }}" + register: chart_info + + - name: Assert chart was successfully deployed + assert: + that: + - '"status" not in chart_info' + + - name: Remove chart repository + helm_repository: + binary_path: "{{ helm_binary }}" + name: test_bitnami + kubeconfig: "{{ custom_kubeconfig }}" + state: absent + register: remove + + - name: Assert that repository was removed + assert: + that: + - remove is changed + + always: + - name: Return kubeconfig + copy: + remote_src: true + src: "{{ custom_kubeconfig_path }}" + dest: "{{ default_kubeconfig_path }}" + ignore_errors: true + + - name: Delete custom config + file: + path: "{{ custom_kubeconfig_path }}" + state: absent + ignore_errors: true + + - name: Remove subenv plugin + helm_plugin: + binary_path: "{{ helm_binary }}" + plugin_name: subenv + state: absent + ignore_errors: true + + - name: Delete namespace + k8s: + kind: Namespace + name: "{{ helm_in_mem_kubeconf_ns }}" + ignore_errors: true + + - name: Delete helm repository + helm_repository: + binary_path: "{{ helm_binary }}" + name: test_bitnami + state: absent + ignore_errors: true diff --git a/tests/unit/module_utils/test_helm.py b/tests/unit/module_utils/test_helm.py index 129cde75..39ac33d3 100644 --- a/tests/unit/module_utils/test_helm.py +++ b/tests/unit/module_utils/test_helm.py @@ -7,8 +7,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import os.path - import yaml +import tempfile +import json from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( @@ -31,11 +32,22 @@ class MockedModule: } self.r = {} + self.files_to_delete = [] def run_command(self, command, environ_update=None): self.r = {"command": command, "environ_update": environ_update} return 0, "", "" + def add_cleanup_file(self, file_path): + self.files_to_delete.append(file_path) + + def do_cleanup_files(self): + for file in self.files_to_delete: + try: + os.remove(file) + except Exception: + pass + def test_write_temp_kubeconfig_server_only(): file_name = write_temp_kubeconfig("ff") @@ -97,4 +109,60 @@ def test_run_helm_with_params(): assert module.r["environ_update"]["HELM_KUBETOKEN"] == "my-api-key" assert module.r["environ_update"]["HELM_NAMESPACE"] == "a-release-namespace" assert module.r["environ_update"]["KUBECONFIG"] - assert not os.path.exists(module.r["environ_update"]["KUBECONFIG"]) + assert os.path.exists(module.r["environ_update"]["KUBECONFIG"]) + module.do_cleanup_files() + + +def test_run_helm_with_kubeconfig(): + + custom_config = { + "apiVersion": "v1", + "clusters": [ + { + "cluster": { + "certificate-authority-data": "LS0tLS1CRUdJTiBDRV", + "server": "https://api.cluster.testing:6443", + }, + "name": "api-cluster-testing:6443", + } + ], + "contexts": [ + { + "context": { + "cluster": "api-cluster-testing:6443", + "namespace": "default", + "user": "kubeadmin", + }, + "name": "context-1", + } + ], + "current-context": "context-1", + "kind": "Config", + "users": [ + { + "name": "developer", + "user": {"token": "sha256~jbIvVieBC_8W6Pb-iH5vqC_BvvPHIxQMxUPLDnYvHYM"}, + } + ], + } + + # kubeconfig defined as path + _fd, tmpfile_name = tempfile.mkstemp() + with os.fdopen(_fd, "w") as fp: + yaml.dump(custom_config, fp) + + k1_module = MockedModule() + k1_module.params = {"kubeconfig": tmpfile_name} + run_helm(k1_module, "helm foo") + assert k1_module.r["environ_update"] == {"KUBECONFIG": tmpfile_name} + os.remove(tmpfile_name) + + # kubeconfig defined as string + k2_module = MockedModule() + k2_module.params = {"kubeconfig": custom_config} + run_helm(k2_module, "helm foo") + + assert os.path.exists(k2_module.r["environ_update"]["KUBECONFIG"]) + with open(k2_module.r["environ_update"]["KUBECONFIG"]) as f: + assert json.loads(f.read()) == custom_config + k2_module.do_cleanup_files()