-
Notifications
You must be signed in to change notification settings - Fork 26
/
helm.go
347 lines (285 loc) · 9.97 KB
/
helm.go
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
package modules
import (
"context"
"fmt"
"os"
"path/filepath"
"runtime/trace"
"strings"
"time"
"github.com/gofrs/uuid/v5"
"github.com/kennygrant/sanitize"
log "github.com/sirupsen/logrus"
"github.com/flant/addon-operator/pkg/app"
"github.com/flant/addon-operator/pkg/helm"
"github.com/flant/addon-operator/pkg/helm/client"
"github.com/flant/addon-operator/pkg/utils"
"github.com/flant/kube-client/manifest"
"github.com/flant/shell-operator/pkg/utils/measure"
)
// HelmModule representation of the module, which has Helm Chart and could be installed with the helm lib
type HelmModule struct {
// Name of the module
name string
// Path of the module on the fs
path string
values utils.Values
tmpDir string
dependencies *HelmModuleDependencies
validator HelmValuesValidator
}
type HelmValuesValidator interface {
ValidateModuleHelmValues(string, utils.Values) error
}
type HelmResourceManager interface {
GetAbsentResources(manifests []manifest.Manifest, defaultNamespace string) ([]manifest.Manifest, error)
StartMonitor(moduleName string, manifests []manifest.Manifest, defaultNamespace string)
HasMonitor(moduleName string) bool
}
type MetricsStorage interface {
HistogramObserve(metric string, value float64, labels map[string]string, buckets []float64)
}
type HelmModuleDependencies struct {
HelmClientFactory *helm.ClientFactory
HelmResourceManager HelmResourceManager
MetricsStorage MetricsStorage
HelmValuesValidator HelmValuesValidator
}
// NewHelmModule build HelmModule from the Module templates and values + global values
func NewHelmModule(bm *BasicModule, tmpDir string, deps *HelmModuleDependencies, validator HelmValuesValidator) (*HelmModule, error) {
moduleValues := bm.GetValues(false)
chartValues := map[string]interface{}{
"global": bm.dc.GlobalValuesGetter.GetValues(false),
utils.ModuleNameToValuesKey(bm.GetName()): moduleValues,
}
hm := &HelmModule{
name: bm.Name,
path: bm.Path,
values: chartValues,
tmpDir: tmpDir,
dependencies: deps,
validator: validator,
}
isHelm, err := hm.isHelmChart()
if err != nil {
return nil, fmt.Errorf("create HelmModule failed: %w", err)
}
if !isHelm {
log.Infof("module %q has neither Chart.yaml nor templates/ dir, is't not a helm chart", bm.Name)
return nil, nil
}
return hm, nil
}
// isHelmChart check, could it be considered as helm chart or not
func (hm *HelmModule) isHelmChart() (bool, error) {
chartPath := filepath.Join(hm.path, "Chart.yaml")
_, err := os.Stat(chartPath)
if err == nil {
// Chart.yaml exists, consider this module as helm chart
return true, nil
}
if os.IsNotExist(err) {
// Chart.yaml does not exist
// check that templates/ dir exists
_, err = os.Stat(filepath.Join(hm.path, "templates"))
if err == nil {
return true, hm.createChartYaml(chartPath)
}
if err != nil && os.IsNotExist(err) {
// if templates not exists - it's not a helm module
return false, nil
}
}
return false, err
}
func (hm *HelmModule) createChartYaml(chartPath string) error {
// we already have versions like 0.1.0 or 0.1.1
// to keep helm updatable, we have to increment this version
// new minor version of addon-operator seems reasonable to increase minor version of a helm chart
data := fmt.Sprintf(`name: %s
version: 0.2.0`, hm.name)
return os.WriteFile(chartPath, []byte(data), 0o644)
}
// checkHelmValues returns error if there is a wrong patch or values are not satisfied
// a Helm values contract defined by schemas in 'openapi' directory.
func (hm *HelmModule) checkHelmValues() error {
// TODO: key
return hm.validator.ValidateModuleHelmValues(utils.ModuleNameToValuesKey(hm.name), hm.values)
}
func (hm *HelmModule) RunHelmInstall(logLabels map[string]string) error {
metricLabels := map[string]string{
"module": hm.name,
"activation": logLabels["event.type"],
}
defer measure.Duration(func(d time.Duration) {
hm.dependencies.MetricsStorage.HistogramObserve("{PREFIX}module_helm_seconds", d.Seconds(), metricLabels, nil)
})()
logEntry := log.WithFields(utils.LabelsToLogFields(logLabels))
err := hm.checkHelmValues()
if err != nil {
return fmt.Errorf("check helm values: %v", err)
}
// TODO Now it returns just a module name. Should it be cleaned from special symbols?
helmReleaseName := hm.name
valuesPath, err := hm.PrepareValuesYamlFile()
if err != nil {
return err
}
defer os.Remove(valuesPath)
helmClient := hm.dependencies.HelmClientFactory.NewClient(logLabels)
// Render templates to prevent excess helm runs.
var renderedManifests string
func() {
defer trace.StartRegion(context.Background(), "ModuleRun-HelmPhase-helm-render").End()
metricLabels := map[string]string{
"module": hm.name,
"activation": logLabels["event.type"],
"operation": "template",
}
defer measure.Duration(func(d time.Duration) {
hm.dependencies.MetricsStorage.HistogramObserve("{PREFIX}helm_operation_seconds", d.Seconds(), metricLabels, nil)
})()
renderedManifests, err = helmClient.Render(
helmReleaseName,
hm.path,
[]string{valuesPath},
[]string{},
app.Namespace,
false,
)
}()
if err != nil {
return err
}
checksum := utils.CalculateStringsChecksum(renderedManifests)
manifests, err := manifest.ListFromYamlDocs(renderedManifests)
if err != nil {
return err
}
logEntry.Debugf("chart has %d resources", len(manifests))
// Skip upgrades if nothing is changes
var runUpgradeRelease bool
func() {
defer trace.StartRegion(context.Background(), "ModuleRun-HelmPhase-helm-check-upgrade").End()
metricLabels := map[string]string{
"module": hm.name,
"activation": logLabels["event.type"],
"operation": "check-upgrade",
}
defer measure.Duration(func(d time.Duration) {
hm.dependencies.MetricsStorage.HistogramObserve("{PREFIX}helm_operation_seconds", d.Seconds(), metricLabels, nil)
})()
runUpgradeRelease, err = hm.shouldRunHelmUpgrade(helmClient, helmReleaseName, checksum, manifests, logLabels)
}()
if err != nil {
return err
}
if !runUpgradeRelease {
// Start resources monitor if release is not changed
if !hm.dependencies.HelmResourceManager.HasMonitor(hm.name) {
hm.dependencies.HelmResourceManager.StartMonitor(hm.name, manifests, app.Namespace)
}
return nil
}
// Run helm upgrade. Trace and measure its time.
func() {
defer trace.StartRegion(context.Background(), "ModuleRun-HelmPhase-helm-upgrade").End()
metricLabels := map[string]string{
"module": hm.name,
"activation": logLabels["event.type"],
"operation": "upgrade",
}
defer measure.Duration(func(d time.Duration) {
hm.dependencies.MetricsStorage.HistogramObserve("{PREFIX}helm_operation_seconds", d.Seconds(), metricLabels, nil)
})()
err = helmClient.UpgradeRelease(
helmReleaseName,
hm.path,
[]string{valuesPath},
[]string{fmt.Sprintf("_addonOperatorModuleChecksum=%s", checksum)},
app.Namespace,
)
}()
if err != nil {
return err
}
// Start monitor resources if release was successful
hm.dependencies.HelmResourceManager.StartMonitor(hm.name, manifests, app.Namespace)
return nil
}
// If all these conditions aren't met, helm upgrade can be skipped.
func (hm *HelmModule) shouldRunHelmUpgrade(helmClient client.HelmClient, releaseName string, checksum string, manifests []manifest.Manifest, logLabels map[string]string) (bool, error) {
logEntry := log.WithFields(utils.LabelsToLogFields(logLabels))
revision, status, err := helmClient.LastReleaseStatus(releaseName)
if revision == "0" {
logEntry.Debugf("helm release '%s' not exists: should run upgrade", releaseName)
return true, nil
}
if err != nil {
return false, err
}
// Run helm upgrade if last release is failed
if strings.ToLower(status) == "failed" {
logEntry.Debugf("helm release '%s' has FAILED status: should run upgrade", releaseName)
return true, nil
}
// Get values for a non-failed release.
releaseValues, err := helmClient.GetReleaseValues(releaseName)
if err != nil {
logEntry.Debugf("helm release '%s' get values error, no upgrade: %v", releaseName, err)
return false, err
}
// Run helm upgrade if there is no stored checksum
recordedChecksum, hasKey := releaseValues["_addonOperatorModuleChecksum"]
if !hasKey {
logEntry.Debugf("helm release '%s' has no saved checksum of values: should run upgrade", releaseName)
return true, nil
}
// Calculate a checksum of current values and compare to a stored checksum.
// Run helm upgrade if checksum is changed.
if recordedChecksumStr, ok := recordedChecksum.(string); ok {
if recordedChecksumStr != checksum {
logEntry.Debugf("helm release '%s' checksum '%s' is changed to '%s': should run upgrade", releaseName, recordedChecksumStr, checksum)
return true, nil
}
}
// Check if there are absent resources
absent, err := hm.dependencies.HelmResourceManager.GetAbsentResources(manifests, app.Namespace)
if err != nil {
return false, err
}
// Run helm upgrade if there are absent resources
if len(absent) > 0 {
logEntry.Debugf("helm release '%s' has %d absent resources: should run upgrade", releaseName, len(absent))
return true, nil
}
logEntry.Debugf("helm release '%s' is unchanged: skip release upgrade", releaseName)
return false, nil
}
func (hm *HelmModule) PrepareValuesYamlFile() (string, error) {
data, err := hm.values.YamlBytes()
if err != nil {
return "", err
}
path := filepath.Join(hm.tmpDir, fmt.Sprintf("%s.module-values.yaml-%s", hm.safeName(), uuid.Must(uuid.NewV4()).String()))
err = utils.DumpData(path, data)
if err != nil {
return "", err
}
log.Debugf("Prepared module %s helm values:\n%s", hm.name, hm.values.DebugString())
return path, nil
}
func (hm *HelmModule) safeName() string {
return sanitize.BaseName(hm.name)
}
func (hm *HelmModule) Render(namespace string, debug bool) (string, error) {
if namespace == "" {
namespace = "default"
}
valuesPath, err := hm.PrepareValuesYamlFile()
if err != nil {
return "", err
}
defer os.Remove(valuesPath)
return hm.dependencies.HelmClientFactory.NewClient().Render(hm.name, hm.path, []string{valuesPath}, nil, namespace, debug)
}