-
Notifications
You must be signed in to change notification settings - Fork 29
/
replace.go
309 lines (249 loc) · 9.95 KB
/
replace.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
package chartify
import (
"fmt"
"os"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
type ReplaceWithRenderedOpts struct {
// Debug when set to true passes `--debug` flag to `helm` in order to enable debug logging
Debug bool
// ValuesFiles are a list of Helm chart values files
ValuesFiles []string
// SetValues is a list of adhoc Helm chart values being passed via helm's `--set` flags
SetValues []string
// SetFlags is the list of set flags like --set k=v, --set-file k=path, --set-string k=str
// used while rendering the chart.
SetFlags []string
// Namespace is the default namespace in which the K8s manifests rendered by the chart are associated
Namespace string
// ChartVersion is the semver of the Helm chart being used to render the original K8s manifests before various tweaks applied by helm-x
ChartVersion string
// IncludeCRDs is a Helm 3 only option. When it is true, chartify passes a `--include-crds` flag
// to helm-template.
IncludeCRDs bool
// Validate is a Helm 3 only option. When it is true, chartify passes --validate while running helm-template
// It is required when your chart contains any template that relies on Capabilities.APIVersions
// for rendering resourecs depending on the API resources and versions available on a live cluster.
// In other words, setting this to true means that you need access to a Kubernetes cluster,
// even if you aren't trying to install the generated chart onto the cluster.
Validate bool
// KubeVersion specifies the Kubernetes version used for Capabilities.KubeVersion
// when running `helm template` to produce the temporary chart to apply various customizations.
// If the upstream command that calls chartify was going to pass `kube-version` while rendering the original chart,
// you must also pass the same value to this field.
// Otherwise the temporary chart rendered by chartify lacks `kube-version` at helm-template time
// and it my produce output unexpected to you.
KubeVersion string
// ApiVersions is a string of kubernetes APIVersions and passed to helm template via --api-versions
// It is required if your chart contains any template that relies on Capabilities.APIVersion for rendering
// resources depending on the API resources and versions available in a target cluster.
// Setting this value defines a set of static capabilities and avoids the need for access to a live cluster during
// templating in contrast to --validate
ApiVersions []string
// WorkaroundOutputDirIssue prevents chartify from using `helm template --output-dir` and let it use `helm template > some.yaml` instead to
// workaround the potential helm issue
// See https://github.com/roboll/helmfile/issues/1279#issuecomment-636839395
WorkaroundOutputDirIssue bool
}
func (r *Runner) ReplaceWithRendered(name, chartName, chartPath string, o ReplaceWithRenderedOpts) ([]string, error) {
var additionalFlags string
additionalFlags += createFlagChain("set", o.SetValues)
if len(o.SetFlags) > 0 {
additionalFlags += " " + strings.Join(o.SetFlags, " ")
}
defaultValuesPath := filepath.Join(chartPath, "values.yaml")
exists, err := r.Exists(defaultValuesPath)
if err != nil {
return nil, err
}
if exists {
additionalFlags += createFlagChain("f", []string{defaultValuesPath})
}
additionalFlags += createFlagChain("f", o.ValuesFiles)
if o.Namespace != "" {
additionalFlags += createFlagChain("namespace", []string{o.Namespace})
}
if o.KubeVersion != "" {
additionalFlags += createFlagChain("kube-version", []string{o.KubeVersion})
}
additionalFlags += createFlagChain("api-versions", o.ApiVersions)
r.Logf("options: %v", o)
helmOutputDir := filepath.Join(chartPath, "helmx.1.rendered")
if err := os.Mkdir(helmOutputDir, 0755); err != nil {
return nil, err
}
var command string
writtenFiles := map[string]bool{}
if r.isHelm3 {
args := []string{
fmt.Sprintf("--debug=%v", o.Debug),
fmt.Sprintf("--output-dir=%s", helmOutputDir),
}
if o.IncludeCRDs {
args = append(args, "--include-crds")
}
if o.Validate {
args = append(args, "--validate")
}
args = append(args, name, chartPath)
command = fmt.Sprintf("%s template %s%s", r.helmBin(), strings.Join(args, " "), additionalFlags)
} else {
command = fmt.Sprintf("%s template --debug=%v %s --name %s%s --output-dir %s", r.helmBin(), o.Debug, chartPath, name, additionalFlags, helmOutputDir)
}
stdout, err := r.run(command)
if err != nil {
return nil, err
}
helmOutputDirEntries, err := os.ReadDir(helmOutputDir)
if err != nil {
return nil, fmt.Errorf("unable to read helm output dir entries: %w", err)
}
// This directory contains templates/ and charts/SUBCHART/templates
var chartOutputDir string
for _, e := range helmOutputDirEntries {
if !e.IsDir() {
return nil, fmt.Errorf("encountered unexpected dir entry at %s: it must be a dir but was not", e.Name())
}
if chartOutputDir != "" {
return nil, fmt.Errorf("assertion failed: there should be only one dir entry under the helm output dir %s", chartOutputDir)
}
chartOutputDir = filepath.Join(helmOutputDir, e.Name())
}
if !filepath.IsAbs(chartOutputDir) {
return nil, fmt.Errorf("assertion failed: unexpected dir entry %q it must be the abs path to the output directory", chartOutputDir)
}
// - Replace templates/**/*.yaml with rendered templates/**/*.yaml
// - Replace charts/SUBCHART.tgz with rendered charts/SUBCHART/templates/*.yaml
// - Replace crds/*.yaml with rendered crds/*.yaml
for _, d := range ContentDirs {
origDir := filepath.Join(chartPath, d)
if err := os.RemoveAll(origDir); err != nil {
return nil, err
}
newDir := filepath.Join(chartOutputDir, d)
if _, err := os.Stat(newDir); err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
if err := os.Rename(newDir, origDir); err != nil {
return nil, err
}
usedDir := filepath.Join(chartPath, "files", d)
if err := os.RemoveAll(usedDir); err != nil && !os.IsNotExist(err) {
return nil, err
}
}
lines := strings.Split(stdout, "\n")
for _, line := range lines {
if strings.HasPrefix(line, "wrote ") {
file := strings.Split(line, "wrote ")[1]
for _, d := range ContentDirs {
origDir := filepath.Join(chartPath, d)
newDir := filepath.Join(chartOutputDir, d)
file = strings.ReplaceAll(strings.ReplaceAll(file, "/", string(filepath.Separator)), newDir, origDir)
}
writtenFiles[file] = true
}
}
if len(writtenFiles) == 0 {
return nil, fmt.Errorf("invalid state: no files rendered")
}
if err := os.RemoveAll(helmOutputDir); err != nil {
return nil, fmt.Errorf("cleaning up unnecessary files after replace: %v", err)
}
results := make([]string, 0, len(writtenFiles))
for f := range writtenFiles {
results = append(results, f)
}
// We need to remove the Chart.yaml's `dependencies` field to
// avoid failing due to unnecessarily trying to fetch adhoc chart dependencies we've added just before this function.
//
// The adhoc chart dependencies should already be rendered, patched, and included in the temporary chart
// we've generated so far. So we don't need to tell Helm to fetch chart dependencies again. They are already included.
//
// This avoids errors like the below due to Chart.yaml containing adhoc dependencies that are already rendered
// and included in the files and the templates directories.
// Error: found in Chart.yaml, but missing in charts/ directory: common, kibana
//
// Note that this is the fix for adhoc chart dependencies.
// The standard chart dependencies that are declared in the original Chart.yaml or requirements.yaml,
// should have been downloaded by `helm fetch` that run in an even earlier phase of chartify.
if r.IsHelm3() {
type ChartMeta struct {
Dependencies []Dependency `yaml:"dependencies,omitempty"`
Data map[string]interface{} `yaml:",inline"`
}
var chartMeta ChartMeta
chartYamlPath := filepath.Join(chartPath, "Chart.yaml")
bytes, err := r.ReadFile(chartYamlPath)
if os.IsNotExist(err) {
} else if err != nil {
return nil, err
} else {
if err := yaml.Unmarshal(bytes, &chartMeta); err != nil {
return nil, err
}
}
chartMeta.Dependencies = nil
chartYamlContent, err := yaml.Marshal(&chartMeta)
if err != nil {
return nil, fmt.Errorf("marshaling-back %s's Chart.yaml: %w", chartName, err)
}
r.Logf("Removing the dependencies field from the original Chart.yaml.")
if err := r.WriteFile(chartYamlPath, chartYamlContent, 0644); err != nil {
return nil, err
}
} else {
var reqs Requirements
bytes, err := r.ReadFile(filepath.Join(chartPath, "requirements.yaml"))
if os.IsNotExist(err) {
} else if err != nil {
return nil, err
} else {
if err := yaml.Unmarshal(bytes, &reqs); err != nil {
return nil, err
}
}
reqs.Dependencies = nil
requirementsYamlContent, err := yaml.Marshal(&reqs)
if err != nil {
return nil, fmt.Errorf("marshaling %s's requirements as YAML: %w", chartName, err)
}
if err := r.WriteFile(filepath.Join(chartPath, "requirements.yaml"), requirementsYamlContent, 0644); err != nil {
return nil, err
}
{
debugOut, err := r.ReadFile(filepath.Join(chartPath, "requirements.yaml"))
if err != nil {
return nil, err
}
r.Logf("using requirements.yaml:\n%s", debugOut)
}
}
// We need to remove dangling Chart.lock and requirements.lock too, as the corresponding dependencies
// are already deleted out of Chart.yaml/requirements.yaml as above.
// Otherwise, you may end up with issues like:
// https://github.com/roboll/helmfile/issues/2074#issuecomment-1068335836
reqLock := filepath.Join(chartPath, "requirements.lock")
chartLock := filepath.Join(chartPath, "Chart.lock")
if err := removeFileIfExists(reqLock); err != nil {
r.Logf("Error removing %s: %v", reqLock, err)
}
if err := removeFileIfExists(chartLock); err != nil {
r.Logf("Error removing %s: %v", chartLock, err)
}
return results, nil
}
func removeFileIfExists(f string) error {
if _, err := os.Stat(f); err != nil {
return nil
}
if err := os.Remove(f); err != nil {
return err
}
return nil
}