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/README.rst b/README.rst
index 3cf026d..7de80e1 100644
--- a/README.rst
+++ b/README.rst
@@ -1208,6 +1208,31 @@
               enabled: true
               key: mylabel
 
+Install Helm charts:
+
+.. code-block:: yaml
+
+    kubernetes:
+      client:
+        helm:
+          enabled: True
+          repos:
+            repo1:
+              enabled: True
+              repository:
+                test: https://mirantisworkloads.storage.googleapis.com/
+          charts:
+            010_ingress_kube_system:
+              enabled: True
+              release: ingress-kube-system
+              chart_name: local/ingress
+              namespace: kube-system
+              values:
+                deployment:
+                  mode: cluster
+                  type: DaemonSet
+                network:
+                  host_namespace: True
 More Information
 ================
 
diff --git a/_modules/k8s_helm.py b/_modules/k8s_helm.py
new file mode 100644
index 0000000..81660d1
--- /dev/null
+++ b/_modules/k8s_helm.py
@@ -0,0 +1,381 @@
+import logging
+import re
+
+from salt.serializers import yaml
+from salt.exceptions import CommandExecutionError
+
+LOG = logging.getLogger(__name__)
+
+class HelmExecutionError(CommandExecutionError):
+    def __init__(self, cmd, error):
+        self.cmd = cmd
+        self.error = error
+
+def _helm_cmd(*args, **kwargs):
+    if kwargs.get('tiller_host'):
+        addtl_args = ('--host', kwargs['tiller_host'])
+    elif kwargs.get('tiller_namespace'):
+        addtl_args = ('--tiller-namespace', kwargs['tiller_namespace'])
+    else:
+        addtl_args = ()
+
+    if kwargs.get('helm_home'):
+        addtl_args = addtl_args + ('--home', kwargs['helm_home'])
+
+    env = {}
+    if kwargs.get('kube_config'):
+        env['KUBECONFIG'] = kwargs['kube_config']
+    if kwargs.get('gce_service_token'):
+        env['GOOGLE_APPLICATION_CREDENTIALS'] = \
+            kwargs['gce_service_token']
+    return {
+        'cmd': ('helm',) + args + addtl_args,
+        'env': env,
+    }
+
+def _cmd_and_result(*args, **kwargs):
+    cmd = _helm_cmd(*args, **kwargs)
+    env_string = "".join(['%s="%s" ' % (k, v) for (k, v) in cmd.get('env', {}).items()])
+    cmd_string = env_string + " ".join(cmd['cmd'])
+    result = None
+    try:
+        result = __salt__['cmd.run_all'](**cmd)
+        if result['retcode'] != 0:
+            raise CommandExecutionError(result['stderr'])
+        return {
+            'cmd': cmd_string,
+          'stdout': result['stdout'],
+          'stderr': result['stderr']
+        }
+    except CommandExecutionError as e:
+        raise HelmExecutionError(cmd_string, e)
+
+
+def _parse_release(output):
+    result = {}
+    chart_match = re.search(r'CHART\: ([^0-9]+)-([^\s]+)', output)
+    if chart_match:
+        result['chart'] = chart_match.group(1)
+        result['version'] = chart_match.group(2)
+
+    user_values_match = re.search(r"(?<=USER-SUPPLIED VALUES\:\n)(\n*.+)+?(?=\n*COMPUTED VALUES\:)", output, re.MULTILINE)
+    if user_values_match:
+        result['values'] = yaml.deserialize(user_values_match.group(0))
+
+    computed_values_match = re.search(r"(?<=COMPUTED VALUES\:\n)(\n*.+)+?(?=\n*HOOKS\:)", output, re.MULTILINE)
+    if computed_values_match:
+        result['computed_values'] = yaml.deserialize(computed_values_match.group(0))
+
+    manifest_match = re.search(r"(?<=MANIFEST\:\n)(\n*(?!Release \".+\" has been upgraded).*)+", output, re.MULTILINE)
+    if manifest_match:
+        result['manifest'] = manifest_match.group(0)
+
+    namespace_match = re.search(r"(?<=NAMESPACE\: )(.*)", output)
+    if namespace_match:
+        result['namespace'] = namespace_match.group(0)
+
+    return result
+
+def _parse_repo(repo_string = None):
+    split_string = repo_string.split('\t')
+    return {
+        "name": split_string[0].strip(),
+      "url": split_string[1].strip()
+    }
+
+
+def _get_release_namespace(name, tiller_namespace="kube-system", **kwargs):
+    cmd = _helm_cmd("list", name, **kwargs)
+    result = __salt__['cmd.run_stdout'](**cmd)
+    if not result or len(result.split("\n")) < 2:
+        return None
+
+    return result.split("\n")[1].split("\t")[6]
+
+def list_repos(**kwargs):
+    '''
+    Get the result of running `helm repo list` on the target minion, formatted
+    as a list of dicts with two keys:
+
+      * name: the name with which the repository is registered
+      * url: the url registered for the repository
+    '''
+    cmd = _helm_cmd('repo', 'list', **kwargs)
+    result = __salt__['cmd.run_stdout'](**cmd)
+    if result is None:
+        return result
+
+    result = result.split("\n")
+    result.pop(0)
+    return {
+        repo['name']: repo['url'] for repo in [_parse_repo(line) for line in result]
+    }
+
+def add_repo(name, url, **kwargs):
+    '''
+    Register the repository located at the supplied url with the supplied name.
+    Note that re-using an existing name will overwrite the repository url for
+    that registered repository to point to the supplied url.
+
+    name
+        The name with which to register the repository with the Helm client.
+
+    url
+        The url for the chart repository.
+    '''
+    return _cmd_and_result('repo', 'add', name, url, **kwargs)
+
+def remove_repo(name, **kwargs):
+    '''
+    Remove the repository from the Helm client registered with the supplied
+    name.
+
+    name
+        The name (as registered with the Helm client) for the repository to remove
+    '''
+    return _cmd_and_result('repo', 'remove', name, **kwargs)
+
+def manage_repos(present={}, absent=[], exclusive=False, **kwargs):
+    '''
+    Manage the repositories registered with the Helm client's local cache.
+
+    *ensuring repositories are present*
+    Repositories that should be present in the helm client can be supplied via
+    the `present` dict parameter; each key in the dict is a release name, and the
+    value is the repository url that should be registered.
+
+    *ensuring repositories are absent*
+    Repository names supplied via the `absent` parameter must be a string. If the
+    `exclusive` flag is set to True, the `absent` parameter will be ignored, even
+    if it has been supplied.
+
+    This function returns a dict with the following keys:
+
+      * already_present: a listing of supplied repository definitions to add that
+        are already registered with the Helm client
+
+      * added: a list of repositories that are newly registered with the Helm
+        client. Each item in the list is a dict with the following keys:
+          * name: the repo name
+          * url: the repo url
+          * stdout: the output from the `helm repo add` command call for the repo
+
+      * already_absent: any repository name supplied via the `absent` parameter
+        that was already not registered with the Helm client
+
+      * removed: the result of attempting to remove any repositories
+
+      * failed: a list of repositores that were unable to be added. Each item in
+        the list is a dict with the following keys:
+          * type: the text "removal" or "addition", as appropriate
+          * name: the repo name
+          * url: the repo url (if appropriate)
+          * error: the output from add or remove command attempted for the
+            repository
+
+    present
+        The dict of repositories that should be registered with the Helm client.
+        Each dict key is the name with which the repository url (the corresponding
+        value) should be registered with the Helm client.
+
+    absent
+        The list of repositories to ensure are not registered with the Helm client.
+        Each entry in the list must be the (string) name of the repository.
+
+    exclusive
+        A flag indicating whether only the supplied repos should be available in
+        the target minion's Helm client. If configured to true, the `absent`
+        parameter will be ignored and only the repositories configured via the
+        `present` parameter will be registered with the Helm client. Defaults to
+        False.
+    '''
+    existing_repos = list_repos(**kwargs)
+    result = {
+        "already_present": [],
+      "added": [],
+      "already_absent": [],
+      "removed": [],
+      "failed": []
+    }
+
+    for name, url in present.iteritems():
+        if not name or not url:
+            raise CommandExecutionError(('Supplied repo to add must have a name (%s) '
+                                         'and url (%s)' % (name, url)))
+
+        if name in existing_repos and existing_repos[name] == url:
+            result['already_present'].append({ "name": name, "url": url })
+            continue
+
+        try:
+            result['added'].append({
+                'name': name,
+              'url': url,
+              'stdout': add_repo(name, url, **kwargs)['stdout']
+            })
+            existing_repos = {
+                n: u for (n, u) in existing_repos.iteritems() if name != n
+            }
+        except CommandExecutionError as e:
+            result['failed'].append({
+                "type": "addition",
+              "name": name,
+              'url': url,
+              'error': '%s' % e
+            })
+
+    #
+    # Handle removal of repositories configured to be absent (or not configured
+    # to be present if the `exclusive` flag is set)
+    #
+    existing_names = [name for (name, url) in existing_repos.iteritems()]
+    if exclusive:
+        present['stable'] = "exclude"
+        absent = [name for name in existing_names if not name in present]
+
+    for name in absent:
+        if not name or not isinstance(name, str):
+            raise CommandExecutionError(('Supplied repo name to be absent must be a '
+                                         'string: %s' % name))
+
+        if name not in existing_names:
+            result['already_absent'].append(name)
+            continue
+
+        try:
+            result['removed'].append({
+                'name': name,
+              'stdout': remove_repo(name, **kwargs) ['stdout']
+            })
+        except CommandExecutionError as e:
+            result['failed'].append({
+                "type": "removal", "name": name, "error": '%s' % e
+            })
+
+    return result
+
+def update_repos(**kwargs):
+    '''
+    Ensures the local helm repository cache for each repository is up to date.
+    Proxies the `helm repo update` command.
+    '''
+    return _cmd_and_result('repo', 'update', **kwargs)
+
+def get_release(name, tiller_namespace="kube-system", **kwargs):
+    '''
+    Get the parsed release metadata from calling `helm get {{ release }}` for the
+    supplied release name, or None if no release is found. The following keys may
+    or may not be in the returned dict:
+
+      * chart
+      * version
+      * values
+      * computed_values
+      * manifest
+      * namespace
+    '''
+    kwargs['tiller_namespace'] = tiller_namespace
+    cmd = _helm_cmd('get', name, **kwargs)
+    result = __salt__['cmd.run_stdout'](**cmd)
+    if not result:
+        return None
+
+    release = _parse_release(result)
+
+    #
+    # `helm get {{ release }}` doesn't currently (2.6.2) return the namespace, so
+    # separately retrieve it if it's not available
+    #
+    if not 'namespace' in release:
+        release['namespace'] = _get_release_namespace(name, **kwargs)
+    return release
+
+def release_exists(name, tiller_namespace="kube-system", **kwargs):
+    '''
+    Determine whether a release exists in the cluster with the supplied name
+    '''
+    kwargs['tiller_namespace'] = tiller_namespace
+    return get_release(name, **kwargs) is not None
+
+def release_create(name, chart_name, namespace='default',
+                   version=None, values_file=None,
+                   tiller_namespace='kube-system', **kwargs):
+    '''
+    Install a release. There must not be a release with the supplied name
+    already installed to the Kubernetes cluster.
+
+    Note that if a release already exists with the specified name, you'll need
+    to use the release_upgrade function instead; unless the release is in a
+    different namespace, in which case you'll need to delete and purge the
+    existing release (using release_delete) and *then* use this function to
+    install a new release to the desired namespace.
+    '''
+    args = []
+    if version is not None:
+        args += ['--version', version]
+    if values_file is not None:
+        args += ['--values', values_file]
+    return _cmd_and_result(
+        'install', chart_name,
+      '--namespace', namespace,
+      '--name', name,
+      *args, **kwargs
+    )
+
+def release_delete(name, tiller_namespace='kube-system', **kwargs):
+    '''
+    Delete and purge any release found with the supplied name.
+    '''
+    kwargs['tiller_namespace'] = tiller_namespace
+    return _cmd_and_result('delete', '--purge', name, **kwargs)
+
+
+def release_upgrade(name, chart_name, namespace='default',
+                    version=None, values_file=None,
+                    tiller_namespace='kube-system', **kwargs):
+    '''
+    Upgrade an existing release. There must be a release with the supplied name
+    already installed to the Kubernetes cluster.
+
+    If attempting to change the namespace for the release, this function will
+    fail; you will need to first delete and purge the release and then use the
+    release_create function to create a new release in the desired namespace.
+    '''
+    kwargs['tiller_namespace'] = tiller_namespace
+    args = []
+    if version is not None:
+        args += ['--version', version]
+    if values_file is not None:
+        args += ['--values', values_file]
+    return _cmd_and_result(
+        'upgrade', name, chart_name,
+      '--namespace', namespace,
+      *args, **kwargs
+    )
+
+def install_chart_dependencies(chart_path, **kwargs):
+    '''
+    Install the chart dependencies for the chart definition located at the
+    specified chart_path.
+
+    chart_path
+        The path to the chart for which to install dependencies
+    '''
+    return _cmd_and_result('dependency', 'build', chart_path, **kwargs)
+
+def package(path, destination = None, **kwargs):
+    '''
+    Package a chart definition, optionally to a specific destination. Proxies the
+    `helm package` command on the target minion
+
+    path
+        The path to the chart definition to package.
+
+    destination : None
+        An optional alternative destination folder.
+    '''
+    args = []
+    if destination:
+        args += ["-d", destination]
+
+    return _cmd_and_result('package', path, *args, **kwargs)
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)
+
diff --git a/_states/k8s_helm_repos.py b/_states/k8s_helm_repos.py
new file mode 100644
index 0000000..f192e4b
--- /dev/null
+++ b/_states/k8s_helm_repos.py
@@ -0,0 +1,118 @@
+import re
+
+from salt.exceptions import CommandExecutionError
+
+def managed(name, present={}, absent=[], exclusive=False, helm_home=None):
+    '''
+    Ensure the supplied repositories are available to the helm client. If the
+    `exclusive` flag is set to a truthy value, any extra repositories in the
+    helm client will be removed.
+
+    name
+        The name of the state
+
+    present
+        A dict of repository names to urls to ensure are registered with the
+        Helm client
+
+    absent
+        A list of repository names to ensure are unregistered from the Helm client
+
+    exclusive
+        A boolean flag indicating whether the state should ensure only the
+        supplied repositories are availabe to the target minion.
+
+    helm_home
+        An optional path to the Helm home directory
+    '''
+
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': ''}
+
+    if __opts__['test'] == True:
+        ret['result'] = None
+        if len(present) == 0:
+            ret['comment'] = 'Helm repository "{0}" will be added'.format(absent)
+        else:
+            ret['comment'] = 'Helm repository "{0}" will be added'.format(present.keys())
+        ret['changes']['repository'] = present.items()
+        return ret
+
+    try:
+        result = __salt__['k8s_helm.manage_repos'](
+            present=present,
+          absent=absent,
+          exclusive=exclusive,
+          helm_home=helm_home
+        )
+
+        if result['failed']:
+            ret['comment'] = 'Failed to add or remove some repositories'
+            ret['changes'] = result
+            ret['result'] = False
+            return ret
+
+        if result['added'] or result['removed']:
+            ret['comment'] = 'Repositories were added or removed'
+            ret['changes'] = result
+            return ret
+
+        ret['comment'] = ("Repositories were in the desired state: "
+                         "%s" % [name for (name, url) in present.iteritems()])
+        return ret
+    except CommandExecutionError as e:
+        ret['result'] = False
+        ret['comment'] = "Failed to add some repositories: %s" % e
+        return ret
+
+def updated(name, helm_home=None):
+    '''
+    Ensure the local Helm repository cache is up to date with each of the
+    helm client's configured remote chart repositories. Because the `helm repo
+    update` command doesn't indicate whether any changes were made to the local
+    cache, this will only indicate change if the Helm client failed to retrieve
+    an update from one or more of the repositories, regardless of whether an
+    update was made to the local Helm chart repository cache.
+
+    name
+        The name of the state
+
+    helm_home
+        An optional path to the Helm home directory
+    '''
+    ret = {'name': name,
+           'changes': {},
+           'result': True,
+           'comment': 'Successfully synced repositories: ' }
+
+    if __opts__['test'] == True:
+        ret['result'] = None
+        ret['comment'] = 'Repositories will be updated'
+        return ret
+    try:
+        result = __salt__['k8s_helm.update_repos'](helm_home=helm_home)
+        cmd_str = "\nExecuted command: %s" % result['cmd']
+
+        success_repos = re.findall(
+            r'Successfully got an update from the \"([^\"]+)\"', result['stdout'])
+        failed_repos = re.findall(
+            r'Unable to get an update from the \"([^\"]+)\"', result['stdout'])
+
+        if failed_repos and len(failed_repos) > 0:
+            ret['result'] = False
+            ret['changes']['succeeded'] = success_repos
+            ret['changes']['failed'] = failed_repos
+            ret['comment'] = 'Failed to sync against some repositories' + cmd_str
+        else:
+            ret['comment'] += "%s" % success_repos + cmd_str
+
+    except CommandExecutionError as e:
+        ret['name'] = e.cmd
+        ret['result'] = False
+        ret['comment'] = ("Failed to update repos: %s" % e.error +
+                          "\nExecuted command: %s" % e.cmd)
+        return ret
+
+    return ret
diff --git a/kubernetes/_common.sls b/kubernetes/_common.sls
index b4762a1..4f84ecf 100644
--- a/kubernetes/_common.sls
+++ b/kubernetes/_common.sls
@@ -354,4 +354,12 @@
   - require:
     - archive: extract_helm
 
