1
2
3
4 package builtins
5
6 import (
7 "bytes"
8 "fmt"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "regexp"
13 "strings"
14
15 "sigs.k8s.io/kustomize/api/resmap"
16 "sigs.k8s.io/kustomize/api/types"
17 "sigs.k8s.io/kustomize/kyaml/errors"
18 "sigs.k8s.io/kustomize/kyaml/kio"
19 kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
20 "sigs.k8s.io/kustomize/kyaml/yaml/merge2"
21 "sigs.k8s.io/yaml"
22 )
23
24
25 type HelmChartInflationGeneratorPlugin struct {
26 h *resmap.PluginHelpers
27 types.HelmGlobals
28 types.HelmChart
29 tmpDir string
30 }
31
32 const (
33 valuesMergeOptionMerge = "merge"
34 valuesMergeOptionOverride = "override"
35 valuesMergeOptionReplace = "replace"
36 )
37
38 var legalMergeOptions = []string{
39 valuesMergeOptionMerge,
40 valuesMergeOptionOverride,
41 valuesMergeOptionReplace,
42 }
43
44
45
46 func (p *HelmChartInflationGeneratorPlugin) Config(
47 h *resmap.PluginHelpers, config []byte) (err error) {
48 if h.GeneralConfig() == nil {
49 return fmt.Errorf("unable to access general config")
50 }
51 if !h.GeneralConfig().HelmConfig.Enabled {
52 return fmt.Errorf("must specify --enable-helm")
53 }
54 if h.GeneralConfig().HelmConfig.Command == "" {
55 return fmt.Errorf("must specify --helm-command")
56 }
57
58
59 if h.GeneralConfig().HelmConfig.KubeVersion != "" {
60 p.HelmChart.KubeVersion = h.GeneralConfig().HelmConfig.KubeVersion
61 }
62 if len(h.GeneralConfig().HelmConfig.ApiVersions) != 0 {
63 p.HelmChart.ApiVersions = h.GeneralConfig().HelmConfig.ApiVersions
64 }
65
66 p.h = h
67 if err = yaml.Unmarshal(config, p); err != nil {
68 return
69 }
70 return p.validateArgs()
71 }
72
73
74
75
76
77
78 func (p *HelmChartInflationGeneratorPlugin) establishTmpDir() (err error) {
79 if p.tmpDir != "" {
80
81 return nil
82 }
83 p.tmpDir, err = os.MkdirTemp("", "kustomize-helm-")
84 return err
85 }
86
87 func (p *HelmChartInflationGeneratorPlugin) validateArgs() (err error) {
88 if p.Name == "" {
89 return fmt.Errorf("chart name cannot be empty")
90 }
91
92
93
94
95
96 if p.ChartHome == "" {
97 p.ChartHome = types.HelmDefaultHome
98 }
99
100
101
102
103 if p.ValuesFile == "" {
104 p.ValuesFile = filepath.Join(p.absChartHome(), p.Name, "values.yaml")
105 }
106 for i, file := range p.AdditionalValuesFiles {
107
108 if _, err := p.h.Loader().Load(file); err != nil {
109 return errors.WrapPrefixf(err, "could not load additionalValuesFile")
110 }
111
112 p.AdditionalValuesFiles[i] = filepath.Join(p.h.Loader().Root(), file)
113 }
114
115 if err = p.errIfIllegalValuesMerge(); err != nil {
116 return err
117 }
118
119
120 if p.ConfigHome == "" {
121 if err = p.establishTmpDir(); err != nil {
122 return errors.WrapPrefixf(
123 err, "unable to create tmp dir for HELM_CONFIG_HOME")
124 }
125 p.ConfigHome = filepath.Join(p.tmpDir, "helm")
126 }
127 return nil
128 }
129
130 func (p *HelmChartInflationGeneratorPlugin) errIfIllegalValuesMerge() error {
131 if p.ValuesMerge == "" {
132
133 p.ValuesMerge = valuesMergeOptionOverride
134 return nil
135 }
136 for _, opt := range legalMergeOptions {
137 if p.ValuesMerge == opt {
138 return nil
139 }
140 }
141 return fmt.Errorf("valuesMerge must be one of %v", legalMergeOptions)
142 }
143
144 func (p *HelmChartInflationGeneratorPlugin) absChartHome() string {
145 var chartHome string
146 if filepath.IsAbs(p.ChartHome) {
147 chartHome = p.ChartHome
148 } else {
149 chartHome = filepath.Join(p.h.Loader().Root(), p.ChartHome)
150 }
151
152 if p.Version != "" && p.Repo != "" {
153 return filepath.Join(chartHome, fmt.Sprintf("%s-%s", p.Name, p.Version))
154 }
155 return chartHome
156 }
157
158 func (p *HelmChartInflationGeneratorPlugin) runHelmCommand(
159 args []string) ([]byte, error) {
160 stdout := new(bytes.Buffer)
161 stderr := new(bytes.Buffer)
162 cmd := exec.Command(p.h.GeneralConfig().HelmConfig.Command, args...)
163 cmd.Stdout = stdout
164 cmd.Stderr = stderr
165 env := []string{
166 fmt.Sprintf("HELM_CONFIG_HOME=%s", p.ConfigHome),
167 fmt.Sprintf("HELM_CACHE_HOME=%s/.cache", p.ConfigHome),
168 fmt.Sprintf("HELM_DATA_HOME=%s/.data", p.ConfigHome)}
169 cmd.Env = append(os.Environ(), env...)
170 err := cmd.Run()
171 if err != nil {
172 helm := p.h.GeneralConfig().HelmConfig.Command
173 err = errors.WrapPrefixf(
174 fmt.Errorf(
175 "unable to run: '%s %s' with env=%s (is '%s' installed?): %w",
176 helm, strings.Join(args, " "), env, helm, err),
177 stderr.String(),
178 )
179 }
180 return stdout.Bytes(), err
181 }
182
183
184 func (p *HelmChartInflationGeneratorPlugin) createNewMergedValuesFile() (
185 path string, err error) {
186 if p.ValuesMerge == valuesMergeOptionMerge ||
187 p.ValuesMerge == valuesMergeOptionOverride {
188 if err = p.replaceValuesInline(); err != nil {
189 return "", err
190 }
191 }
192 var b []byte
193 b, err = yaml.Marshal(p.ValuesInline)
194 if err != nil {
195 return "", err
196 }
197 return p.writeValuesBytes(b)
198 }
199
200 func (p *HelmChartInflationGeneratorPlugin) replaceValuesInline() error {
201 pValues, err := p.h.Loader().Load(p.ValuesFile)
202 if err != nil {
203 return err
204 }
205 chValues, err := kyaml.Parse(string(pValues))
206 if err != nil {
207 return errors.WrapPrefixf(err, "could not parse values file into rnode")
208 }
209 inlineValues, err := kyaml.FromMap(p.ValuesInline)
210 if err != nil {
211 return errors.WrapPrefixf(err, "could not parse values inline into rnode")
212 }
213 var outValues *kyaml.RNode
214 switch p.ValuesMerge {
215
216
217
218
219 case valuesMergeOptionOverride:
220 outValues, err = merge2.Merge(inlineValues, chValues.Copy(), kyaml.MergeOptions{})
221 case valuesMergeOptionMerge:
222 outValues, err = merge2.Merge(chValues, inlineValues.Copy(), kyaml.MergeOptions{})
223 }
224 if err != nil {
225 return errors.WrapPrefixf(err, "could not merge values")
226 }
227 mapValues, err := outValues.Map()
228 if err != nil {
229 return errors.WrapPrefixf(err, "could not parse merged values into map")
230 }
231 p.ValuesInline = mapValues
232 return err
233 }
234
235
236 func (p *HelmChartInflationGeneratorPlugin) copyValuesFile() (string, error) {
237 b, err := p.h.Loader().Load(p.ValuesFile)
238 if err != nil {
239 return "", err
240 }
241 return p.writeValuesBytes(b)
242 }
243
244
245 func (p *HelmChartInflationGeneratorPlugin) writeValuesBytes(
246 b []byte) (string, error) {
247 if err := p.establishTmpDir(); err != nil {
248 return "", fmt.Errorf("cannot create tmp dir to write helm values")
249 }
250 path := filepath.Join(p.tmpDir, p.Name+"-kustomize-values.yaml")
251 return path, errors.WrapPrefixf(os.WriteFile(path, b, 0644), "failed to write values file")
252 }
253
254 func (p *HelmChartInflationGeneratorPlugin) cleanup() {
255 if p.tmpDir != "" {
256 os.RemoveAll(p.tmpDir)
257 }
258 }
259
260
261 func (p *HelmChartInflationGeneratorPlugin) Generate() (rm resmap.ResMap, err error) {
262 defer p.cleanup()
263 if err = p.checkHelmVersion(); err != nil {
264 return nil, err
265 }
266 if path, exists := p.chartExistsLocally(); !exists {
267 if p.Repo == "" {
268 return nil, fmt.Errorf(
269 "no repo specified for pull, no chart found at '%s'", path)
270 }
271 if _, err := p.runHelmCommand(p.pullCommand()); err != nil {
272 return nil, err
273 }
274 }
275 if len(p.ValuesInline) > 0 {
276 p.ValuesFile, err = p.createNewMergedValuesFile()
277 } else {
278 p.ValuesFile, err = p.copyValuesFile()
279 }
280 if err != nil {
281 return nil, err
282 }
283 var stdout []byte
284 stdout, err = p.runHelmCommand(p.AsHelmArgs(p.absChartHome()))
285 if err != nil {
286 return nil, err
287 }
288
289 rm, resMapErr := p.h.ResmapFactory().NewResMapFromBytes(stdout)
290 if resMapErr == nil {
291 return rm, nil
292 }
293
294
295 r := &kio.ByteReader{Reader: bytes.NewBufferString(string(stdout)), OmitReaderAnnotations: true}
296 nodes, err := r.Read()
297 if err != nil {
298 return nil, fmt.Errorf("error reading helm output: %w", err)
299 }
300
301 if len(nodes) != 0 {
302 rm, err = p.h.ResmapFactory().NewResMapFromRNodeSlice(nodes)
303 if err != nil {
304 return nil, fmt.Errorf("could not parse rnode slice into resource map: %w", err)
305 }
306 return rm, nil
307 }
308 return nil, fmt.Errorf("could not parse bytes into resource map: %w", resMapErr)
309 }
310
311 func (p *HelmChartInflationGeneratorPlugin) pullCommand() []string {
312 args := []string{
313 "pull",
314 "--untar",
315 "--untardir", p.absChartHome(),
316 }
317
318 switch {
319 case strings.HasPrefix(p.Repo, "oci://"):
320 args = append(args, strings.TrimSuffix(p.Repo, "/")+"/"+p.Name)
321 case p.Repo != "":
322 args = append(args, "--repo", p.Repo)
323 fallthrough
324 default:
325 args = append(args, p.Name)
326 }
327
328 if p.Version != "" {
329 args = append(args, "--version", p.Version)
330 }
331 return args
332 }
333
334
335
336 func (p *HelmChartInflationGeneratorPlugin) chartExistsLocally() (string, bool) {
337 path := filepath.Join(p.absChartHome(), p.Name)
338 s, err := os.Stat(path)
339 if err != nil {
340 return "", false
341 }
342 return path, s.IsDir()
343 }
344
345
346 func (p *HelmChartInflationGeneratorPlugin) checkHelmVersion() error {
347 stdout, err := p.runHelmCommand([]string{"version", "-c", "--short"})
348 if err != nil {
349 return err
350 }
351 r, err := regexp.Compile(`v?\d+(\.\d+)+`)
352 if err != nil {
353 return err
354 }
355 v := r.FindString(string(stdout))
356 if v == "" {
357 return fmt.Errorf("cannot find version string in %s", string(stdout))
358 }
359 if v[0] == 'v' {
360 v = v[1:]
361 }
362 majorVersion := strings.Split(v, ".")[0]
363 if majorVersion != "3" {
364 return fmt.Errorf("this plugin requires helm V3 but got v%s", v)
365 }
366 return nil
367 }
368
369 func NewHelmChartInflationGeneratorPlugin() resmap.GeneratorPlugin {
370 return &HelmChartInflationGeneratorPlugin{}
371 }
372
View as plain text