helm - add support for in-memory kubeconfig (#497)

helm - add support for in-memory kubeconfig

SUMMARY

closes #492

ISSUE TYPE


Feature Pull Request

Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Bikouo Aubin <None>
This commit is contained in:
Bikouo Aubin
2022-09-12 11:13:19 +02:00
committed by GitHub
parent 5ff3566f30
commit a3a5f3cf4b
15 changed files with 478 additions and 240 deletions

View File

@@ -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).

View File

@@ -75,6 +75,7 @@ DOCUMENTATION = r"""
kubectl_kubeconfig: kubectl_kubeconfig:
description: description:
- Path to a kubectl config file. Defaults to I(~/.kube/config) - 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: '' default: ''
vars: vars:
- name: ansible_kubectl_kubeconfig - name: ansible_kubectl_kubeconfig
@@ -175,6 +176,8 @@ import os
import os.path import os.path
import shutil import shutil
import subprocess import subprocess
import tempfile
import json
from ansible.parsing.yaml.loader import AnsibleLoader from ansible.parsing.yaml.loader import AnsibleLoader
from ansible.errors import AnsibleError, AnsibleFileNotFound from ansible.errors import AnsibleError, AnsibleFileNotFound
@@ -222,6 +225,12 @@ class Connection(ConnectionBase):
self.transport_cmd = kwargs.get(cmd_arg, shutil.which(self.transport)) self.transport_cmd = kwargs.get(cmd_arg, shutil.which(self.transport))
if not self.transport_cmd: if not self.transport_cmd:
raise AnsibleError("{0} command not found in PATH".format(self.transport)) 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): def _build_exec_cmd(self, cmd):
"""Build the local kubectl exec command to run cmd on remote_host""" """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() 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 ( elif (
not key.endswith("container") not key.endswith("container")
and self.get_option(key) and self.get_option(key)
@@ -311,6 +332,7 @@ class Connection(ConnectionBase):
) )
stdout, stderr = p.communicate(in_data) stdout, stderr = p.communicate(in_data)
self.delete_temporary_file()
return (p.returncode, stdout, stderr) return (p.returncode, stdout, stderr)
def _prefix_login_path(self, remote_path): 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" "kubectl connection requires dd command in the container to put files"
) )
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
self.delete_temporary_file()
if p.returncode != 0: if p.returncode != 0:
raise AnsibleError( raise AnsibleError(
@@ -401,6 +424,7 @@ class Connection(ConnectionBase):
) )
) )
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
self.delete_temporary_file()
if p.returncode != 0: if p.returncode != 0:
raise AnsibleError( raise AnsibleError(

View File

@@ -30,7 +30,8 @@ options:
description: description:
- Helm option to specify kubeconfig path to use. - 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. - 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 ] aliases: [ kubeconfig_path ]
host: host:
description: description:

View File

@@ -7,14 +7,14 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
from contextlib import contextmanager
import os import os
import tempfile import tempfile
import traceback import traceback
import re import re
import json
from ansible.module_utils.basic import missing_required_lib from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.six import string_types
try: try:
import yaml import yaml
@@ -25,11 +25,18 @@ except ImportError:
HAS_YAML = False HAS_YAML = False
@contextmanager
def prepare_helm_environ_update(module): def prepare_helm_environ_update(module):
environ_update = {} environ_update = {}
file_to_cleam_up = None kubeconfig_path = None
kubeconfig_path = module.params.get("kubeconfig") 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: if module.params.get("context") is not None:
environ_update["HELM_KUBECONTEXT"] = module.params.get("context") environ_update["HELM_KUBECONTEXT"] = module.params.get("context")
if module.params.get("release_namespace"): if module.params.get("release_namespace"):
@@ -44,23 +51,19 @@ def prepare_helm_environ_update(module):
validate_certs=module.params["validate_certs"], validate_certs=module.params["validate_certs"],
ca_cert=module.params["ca_cert"], ca_cert=module.params["ca_cert"],
) )
file_to_cleam_up = kubeconfig_path module.add_cleanup_file(kubeconfig_path)
if kubeconfig_path is not None: if kubeconfig_path is not None:
environ_update["KUBECONFIG"] = kubeconfig_path environ_update["KUBECONFIG"] = kubeconfig_path
try: return environ_update
yield environ_update
finally:
if file_to_cleam_up:
os.remove(file_to_cleam_up)
def run_helm(module, command, fails_on_error=True): def run_helm(module, command, fails_on_error=True):
if not HAS_YAML: if not HAS_YAML:
module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR) module.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
with prepare_helm_environ_update(module) as environ_update: environ_update = prepare_helm_environ_update(module)
rc, out, err = module.run_command(command, environ_update=environ_update) rc, out, err = module.run_command(command, environ_update=environ_update)
if fails_on_error and rc != 0: if fails_on_error and rc != 0:
module.fail_json( module.fail_json(
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format( 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: if not helm_bin:
return [] return []
helm_plugin_list = helm_bin + " list" helm_plugin_list = helm_bin + " plugin list"
rc, out, err = run_helm(module, helm_plugin_list) rc, out, err = run_helm(module, helm_plugin_list)
if rc != 0 or (out == "" and err == ""): if rc != 0 or (out == "" and err == ""):
module.fail_json( module.fail_json(
@@ -162,3 +165,9 @@ def get_helm_version(module, helm_bin):
if m: if m:
return m.group(1) return m.group(1)
return None return None
def get_helm_binary(module):
return module.params.get("binary_path") or module.get_bin_path(
"helm", required=True
)

View File

@@ -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"),
]

View File

@@ -336,6 +336,7 @@ command:
import re import re
import tempfile import tempfile
import traceback import traceback
import copy
from ansible_collections.kubernetes.core.plugins.module_utils.version import ( from ansible_collections.kubernetes.core.plugins.module_utils.version import (
LooseVersion, LooseVersion,
) )
@@ -348,13 +349,17 @@ except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False 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 ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm, run_helm,
get_values, get_values,
get_helm_plugin_list, get_helm_plugin_list,
parse_helm_plugin_list, parse_helm_plugin_list,
get_helm_version, 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 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 Check if helm plugin is installed and return corresponding version
""" """
cmd = command + " plugin" rc, output, err = get_helm_plugin_list(module, helm_bin=helm_bin)
rc, output, err = get_helm_plugin_list(module, helm_bin=cmd)
out = parse_helm_plugin_list(module, output=output.splitlines()) out = parse_helm_plugin_list(module, output=output.splitlines())
if not out: if not out:
@@ -612,11 +616,10 @@ def default_check(release_status, chart_info, values=None, values_files=None):
) )
def main(): def argument_spec():
global module arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
module = AnsibleModule( arg_spec.update(
argument_spec=dict( dict(
binary_path=dict(type="path"),
chart_ref=dict(type="path"), chart_ref=dict(type="path"),
chart_repo_url=dict(type="str"), chart_repo_url=dict(type="str"),
chart_version=dict(type="str"), chart_version=dict(type="str"),
@@ -629,19 +632,8 @@ def main():
release_values=dict(type="dict", default={}, aliases=["values"]), release_values=dict(type="dict", default={}, aliases=["values"]),
values_files=dict(type="list", default=[], elements="str"), values_files=dict(type="list", default=[], elements="str"),
update_repo_cache=dict(type="bool", default=False), update_repo_cache=dict(type="bool", default=False),
# Helm options
disable_hook=dict(type="bool", default=False), disable_hook=dict(type="bool", default=False),
force=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), purge=dict(type="bool", default=True),
wait=dict(type="bool", default=False), wait=dict(type="bool", default=False),
wait_timeout=dict(type="str"), wait_timeout=dict(type="str"),
@@ -652,23 +644,15 @@ def main():
replace=dict(type="bool", default=False), replace=dict(type="bool", default=False),
skip_crds=dict(type="bool", default=False), skip_crds=dict(type="bool", default=False),
history_max=dict(type="int"), history_max=dict(type="int"),
# Generic auth key )
host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), )
ca_cert=dict( return arg_spec
type="path",
aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), def main():
), global module
validate_certs=dict( module = AnsibleModule(
type="bool", argument_spec=argument_spec(),
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"])
),
),
required_if=[ required_if=[
("release_state", "present", ["release_name", "chart_ref"]), ("release_state", "present", ["release_name", "chart_ref"]),
("release_state", "absent", ["release_name"]), ("release_state", "absent", ["release_name"]),
@@ -687,7 +671,6 @@ def main():
changed = False changed = False
bin_path = module.params.get("binary_path")
chart_ref = module.params.get("chart_ref") chart_ref = module.params.get("chart_ref")
chart_repo_url = module.params.get("chart_repo_url") chart_repo_url = module.params.get("chart_repo_url")
chart_version = module.params.get("chart_version") chart_version = module.params.get("chart_version")
@@ -712,10 +695,8 @@ def main():
history_max = module.params.get("history_max") history_max = module.params.get("history_max")
timeout = module.params.get("timeout") timeout = module.params.get("timeout")
if bin_path is not None: helm_cmd_common = get_helm_binary(module)
helm_cmd_common = bin_path helm_bin = helm_cmd_common
else:
helm_cmd_common = module.get_bin_path("helm", required=True)
if update_repo_cache: if update_repo_cache:
run_repo_update(module, helm_cmd_common) run_repo_update(module, helm_cmd_common)
@@ -808,7 +789,7 @@ def main():
else: 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 ( if helm_diff_version and (
not chart_repo_url not chart_repo_url
or ( or (

View File

@@ -112,6 +112,7 @@ status:
""" """
import traceback import traceback
import copy
try: try:
import yaml import yaml
@@ -121,10 +122,15 @@ except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False 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 ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm, run_helm,
get_values, 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 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(): def main():
global module global module
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=argument_spec(),
binary_path=dict(type="path"), mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE,
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"),
],
supports_check_mode=True, supports_check_mode=True,
) )
if not IMP_YAML: if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) 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_name = module.params.get("release_name")
release_state = module.params.get("release_state") release_state = module.params.get("release_state")
if bin_path is not None: helm_cmd_common = get_helm_binary(module)
helm_cmd_common = bin_path
else:
helm_cmd_common = module.get_bin_path("helm", required=True)
release_status = get_release_status( release_status = get_release_status(
module, helm_cmd_common, release_name, release_state module, helm_cmd_common, release_name, release_state

View File

@@ -108,21 +108,24 @@ rc:
sample: 1 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 ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm, run_helm,
get_helm_plugin_list, get_helm_plugin_list,
parse_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(): def argument_spec():
module = AnsibleModule( arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
argument_spec=dict( arg_spec.update(
binary_path=dict(type="path"), dict(
state=dict(
type="str", default="present", choices=["present", "absent", "latest"]
),
plugin_path=dict( plugin_path=dict(
type="str", type="str",
), ),
@@ -132,60 +135,38 @@ def main():
plugin_version=dict( plugin_version=dict(
type="str", type="str",
), ),
# Helm options state=dict(
context=dict(
type="str", type="str",
aliases=["kube_context"], default="present",
fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), choices=["present", "absent", "latest"],
), ),
kubeconfig=dict( )
type="path", )
aliases=["kubeconfig_path"], return arg_spec
fallback=(env_fallback, ["K8S_AUTH_KUBECONFIG"]),
),
# Generic auth key def mutually_exclusive():
host=dict(type="str", fallback=(env_fallback, ["K8S_AUTH_HOST"])), mutually_ex = copy.deepcopy(HELM_AUTH_MUTUALLY_EXCLUSIVE)
ca_cert=dict( mutually_ex.append(("plugin_name", "plugin_path"))
type="path", return mutually_ex
aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]),
), def main():
validate_certs=dict( module = AnsibleModule(
type="bool", argument_spec=argument_spec(),
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"])
),
),
supports_check_mode=True, supports_check_mode=True,
required_if=[ required_if=[
("state", "present", ("plugin_path",)), ("state", "present", ("plugin_path",)),
("state", "absent", ("plugin_name",)), ("state", "absent", ("plugin_name",)),
("state", "latest", ("plugin_name",)), ("state", "latest", ("plugin_name",)),
], ],
mutually_exclusive=[ mutually_exclusive=mutually_exclusive(),
("plugin_name", "plugin_path"),
("context", "ca_cert"),
("context", "validate_certs"),
("kubeconfig", "ca_cert"),
("kubeconfig", "validate_certs"),
],
) )
bin_path = module.params.get("binary_path")
state = module.params.get("state") state = module.params.get("state")
helm_bin = get_helm_binary(module)
if bin_path is not None: helm_cmd_common = helm_bin + " plugin"
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"
if state == "present": if state == "present":
helm_cmd_common += " install %s" % module.params.get("plugin_path") helm_cmd_common += " install %s" % module.params.get("plugin_path")
@@ -227,7 +208,7 @@ def main():
) )
elif state == "absent": elif state == "absent":
plugin_name = module.params.get("plugin_name") 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()) out = parse_helm_plugin_list(module, output=output.splitlines())
if not out: if not out:
@@ -281,7 +262,7 @@ def main():
) )
elif state == "latest": elif state == "latest":
plugin_name = module.params.get("plugin_name") 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()) out = parse_helm_plugin_list(module, output=output.splitlines())
if not out: if not out:

