blob: a4ce0b3c7bffc27c2729b6faf5297219ffd1015d [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
Yuriy Taraday6618fb92017-08-11 17:11:48 +04009def ok_or_output(cmd, prefix=None):
10 ret = __salt__['cmd.run_all'](**cmd)
11 if ret['retcode'] == 0:
12 return None
13 msg = "Stdout:\n{0[stdout]}\nStderr:\n{0[stderr]}".format(ret)
14 if prefix:
15 msg = prefix + ':\n' + msg
16 return msg
17
18
tmeneau8cf4fce2017-10-17 15:05:35 -040019def _helm_cmd(*args, **kwargs):
20 if kwargs.get('tiller_host'):
21 addtl_args = ('--host', kwargs['tiller_host'])
22 elif kwargs.get('tiller_namespace'):
23 addtl_args = ('--tiller-namespace', kwargs['tiller_namespace'])
tmeneau61efbef2017-10-17 11:19:46 -040024 else:
tmeneau8cf4fce2017-10-17 15:05:35 -040025 addtl_args = ()
26
27 if kwargs.get('helm_home'):
28 addtl_args = addtl_args + ('--home', kwargs['helm_home'])
29
30 env = {}
31 if kwargs.get('kube_config'):
32 env['KUBECONFIG'] = kwargs['kube_config']
33 if kwargs.get('gce_service_token'):
Yuriy Taradaye9f982d2017-08-17 18:06:58 +040034 env['GOOGLE_APPLICATION_CREDENTIALS'] = \
tmeneau8cf4fce2017-10-17 15:05:35 -040035 kwargs['gce_service_token']
Yuriy Taraday84a21032017-06-27 11:13:16 +040036 return {
tmeneau8cf4fce2017-10-17 15:05:35 -040037 'cmd': ('helm',) + addtl_args + args,
Yuriy Taradayf9dd0122017-08-17 16:26:16 +040038 'env': env,
Yuriy Taraday84a21032017-06-27 11:13:16 +040039 }
40
tmeneaud92f4742017-10-18 09:57:19 -040041def _parse_release(output):
42 result = {}
43 chart_match = re.search(r'CHART\: ([^0-9]+)-([^\s]+)', output)
44 if chart_match:
45 result['chart'] = chart_match.group(1)
46 result['version'] = chart_match.group(2)
47
48 user_values_match = re.search(r"(?<=USER-SUPPLIED VALUES\:\n)(\n*.+)+?(?=\n*COMPUTED VALUES\:)", output, re.MULTILINE)
49 if user_values_match:
50 result['values'] = yaml.deserialize(user_values_match.group(0))
51
52 computed_values_match = re.search(r"(?<=COMPUTED VALUES\:\n)(\n*.+)+?(?=\n*HOOKS\:)", output, re.MULTILINE)
53 if computed_values_match:
54 result['computed_values'] = yaml.deserialize(computed_values_match.group(0))
55
56 manifest_match = re.search(r"(?<=MANIFEST\:\n)(\n*(?!Release \".+\" has been upgraded).*)+", output, re.MULTILINE)
57 if manifest_match:
58 result['manifest'] = manifest_match.group(0)
59
60 namespace_match = re.search(r"(?<=NAMESPACE\: )(.*)", output)
61 if namespace_match:
62 result['namespace'] = namespace_match.group(0)
63
64 return result
65
tmeneau61efbef2017-10-17 11:19:46 -040066def _parse_repo(repo_string = None):
67 split_string = repo_string.split('\t')
68 return {
69 "name": split_string[0].strip(),
70 "url": split_string[1].strip()
71 }
tmeneaud92f4742017-10-18 09:57:19 -040072
73
74def _get_release_namespace(name, tiller_namespace="kube-system", **kwargs):
75 cmd = _helm_cmd("list", name, **kwargs)
76 result = __salt__['cmd.run_stdout'](**cmd)
77 if not result or len(result.split("\n")) < 2:
78 return None
79
80 return result.split("\n")[1].split("\t")[5]
tmeneau61efbef2017-10-17 11:19:46 -040081
tmeneau8cf4fce2017-10-17 15:05:35 -040082def list_repos(**kwargs):
tmeneau61efbef2017-10-17 11:19:46 -040083 '''
84 Get the result of running `helm repo list` on the target minion, formatted
85 as a list of dicts with two keys:
86
87 * name: the name with which the repository is registered
88 * url: the url registered for the repository
89 '''
tmeneau8cf4fce2017-10-17 15:05:35 -040090 cmd = _helm_cmd('repo', 'list', **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -040091 result = __salt__['cmd.run_stdout'](**cmd)
92 if result is None:
93 return result
94
95 result = result.split("\n")
96 result.pop(0)
97 return {
98 repo['name']: repo['url'] for repo in [_parse_repo(line) for line in result]
99 }
100
tmeneau8cf4fce2017-10-17 15:05:35 -0400101def add_repo(name, url, **kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400102 '''
103 Register the repository located at the supplied url with the supplied name.
104 Note that re-using an existing name will overwrite the repository url for
105 that registered repository to point to the supplied url.
106
107 name
108 The name with which to register the repository with the Helm client.
109
110 url
111 The url for the chart repository.
112 '''
tmeneau8cf4fce2017-10-17 15:05:35 -0400113 cmd = _helm_cmd('repo', 'add', name, url, **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400114 ret = __salt__['cmd.run_all'](**cmd)
115 if ret['retcode'] != 0:
116 raise CommandExecutionError(ret['stderr'])
117 return ret['stdout']
118
tmeneau8cf4fce2017-10-17 15:05:35 -0400119def remove_repo(name, **kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400120 '''
121 Remove the repository from the Helm client registered with the supplied
122 name.
123
124 name
125 The name (as registered with the Helm client) for the repository to remove
126 '''
tmeneau8cf4fce2017-10-17 15:05:35 -0400127 cmd = _helm_cmd('repo', 'remove', name, **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400128 ret = __salt__['cmd.run_all'](**cmd)
129 if ret['retcode'] != 0:
130 raise CommandExecutionError(ret['stderr'])
131 return ret['stdout']
132
tmeneau8cf4fce2017-10-17 15:05:35 -0400133def manage_repos(present={}, absent=[], exclusive=False, **kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400134 '''
135 Manage the repositories registered with the Helm client's local cache.
136
137 *ensuring repositories are present*
138 Repositories that should be present in the helm client can be supplied via
139 the `present` dict parameter; each key in the dict is a release name, and the
140 value is the repository url that should be registered.
141
142 *ensuring repositories are absent*
143 Repository names supplied via the `absent` parameter must be a string. If the
144 `exclusive` flag is set to True, the `absent` parameter will be ignored, even
145 if it has been supplied.
146
147 This function returns a dict with the following keys:
148
149 * already_present: a listing of supplied repository definitions to add that
150 are already registered with the Helm client
151
152 * added: a list of repositories that are newly registered with the Helm
153 client. Each item in the list is a dict with the following keys:
154 * name: the repo name
155 * url: the repo url
156 * stdout: the output from the `helm repo add` command call for the repo
157
158 * already_absent: any repository name supplied via the `absent` parameter
159 that was already not registered with the Helm client
160
161 * removed: the result of attempting to remove any repositories
162
163 * failed: a list of repositores that were unable to be added. Each item in
164 the list is a dict with the following keys:
165 * type: the text "removal" or "addition", as appropriate
166 * name: the repo name
167 * url: the repo url (if appropriate)
168 * error: the output from add or remove command attempted for the
169 repository
170
171 present
172 The dict of repositories that should be registered with the Helm client.
173 Each dict key is the name with which the repository url (the corresponding
174 value) should be registered with the Helm client.
175
176 absent
177 The list of repositories to ensure are not registered with the Helm client.
178 Each entry in the list must be the (string) name of the repository.
179
180 exclusive
181 A flag indicating whether only the supplied repos should be available in
182 the target minion's Helm client. If configured to true, the `absent`
183 parameter will be ignored and only the repositories configured via the
184 `present` parameter will be registered with the Helm client. Defaults to
185 False.
186 '''
tmeneau8cf4fce2017-10-17 15:05:35 -0400187 existing_repos = list_repos(**kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400188 result = {
189 "already_present": [],
190 "added": [],
191 "already_absent": [],
192 "removed": [],
193 "failed": []
194 }
195
196 for name, url in present.iteritems():
197 if not name or not url:
198 raise CommandExecutionError(('Supplied repo to add must have a name (%s) '
199 'and url (%s)' % (name, url)))
200
201 if name in existing_repos and existing_repos[name] == url:
202 result['already_present'].append({ "name": name, "url": url })
203 continue
204
205 try:
206 result['added'].append({
207 'name': name,
208 'url': url,
tmeneau8cf4fce2017-10-17 15:05:35 -0400209 'stdout': add_repo(name, url, **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400210 })
211 existing_repos = {
212 n: u for (n, u) in existing_repos.iteritems() if name != n
213 }
214 except CommandExecutionError as e:
215 result['failed'].append({
216 "type": "addition",
217 "name": name,
218 'url': url,
219 'error': '%s' % e
220 })
221
222 #
223 # Handle removal of repositories configured to be absent (or not configured
224 # to be present if the `exclusive` flag is set)
225 #
226 existing_names = [name for (name, url) in existing_repos.iteritems()]
227 if exclusive:
228 present['stable'] = "exclude"
229 absent = [name for name in existing_names if not name in present]
230
231 for name in absent:
232 if not name or not isinstance(name, str):
233 raise CommandExecutionError(('Supplied repo name to be absent must be a '
234 'string: %s' % name))
235
236 if name not in existing_names:
237 result['already_absent'].append(name)
238 continue
239
240 try:
tmeneau8cf4fce2017-10-17 15:05:35 -0400241 result['removed'].append({
242 'name': name,
243 'stdout': remove_repo(name, **kwargs)
244 })
tmeneau61efbef2017-10-17 11:19:46 -0400245 except CommandExecutionError as e:
246 result['failed'].append({
247 "type": "removal", "name": name, "error": '%s' % e
248 })
249
250 return result
251
tmeneau8cf4fce2017-10-17 15:05:35 -0400252def update_repos(**kwargs):
tmeneau61efbef2017-10-17 11:19:46 -0400253 '''
254 Ensures the local helm repository cache for each repository is up to date.
255 Proxies the `helm repo update` command.
256 '''
tmeneau8cf4fce2017-10-17 15:05:35 -0400257 cmd = _helm_cmd('repo', 'update', **kwargs)
tmeneau61efbef2017-10-17 11:19:46 -0400258 return __salt__['cmd.run_stdout'](**cmd)
Yuriy Taraday84a21032017-06-27 11:13:16 +0400259
tmeneaud92f4742017-10-18 09:57:19 -0400260def get_release(name, tiller_namespace="kube-system", **kwargs):
261 '''
262 Get the parsed release metadata from calling `helm get {{ release }}` for the
263 supplied release name, or None if no release is found. The following keys may
264 or may not be in the returned dict:
Yuriy Taraday84a21032017-06-27 11:13:16 +0400265
tmeneaud92f4742017-10-18 09:57:19 -0400266 * chart
267 * version
268 * values
269 * computed_values
270 * manifest
271 * namespace
272 '''
273 kwargs['tiller_namespace'] = tiller_namespace
274 cmd = _helm_cmd('get', name, **kwargs)
275 result = __salt__['cmd.run_stdout'](**cmd)
276 if not result:
277 return None
278
279 release = _parse_release(result)
280
281 #
282 # `helm get {{ release }}` doesn't currently (2.6.2) return the namespace, so
283 # separately retrieve it if it's not available
284 #
285 if not 'namespace' in release:
286 release['namespace'] = _get_release_namespace(name, **kwargs)
287 return release
288
289def release_exists(name, tiller_namespace="kube-system", **kwargs):
290 '''
291 Determine whether a release exists in the cluster with the supplied name
292 '''
293 kwargs['tiller_namespace'] = tiller_namespace
294 return get_release(name, **kwargs) is not None
Yuriy Taraday84a21032017-06-27 11:13:16 +0400295
Yuriy Taradayf169d822017-08-14 13:40:21 +0400296def release_create(name, chart_name, namespace='default',
tmeneau94bf68e2017-10-17 15:55:34 -0400297 version=None, values_file=None,
tmeneaud92f4742017-10-18 09:57:19 -0400298 tiller_namespace='kube-system', **kwargs):
299 '''
300 Install a release. There must not be a release with the supplied name
301 already installed to the Kubernetes cluster.
302
303 Note that if a release already exists with the specified name, you'll need
304 to use the release_upgrade function instead; unless the release is in a
305 different namespace, in which case you'll need to delete and purge the
306 existing release (using release_delete) and *then* use this function to
307 install a new release to the desired namespace.
308 '''
Yuriy Taraday84a21032017-06-27 11:13:16 +0400309 args = []
310 if version is not None:
311 args += ['--version', version]
tmeneau94bf68e2017-10-17 15:55:34 -0400312 if values_file is not None:
313 args += ['--values', values_file]
tmeneaud92f4742017-10-18 09:57:19 -0400314 cmd = _helm_cmd('install', '--namespace', namespace, '--name', name, chart_name,
315 *args, **kwargs)
Yuriy Taraday84a21032017-06-27 11:13:16 +0400316 LOG.debug('Creating release with args: %s', cmd)
Yuriy Taraday6618fb92017-08-11 17:11:48 +0400317 return ok_or_output(cmd, 'Failed to create release "{}"'.format(name))
Yuriy Taradayaeeaa742017-06-28 15:54:56 +0400318
319
tmeneaud92f4742017-10-18 09:57:19 -0400320def release_delete(name, tiller_namespace='kube-system', **kwargs):
321 '''
322 Delete and purge any release found with the supplied name.
323 '''
324 kwargs['tiller_namespace'] = tiller_namespace
325 cmd = _helm_cmd('delete', '--purge', name, **kwargs)
Yuriy Taraday6618fb92017-08-11 17:11:48 +0400326 return ok_or_output(cmd, 'Failed to delete release "{}"'.format(name))
Yuriy Taraday893b3fb2017-07-03 16:22:57 +0400327
328
Yuriy Taradayf169d822017-08-14 13:40:21 +0400329def release_upgrade(name, chart_name, namespace='default',
tmeneau94bf68e2017-10-17 15:55:34 -0400330 version=None, values_file=None,
tmeneaud92f4742017-10-18 09:57:19 -0400331 tiller_namespace='kube-system', **kwargs):
332 '''
333 Upgrade an existing release. There must be a release with the supplied name
334 already installed to the Kubernetes cluster.
335
336 If attempting to change the namespace for the release, this function will
337 fail; you will need to first delete and purge the release and then use the
338 release_create function to create a new release in the desired namespace.
339 '''
340 kwargs['tiller_namespace'] = tiller_namespace
Yuriy Taradayaeeaa742017-06-28 15:54:56 +0400341 args = []
342 if version is not None:
tmeneau94bf68e2017-10-17 15:55:34 -0400343 args += ['--version', version]
344 if values_file is not None:
345 args += ['--values', values_file]
tmeneaud92f4742017-10-18 09:57:19 -0400346 cmd = _helm_cmd('upgrade', '--namespace', namespace, name, chart_name, **kwargs)
Yuriy Taraday893b3fb2017-07-03 16:22:57 +0400347 LOG.debug('Upgrading release with args: %s', cmd)
Yuriy Taraday6618fb92017-08-11 17:11:48 +0400348 return ok_or_output(cmd, 'Failed to upgrade release "{}"'.format(name))