blob: 81660d1714fce3fb4557bd1237a8ba48f713b0f4 [file] [log] [blame]
Oleksii Grudev4cf21532019-04-16 13:17:57 +00001import logging
2import re
3
4from salt.serializers import yaml
5from salt.exceptions import CommandExecutionError
6
7LOG = logging.getLogger(__name__)
8
9class HelmExecutionError(CommandExecutionError):
10 def __init__(self, cmd, error):
11 self.cmd = cmd
12 self.error = error
13
14def _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'])
19 else:
20 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'):
29 env['GOOGLE_APPLICATION_CREDENTIALS'] = \
30 kwargs['gce_service_token']
31 return {
32 'cmd': ('helm',) + args + addtl_args,
33 'env': env,
34 }
35
36def _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
54def _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
79def _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 }
85
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")[6]
94
95def list_repos(**kwargs):
96 '''
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 '''
103 cmd = _helm_cmd('repo', 'list', **kwargs)
104 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
114def add_repo(name, url, **kwargs):
115 '''
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 '''
126 return _cmd_and_result('repo', 'add', name, url, **kwargs)
127
128def remove_repo(name, **kwargs):
129 '''
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 '''
136 return _cmd_and_result('repo', 'remove', name, **kwargs)
137
138def manage_repos(present={}, absent=[], exclusive=False, **kwargs):
139 '''
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 '''
192 existing_repos = list_repos(**kwargs)
193 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,
214 'stdout': add_repo(name, url, **kwargs)['stdout']
215 })
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:
246 result['removed'].append({
247 'name': name,
248 'stdout': remove_repo(name, **kwargs) ['stdout']
249 })
250 except CommandExecutionError as e:
251 result['failed'].append({
252 "type": "removal", "name": name, "error": '%s' % e
253 })
254
255 return result
256
257def update_repos(**kwargs):
258 '''
259 Ensures the local helm repository cache for each repository is up to date.
260 Proxies the `helm repo update` command.
261 '''
262 return _cmd_and_result('repo', 'update', **kwargs)
263
264def 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:
269
270 * 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
299
300def release_create(name, chart_name, namespace='default',
301 version=None, values_file=None,
302 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 '''
313 args = []
314 if version is not None:
315 args += ['--version', version]
316 if values_file is not None:
317 args += ['--values', values_file]
318 return _cmd_and_result(
319 'install', chart_name,
320 '--namespace', namespace,
321 '--name', name,
322 *args, **kwargs
323 )
324
325def 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
330 return _cmd_and_result('delete', '--purge', name, **kwargs)
331
332
333def release_upgrade(name, chart_name, namespace='default',
334 version=None, values_file=None,
335 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
345 args = []
346 if version is not None:
347 args += ['--version', version]
348 if values_file is not None:
349 args += ['--values', values_file]
350 return _cmd_and_result(
351 'upgrade', name, chart_name,
352 '--namespace', namespace,
353 *args, **kwargs
354 )
355
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 '''
364 return _cmd_and_result('dependency', 'build', chart_path, **kwargs)
365
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
381 return _cmd_and_result('package', path, *args, **kwargs)