-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathtemplate.go
349 lines (304 loc) · 13.2 KB
/
template.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
348
349
package helm
import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/gonvenience/ytbx"
"github.com/gruntwork-io/go-commons/errors"
"github.com/gruntwork-io/terratest/modules/files"
"github.com/gruntwork-io/terratest/modules/testing"
"github.com/homeport/dyff/pkg/dyff"
"github.com/stretchr/testify/require"
goyaml "gopkg.in/yaml.v3"
)
// RenderTemplate runs `helm template` to render the template given the provided options and returns stdout/stderr from
// the template command. If you pass in templateFiles, this will only render those templates. This function will fail
// the test if there is an error rendering the template.
func RenderTemplate(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) string {
out, err := RenderTemplateE(t, options, chartDir, releaseName, templateFiles, extraHelmArgs...)
require.NoError(t, err)
return out
}
// RenderTemplateE runs `helm template` to render the template given the provided options and returns stdout/stderr from
// the template command. If you pass in templateFiles, this will only render those templates.
func RenderTemplateE(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, error) {
// Get render arguments
args, err := getRenderArgs(t, options, chartDir, releaseName, templateFiles, extraHelmArgs...)
if err != nil {
return "", err
}
// Finally, call out to helm template command
return RunHelmCommandAndGetStdOutE(t, options, "template", args...)
}
// RenderTemplateAndGetStdOutErrE runs `helm template` to render the template given the provided options and returns stdout and stderr separately from
// the template command. If you pass in templateFiles, this will only render those templates.
func RenderTemplateAndGetStdOutErrE(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, string, error) {
args, err := getRenderArgs(t, options, chartDir, releaseName, templateFiles, extraHelmArgs...)
if err != nil {
return "", "", err
}
// Finally, call out to helm template command
return RunHelmCommandAndGetStdOutErrE(t, options, "template", args...)
}
func getRenderArgs(t testing.TestingT, options *Options, chartDir string, releaseName string, templateFiles []string, extraHelmArgs ...string) ([]string, error) {
// First, verify the charts dir exists
absChartDir, err := filepath.Abs(chartDir)
if err != nil {
return nil, errors.WithStackTrace(err)
}
if !files.FileExists(chartDir) {
return nil, errors.WithStackTrace(ChartNotFoundError{chartDir})
}
// check chart dependencies
if options.BuildDependencies {
if _, err := RunHelmCommandAndGetOutputE(t, options, "dependency", "build", chartDir); err != nil {
return nil, errors.WithStackTrace(err)
}
}
// Now construct the args
// We first construct the template args
args := []string{}
if options.KubectlOptions != nil && options.KubectlOptions.Namespace != "" {
args = append(args, "--namespace", options.KubectlOptions.Namespace)
}
args, err = getValuesArgsE(t, options, args...)
if err != nil {
return nil, err
}
for _, templateFile := range templateFiles {
// validate this is a valid template file
absTemplateFile := filepath.Join(absChartDir, templateFile)
if !strings.HasPrefix(templateFile, "charts") && !files.FileExists(absTemplateFile) {
return nil, errors.WithStackTrace(TemplateFileNotFoundError{Path: templateFile, ChartDir: absChartDir})
}
// Note: we only get the abs template file path to check it actually exists, but the `helm template` command
// expects the relative path from the chart.
args = append(args, "--show-only", templateFile)
}
// deal extraHelmArgs
args = append(args, extraHelmArgs...)
// ... and add the name and chart at the end as the command expects
args = append(args, releaseName, chartDir)
return args, nil
}
// RenderRemoteTemplate runs `helm template` to render a *remote* chart given the provided options and returns stdout/stderr from
// the template command. If you pass in templateFiles, this will only render those templates. This function will fail
// the test if there is an error rendering the template.
func RenderRemoteTemplate(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) string {
out, err := RenderRemoteTemplateE(t, options, chartURL, releaseName, templateFiles, extraHelmArgs...)
require.NoError(t, err)
return out
}
// RenderRemoteTemplateE runs `helm template` to render a *remote* helm chart given the provided options and returns stdout/stderr from
// the template command. If you pass in templateFiles, this will only render those templates.
func RenderRemoteTemplateE(t testing.TestingT, options *Options, chartURL string, releaseName string, templateFiles []string, extraHelmArgs ...string) (string, error) {
// Now construct the args
// We first construct the template args
args := []string{}
if options.KubectlOptions != nil && options.KubectlOptions.Namespace != "" {
args = append(args, "--namespace", options.KubectlOptions.Namespace)
}
args, err := getValuesArgsE(t, options, args...)
if err != nil {
return "", err
}
for _, templateFile := range templateFiles {
// As the helm command fails if a non valid template is given as input
// we do not check if the template file exists or not as we do for local charts
// as it would add unecessary networking calls
args = append(args, "--show-only", templateFile)
}
// deal extraHelmArgs
args = append(args, extraHelmArgs...)
// ... and add the helm chart name, the remote repo and chart URL at the end
args = append(args, releaseName, "--repo", chartURL)
if options.Version != "" {
args = append(args, "--version", options.Version)
}
// Finally, call out to helm template command
return RunHelmCommandAndGetStdOutE(t, options, "template", args...)
}
// UnmarshalK8SYaml is the same as UnmarshalK8SYamlE, but will fail the test if there is an error.
func UnmarshalK8SYaml(t testing.TestingT, yamlData string, destinationObj interface{}) {
require.NoError(t, UnmarshalK8SYamlE(t, yamlData, destinationObj))
}
// UnmarshalK8SYamlE can be used to take template outputs and unmarshal them into the corresponding client-go struct. For
// example, suppose you render the template into a Deployment object. You can unmarshal the yaml as follows:
//
// var deployment appsv1.Deployment
// UnmarshalK8SYamlE(t, renderedOutput, &deployment)
//
// At the end of this, the deployment variable will be populated.
func UnmarshalK8SYamlE(t testing.TestingT, yamlData string, destinationObj interface{}) error {
decoder := goyaml.NewDecoder(strings.NewReader(yamlData))
// Ensure destinationObj is a pointer
destVal := reflect.ValueOf(destinationObj)
if destVal.Kind() != reflect.Ptr {
return fmt.Errorf("destinationObj must be a pointer")
}
destElem := destVal.Elem()
// Handle single object or list as root
if destElem.Kind() != reflect.Slice {
// Decode only the first document
var rawYaml interface{}
if err := decoder.Decode(&rawYaml); err != nil {
return errors.WithStackTrace(err)
}
// If the root is an array but destinationObj is a single object, return an error
if reflect.TypeOf(rawYaml).Kind() == reflect.Slice {
return fmt.Errorf("YAML root is an array, but destinationObj is a single object")
}
jsonData, err := json.Marshal(rawYaml)
if err != nil {
return errors.WithStackTrace(err)
}
if err := json.Unmarshal(jsonData, destinationObj); err != nil {
return errors.WithStackTrace(err)
}
return nil
}
// Handle multiple YAML documents (destinationObj is a slice)
slicePtr := destVal
sliceVal := slicePtr.Elem()
for {
var rawYaml interface{}
if err := decoder.Decode(&rawYaml); err != nil {
if err == io.EOF {
break // No more documents
}
return errors.WithStackTrace(err)
}
jsonData, err := json.Marshal(rawYaml)
if err != nil {
return errors.WithStackTrace(err)
}
// If root object is a slice, append elements individually
if reflect.TypeOf(rawYaml).Kind() == reflect.Slice {
var items []json.RawMessage
if err := json.Unmarshal(jsonData, &items); err != nil {
return errors.WithStackTrace(err)
}
for _, item := range items {
newElem := reflect.New(sliceVal.Type().Elem()) // Create new element
if err := json.Unmarshal(item, newElem.Interface()); err != nil {
return errors.WithStackTrace(err)
}
sliceVal.Set(reflect.Append(sliceVal, newElem.Elem()))
}
} else {
newElem := reflect.New(sliceVal.Type().Elem()) // Create new element
if err := json.Unmarshal(jsonData, newElem.Interface()); err != nil {
return errors.WithStackTrace(err)
}
sliceVal.Set(reflect.Append(sliceVal, newElem.Elem()))
}
}
return nil
}
// UpdateSnapshot creates or updates the k8s manifest snapshot of a chart (e.g bitnami/nginx).
// It is one of the two functions needed to implement snapshot based testing for helm.
// see https://github.com/gruntwork-io/terratest/issues/1377
// A snapshot is used to compare the current manifests of a chart with the previous manifests.
// A global diff is run against the two snapshosts and the number of differences is returned.
func UpdateSnapshot(t testing.TestingT, options *Options, yamlData string, releaseName string) {
require.NoError(t, UpdateSnapshotE(t, options, yamlData, releaseName))
}
// UpdateSnapshotE creates or updates the k8s manifest snapshot of a chart (e.g bitnami/nginx).
// It is one of the two functions needed to implement snapshot based testing for helm.
// see https://github.com/gruntwork-io/terratest/issues/1377
// A snapshot is used to compare the current manifests of a chart with the previous manifests.
// A global diff is run against the two snapshosts and the number of differences is returned.
// It will failed the test if there is an error while writing the manifests' snapshot in the file system
func UpdateSnapshotE(t testing.TestingT, options *Options, yamlData string, releaseName string) error {
var snapshotDir = "__snapshot__"
if options.SnapshotPath != "" {
snapshotDir = options.SnapshotPath
}
// Create a directory if not exists
if !files.FileExists(snapshotDir) {
if err := os.Mkdir(snapshotDir, 0755); err != nil {
return errors.WithStackTrace(err)
}
}
filename := filepath.Join(snapshotDir, releaseName+".yaml")
// Open a file in write mode
file, err := os.Create(filename)
if err != nil {
return errors.WithStackTrace(err)
}
defer file.Close()
// Write the k8s manifest into the file
if _, err = file.WriteString(yamlData); err != nil {
return errors.WithStackTrace(err)
}
if options.Logger != nil {
options.Logger.Logf(t, "helm chart manifest written into file: %s", filename)
}
return nil
}
// DiffAgainstSnapshot compare the current manifests of a chart (e.g bitnami/nginx)
// with the previous manifests stored in the snapshot.
// see https://github.com/gruntwork-io/terratest/issues/1377
// It returns the number of difference between the two manifests or -1 in case of error
// It will fail the test if there is an error while reading or writing the two manifests in the file system
func DiffAgainstSnapshot(t testing.TestingT, options *Options, yamlData string, releaseName string) int {
numberOfDiffs, err := DiffAgainstSnapshotE(t, options, yamlData, releaseName)
require.NoError(t, err)
return numberOfDiffs
}
// DiffAgainstSnapshotE compare the current manifests of a chart (e.g bitnami/nginx)
// with the previous manifests stored in the snapshot.
// see https://github.com/gruntwork-io/terratest/issues/1377
// It returns the number of difference between the manifests or -1 in case of error
func DiffAgainstSnapshotE(t testing.TestingT, options *Options, yamlData string, releaseName string) (int, error) {
var snapshotDir = "__snapshot__"
if options.SnapshotPath != "" {
snapshotDir = options.SnapshotPath
}
// load the yaml snapshot file
snapshot := filepath.Join(snapshotDir, releaseName+".yaml")
from, err := ytbx.LoadFile(snapshot)
if err != nil {
return -1, errors.WithStackTrace(err)
}
// write the current manifest into a file as `dyff` does not support string input
currentManifests := releaseName + ".yaml"
file, err := os.Create(currentManifests)
if err != nil {
return -1, errors.WithStackTrace(err)
}
if _, err = file.WriteString(yamlData); err != nil {
return -1, errors.WithStackTrace(err)
}
defer file.Close()
defer os.Remove(currentManifests)
to, err := ytbx.LoadFile(currentManifests)
if err != nil {
return -1, errors.WithStackTrace(err)
}
// compare the two manifests using `dyff`
compOpt := dyff.KubernetesEntityDetection(false)
// create a report
report, err := dyff.CompareInputFiles(from, to, compOpt)
if err != nil {
return -1, errors.WithStackTrace(err)
}
// write any difference to stdout
reportWriter := &dyff.HumanReport{
Report: report,
DoNotInspectCerts: false,
NoTableStyle: false,
OmitHeader: false,
UseGoPatchPaths: false,
}
err = reportWriter.WriteReport(os.Stdout)
if err != nil {
return -1, errors.WithStackTrace(err)
}
// return the number of diffs to use in assertion while testing: 0 = no differences
return len(reportWriter.Diffs), nil
}