Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 1 | import logging |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 2 | import re |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 3 | |
| 4 | from salt.serializers import yaml |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 5 | from salt.exceptions import CommandExecutionError |
| 6 | |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 7 | LOG = logging.getLogger(__name__) |
| 8 | |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 9 | class HelmExecutionError(CommandExecutionError): |
| 10 | def __init__(self, cmd, error): |
| 11 | self.cmd = cmd |
| 12 | self.error = error |
Yuriy Taraday | 6618fb9 | 2017-08-11 17:11:48 +0400 | [diff] [blame] | 13 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 14 | def _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']) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 19 | else: |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 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'): |
Yuriy Taraday | e9f982d | 2017-08-17 18:06:58 +0400 | [diff] [blame] | 29 | env['GOOGLE_APPLICATION_CREDENTIALS'] = \ |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 30 | kwargs['gce_service_token'] |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 31 | return { |
tmeneau | f2392a1 | 2017-10-24 20:57:30 -0400 | [diff] [blame] | 32 | 'cmd': ('helm',) + args + addtl_args, |
Yuriy Taraday | f9dd012 | 2017-08-17 16:26:16 +0400 | [diff] [blame] | 33 | 'env': env, |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 34 | } |
| 35 | |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 36 | def _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 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 54 | def _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 | |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 79 | def _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 | } |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 85 | |
| 86 | |
| 87 | def _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] |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 94 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 95 | def list_repos(**kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 103 | cmd = _helm_cmd('repo', 'list', **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 114 | def add_repo(name, url, **kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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 | ''' |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 126 | return _cmd_and_result('repo', 'add', name, url, **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 127 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 128 | def remove_repo(name, **kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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 | ''' |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 136 | return _cmd_and_result('repo', 'remove', name, **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 137 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 138 | def manage_repos(present={}, absent=[], exclusive=False, **kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 192 | existing_repos = list_repos(**kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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, |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 214 | 'stdout': add_repo(name, url, **kwargs)['stdout'] |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 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: |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 246 | result['removed'].append({ |
| 247 | 'name': name, |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 248 | 'stdout': remove_repo(name, **kwargs) ['stdout'] |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 249 | }) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 250 | except CommandExecutionError as e: |
| 251 | result['failed'].append({ |
| 252 | "type": "removal", "name": name, "error": '%s' % e |
| 253 | }) |
| 254 | |
| 255 | return result |
| 256 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 257 | def update_repos(**kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 258 | ''' |
| 259 | Ensures the local helm repository cache for each repository is up to date. |
| 260 | Proxies the `helm repo update` command. |
| 261 | ''' |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 262 | return _cmd_and_result('repo', 'update', **kwargs) |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 263 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 264 | def 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 Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 269 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 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 | |
| 293 | def 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 Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 299 | |
Yuriy Taraday | f169d82 | 2017-08-14 13:40:21 +0400 | [diff] [blame] | 300 | def release_create(name, chart_name, namespace='default', |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 301 | version=None, values_file=None, |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 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 | ''' |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 313 | args = [] |
| 314 | if version is not None: |
| 315 | args += ['--version', version] |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 316 | if values_file is not None: |
| 317 | args += ['--values', values_file] |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 318 | return _cmd_and_result( |
| 319 | 'install', chart_name, |
| 320 | '--namespace', namespace, |
| 321 | '--name', name, |
| 322 | *args, **kwargs |
| 323 | ) |
Yuriy Taraday | aeeaa74 | 2017-06-28 15:54:56 +0400 | [diff] [blame] | 324 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 325 | def 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 |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 330 | return _cmd_and_result('delete', '--purge', name, **kwargs) |
Yuriy Taraday | 893b3fb | 2017-07-03 16:22:57 +0400 | [diff] [blame] | 331 | |
| 332 | |
Yuriy Taraday | f169d82 | 2017-08-14 13:40:21 +0400 | [diff] [blame] | 333 | def release_upgrade(name, chart_name, namespace='default', |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 334 | version=None, values_file=None, |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame] | 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 |
Yuriy Taraday | aeeaa74 | 2017-06-28 15:54:56 +0400 | [diff] [blame] | 345 | args = [] |
| 346 | if version is not None: |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 347 | args += ['--version', version] |
| 348 | if values_file is not None: |
| 349 | args += ['--values', values_file] |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 350 | return _cmd_and_result( |
| 351 | 'upgrade', name, chart_name, |
| 352 | '--namespace', namespace, |
tmeneau | 6c29c00 | 2017-10-27 09:13:06 -0400 | [diff] [blame] | 353 | *args, **kwargs |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 354 | ) |
tmeneau | f2392a1 | 2017-10-24 20:57:30 -0400 | [diff] [blame] | 355 | |
| 356 | def 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 | ''' |
tmeneau | f609837 | 2017-10-27 15:26:03 -0400 | [diff] [blame] | 364 | return _cmd_and_result('dependency', 'build', chart_path, **kwargs) |
tmeneau | f2392a1 | 2017-10-24 20:57:30 -0400 | [diff] [blame] | 365 | |
| 366 | def 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 | |
tmeneau | b858a00 | 2017-10-27 09:12:30 -0400 | [diff] [blame] | 381 | return _cmd_and_result('package', path, *args, **kwargs) |