Source code for idem_helm.states.helm.release

"""States module for Helm Release."""
import copy
import re
from typing import Any
from typing import Dict
from typing import List

import dict_tools.differ as differ
import yaml

__contracts__ = ["resource", "soft_fail"]


[docs]async def present( hub, ctx, name: str, chart: str, namespace: str, repository: str = None, resource_id: str = None, values: Dict = None, version: str = None, key_file: str = None, keyring: str = None, ca_file: str = None, cert_file: str = None, username: str = None, password: str = None, values_files: List = None, dependency_update: bool = False, create_namespace: bool = False, atomic: bool = False, devel: bool = False, disable_openapi_validation: bool = False, no_hooks: bool = False, verify: bool = False, timeout: str = None, ) -> Dict[str, Any]: """Create/Update a Helm Release. Args: name(str): An Idem name of the resource. chart(str): Chart name to be installed namespace(str): namespace scope for helm release resource_id(str, Optional): An identifier of the resource in the provider. repository(str, Optional): chart repository url where to locate the requested chart values(Dict, Optional): Specify values in a YAML version(str, Optional): Chart version to install. If this is not specified, the latest version is installed. key_file(str, Optional): The repositories cert key file keyring(str, Optional): location of public keys used for verification (default "~/.gnupg/pubring.gpg") values_files(List, Optional): specify values in a YAML file ca_file(str, Optional): verify certificates of HTTPS-enabled servers using this CA bundle cert_file(str, Optional): identify HTTPS client using this SSL certificate file username(str, Optional): chart repository username where to locate the requested chart password(str, Optional): chart repository password where to locate the requested chart dependency_update(bool, Optional): update dependencies if they are missing before installing the chart create_namespace(bool, Optional): create the release namespace if not present atomic(bool, Optional): if True, the installation process deletes the installation on failure. devel(bool, Optional): use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored disable_openapi_validation(bool, Optional): if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema no_hooks(bool, Optional): prevent hooks from running during install verify(bool, Optional): verify the package before using it. If verify is set, the chart MUST have a provenance file, and the provenance file MUST pass all verification steps. timeout(str, Optional): time to wait for Kubernetes commands to complete. (Ex-: 100s, 200s, 10m) Returns: Dict[str, Any] Examples: .. code-block:: sls idem-helm-release-test: helm.release.present: - name: idem-redis - resource_id: idem-redis - repository: https://charts.bitnami.com/bitnami - chart: redis - namespace: kube-system - timeout: 200s """ result = dict(comment=(), old_state=None, new_state=None, name=name, result=True) before = None list_release_ret = None # Check for existing release by name in namespace if resource_id: list_release_ret = await hub.exec.helm.release.list_releases( ctx, resource_id, namespace ) if not list_release_ret["result"] or not list_release_ret["ret"]: result["comment"] = list_release_ret["comment"] result["result"] = False return result # Update current state if list_release_ret and list_release_ret["ret"]: current_state = list_release_ret["ret"][0] current_value_ret = await hub.exec.helm.release.get_release_values( ctx, current_state ) if not current_value_ret["result"]: result["comment"] = current_value_ret["comment"] result["result"] = False return result before = hub.tool.helm.release_utils.convert_raw_release_to_present( release_metadata=current_state, release_values=current_value_ret["ret"] ) result["old_state"] = before # Handle no change behaviour # Since the repository attributes and other flags are not being persisted as metadata by helm, it will not be set to any value. desired_state = { "resource_id": resource_id, "chart": chart + (("-" + version) if version else ""), "namespace": namespace, "values": values, "name": name, "values_files": values_files, } try: is_change_detected = await __is_update_required( desired_state, result["old_state"] ) except yaml.YAMLError as exception: result["comment"] = result["comment"] + (str(exception),) result["result"] = False return result if not is_change_detected: result["comment"] = hub.tool.helm.comment_utils.already_exists_comment( resource_type="helm.release", name=name ) result["new_state"] = copy.deepcopy(result["old_state"]) return result # Handle test behaviour if ctx.get("test", False): result["new_state"] = hub.tool.helm.test_state_utils.generate_test_state( enforced_state=before, desired_state=desired_state, ) result["comment"] = ( hub.tool.helm.comment_utils.would_update_comment( resource_type="helm.release", name=name ) if before else hub.tool.helm.comment_utils.would_create_comment( resource_type="helm.release", name=name ) ) return result kvflags = { "namespace": namespace, "set": values, "version": version, "repo": repository, "key-file": key_file, "keyring": keyring, "ca-file": ca_file, "cert-file": cert_file, "values": values_files, "username": username, "password": password, "timeout": timeout, } flags = [] if dependency_update: flags.append("dependency-update") if create_namespace: flags.append("create-namespace") if atomic: flags.append("atomic") if devel: flags.append("devel") if disable_openapi_validation: flags.append("disable-openapi-validation") if no_hooks: flags.append("no-hooks") if verify: flags.append("verify") # Handle actual resource create or update if before: upgrade_ret = await hub.exec.helm.release.upgrade_release( ctx, name, chart, flags, kvflags ) if not upgrade_ret["result"]: result["comment"] = upgrade_ret["comment"] result["result"] = False return result result["comment"] = hub.tool.helm.comment_utils.update_comment( resource_type="helm.release", name=name ) else: install_ret = await hub.exec.helm.release.install_release( ctx, name, chart, flags, kvflags ) if not install_ret["result"]: created = False if ( "the server is currently unable to handle the request" in install_ret["comment"] ): list_release_ret = await hub.exec.helm.release.list_releases( ctx, name, namespace ) if list_release_ret["result"] and list_release_ret["ret"]: created = True if not created: result["comment"] = install_ret["comment"] result["result"] = False return result result["comment"] = hub.tool.helm.comment_utils.create_comment( resource_type="helm.release", name=name ) resource_id = name # creation was successful, set resource_id so that we get resource_id from esm even if fetching updated # resource fails. result["new_state"] = {"resource_id": resource_id} # Fetch the updated resource and update new_state list_release_ret = await hub.exec.helm.release.list_releases( ctx, resource_id, namespace ) if not list_release_ret["result"] or not list_release_ret["ret"]: result["comment"] = result["comment"] + list_release_ret["comment"] result["result"] = False return result after = list_release_ret["ret"][0] value_ret = await hub.exec.helm.release.get_release_values(ctx, after) if not value_ret["result"]: result["comment"] = result["comment"] + value_ret["comment"] result["result"] = False return result result["new_state"] = hub.tool.helm.release_utils.convert_raw_release_to_present( release_metadata=after, release_values=value_ret["ret"] ) return result
async def __is_update_required(desired_state, old_state): if not old_state: return True if desired_state.get("values_files"): if not desired_state.get("values"): desired_state["values"] = {} for values_file in desired_state.get("values_files"): with open(values_file) as stream: data = yaml.safe_load(stream) desired_state["values"].update(data) desired_state["values_files"] = None __merge_arguments(desired_state.get("values"), old_state.get("values")) diff = differ.deep_diff(old_state, desired_state) is_change_detected = False for item in diff["new"]: new_value = diff["new"].get(item) if new_value: # if value is not None if item == "chart": old_val = diff["old"].get("chart") is_change_detected = not ( old_val.startswith(new_value) and re.search(r"-(\d+\.)?(\d+\.)?(\*|\d+)$", old_val) ) elif (item == "values" or item == "values_files") and desired_state.get( "values_files" ): continue else: is_change_detected = True return is_change_detected def __merge_arguments(desire_state: Dict[str, Any], current_state: Dict[str, Any]): """ Assign current_state values to desire_state if desire_state value is None or parameter key is missing in desire_state. """ if not desire_state and current_state: desire_state = copy.deepcopy(current_state) return desire_state if isinstance(current_state, dict): for key, value in current_state.items(): if key in desire_state: desire_value = desire_state.get(key) if desire_value is None: desire_state[key] = value elif isinstance(desire_value, dict): desire_state[key] = __merge_arguments(desire_value, value) else: desire_state[key] = value return desire_state
[docs]async def absent( hub, ctx, name: str, namespace: str = "default", resource_id: str = None ) -> Dict[str, Any]: """Delete a helm release Args: name(str): An Idem name of the resource. namespace(str, Optional): namespace scope for helm release. Defaults to 'default' namespace, in case of value not provided in absent state. resource_id(str, Optional): An identifier of the resource in the provider. Returns: Dict[str, Any] Examples: .. code-block:: sls idem-helm-release-test: helm.release.absent: - name: idem-redis - resource_id: idem-redis - namespace: kube-system """ result = dict(comment=(), old_state=None, new_state=None, name=name, result=True) before = None if resource_id: list_release_ret = await hub.exec.helm.release.list_releases( ctx, resource_id, namespace ) if list_release_ret["ret"]: value_ret = await hub.exec.helm.release.get_release_values( ctx, list_release_ret["ret"][0] ) before = hub.tool.helm.release_utils.convert_raw_release_to_present( release_metadata=list_release_ret["ret"][0], release_values=value_ret["ret"], ) result["old_state"] = before if not before: result["comment"] = hub.tool.helm.comment_utils.already_absent_comment( resource_type="helm.release", name=name ) elif ctx.get("test", False): result["comment"] = hub.tool.helm.comment_utils.would_delete_comment( resource_type="helm.release", name=name ) else: cmd_ret = await hub.exec.helm.release.uninstall_release(ctx, name, namespace) if not cmd_ret["result"]: result["comment"] = cmd_ret["comment"] result["result"] = False return result result["comment"] = hub.tool.helm.comment_utils.delete_comment( resource_type="helm.release", name=name ) return result
[docs]async def describe(hub, ctx) -> Dict[str, Dict[str, Any]]: """list of helm releases in all namespaces. Repository attributes and other flags are not being persisted as metadata by helm, it will not be set to any value by default. Returns: Dict[str, Dict[str, Any]] Examples: .. code-block:: bash $ idem describe helm.release """ list_release_ret = await hub.exec.helm.release.list_releases(ctx) if not list_release_ret["result"]: hub.log.debug(f"Could not describe helm release {list_release_ret['comment']}") return {} all_releases = list_release_ret["ret"] result = {} for release in all_releases: resource_id = release["name"] value_ret = await hub.exec.helm.release.get_release_values(ctx, release) if not value_ret["result"]: hub.log.debug( f"Could not describe values for helm release {resource_id} : {value_ret['comment']}" ) continue release_resource = hub.tool.helm.release_utils.convert_raw_release_to_present( release_metadata=release, release_values=value_ret["ret"] ) result[resource_id] = { "helm.release.present": [ {parameter_key: parameter_value} for parameter_key, parameter_value in release_resource.items() ] } return result