Add ability to install helm charts
This patch adds ability to install helm charts and to add helm
repos. It was decided to take python code from salt-formula-helm
instead of using pyhelm module since it uses grpc and requires a
tunnel to tiller since tiller is not exposed and operates with its
client using portforward api.
Change-Id: I992abcf7eb035342a01dca838438ae7e2115fda0
Related-PROD: PROD-28361
diff --git a/_states/k8s_helm_release.py b/_states/k8s_helm_release.py
new file mode 100644
index 0000000..9977551
--- /dev/null
+++ b/_states/k8s_helm_release.py
@@ -0,0 +1,196 @@
+import difflib
+import os
+import logging
+
+from salt.exceptions import CommandExecutionError
+from salt.serializers import yaml
+
+LOG = logging.getLogger(__name__)
+
+def _get_values_from_file(values_file=None):
+ if values_file:
+ try:
+ with open(values_file) as values_stream:
+ values = yaml.deserialize(values_stream)
+ return values
+ except e:
+ raise CommandExecutionError("encountered error reading from values "
+ "file (%s): %s" % (values_file, e))
+ return None
+
+def _get_yaml_diff(new_yaml=None, old_yaml=None):
+ if not new_yaml and not old_yaml:
+ return None
+
+ old_str = yaml.serialize(old_yaml, default_flow_style=False)
+ new_str = yaml.serialize(new_yaml, default_flow_style=False)
+ return difflib.unified_diff(old_str.split('\n'), new_str.split('\n'))
+
+def _failure(name, message, changes={}):
+ return {
+ 'name': name,
+ 'changes': changes,
+ 'result': False,
+ 'comment': message,
+ }
+
+def present(name, chart_name, namespace, version=None, values_file=None,
+ tiller_namespace='kube-system', **kwargs):
+ '''
+ Ensure that a release with the supplied name is in the desired state in the
+ Tiller installation. This state will handle change detection to determine
+ whether an installation or update needs to be made.
+
+ In the event the namespace to which a release is installed changes, the
+ state will first delete and purge the release and the re-install it into
+ the new namespace, since Helm does not support updating a release into a
+ new namespace.
+
+ name
+ The name of the release to ensure is present
+
+ chart_name
+ The name of the chart to install, including the repository name as
+ applicable (such as `stable/mysql`)
+
+ namespace
+ The namespace to which the release should be (re-)installed
+
+ version
+ The version of the chart to install. Defaults to the latest version
+
+ values_file
+ The path to the a values file containing all the chart values that
+ should be applied to the release. Note that this should not be passed
+ if there are not chart value overrides required.
+
+ '''
+ kwargs['tiller_namespace'] = tiller_namespace
+ if __opts__['test'] == True:
+ return {
+ 'result': None,
+ 'name': name,
+ 'comment': 'Helm chart "{0}" will be installed'.format(chart_name),
+ 'changes': {
+ 'chart_name': chart_name,
+ 'namespace': namespace,
+ }
+ }
+ old_release = __salt__['k8s_helm.get_release'](name, **kwargs)
+ if not old_release:
+ try:
+ result = __salt__['k8s_helm.release_create'](
+ name, chart_name, namespace, version, values_file, **kwargs
+ )
+ return {
+ 'name': name,
+ 'changes': {
+ 'name': name,
+ 'chart_name': chart_name,
+ 'namespace': namespace,
+ 'version': version,
+ 'values': _get_values_from_file(values_file),
+ 'stdout': result.get('stdout')
+ },
+ 'result': True,
+ 'comment': ('Release "%s" was created' % name +
+ '\nExecuted command: %s' % result['cmd'])
+ }
+ except CommandExecutionError as e:
+ msg = (("Failed to create new release: %s" % e.error) +
+ "\nExecuted command: %s" % e.cmd)
+ return _failure(name, msg)
+
+ changes = {}
+ warnings = []
+ if old_release.get('chart') != chart_name.split("/")[1]:
+ changes['chart'] = { 'old': old_release['chart'], 'new': chart_name }
+
+ if old_release.get('version') != version:
+ changes['version'] = { 'old': old_release['version'], 'new': version }
+
+ if old_release.get('namespace') != namespace:
+ changes['namespace'] = { 'old': old_release['namespace'], 'new': namespace }
+
+ if (not values_file and old_release.get("values") or
+ not old_release.get("values") and values_file):
+ changes['values'] = { 'old': old_release['values'], 'new': values_file }
+
+ values = _get_values_from_file(values_file)
+ diff = _get_yaml_diff(values, old_release.get('values'))
+
+ if diff:
+ diff_string = '\n'.join(diff)
+ if diff_string:
+ changes['values'] = diff_string
+
+ if not changes:
+ return {
+ 'name': name,
+ 'result': True,
+ 'changes': {},
+ 'comment': 'Release "{}" is already in the desired state'.format(name)
+ }
+
+ module_fn = 'k8s_helm.release_upgrade'
+ if changes.get("namespace"):
+ LOG.debug("purging old release (%s) due to namespace change" % name)
+ try:
+ result = __salt__['k8s_helm.release_delete'](name, **kwargs)
+ except CommandExecutionError as e:
+ msg = ("Failed to delete release for namespace change: %s" % e.error +
+ "\nExecuted command: %s" % e.cmd)
+ return _failure(name, msg, changes)
+
+ module_fn = 'k8s_helm.release_create'
+ warnings.append('Release (%s) was replaced due to namespace change' % name)
+
+ try:
+ result = __salt__[module_fn](
+ name, chart_name, namespace, version, values_file, **kwargs
+ )
+ changes.update({ 'stdout': result.get('stdout') })
+ ret = {
+ 'name': name,
+ 'changes': changes,
+ 'result': True,
+ 'comment': 'Release "%s" was updated\nExecuted command: %s' % (name, result['cmd'])
+ }
+ if warnings:
+ ret['warnings'] = warnings
+
+ return ret
+ except CommandExecutionError as e:
+ msg = ("Failed to delete release for namespace change: %s" % e.error +
+ "\nExecuted command: %s" % e.cmd)
+ return _failure(name, msg, changes)
+
+
+def absent(name, tiller_namespace='kube-system', **kwargs):
+ '''
+ Ensure that any release with the supplied release name is absent from the
+ tiller installation.
+
+ name
+ The name of the release to ensure is absent
+ '''
+ kwargs['tiller_namespace'] = tiller_namespace
+ exists = __salt__['k8s_helm.release_exists'](name, **kwargs)
+ if not exists:
+ return {
+ 'name': name,
+ 'changes': {},
+ 'result': True,
+ 'comment': 'Release "%s" doesn\'t exist' % name
+ }
+ try:
+ result = __salt__['k8s_helm.release_delete'](name, **kwargs)
+ return {
+ 'name': name,
+ 'changes': { name: 'DELETED', 'stdout': result['stdout'] },
+ 'result': True,
+ 'comment': 'Release "%s" was deleted\nExecuted command: %s' % (name, result['cmd'])
+ }
+ except CommandExecutionError as e:
+ return _failure(e.cmd, e.error)
+