View File

@@ -70,73 +70,43 @@ rc:
sample: 1 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 ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
get_helm_plugin_list, get_helm_plugin_list,
parse_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(): def main():
module = AnsibleModule(
argument_spec=dict( argument_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
binary_path=dict(type="path"), argument_spec.update(
dict(
plugin_name=dict( plugin_name=dict(
type="str", type="str",
), ),
# Helm options )
context=dict( )
type="str",
aliases=["kube_context"], module = AnsibleModule(
fallback=(env_fallback, ["K8S_AUTH_CONTEXT"]), argument_spec=argument_spec,
), mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE,
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"),
],
supports_check_mode=True, supports_check_mode=True,
) )
bin_path = module.params.get("binary_path") 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"
plugin_name = module.params.get("plugin_name") plugin_name = module.params.get("plugin_name")
plugin_list = [] 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()) out = parse_helm_plugin_list(module, output=output.splitlines())
@@ -155,7 +125,7 @@ def main():
module.exit_json( module.exit_json(
changed=True, changed=True,
command=helm_cmd_common + " list", command=helm_bin + " plugin list",
stdout=output, stdout=output,
stderr=err, stderr=err,
rc=rc, rc=rc,

View File

@@ -97,6 +97,21 @@ options:
type: path type: path
aliases: [ ssl_ca_cert ] aliases: [ ssl_ca_cert ]
version_added: "2.3.0" 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""" EXAMPLES = r"""
@@ -145,6 +160,7 @@ msg:
""" """
import traceback import traceback
import copy
try: try:
import yaml import yaml
@@ -154,8 +170,15 @@ except ImportError:
IMP_YAML_ERR = traceback.format_exc() IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, env_fallback, missing_required_lib 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,
)
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 # Get repository from all repositories added
@@ -215,12 +238,10 @@ def delete_repository(command, repository_name):
return remove_command return remove_command
def main(): def argument_spec():
global module arg_spec = copy.deepcopy(HELM_AUTH_ARG_SPEC)
arg_spec.update(
module = AnsibleModule( dict(
argument_spec=dict(
binary_path=dict(type="path"),
repo_name=dict(type="str", aliases=["name"], required=True), repo_name=dict(type="str", aliases=["name"], required=True),
repo_url=dict(type="str", aliases=["url"]), repo_url=dict(type="str", aliases=["url"]),
repo_username=dict(type="str", aliases=["username"]), repo_username=dict(type="str", aliases=["username"]),
@@ -229,25 +250,19 @@ def main():
default="present", choices=["present", "absent"], aliases=["state"] default="present", choices=["present", "absent"], aliases=["state"]
), ),
pass_credentials=dict(type="bool", default=False, no_log=True), 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( return arg_spec
type="path",
aliases=["ssl_ca_cert"],
fallback=(env_fallback, ["K8S_AUTH_SSL_CA_CERT"]), def main():
), global module
validate_certs=dict(
type="bool", module = AnsibleModule(
default=True, argument_spec=argument_spec(),
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"])
),
),
required_together=[["repo_username", "repo_password"]], required_together=[["repo_username", "repo_password"]],
required_if=[("repo_state", "present", ["repo_url"])], required_if=[("repo_state", "present", ["repo_url"])],
mutually_exclusive=HELM_AUTH_MUTUALLY_EXCLUSIVE,
supports_check_mode=True, supports_check_mode=True,
) )
@@ -256,7 +271,6 @@ def main():
changed = False changed = False
bin_path = module.params.get("binary_path")
repo_name = module.params.get("repo_name") repo_name = module.params.get("repo_name")
repo_url = module.params.get("repo_url") repo_url = module.params.get("repo_url")
repo_username = module.params.get("repo_username") repo_username = module.params.get("repo_username")
@@ -264,10 +278,7 @@ def main():
repo_state = module.params.get("repo_state") repo_state = module.params.get("repo_state")
pass_credentials = module.params.get("pass_credentials") pass_credentials = module.params.get("pass_credentials")
if bin_path is not None: helm_cmd = get_helm_binary(module)
helm_cmd = bin_path
else:
helm_cmd = module.get_bin_path("helm", required=True)
repository_status = get_repository_status(module, helm_cmd, repo_name) repository_status = get_repository_status(module, helm_cmd, repo_name)

View File

@@ -181,7 +181,10 @@ except ImportError:
IMP_YAML = False IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib 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( def template(
@@ -266,7 +269,6 @@ def main():
) )
check_mode = module.check_mode check_mode = module.check_mode
bin_path = module.params.get("binary_path")
chart_ref = module.params.get("chart_ref") chart_ref = module.params.get("chart_ref")
chart_repo_url = module.params.get("chart_repo_url") chart_repo_url = module.params.get("chart_repo_url")
chart_version = module.params.get("chart_version") chart_version = module.params.get("chart_version")
@@ -284,7 +286,7 @@ def main():
if not IMP_YAML: if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR) 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: if update_repo_cache:
update_cmd = helm_cmd + " repo update" update_cmd = helm_cmd + " repo update"

