blob: 9977551e8b3d36dc3138291067eaad8472cae909 [file] [log] [blame]
Oleksii Grudev4cf21532019-04-16 13:17:57 +00001import difflib
2import os
3import logging
4
5from salt.exceptions import CommandExecutionError
6from salt.serializers import yaml
7
8LOG = logging.getLogger(__name__)
9
10def _get_values_from_file(values_file=None):
11 if values_file:
12 try:
13 with open(values_file) as values_stream:
14 values = yaml.deserialize(values_stream)
15 return values
16 except e:
17 raise CommandExecutionError("encountered error reading from values "
18 "file (%s): %s" % (values_file, e))
19 return None
20
21def _get_yaml_diff(new_yaml=None, old_yaml=None):
22 if not new_yaml and not old_yaml:
23 return None
24
25 old_str = yaml.serialize(old_yaml, default_flow_style=False)
26 new_str = yaml.serialize(new_yaml, default_flow_style=False)
27 return difflib.unified_diff(old_str.split('\n'), new_str.split('\n'))
28
29def _failure(name, message, changes={}):
30 return {
31 'name': name,
32 'changes': changes,
33 'result': False,
34 'comment': message,
35 }
36
37def present(name, chart_name, namespace, version=None, values_file=None,
38 tiller_namespace='kube-system', **kwargs):
39 '''
40 Ensure that a release with the supplied name is in the desired state in the
41 Tiller installation. This state will handle change detection to determine
42 whether an installation or update needs to be made.
43
44 In the event the namespace to which a release is installed changes, the
45 state will first delete and purge the release and the re-install it into
46 the new namespace, since Helm does not support updating a release into a
47 new namespace.
48
49 name
50 The name of the release to ensure is present
51
52 chart_name
53 The name of the chart to install, including the repository name as
54 applicable (such as `stable/mysql`)
55
56 namespace
57 The namespace to which the release should be (re-)installed
58
59 version
60 The version of the chart to install. Defaults to the latest version
61
62 values_file
63 The path to the a values file containing all the chart values that
64 should be applied to the release. Note that this should not be passed
65 if there are not chart value overrides required.
66
67 '''
68 kwargs['tiller_namespace'] = tiller_namespace
69 if __opts__['test'] == True:
70 return {
71 'result': None,
72 'name': name,
73 'comment': 'Helm chart "{0}" will be installed'.format(chart_name),
74 'changes': {
75 'chart_name': chart_name,
76 'namespace': namespace,
77 }
78 }
79 old_release = __salt__['k8s_helm.get_release'](name, **kwargs)
80 if not old_release:
81 try:
82 result = __salt__['k8s_helm.release_create'](
83 name, chart_name, namespace, version, values_file, **kwargs
84 )
85 return {
86 'name': name,
87 'changes': {
88 'name': name,
89 'chart_name': chart_name,
90 'namespace': namespace,
91 'version': version,
92 'values': _get_values_from_file(values_file),
93 'stdout': result.get('stdout')
94 },
95 'result': True,
96 'comment': ('Release "%s" was created' % name +
97 '\nExecuted command: %s' % result['cmd'])
98 }
99 except CommandExecutionError as e:
100 msg = (("Failed to create new release: %s" % e.error) +
101 "\nExecuted command: %s" % e.cmd)
102 return _failure(name, msg)
103
104 changes = {}
105 warnings = []
106 if old_release.get('chart') != chart_name.split("/")[1]:
107 changes['chart'] = { 'old': old_release['chart'], 'new': chart_name }
108
109 if old_release.get('version') != version:
110 changes['version'] = { 'old': old_release['version'], 'new': version }
111
112 if old_release.get('namespace') != namespace:
113 changes['namespace'] = { 'old': old_release['namespace'], 'new': namespace }
114
115 if (not values_file and old_release.get("values") or
116 not old_release.get("values") and values_file):
117 changes['values'] = { 'old': old_release['values'], 'new': values_file }
118
119 values = _get_values_from_file(values_file)
120 diff = _get_yaml_diff(values, old_release.get('values'))
121
122 if diff:
123 diff_string = '\n'.join(diff)
124 if diff_string:
125 changes['values'] = diff_string
126
127 if not changes:
128 return {
129 'name': name,
130 'result': True,
131 'changes': {},
132 'comment': 'Release "{}" is already in the desired state'.format(name)
133 }
134
135 module_fn = 'k8s_helm.release_upgrade'
136 if changes.get("namespace"):
137 LOG.debug("purging old release (%s) due to namespace change" % name)
138 try:
139 result = __salt__['k8s_helm.release_delete'](name, **kwargs)
140 except CommandExecutionError as e:
141 msg = ("Failed to delete release for namespace change: %s" % e.error +
142 "\nExecuted command: %s" % e.cmd)
143 return _failure(name, msg, changes)
144
145 module_fn = 'k8s_helm.release_create'
146 warnings.append('Release (%s) was replaced due to namespace change' % name)
147
148 try:
149 result = __salt__[module_fn](
150 name, chart_name, namespace, version, values_file, **kwargs
151 )
152 changes.update({ 'stdout': result.get('stdout') })
153 ret = {
154 'name': name,
155 'changes': changes,
156 'result': True,
157 'comment': 'Release "%s" was updated\nExecuted command: %s' % (name, result['cmd'])
158 }
159 if warnings:
160 ret['warnings'] = warnings
161
162 return ret
163 except CommandExecutionError as e:
164 msg = ("Failed to delete release for namespace change: %s" % e.error +
165 "\nExecuted command: %s" % e.cmd)
166 return _failure(name, msg, changes)
167
168
169def absent(name, tiller_namespace='kube-system', **kwargs):
170 '''
171 Ensure that any release with the supplied release name is absent from the
172 tiller installation.
173
174 name
175 The name of the release to ensure is absent
176 '''
177 kwargs['tiller_namespace'] = tiller_namespace
178 exists = __salt__['k8s_helm.release_exists'](name, **kwargs)
179 if not exists:
180 return {
181 'name': name,
182 'changes': {},
183 'result': True,
184 'comment': 'Release "%s" doesn\'t exist' % name
185 }
186 try:
187 result = __salt__['k8s_helm.release_delete'](name, **kwargs)
188 return {
189 'name': name,
190 'changes': { name: 'DELETED', 'stdout': result['stdout'] },
191 'result': True,
192 'comment': 'Release "%s" was deleted\nExecuted command: %s' % (name, result['cmd'])
193 }
194 except CommandExecutionError as e:
195 return _failure(e.cmd, e.error)
196