generated from ansible-collections/collection_template
-
Notifications
You must be signed in to change notification settings - Fork 142
/
Copy pathhelm.py
681 lines (576 loc) · 20.5 KB
/
helm.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: helm
short_description: Manages Kubernetes packages with the Helm package manager
version_added: "0.11.0"
author:
- Lucas Boisserie (@LucasBoisserie)
- Matthieu Diehr (@d-matt)
requirements:
- "helm (https://github.com/helm/helm/releases)"
- "yaml (https://pypi.org/project/PyYAML/)"
description:
- Install, upgrade, delete packages with the Helm package manager.
options:
chart_ref:
description:
- chart_reference on chart repository.
- path to a packaged chart.
- path to an unpacked chart directory.
- absolute URL.
- Required when I(release_state) is set to C(present).
required: false
type: path
chart_repo_url:
description:
- Chart repository URL where to locate the requested chart.
required: false
type: str
chart_version:
description:
- Chart version to install. If this is not specified, the latest version is installed.
required: false
type: str
release_name:
description:
- Release name to manage.
required: true
type: str
aliases: [ name ]
release_namespace:
description:
- Kubernetes namespace where the chart should be installed.
required: true
type: str
aliases: [ namespace ]
release_state:
choices: ['present', 'absent']
description:
- Desirated state of release.
required: false
default: present
aliases: [ state ]
type: str
release_values:
description:
- Value to pass to chart.
required: false
default: {}
aliases: [ values ]
type: dict
values_files:
description:
- Value files to pass to chart.
- Paths will be read from the target host's filesystem, not the host running ansible.
- values_files option is evaluated before values option if both are used.
- Paths are evaluated in the order the paths are specified.
required: false
default: []
type: list
elements: str
version_added: '1.1.0'
update_repo_cache:
description:
- Run C(helm repo update) before the operation. Can be run as part of the package installation or as a separate step.
default: false
type: bool
#Helm options
disable_hook:
description:
- Helm option to disable hook on install/upgrade/delete.
default: False
type: bool
force:
description:
- Helm option to force reinstall, ignore on new install.
default: False
type: bool
purge:
description:
- Remove the release from the store and make its name free for later use.
default: True
type: bool
wait:
description:
- Wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful.
default: False
type: bool
wait_timeout:
description:
- Timeout when wait option is enabled (helm2 is a number of seconds, helm3 is a duration).
type: str
atomic:
description:
- If set, the installation process deletes the installation on failure.
type: bool
default: False
create_namespace:
description:
- Create the release namespace if not present.
type: bool
default: False
version_added: "0.11.1"
replace:
description:
- Reuse the given name, only if that name is a deleted release which remains in the history.
- This is unsafe in production environment.
type: bool
default: False
version_added: "1.11.0"
skip_crds:
description:
- Skip custom resource definitions when installing or upgrading.
type: bool
default: False
version_added: "1.2.0"
history_max:
description:
- Limit the maximum number of revisions saved per release.
type: int
version_added: "2.2.0"
extends_documentation_fragment:
- kubernetes.core.helm_common_options
'''
EXAMPLES = r'''
- name: Deploy latest version of Prometheus chart inside monitoring namespace (and create it)
kubernetes.core.helm:
name: test
chart_ref: stable/prometheus
release_namespace: monitoring
create_namespace: true
# From repository
- name: Add stable chart repo
kubernetes.core.helm_repository:
name: stable
repo_url: "https://kubernetes.github.io/ingress-nginx"
- name: Deploy latest version of Grafana chart inside monitoring namespace with values
kubernetes.core.helm:
name: test
chart_ref: stable/grafana
release_namespace: monitoring
values:
replicas: 2
- name: Deploy Grafana chart on 5.0.12 with values loaded from template
kubernetes.core.helm:
name: test
chart_ref: stable/grafana
chart_version: 5.0.12
values: "{{ lookup('template', 'somefile.yaml') | from_yaml }}"
- name: Deploy Grafana chart using values files on target
kubernetes.core.helm:
name: test
chart_ref: stable/grafana
release_namespace: monitoring
values_files:
- /path/to/values.yaml
- name: Remove test release and waiting suppression ending
kubernetes.core.helm:
name: test
state: absent
wait: true
# From git
- name: Git clone stable repo on HEAD
ansible.builtin.git:
repo: "http://github.com/helm/charts.git"
dest: /tmp/helm_repo
- name: Deploy Grafana chart from local path
kubernetes.core.helm:
name: test
chart_ref: /tmp/helm_repo/stable/grafana
release_namespace: monitoring
# From url
- name: Deploy Grafana chart on 5.6.0 from url
kubernetes.core.helm:
name: test
chart_ref: "https://github.com/grafana/helm-charts/releases/download/grafana-5.6.0/grafana-5.6.0.tgz"
release_namespace: monitoring
# Using complex Values
- name: Deploy new-relic client chart
kubernetes.core.helm:
name: newrelic-bundle
chart_ref: newrelic/nri-bundle
release_namespace: default
force: True
wait: True
replace: True
update_repo_cache: True
disable_hook: True
values:
global:
licenseKey: "{{ nr_license_key }}"
cluster: "{{ site_name }}"
newrelic-infrastructure:
privileged: True
ksm:
enabled: True
prometheus:
enabled: True
kubeEvents:
enabled: True
logging:
enabled: True
'''
RETURN = r"""
status:
type: complex
description: A dictionary of status output
returned: on success Creation/Upgrade/Already deploy
contains:
appversion:
type: str
returned: always
description: Version of app deployed
chart:
type: str
returned: always
description: Chart name and chart version
name:
type: str
returned: always
description: Name of the release
namespace:
type: str
returned: always
description: Namespace where the release is deployed
revision:
type: str
returned: always
description: Number of time where the release has been updated
status:
type: str
returned: always
description: Status of release (can be DEPLOYED, FAILED, ...)
updated:
type: str
returned: always
description: The Date of last update
values:
type: str
returned: always
description: Dict of Values used to deploy
stdout:
type: str
description: Full `helm` command stdout, in case you want to display it or examine the event log
returned: always
sample: ''
stderr:
type: str
description: Full `helm` command stderr, in case you want to display it or examine the event log
returned: always
sample: ''
command:
type: str
description: Full `helm` command built by this module, in case you want to re-run the command outside the module or debug a problem.
returned: always
sample: helm upgrade ...
"""
import tempfile
import traceback
try:
import yaml
IMP_YAML = True
except ImportError:
IMP_YAML_ERR = traceback.format_exc()
IMP_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback
from ansible_collections.kubernetes.core.plugins.module_utils.helm import (
run_helm,
get_values,
get_helm_plugin_list,
parse_helm_plugin_list
)
def get_release(state, release_name):
"""
Get Release from all deployed releases
"""
if state is not None:
for release in state:
if release['name'] == release_name:
return release
return None
def get_release_status(module, command, release_name):
"""
Get Release state from deployed release
"""
list_command = command + " list --output=yaml --filter " + release_name
rc, out, err = run_helm(module, list_command)
release = get_release(yaml.safe_load(out), release_name)
if release is None: # not install
return None
release['values'] = get_values(module, command, release_name)
return release
def run_repo_update(module, command):
"""
Run Repo update
"""
repo_update_command = command + " repo update"
rc, out, err = run_helm(module, repo_update_command)
def fetch_chart_info(module, command, chart_ref):
"""
Get chart info
"""
inspect_command = command + " show chart " + chart_ref
rc, out, err = run_helm(module, inspect_command)
return yaml.safe_load(out)
def deploy(command, release_name, release_values, chart_name, wait,
wait_timeout, disable_hook, force, values_files, history_max, atomic=False,
create_namespace=False, replace=False, skip_crds=False):
"""
Install/upgrade/rollback release chart
"""
if replace:
# '--replace' is not supported by 'upgrade -i'
deploy_command = command + " install"
else:
deploy_command = command + " upgrade -i" # install/upgrade
# Always reset values to keep release_values equal to values released
deploy_command += " --reset-values"
if wait:
deploy_command += " --wait"
if wait_timeout is not None:
deploy_command += " --timeout " + wait_timeout
if atomic:
deploy_command += " --atomic"
if force:
deploy_command += " --force"
if replace:
deploy_command += " --replace"
if disable_hook:
deploy_command += " --no-hooks"
if create_namespace:
deploy_command += " --create-namespace"
if values_files:
for value_file in values_files:
deploy_command += " --values=" + value_file
if release_values != {}:
fd, path = tempfile.mkstemp(suffix='.yml')
with open(path, 'w') as yaml_file:
yaml.dump(release_values, yaml_file, default_flow_style=False)
deploy_command += " -f=" + path
if skip_crds:
deploy_command += " --skip-crds"
if history_max is not None:
deploy_command += " --history-max=%s" % str(history_max)
deploy_command += " " + release_name + " " + chart_name
return deploy_command
def delete(command, release_name, purge, disable_hook):
"""
Delete release chart
"""
delete_command = command + " uninstall "
if not purge:
delete_command += " --keep-history"
if disable_hook:
delete_command += " --no-hooks"
delete_command += " " + release_name
return delete_command
def load_values_files(values_files):
values = {}
for values_file in values_files or []:
with open(values_file, 'r') as fd:
content = yaml.safe_load(fd)
if not isinstance(content, dict):
continue
for k, v in content.items():
values[k] = v
return values
def has_plugin(command, plugin):
"""
Check if helm plugin is installed.
"""
cmd = command + " plugin"
rc, output, err = get_helm_plugin_list(module, helm_bin=cmd)
out = parse_helm_plugin_list(module, output=output.splitlines())
if not out:
return False
for line in out:
if line[0] == plugin:
return True
return False
def helmdiff_check(module, helm_cmd, release_name, chart_ref, release_values,
values_files=None, chart_version=None, replace=False):
"""
Use helm diff to determine if a release would change by upgrading a chart.
"""
cmd = helm_cmd + " diff upgrade"
cmd += " " + release_name
cmd += " " + chart_ref
if chart_version is not None:
cmd += " " + "--version=" + chart_version
if not replace:
cmd += " " + "--reset-values"
if release_values != {}:
fd, path = tempfile.mkstemp(suffix='.yml')
with open(path, 'w') as yaml_file:
yaml.dump(release_values, yaml_file, default_flow_style=False)
cmd += " -f=" + path
if values_files:
for values_file in values_files:
cmd += " -f=" + values_file
rc, out, err = run_helm(module, cmd)
return len(out.strip()) > 0
def default_check(release_status, chart_info, values=None, values_files=None):
"""
Use default check to determine if release would change by upgrading a chart.
"""
# the 'appVersion' specification is optional in a chart
chart_app_version = chart_info.get('appVersion', None)
released_app_version = release_status.get('app_version', None)
# when deployed without an 'appVersion' chart value the 'helm list' command will return the entry `app_version: ""`
appversion_is_same = (chart_app_version == released_app_version) or (chart_app_version is None and released_app_version == "")
if values_files:
values_match = release_status['values'] == load_values_files(values_files)
else:
values_match = release_status['values'] == values
return not values_match \
or (chart_info['name'] + '-' + chart_info['version']) != release_status["chart"] \
or not appversion_is_same
def main():
global module
module = AnsibleModule(
argument_spec=dict(
binary_path=dict(type='path'),
chart_ref=dict(type='path'),
chart_repo_url=dict(type='str'),
chart_version=dict(type='str'),
release_name=dict(type='str', required=True, aliases=['name']),
release_namespace=dict(type='str', required=True, aliases=['namespace']),
release_state=dict(default='present', choices=['present', 'absent'], aliases=['state']),
release_values=dict(type='dict', default={}, aliases=['values']),
values_files=dict(type='list', default=[], elements='str'),
update_repo_cache=dict(type='bool', default=False),
# Helm options
disable_hook=dict(type='bool', default=False),
force=dict(type='bool', default=False),
context=dict(type='str', aliases=['kube_context'], fallback=(env_fallback, ['K8S_AUTH_CONTEXT'])),
kubeconfig=dict(type='path', aliases=['kubeconfig_path'], fallback=(env_fallback, ['K8S_AUTH_KUBECONFIG'])),
purge=dict(type='bool', default=True),
wait=dict(type='bool', default=False),
wait_timeout=dict(type='str'),
atomic=dict(type='bool', default=False),
create_namespace=dict(type='bool', default=False),
replace=dict(type='bool', default=False),
skip_crds=dict(type='bool', default=False),
history_max=dict(type='int'),
# Generic auth key
host=dict(type='str', fallback=(env_fallback, ['K8S_AUTH_HOST'])),
ca_cert=dict(type='path', aliases=['ssl_ca_cert'], fallback=(env_fallback, ['K8S_AUTH_SSL_CA_CERT'])),
validate_certs=dict(type='bool', default=True, aliases=['verify_ssl'], fallback=(env_fallback, ['K8S_AUTH_VERIFY_SSL'])),
api_key=dict(type='str', no_log=True, fallback=(env_fallback, ['K8S_AUTH_API_KEY']))
),
required_if=[
('release_state', 'present', ['release_name', 'chart_ref']),
('release_state', 'absent', ['release_name'])
],
mutually_exclusive=[
("context", "ca_cert"),
("kubeconfig", "ca_cert"),
],
supports_check_mode=True,
)
if not IMP_YAML:
module.fail_json(msg=missing_required_lib("yaml"), exception=IMP_YAML_ERR)
changed = False
bin_path = module.params.get('binary_path')
chart_ref = module.params.get('chart_ref')
chart_repo_url = module.params.get('chart_repo_url')
chart_version = module.params.get('chart_version')
release_name = module.params.get('release_name')
release_state = module.params.get('release_state')
release_values = module.params.get('release_values')
values_files = module.params.get('values_files')
update_repo_cache = module.params.get('update_repo_cache')
# Helm options
disable_hook = module.params.get('disable_hook')
force = module.params.get('force')
purge = module.params.get('purge')
wait = module.params.get('wait')
wait_timeout = module.params.get('wait_timeout')
atomic = module.params.get('atomic')
create_namespace = module.params.get('create_namespace')
replace = module.params.get('replace')
skip_crds = module.params.get('skip_crds')
history_max = module.params.get('history_max')
if bin_path is not None:
helm_cmd_common = bin_path
else:
helm_cmd_common = module.get_bin_path('helm', required=True)
if update_repo_cache:
run_repo_update(module, helm_cmd_common)
# Get real/deployed release status
release_status = get_release_status(module, helm_cmd_common, release_name)
# keep helm_cmd_common for get_release_status in module_exit_json
helm_cmd = helm_cmd_common
if release_state == "absent" and release_status is not None:
if replace:
module.fail_json(msg="replace is not applicable when state is absent")
helm_cmd = delete(helm_cmd, release_name, purge, disable_hook)
changed = True
elif release_state == "present":
if chart_version is not None:
helm_cmd += " --version=" + chart_version
if chart_repo_url is not None:
helm_cmd += " --repo=" + chart_repo_url
# Fetch chart info to have real version and real name for chart_ref from archive, folder or url
chart_info = fetch_chart_info(module, helm_cmd, chart_ref)
if release_status is None: # Not installed
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout,
disable_hook, False, values_files=values_files, atomic=atomic,
create_namespace=create_namespace, replace=replace,
skip_crds=skip_crds, history_max=history_max)
changed = True
else:
if has_plugin(helm_cmd_common, "diff") and not chart_repo_url:
would_change = helmdiff_check(module, helm_cmd_common, release_name, chart_ref,
release_values, values_files, chart_version, replace)
else:
module.warn("The default idempotency check can fail to report changes in certain cases. "
"Install helm diff for better results.")
would_change = default_check(release_status, chart_info, release_values, values_files)
if force or would_change:
helm_cmd = deploy(helm_cmd, release_name, release_values, chart_ref, wait, wait_timeout,
disable_hook, force, values_files=values_files, atomic=atomic,
create_namespace=create_namespace, replace=replace,
skip_crds=skip_crds, history_max=history_max)
changed = True
if module.check_mode:
check_status = {
'values': {
"current": {},
"declared": {},
}
}
if release_status:
check_status['values']['current'] = release_status['values']
check_status['values']['declared'] = release_status
module.exit_json(
changed=changed,
command=helm_cmd,
status=check_status,
stdout='',
stderr='',
)
elif not changed:
module.exit_json(
changed=False,
status=release_status,
stdout='',
stderr='',
command=helm_cmd,
)
rc, out, err = run_helm(module, helm_cmd)
module.exit_json(
changed=changed,
stdout=out,
stderr=err,
status=get_release_status(module, helm_cmd_common, release_name),
command=helm_cmd,
)
if __name__ == '__main__':
main()