View File

@@ -26,3 +26,4 @@ test_namespace:
- "helm-local-path-002" - "helm-local-path-002"
- "helm-local-path-003" - "helm-local-path-003"
- "helm-dep" - "helm-dep"
- "helm-kubeconfig"

View File

@@ -43,6 +43,9 @@
- name: Test Skip CRDS feature in helm chart install - name: Test Skip CRDS feature in helm chart install
include_tasks: test_crds.yml include_tasks: test_crds.yml
- name: Test in-memory kubeconfig
include_tasks: tests_in_memory_kubeconfig.yml
- name: Clean helm install - name: Clean helm install
file: file:
path: "{{ item }}" path: "{{ item }}"

View File

@@ -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

View File

@@ -7,8 +7,9 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
import os.path import os.path
import yaml import yaml
import tempfile
import json
from ansible_collections.kubernetes.core.plugins.module_utils.helm import ( from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
@@ -31,11 +32,22 @@ class MockedModule:
} }
self.r = {} self.r = {}
self.files_to_delete = []
def run_command(self, command, environ_update=None): def run_command(self, command, environ_update=None):
self.r = {"command": command, "environ_update": environ_update} self.r = {"command": command, "environ_update": environ_update}
return 0, "", "" 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(): def test_write_temp_kubeconfig_server_only():
file_name = write_temp_kubeconfig("ff") 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_KUBETOKEN"] == "my-api-key"
assert module.r["environ_update"]["HELM_NAMESPACE"] == "a-release-namespace" assert module.r["environ_update"]["HELM_NAMESPACE"] == "a-release-namespace"
assert module.r["environ_update"]["KUBECONFIG"] 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()