| 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")[5] |
| |
| 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) |