mirror of
https://github.com/ansible-collections/kubernetes.core.git
synced 2026-03-26 21:33:02 +00:00
helm - expand kubeconfig path with user's home dir
SUMMARY
Currently the helm module fails when providing the default kubeconfig path explicitly, while the same path is fine for the k8s module.
ISSUE TYPE
Bugfix Pull Request
COMPONENT NAME
helm
ADDITIONAL INFORMATION
- name: Deploy kubelet-csr-approver
delegate_to: client
run_once: true
kubernetes.core.helm:
update_repo_cache: true
kubeconfig: "~/.kube/config"
state: present
name: kubelet-csr-approver
namespace: kubelet-csr-approver
create_namespace: true
chart_ref: kubelet-csr-approver/kubelet-csr-approver
chart_version: 1.0.5
values: "{{ lookup('template', 'values.yaml.j2') | from_yaml }}"
atomic: true
Before change:
TASK [kubernetes/kubelet_csr_approver : Deploy kubelet-csr-approver] ***
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: FileNotFoundError: [Errno 2] No such file or directory: '~/.kube/config'
fatal: [node-1 -> client(192.168.121.56)]: FAILED! => {"changed": false, "module_stderr": "", "module_stdout": "Traceback (most recent call last):\r\n File \"/home/vagrant/.ansible/tmp/ansible-tmp-1697293347.7135417-118207-9805169252135/AnsiballZ_helm.py\", line 107, in <module>\r\n _ansiballz_main()\r\n File \"/home/vagrant/.ansible/tmp/ansible-tmp-1697293347.7135417-118207-9805169252135/AnsiballZ_helm.py\", line 99, in _ansiballz_main\r\n invoke_module(zipped_mod, temp_path, ANSIBALLZ_PARAMS)\r\n File \"/home/vagrant/.ansible/tmp/ansible-tmp-1697293347.7135417-118207-9805169252135/AnsiballZ_helm.py\", line 47, in invoke_module\r\n runpy.run_module(mod_name='ansible_collections.kubernetes.core.plugins.modules.helm', init_globals=dict(_module_fqn='ansible_collections.kubernetes.core.plugins.modules.helm', _modlib_path=modlib_path),\r\n File \"/usr/lib/python3.10/runpy.py\", line 224, in run_module\r\n return _run_module_code(code, init_globals, run_name, mod_spec)\r\n File \"/usr/lib/python3.10/runpy.py\", line 96, in _run_module_code\r\n _run_code(code, mod_globals, init_globals,\r\n File \"/usr/lib/python3.10/runpy.py\", line 86, in _run_code\r\n exec(code, run_globals)\r\n File \"/tmp/ansible_kubernetes.core.helm_payload_o8s36dti/ansible_kubernetes.core.helm_payload.zip/ansible_collections/kubernetes/core/plugins/modules/helm.py\", line 924, in <module>\r\n File \"/tmp/ansible_kubernetes.core.helm_payload_o8s36dti/ansible_kubernetes.core.helm_payload.zip/ansible_collections/kubernetes/core/plugins/modules/helm.py\", line 737, in main\r\n File \"/tmp/ansible_kubernetes.core.helm_payload_o8s36dti/ansible_kubernetes.core.helm_payload.zip/ansible_collections/kubernetes/core/plugins/modules/helm.py\", line 435, in run_repo_update\r\n File \"/tmp/ansible_kubernetes.core.helm_payload_o8s36dti/ansible_kubernetes.core.helm_payload.zip/ansible_collections/kubernetes/core/plugins/module_utils/helm.py\", line 169, in run_helm_command\r\n File \"/tmp/ansible_kubernetes.core.helm_payload_o8s36dti/ansible_kubernetes.core.helm_payload.zip/ansible_collections/kubernetes/core/plugins/module_utils/helm.py\", line 162, in env_update\r\n File \"/tmp/ansible_kubernetes.core.helm_payload_o8s36dti/ansible_kubernetes.core.helm_payload.zip/ansible_collections/kubernetes/core/plugins/module_utils/helm.py\", line 120, in _prepare_helm_environment\r\nFileNotFoundError: [Errno 2] No such file or directory: '~/.kube/config'\r\n", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", "rc": 1}
After change:
TASK [kubernetes/kubelet_csr_approver : Deploy kubelet-csr-approver] ***
changed: [node-1 -> client(192.168.121.56)]
Reviewed-by: Mike Graves <mgraves@redhat.com>
Reviewed-by: Bikouo Aubin
298 lines
9.2 KiB
Python
298 lines
9.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright: (c) 2020, 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
|
|
|
|
|
|
import copy
|
|
import json
|
|
import os
|
|
import re
|
|
import tempfile
|
|
import traceback
|
|
|
|
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
|
from ansible.module_utils.six import string_types
|
|
from ansible_collections.kubernetes.core.plugins.module_utils.version import (
|
|
LooseVersion,
|
|
)
|
|
|
|
try:
|
|
import yaml
|
|
|
|
HAS_YAML = True
|
|
YAML_IMP_ERR = None
|
|
except ImportError:
|
|
YAML_IMP_ERR = traceback.format_exc()
|
|
HAS_YAML = False
|
|
|
|
|
|
def parse_helm_plugin_list(output=None):
|
|
"""
|
|
Parse `helm plugin list`, return list of plugins
|
|
"""
|
|
ret = []
|
|
if not output:
|
|
return ret
|
|
|
|
for line in output:
|
|
if line.startswith("NAME"):
|
|
continue
|
|
name, version, description = line.split("\t", 3)
|
|
name = name.strip()
|
|
version = version.strip()
|
|
description = description.strip()
|
|
if name == "":
|
|
continue
|
|
ret.append((name, version, description))
|
|
|
|
return ret
|
|
|
|
|
|
def write_temp_kubeconfig(server, validate_certs=True, ca_cert=None, kubeconfig=None):
|
|
# Workaround until https://github.com/helm/helm/pull/8622 is merged
|
|
content = {
|
|
"apiVersion": "v1",
|
|
"kind": "Config",
|
|
"clusters": [{"cluster": {"server": server}, "name": "generated-cluster"}],
|
|
"contexts": [
|
|
{"context": {"cluster": "generated-cluster"}, "name": "generated-context"}
|
|
],
|
|
"current-context": "generated-context",
|
|
}
|
|
if kubeconfig:
|
|
content = copy.deepcopy(kubeconfig)
|
|
|
|
for cluster in content["clusters"]:
|
|
if server:
|
|
cluster["cluster"]["server"] = server
|
|
if not validate_certs:
|
|
cluster["cluster"]["insecure-skip-tls-verify"] = True
|
|
if ca_cert:
|
|
cluster["cluster"]["certificate-authority"] = ca_cert
|
|
return content
|
|
|
|
|
|
class AnsibleHelmModule(object):
|
|
|
|
"""
|
|
An Ansible module class for Kubernetes.core helm modules
|
|
"""
|
|
|
|
def __init__(self, **kwargs):
|
|
self._module = None
|
|
if "module" in kwargs:
|
|
self._module = kwargs.get("module")
|
|
else:
|
|
self._module = AnsibleModule(**kwargs)
|
|
|
|
self.helm_env = None
|
|
|
|
def __getattr__(self, name):
|
|
return getattr(self._module, name)
|
|
|
|
@property
|
|
def params(self):
|
|
return self._module.params
|
|
|
|
def _prepare_helm_environment(self):
|
|
param_to_env_mapping = [
|
|
("context", "HELM_KUBECONTEXT"),
|
|
("release_namespace", "HELM_NAMESPACE"),
|
|
("api_key", "HELM_KUBETOKEN"),
|
|
("host", "HELM_KUBEAPISERVER"),
|
|
]
|
|
|
|
env_update = {}
|
|
for p, env in param_to_env_mapping:
|
|
if self.params.get(p):
|
|
env_update[env] = self.params.get(p)
|
|
|
|
kubeconfig_content = None
|
|
kubeconfig = self.params.get("kubeconfig")
|
|
if kubeconfig:
|
|
if isinstance(kubeconfig, string_types):
|
|
with open(os.path.expanduser(kubeconfig)) as fd:
|
|
kubeconfig_content = yaml.safe_load(fd)
|
|
elif isinstance(kubeconfig, dict):
|
|
kubeconfig_content = kubeconfig
|
|
|
|
if self.params.get("ca_cert"):
|
|
ca_cert = self.params.get("ca_cert")
|
|
if LooseVersion(self.get_helm_version()) < LooseVersion("3.5.0"):
|
|
# update certs from kubeconfig
|
|
kubeconfig_content = write_temp_kubeconfig(
|
|
server=self.params.get("host"),
|
|
ca_cert=ca_cert,
|
|
kubeconfig=kubeconfig_content,
|
|
)
|
|
else:
|
|
env_update["HELM_KUBECAFILE"] = ca_cert
|
|
|
|
if self.params.get("validate_certs") is False:
|
|
validate_certs = self.params.get("validate_certs")
|
|
if LooseVersion(self.get_helm_version()) < LooseVersion("3.10.0"):
|
|
# update certs from kubeconfig
|
|
kubeconfig_content = write_temp_kubeconfig(
|
|
server=self.params.get("host"),
|
|
validate_certs=validate_certs,
|
|
kubeconfig=kubeconfig_content,
|
|
)
|
|
else:
|
|
env_update["HELM_KUBEINSECURE_SKIP_TLS_VERIFY"] = "true"
|
|
|
|
if kubeconfig_content:
|
|
fd, kubeconfig_path = tempfile.mkstemp()
|
|
with os.fdopen(fd, "w") as fp:
|
|
json.dump(kubeconfig_content, fp)
|
|
|
|
env_update["KUBECONFIG"] = kubeconfig_path
|
|
self.add_cleanup_file(kubeconfig_path)
|
|
|
|
return env_update
|
|
|
|
@property
|
|
def env_update(self):
|
|
if self.helm_env is None:
|
|
self.helm_env = self._prepare_helm_environment()
|
|
return self.helm_env
|
|
|
|
def run_helm_command(self, command, fails_on_error=True):
|
|
if not HAS_YAML:
|
|
self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
|
|
|
rc, out, err = self.run_command(command, environ_update=self.env_update)
|
|
if fails_on_error and rc != 0:
|
|
self.fail_json(
|
|
msg="Failure when executing Helm command. Exited {0}.\nstdout: {1}\nstderr: {2}".format(
|
|
rc, out, err
|
|
),
|
|
stdout=out,
|
|
stderr=err,
|
|
command=command,
|
|
)
|
|
return rc, out, err
|
|
|
|
def get_helm_binary(self):
|
|
return self.params.get("binary_path") or self.get_bin_path(
|
|
"helm", required=True
|
|
)
|
|
|
|
def get_helm_version(self):
|
|
command = self.get_helm_binary() + " version"
|
|
rc, out, err = self.run_command(command)
|
|
m = re.match(r'version.BuildInfo{Version:"v([0-9\.]*)",', out)
|
|
if m:
|
|
return m.group(1)
|
|
m = re.match(r'Client: &version.Version{SemVer:"v([0-9\.]*)", ', out)
|
|
if m:
|
|
return m.group(1)
|
|
return None
|
|
|
|
def get_values(self, release_name, get_all=False):
|
|
"""
|
|
Get Values from deployed release
|
|
"""
|
|
if not HAS_YAML:
|
|
self.fail_json(msg=missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
|
|
|
get_command = (
|
|
self.get_helm_binary() + " get values --output=yaml " + release_name
|
|
)
|
|
|
|
if get_all:
|
|
get_command += " -a"
|
|
|
|
rc, out, err = self.run_helm_command(get_command)
|
|
# Helm 3 return "null" string when no values are set
|
|
if out.rstrip("\n") == "null":
|
|
return {}
|
|
return yaml.safe_load(out)
|
|
|
|
def parse_yaml_content(self, content):
|
|
if not HAS_YAML:
|
|
self.fail_json(msg=missing_required_lib("yaml"), exception=HAS_YAML)
|
|
|
|
try:
|
|
return list(yaml.safe_load_all(content))
|
|
except (IOError, yaml.YAMLError) as exc:
|
|
self.fail_json(
|
|
msg="Error parsing YAML content: {0}".format(exc), raw_data=content
|
|
)
|
|
|
|
def get_manifest(self, release_name):
|
|
command = [
|
|
self.get_helm_binary(),
|
|
"get",
|
|
"manifest",
|
|
release_name,
|
|
]
|
|
rc, out, err = self.run_helm_command(" ".join(command))
|
|
if rc != 0:
|
|
self.fail_json(msg=err)
|
|
return self.parse_yaml_content(out)
|
|
|
|
def get_notes(self, release_name):
|
|
command = [
|
|
self.get_helm_binary(),
|
|
"get",
|
|
"notes",
|
|
release_name,
|
|
]
|
|
rc, out, err = self.run_helm_command(" ".join(command))
|
|
if rc != 0:
|
|
self.fail_json(msg=err)
|
|
return out
|
|
|
|
def get_hooks(self, release_name):
|
|
command = [
|
|
self.get_helm_binary(),
|
|
"get",
|
|
"hooks",
|
|
release_name,
|
|
]
|
|
rc, out, err = self.run_helm_command(" ".join(command))
|
|
if rc != 0:
|
|
self.fail_json(msg=err)
|
|
return self.parse_yaml_content(out)
|
|
|
|
def get_helm_plugin_list(self):
|
|
"""
|
|
Return `helm plugin list`
|
|
"""
|
|
helm_plugin_list = self.get_helm_binary() + " plugin list"
|
|
rc, out, err = self.run_helm_command(helm_plugin_list)
|
|
if rc != 0 or (out == "" and err == ""):
|
|
self.fail_json(
|
|
msg="Failed to get Helm plugin info",
|
|
command=helm_plugin_list,
|
|
stdout=out,
|
|
stderr=err,
|
|
rc=rc,
|
|
)
|
|
return (rc, out, err, helm_plugin_list)
|
|
|
|
def get_helm_set_values_args(self, set_values):
|
|
if any(v.get("value_type") == "json" for v in set_values):
|
|
if LooseVersion(self.get_helm_version()) < LooseVersion("3.10.0"):
|
|
self.fail_json(
|
|
msg="This module requires helm >= 3.10.0, to use set_values parameter with value type set to 'json'. current version is {0}".format(
|
|
self.get_helm_version()
|
|
)
|
|
)
|
|
|
|
options = []
|
|
for opt in set_values:
|
|
value_type = opt.get("value_type", "raw")
|
|
value = opt.get("value")
|
|
|
|
if value_type == "raw":
|
|
options.append("--set " + value)
|
|
else:
|
|
options.append("--set-{0} '{1}'".format(value_type, value))
|
|
|
|
return " ".join(options)
|