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 | |
Yuriy Taraday | 6618fb9 | 2017-08-11 17:11:48 +0400 | [diff] [blame] | 9 | def 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 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 19 | def _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']) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 24 | else: |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 25 | 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 Taraday | e9f982d | 2017-08-17 18:06:58 +0400 | [diff] [blame] | 34 | env['GOOGLE_APPLICATION_CREDENTIALS'] = \ |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 35 | kwargs['gce_service_token'] |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 36 | return { |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 37 | 'cmd': ('helm',) + addtl_args + args, |
Yuriy Taraday | f9dd012 | 2017-08-17 16:26:16 +0400 | [diff] [blame] | 38 | 'env': env, |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 39 | } |
| 40 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 41 | def _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 | |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 66 | def _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 | } |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 72 | |
| 73 | |
| 74 | def _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] |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 81 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 82 | def list_repos(**kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 83 | ''' |
| 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 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 90 | cmd = _helm_cmd('repo', 'list', **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 91 | 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 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 101 | def add_repo(name, url, **kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 102 | ''' |
| 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 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 113 | cmd = _helm_cmd('repo', 'add', name, url, **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 114 | ret = __salt__['cmd.run_all'](**cmd) |
| 115 | if ret['retcode'] != 0: |
| 116 | raise CommandExecutionError(ret['stderr']) |
| 117 | return ret['stdout'] |
| 118 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 119 | def remove_repo(name, **kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 120 | ''' |
| 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 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 127 | cmd = _helm_cmd('repo', 'remove', name, **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 128 | ret = __salt__['cmd.run_all'](**cmd) |
| 129 | if ret['retcode'] != 0: |
| 130 | raise CommandExecutionError(ret['stderr']) |
| 131 | return ret['stdout'] |
| 132 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 133 | def manage_repos(present={}, absent=[], exclusive=False, **kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 134 | ''' |
| 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 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 187 | existing_repos = list_repos(**kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 188 | 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, |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 209 | 'stdout': add_repo(name, url, **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 210 | }) |
| 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: |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 241 | result['removed'].append({ |
| 242 | 'name': name, |
| 243 | 'stdout': remove_repo(name, **kwargs) |
| 244 | }) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 245 | except CommandExecutionError as e: |
| 246 | result['failed'].append({ |
| 247 | "type": "removal", "name": name, "error": '%s' % e |
| 248 | }) |
| 249 | |
| 250 | return result |
| 251 | |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 252 | def update_repos(**kwargs): |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 253 | ''' |
| 254 | Ensures the local helm repository cache for each repository is up to date. |
| 255 | Proxies the `helm repo update` command. |
| 256 | ''' |
tmeneau | 8cf4fce | 2017-10-17 15:05:35 -0400 | [diff] [blame] | 257 | cmd = _helm_cmd('repo', 'update', **kwargs) |
tmeneau | 61efbef | 2017-10-17 11:19:46 -0400 | [diff] [blame] | 258 | return __salt__['cmd.run_stdout'](**cmd) |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 259 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 260 | def 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 Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 265 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 266 | * 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 | |
| 289 | def 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 Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 295 | |
Yuriy Taraday | f169d82 | 2017-08-14 13:40:21 +0400 | [diff] [blame] | 296 | def release_create(name, chart_name, namespace='default', |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 297 | version=None, values_file=None, |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 298 | 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 Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 309 | args = [] |
| 310 | if version is not None: |
| 311 | args += ['--version', version] |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 312 | if values_file is not None: |
| 313 | args += ['--values', values_file] |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 314 | cmd = _helm_cmd('install', '--namespace', namespace, '--name', name, chart_name, |
| 315 | *args, **kwargs) |
Yuriy Taraday | 84a2103 | 2017-06-27 11:13:16 +0400 | [diff] [blame] | 316 | LOG.debug('Creating release with args: %s', cmd) |
Yuriy Taraday | 6618fb9 | 2017-08-11 17:11:48 +0400 | [diff] [blame] | 317 | return ok_or_output(cmd, 'Failed to create release "{}"'.format(name)) |
Yuriy Taraday | aeeaa74 | 2017-06-28 15:54:56 +0400 | [diff] [blame] | 318 | |
| 319 | |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 320 | def 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 Taraday | 6618fb9 | 2017-08-11 17:11:48 +0400 | [diff] [blame] | 326 | return ok_or_output(cmd, 'Failed to delete release "{}"'.format(name)) |
Yuriy Taraday | 893b3fb | 2017-07-03 16:22:57 +0400 | [diff] [blame] | 327 | |
| 328 | |
Yuriy Taraday | f169d82 | 2017-08-14 13:40:21 +0400 | [diff] [blame] | 329 | def release_upgrade(name, chart_name, namespace='default', |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 330 | version=None, values_file=None, |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 331 | 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 Taraday | aeeaa74 | 2017-06-28 15:54:56 +0400 | [diff] [blame] | 341 | args = [] |
| 342 | if version is not None: |
tmeneau | 94bf68e | 2017-10-17 15:55:34 -0400 | [diff] [blame] | 343 | args += ['--version', version] |
| 344 | if values_file is not None: |
| 345 | args += ['--values', values_file] |
tmeneau | d92f474 | 2017-10-18 09:57:19 -0400 | [diff] [blame^] | 346 | cmd = _helm_cmd('upgrade', '--namespace', namespace, name, chart_name, **kwargs) |
Yuriy Taraday | 893b3fb | 2017-07-03 16:22:57 +0400 | [diff] [blame] | 347 | LOG.debug('Upgrading release with args: %s', cmd) |
Yuriy Taraday | 6618fb9 | 2017-08-11 17:11:48 +0400 | [diff] [blame] | 348 | return ok_or_output(cmd, 'Failed to upgrade release "{}"'.format(name)) |