blob: c51bfd3e96c9649fb7081bc76a0a5028393d2411 [file] [log] [blame]
Yuriy Taraday84a21032017-06-27 11:13:16 +04001import logging
tmeneaud92f4742017-10-18 09:57:19 -04002import re
Yuriy Taraday84a21032017-06-27 11:13:16 +04003
4from salt.serializers import yaml
tmeneau61efbef2017-10-17 11:19:46 -04005from salt.exceptions import CommandExecutionError
6
Yuriy Taraday84a21032017-06-27 11:13:16 +04007LOG = logging.getLogger(__name__)
8
tmeneaub858a002017-10-27 09:12:30 -04009class HelmExecutionError(CommandExecutionError):
10 def __init__(self, cmd, error):
11 self.cmd = cmd
12 self.error = error
Yuriy Taraday6618fb92017-08-11 17:11:48 +040013
tmeneau8cf4fce2017-10-17 15:05:35 -040014def _helm_cmd(*args, **kwargs):
15 if kwargs.get('tiller_host'):
16 addtl_args = ('--host', kwargs['tiller_host'])
17 elif kwargs.get('tiller_namespace'):
18 addtl_args = ('--tiller-namespace', kwargs['tiller_namespace'])
tmeneau61efbef2017-10-17 11:19:46 -040019 else:
tmeneau8cf4fce2017-10-17 15:05:35 -040020 addtl_args = ()
21
22 if kwargs.get('helm_home'):
23 addtl_args = addtl_args + ('--home', kwargs['helm_home'])
24
25 env = {}
26 if kwargs.get('kube_config'):
27 env['KUBECONFIG'] = kwargs['kube_config']
28 if kwargs.get('gce_service_token'):
Yuriy Taradaye9f982d2017-08-17 18:06:58 +040029 env['GOOGLE_APPLICATION_CREDENTIALS'] = \
tmeneau8cf4fce2017-10-17 15:05:35 -040030 kwargs['gce_service_token']
Yuriy Taraday84a21032017-06-27 11:13:16 +040031 return {
tmeneauf2392a12017-10-24 20:57:30 -040032 'cmd': ('helm',) + args + addtl_args,
Yuriy Taradayf9dd0122017-08-17 16:26:16 +040033 'env': env,
Yuriy Taraday84a21032017-06-27 11:13:16 +040034 }
35
tmeneaub858a002017-10-27 09:12:30 -040036def _cmd_and_result(*args, **kwargs):
37 cmd = _helm_cmd(*args, **kwargs)
38 env_string = "".join(['%s="%s" ' % (k, v) for (k, v) in cmd.get('env', {}).items()])
39 cmd_string = env_string + " ".join(cmd['cmd'])
40 result = None
41 try:
42 result = __salt__['cmd.run_all'](**cmd)
43 if result['retcode'] != 0:
44 raise CommandExecutionError(result['stderr'])
45 return {
46 'cmd': cmd_string,
47 'stdout': result['stdout'],
48 'stderr': result['stderr']
49 }
50 except CommandExecutionError as e:
51 raise HelmExecutionError(cmd_string, e)
52
53
tmeneaud92f4742017-10-18 09:57:19 -040054def _parse_release(output):
55 result = {}
56 chart_match = re.search(r'CHART\: ([^0-9]+)-([^\s]+)', output)
57 if chart_match:
58 result['chart'] = chart_match.group(1)
59 result['version'] = chart_match.group(2)
60
61 user_values_match = re.search(r"(?<=USER-SUPPLIED VALUES\:\n)(\n*.+)+?(?=\n*COMPUTED VALUES\:)", output, re.MULTILINE)
62 if user_values_match:
63 result['values'] = yaml.deserialize(user_values_match.group(0))
64
65 computed_values_match = re.search(r"(?<=COMPUTED VALUES\:\n)(\n*.+)+?(?=\n*HOOKS\:)", output, re.MULTILINE)
66 if computed_values_match:
67 result['computed_values'] = yaml.deserialize(computed_values_match.group(0))
68
69 manifest_match = re.search(r"(?<=MANIFEST\:\n)(\n*(?!Release \".+\" has been upgraded).*)+", output, re.MULTILINE)
70 if manifest_match:
71 result['manifest'] = manifest_match.group(0)
72
73 namespace_match = re.search(r"(?<=NAMESPACE\: )(.*)", output)
74 if namespace_match:
75 result['namespace'] = namespace_match.group(0)
76
77 return result
78
tmeneau61efbef2017-10-17 11:19:46 -040079def _parse_repo(repo_string = None):
80 split_string = repo_string.split('\t')
81 return {
82 "name": split_string[0].strip(),
83 "url": split_string[1].strip()
84 }
tmeneaud92f4742017-10-18 09:57:19 -040085
86
87def _get_release_namespace(name, tiller_namespace="kube-system", **kwargs):
88 cmd = _helm_cmd("list", name, **kwargs)
89 result = __salt__['cmd.run_stdout'](**cmd)
90 if not result or len(result.split("\n")) < 2:
91 return None
92
93 return result.split("\n")[1].split("\t")[5]
tmeneau61efbef2017-10-17 11:19:46 -040094
tmeneau8cf4fce2017-10-17 15:05:35 -040095def list_repos(**kwargs):
tmeneau61efbef2017-10-17 11:19:46 -040096 '''
97 Get the result of running `helm repo list` on the target minion, formatted
98 as a list of dicts with two keys:
99
100 * name: the name with which the repository is registered
101 * url: the url registered for the repository
102 '''
tmeneau8cf4fce2017-10-17 15:05:35 -0400103 cmd = _helm_cmd('repo', 'list', **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400104 result = __salt__['cmd.run_stdout'](**cmd)
105 if result is None:
106 return result
107
108 result = result.split("\n")
109 result.pop(0)
110 return {
111 repo['name']: repo['url'] for repo in [_parse_repo(line) for line in result]
112 }
113
tmeneau8cf4fce2017-10-17 15:05:35 -0400114def add_repo(name, url, **kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400115 '''
116 Register the repository located at the supplied url with the supplied name.
117 Note that re-using an existing name will overwrite the repository url for
118 that registered repository to point to the supplied url.
119
120 name
121 The name with which to register the repository with the Helm client.
122
123 url
124 The url for the chart repository.
125 '''
tmeneaub858a002017-10-27 09:12:30 -0400126 return _cmd_and_result('repo', 'add', name, url, **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400127
tmeneau8cf4fce2017-10-17 15:05:35 -0400128def remove_repo(name, **kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400129 '''
130 Remove the repository from the Helm client registered with the supplied
131 name.
132
133 name
134 The name (as registered with the Helm client) for the repository to remove
135 '''
tmeneaub858a002017-10-27 09:12:30 -0400136 return _cmd_and_result('repo', 'remove', name, **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400137
tmeneau8cf4fce2017-10-17 15:05:35 -0400138def manage_repos(present={}, absent=[], exclusive=False, **kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400139 '''
140 Manage the repositories registered with the Helm client's local cache.
141
142 *ensuring repositories are present*
143 Repositories that should be present in the helm client can be supplied via
144 the `present` dict parameter; each key in the dict is a release name, and the
145 value is the repository url that should be registered.
146
147 *ensuring repositories are absent*
148 Repository names supplied via the `absent` parameter must be a string. If the
149 `exclusive` flag is set to True, the `absent` parameter will be ignored, even
150 if it has been supplied.
151
152 This function returns a dict with the following keys:
153
154 * already_present: a listing of supplied repository definitions to add that
155 are already registered with the Helm client
156
157 * added: a list of repositories that are newly registered with the Helm
158 client. Each item in the list is a dict with the following keys:
159 * name: the repo name
160 * url: the repo url
161 * stdout: the output from the `helm repo add` command call for the repo
162
163 * already_absent: any repository name supplied via the `absent` parameter
164 that was already not registered with the Helm client
165
166 * removed: the result of attempting to remove any repositories
167
168 * failed: a list of repositores that were unable to be added. Each item in
169 the list is a dict with the following keys:
170 * type: the text "removal" or "addition", as appropriate
171 * name: the repo name
172 * url: the repo url (if appropriate)
173 * error: the output from add or remove command attempted for the
174 repository
175
176 present
177 The dict of repositories that should be registered with the Helm client.
178 Each dict key is the name with which the repository url (the corresponding
179 value) should be registered with the Helm client.
180
181 absent
182 The list of repositories to ensure are not registered with the Helm client.
183 Each entry in the list must be the (string) name of the repository.
184
185 exclusive
186 A flag indicating whether only the supplied repos should be available in
187 the target minion's Helm client. If configured to true, the `absent`
188 parameter will be ignored and only the repositories configured via the
189 `present` parameter will be registered with the Helm client. Defaults to
190 False.
191 '''
tmeneau8cf4fce2017-10-17 15:05:35 -0400192 existing_repos = list_repos(**kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400193 result = {
194 "already_present": [],
195 "added": [],
196 "already_absent": [],
197 "removed": [],
198 "failed": []
199 }
200
201 for name, url in present.iteritems():
202 if not name or not url:
203 raise CommandExecutionError(('Supplied repo to add must have a name (%s) '
204 'and url (%s)' % (name, url)))
205
206 if name in existing_repos and existing_repos[name] == url:
207 result['already_present'].append({ "name": name, "url": url })
208 continue
209
210 try:
211 result['added'].append({
212 'name': name,
213 'url': url,
tmeneaub858a002017-10-27 09:12:30 -0400214 'stdout': add_repo(name, url, **kwargs)['stdout']
tmeneau61efbef2017-10-17 11:19:46 -0400215 })
216 existing_repos = {
217 n: u for (n, u) in existing_repos.iteritems() if name != n
218 }
219 except CommandExecutionError as e:
220 result['failed'].append({
221 "type": "addition",
222 "name": name,
223 'url': url,
224 'error': '%s' % e
225 })
226
227 #
228 # Handle removal of repositories configured to be absent (or not configured
229 # to be present if the `exclusive` flag is set)
230 #
231 existing_names = [name for (name, url) in existing_repos.iteritems()]
232 if exclusive:
233 present['stable'] = "exclude"
234 absent = [name for name in existing_names if not name in present]
235
236 for name in absent:
237 if not name or not isinstance(name, str):
238 raise CommandExecutionError(('Supplied repo name to be absent must be a '
239 'string: %s' % name))
240
241 if name not in existing_names:
242 result['already_absent'].append(name)
243 continue
244
245 try:
tmeneau8cf4fce2017-10-17 15:05:35 -0400246 result['removed'].append({
247 'name': name,
tmeneaub858a002017-10-27 09:12:30 -0400248 'stdout': remove_repo(name, **kwargs) ['stdout']
tmeneau8cf4fce2017-10-17 15:05:35 -0400249 })
tmeneau61efbef2017-10-17 11:19:46 -0400250 except CommandExecutionError as e:
251 result['failed'].append({
252 "type": "removal", "name": name, "error": '%s' % e
253 })
254
255 return result
256
tmeneau8cf4fce2017-10-17 15:05:35 -0400257def update_repos(**kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400258 '''
259 Ensures the local helm repository cache for each repository is up to date.
260 Proxies the `helm repo update` command.
261 '''
tmeneaub858a002017-10-27 09:12:30 -0400262 return _cmd_and_result('repo', 'update', **kwargs)
Yuriy Taraday84a21032017-06-27 11:13:16 +0400263
tmeneaud92f4742017-10-18 09:57:19 -0400264def get_release(name, tiller_namespace="kube-system", **kwargs):
265 '''
266 Get the parsed release metadata from calling `helm get {{ release }}` for the
267 supplied release name, or None if no release is found. The following keys may
268 or may not be in the returned dict:
Yuriy Taraday84a21032017-06-27 11:13:16 +0400269
tmeneaud92f4742017-10-18 09:57:19 -0400270 * chart
271 * version
272 * values
273 * computed_values
274 * manifest
275 * namespace
276 '''
277 kwargs['tiller_namespace'] = tiller_namespace
278 cmd = _helm_cmd('get', name, **kwargs)
279 result = __salt__['cmd.run_stdout'](**cmd)
280 if not result:
281 return None
282
283 release = _parse_release(result)
284
285 #
286 # `helm get {{ release }}` doesn't currently (2.6.2) return the namespace, so
287 # separately retrieve it if it's not available
288 #
289 if not 'namespace' in release:
290 release['namespace'] = _get_release_namespace(name, **kwargs)
291 return release
292
293def release_exists(name, tiller_namespace="kube-system", **kwargs):
294 '''
295 Determine whether a release exists in the cluster with the supplied name
296 '''
297 kwargs['tiller_namespace'] = tiller_namespace
298 return get_release(name, **kwargs) is not None
Yuriy Taraday84a21032017-06-27 11:13:16 +0400299
Yuriy Taradayf169d822017-08-14 13:40:21 +0400300def release_create(name, chart_name, namespace='default',
tmeneau94bf68e2017-10-17 15:55:34 -0400301 version=None, values_file=None,
tmeneaud92f4742017-10-18 09:57:19 -0400302 tiller_namespace='kube-system', **kwargs):
303 '''
304 Install a release. There must not be a release with the supplied name
305 already installed to the Kubernetes cluster.
306
307 Note that if a release already exists with the specified name, you'll need
308 to use the release_upgrade function instead; unless the release is in a
309 different namespace, in which case you'll need to delete and purge the
310 existing release (using release_delete) and *then* use this function to
311 install a new release to the desired namespace.
312 '''
Yuriy Taraday84a21032017-06-27 11:13:16 +0400313 args = []
314 if version is not None:
315 args += ['--version', version]
tmeneau94bf68e2017-10-17 15:55:34 -0400316 if values_file is not None:
317 args += ['--values', values_file]
tmeneaub858a002017-10-27 09:12:30 -0400318 return _cmd_and_result(
319 'install', chart_name,
320 '--namespace', namespace,
321 '--name', name,
322 *args, **kwargs
323 )
Yuriy Taradayaeeaa742017-06-28 15:54:56 +0400324
tmeneaud92f4742017-10-18 09:57:19 -0400325def release_delete(name, tiller_namespace='kube-system', **kwargs):
326 '''
327 Delete and purge any release found with the supplied name.
328 '''
329 kwargs['tiller_namespace'] = tiller_namespace
tmeneaub858a002017-10-27 09:12:30 -0400330 return _cmd_and_result('delete', '--purge', name, **kwargs)
Yuriy Taraday893b3fb2017-07-03 16:22:57 +0400331
332
Yuriy Taradayf169d822017-08-14 13:40:21 +0400333def release_upgrade(name, chart_name, namespace='default',
tmeneau94bf68e2017-10-17 15:55:34 -0400334 version=None, values_file=None,
tmeneaud92f4742017-10-18 09:57:19 -0400335 tiller_namespace='kube-system', **kwargs):
336 '''
337 Upgrade an existing release. There must be a release with the supplied name
338 already installed to the Kubernetes cluster.
339
340 If attempting to change the namespace for the release, this function will
341 fail; you will need to first delete and purge the release and then use the
342 release_create function to create a new release in the desired namespace.
343 '''
344 kwargs['tiller_namespace'] = tiller_namespace
Yuriy Taradayaeeaa742017-06-28 15:54:56 +0400345 args = []
346 if version is not None:
tmeneau94bf68e2017-10-17 15:55:34 -0400347 args += ['--version', version]
348 if values_file is not None:
349 args += ['--values', values_file]
tmeneaub858a002017-10-27 09:12:30 -0400350 return _cmd_and_result(
351 'upgrade', name, chart_name,
352 '--namespace', namespace,
tmeneau6c29c002017-10-27 09:13:06 -0400353 *args, **kwargs
tmeneaub858a002017-10-27 09:12:30 -0400354 )
tmeneauf2392a12017-10-24 20:57:30 -0400355
356def install_chart_dependencies(chart_path, **kwargs):
357 '''
358 Install the chart dependencies for the chart definition located at the
359 specified chart_path.
360
361 chart_path
362 The path to the chart for which to install dependencies
363 '''
tmeneauf6098372017-10-27 15:26:03 -0400364 return _cmd_and_result('dependency', 'build', chart_path, **kwargs)
tmeneauf2392a12017-10-24 20:57:30 -0400365
366def package(path, destination = None, **kwargs):
367 '''
368 Package a chart definition, optionally to a specific destination. Proxies the
369 `helm package` command on the target minion
370
371 path
372 The path to the chart definition to package.
373
374 destination : None
375 An optional alternative destination folder.
376 '''
377 args = []
378 if destination:
379 args += ["-d", destination]
380
tmeneaub858a002017-10-27 09:12:30 -0400381 return _cmd_and_result('package', path, *args, **kwargs)