fix: add repo update state and improve repo change detection
Also rename the repos_synchronized SLS state to repos_managed
Fixes salt-formulas/salt-formula-helm#4
diff --git a/_modules/helm.py b/_modules/helm.py
index 6493aa5..9e62154 100644
--- a/_modules/helm.py
+++ b/_modules/helm.py
@@ -1,6 +1,8 @@
import logging
from salt.serializers import yaml
+from salt.exceptions import CommandExecutionError
+
HELM_HOME = '/srv/helm/home'
LOG = logging.getLogger(__name__)
@@ -16,14 +18,16 @@
def _helm_cmd(*args, **tiller_kwargs):
- if tiller_kwargs['tiller_host']:
+ if tiller_kwargs.get('tiller_host'):
tiller_args = ('--host', tiller_kwargs['tiller_host'])
- else:
+ elif tiller_kwargs.get('tiller_namespace'):
tiller_args = ('--tiller-namespace', tiller_kwargs['tiller_namespace'])
+ else:
+ tiller_args = ()
env = {'HELM_HOME': HELM_HOME}
- if tiller_kwargs['kube_config']:
+ if tiller_kwargs.get('kube_config'):
env['KUBECONFIG'] = tiller_kwargs['kube_config']
- if tiller_kwargs['gce_service_token']:
+ if tiller_kwargs.get('gce_service_token'):
env['GOOGLE_APPLICATION_CREDENTIALS'] = \
tiller_kwargs['gce_service_token']
return {
@@ -31,6 +35,187 @@
'env': env,
}
+def _parse_repo(repo_string = None):
+ split_string = repo_string.split('\t')
+ return {
+ "name": split_string[0].strip(),
+ "url": split_string[1].strip()
+ }
+
+def list_repos():
+ '''
+ 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')
+ 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):
+ '''
+ 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.
+ '''
+ cmd = _helm_cmd('repo', 'add', name, url)
+ ret = __salt__['cmd.run_all'](**cmd)
+ if ret['retcode'] != 0:
+ raise CommandExecutionError(ret['stderr'])
+ return ret['stdout']
+
+def remove_repo(name):
+ '''
+ 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
+ '''
+ cmd = _helm_cmd('repo', 'remove', name)
+ ret = __salt__['cmd.run_all'](**cmd)
+ if ret['retcode'] != 0:
+ raise CommandExecutionError(ret['stderr'])
+ return ret['stdout']
+
+def manage_repos(present={}, absent=[], exclusive=False):
+ '''
+ 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()
+ 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)
+ })
+ 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) })
+ except CommandExecutionError as e:
+ result['failed'].append({
+ "type": "removal", "name": name, "error": '%s' % e
+ })
+
+ return result
+
+def update_repos():
+ '''
+ Ensures the local helm repository cache for each repository is up to date.
+ Proxies the `helm repo update` command.
+ '''
+ cmd = _helm_cmd('repo', 'update')
+ return __salt__['cmd.run_stdout'](**cmd)
def release_exists(name, namespace='default',
tiller_namespace='kube-system', tiller_host=None,
diff --git a/_states/helm_repos.py b/_states/helm_repos.py
new file mode 100644
index 0000000..88f0894
--- /dev/null
+++ b/_states/helm_repos.py
@@ -0,0 +1,93 @@
+import re
+
+from salt.exceptions import CommandExecutionError
+
+def managed(name, present={}, absent=[], exclusive=False):
+ '''
+ 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.
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': True,
+ 'comment': ''}
+
+ try:
+ result = __salt__['helm.manage_repos'](
+ present=present,
+ absent=absent,
+ exclusive=exclusive
+ )
+
+ 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):
+ '''
+ 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
+ '''
+ ret = {'name': name,
+ 'changes': {},
+ 'result': True,
+ 'comment': 'Successfully synced repositories: ' }
+
+ output = None
+ try:
+ output = __salt__['helm.update_repos']()
+ except CommandExecutionError as e:
+ ret['result'] = False
+ ret['comment'] = "Failed to update repos: %s" % e
+ return ret
+
+ success_repos = re.findall(
+ r'Successfully got an update from the \"([^\"]+)\"', output)
+ failed_repos = re.findall(
+ r'Unable to get an update from the \"([^\"]+)\"', output)
+
+ 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'
+ else:
+ ret['comment'] += "%s" % success_repos
+
+ return ret
\ No newline at end of file
diff --git a/helm/releases_managed.sls b/helm/releases_managed.sls
index 152e09c..968a5d5 100644
--- a/helm/releases_managed.sls
+++ b/helm/releases_managed.sls
@@ -4,7 +4,7 @@
- .client_installed
- .tiller_installed
- .kubectl_configured
- - .repos_synchronized
+ - .repos_managed
{%- if "releases" in config %}
{%- for release_id, release in config.releases.items() %}
diff --git a/helm/repos_managed.sls b/helm/repos_managed.sls
new file mode 100644
index 0000000..a870138
--- /dev/null
+++ b/helm/repos_managed.sls
@@ -0,0 +1,19 @@
+{%- from slspath + "/map.jinja" import config, constants with context %}
+
+include:
+ - .client_installed
+
+{%- if "repos" in config %}
+repos_managed:
+ helm_repos.managed:
+ - present:
+ {{ config.repos | yaml(false) | indent(8) }}
+ - exclusive: true
+ - require:
+ - sls: {{ slspath }}.client_installed
+{%- endif %}
+
+repos_updated:
+ helm_repos.updated:
+ - require:
+ - sls: {{ slspath }}.client_installed
\ No newline at end of file
diff --git a/helm/repos_synchronized.sls b/helm/repos_synchronized.sls
deleted file mode 100644
index f4c3f91..0000000
--- a/helm/repos_synchronized.sls
+++ /dev/null
@@ -1,17 +0,0 @@
-{%- from slspath + "/map.jinja" import config, constants with context %}
-
-include:
- - .client_installed
-
-{%- if "repos" in config %}
-{%- for repo_name, repo_url in config.repos.items() %}
-ensure_{{ repo_name }}_repo:
- cmd.run:
- - name: {{ constants.helm.cmd }} repo add {{ repo_name }} {{ repo_url }}
- - env:
- - HELM_HOME: {{ constants.helm.home }}
- - unless: {{ constants.helm.cmd }} repo list | grep '^{{ repo_name }} {{ repo_url|replace(".", "\.") }}'
- - require:
- - sls: {{ slspath }}.client_installed
-{%- endfor %}
-{%- endif %}{# "repos" in client #}