+helm_client_init:
+  cmd.run:
+    - name: "helm init --client-only"
+    - env:
+      - HELM_HOME: {{ common.helm_home }}
+    - require:
+      - file: /usr/local/bin/helm
+
 {%- endif %}
diff --git a/kubernetes/client.sls b/kubernetes/client.sls
index 9c1c383..7247c20 100644
--- a/kubernetes/client.sls
+++ b/kubernetes/client.sls
@@ -1,4 +1,4 @@
-{%- from "kubernetes/map.jinja" import client with context -%}
+{%- from "kubernetes/map.jinja" import client, common with context -%}
 {%- if client.enabled %}
   {%- if client.get('resources', {}).get('enabled') %}
 
@@ -30,4 +30,61 @@
       {%- endif %} # endif label.enabled
     {%- endfor %} # endfor client.resources.label
   {%- endif %} # endif client.resources.enabled
-{%- endif %} # endif client.enabled
+
+{%- if client.helm is defined %}
+
+openstack_helm_packages:
+  pkg.installed:
+  - names: {{ client.pkgs }}
+
+{%- set _helm = client.helm %}
+{%- if _helm.get('enabled', false) %}
+  {%- if _helm.repos is defined  %}
+    {%- for repo_id, helm_repo in _helm.repos.iteritems() %}
+      {%- if helm_repo.get('enabled', True) %}
+repo_{{ repo_id }}_managed:
+  k8s_helm_repos.managed:
+    - present:
+        {{ helm_repo['repository'] }}
+    - helm_home: {{ common.helm_home }}
+      {%- endif %}
+    {%- endfor %}
+repos_updated:
+  k8s_helm_repos.updated:
+    - helm_home: {{ common.helm_home }}
+  {%- endif %}
+
+  {%- if _helm.charts is defined  %}
+    {%- for release_id, helm_chart in _helm.charts|dictsort %}
+      {%- set release_name = helm_chart.get('release', release_id) %}
+      {%- set namespace = helm_chart.get('namespace', 'default') %}
+      {%- set values_file = "/tmp/helm_chart_" + release_name + "_values.yaml" %}
+      {%- if helm_chart.get('enabled', True) %}
+        {%- if helm_chart.get("values") %}
+{{ values_file }}:
+  file.managed:
+    - makedirs: True
+    - contents: |
+        {{ helm_chart['values'] | yaml(false) | indent(8) }}
+        {%- endif %}
+
+ensure_{{ release_id }}_release:
+  k8s_helm_release.present:
+    - name: {{ release_name }}
+    - chart_name: {{ helm_chart.chart_name }}
+    - namespace: {{ namespace }}
+    - helm_home: {{ common.helm_home }}
+    {%- if helm_chart.version is defined %}
+    - version: {{ helm_chart.version }}
+    {%- endif %}
+    {%- if helm_chart.values is defined %}
+    - values_file: {{ values_file }}
+    {%- endif %}
+      {%- endif %}
+
+    {%- endfor %}
+  {%- endif %}
+{%- endif %}
+{%- endif %}
+
+{%- endif %}
diff --git a/kubernetes/map.jinja b/kubernetes/map.jinja
index 3c3d4cb..cf98d6e 100644
--- a/kubernetes/map.jinja
+++ b/kubernetes/map.jinja
@@ -3,14 +3,16 @@
 
 {% set common = salt['grains.filter_by']({
     'Debian': {
-        'pkgs': ['curl', 'git', 'apt-transport-https', 'python-apt', 'socat', 'openssl', 'conntrack'],
+        'pkgs': ['curl', 'git', 'apt-transport-https', 'python-apt', 'socat', 'openssl', 'conntrack', 'nfs-common', 'cifs-utils'],
         'services': [],
         'version': version|float,
+        'helm_home': '/root/.helm',
     },
     'RedHat': {
         'pkgs': ['curl', 'git', 'socat', 'python', 'openssl'],
         'services': [],
         'version': version|float,
+        'helm_home': '/root/.helm',
     },
 }, merge=salt['pillar.get']('kubernetes:common')) %}
 
@@ -112,6 +114,12 @@
 }, merge=salt['pillar.get']('kubernetes:control')) %}
 
 {% set client = salt['grains.filter_by']({
+'Debian': {
+        'pkgs': ['python-openstackclient', 'python-ironicclient', 'python-heatclient'],
+    },
+    'RedHat': {
+        'pkgs': ['python-openstackclient', 'python-ironicclient', 'python-heatclient'],
+    },
 }, merge=salt['pillar.get']('kubernetes:client')) %}
 
 {%- set monitoring = salt['grains.filter_by']({