1package state
2
3import (
4	"bytes"
5	"crypto/sha1"
6	"encoding/hex"
7	"errors"
8	"fmt"
9	"io"
10	"io/ioutil"
11	"os"
12	"path"
13	"path/filepath"
14	"regexp"
15	"sort"
16	"strconv"
17	"strings"
18	"sync"
19	"text/template"
20
21	"github.com/imdario/mergo"
22	"github.com/variantdev/chartify"
23
24	"github.com/roboll/helmfile/pkg/environment"
25	"github.com/roboll/helmfile/pkg/event"
26	"github.com/roboll/helmfile/pkg/helmexec"
27	"github.com/roboll/helmfile/pkg/remote"
28	"github.com/roboll/helmfile/pkg/tmpl"
29
30	"github.com/tatsushid/go-prettytable"
31	"github.com/variantdev/vals"
32	"go.uber.org/zap"
33	"gopkg.in/yaml.v2"
34)
35
36const (
37	// EmptyTimeout represents the `--timeout` value passed to helm commands not being specified via helmfile flags.
38	// This is used by an interim solution to make the urfave/cli command report to the helmfile internal about that the
39	// --timeout flag is missingl
40	EmptyTimeout = -1
41)
42
43type ReleaseSetSpec struct {
44	DefaultHelmBinary string `yaml:"helmBinary,omitempty"`
45
46	// DefaultValues is the default values to be overrode by environment values and command-line overrides
47	DefaultValues []interface{} `yaml:"values,omitempty"`
48
49	Environments map[string]EnvironmentSpec `yaml:"environments,omitempty"`
50
51	Bases              []string          `yaml:"bases,omitempty"`
52	HelmDefaults       HelmSpec          `yaml:"helmDefaults,omitempty"`
53	Helmfiles          []SubHelmfileSpec `yaml:"helmfiles,omitempty"`
54	DeprecatedContext  string            `yaml:"context,omitempty"`
55	DeprecatedReleases []ReleaseSpec     `yaml:"charts,omitempty"`
56	OverrideNamespace  string            `yaml:"namespace,omitempty"`
57	Repositories       []RepositorySpec  `yaml:"repositories,omitempty"`
58	CommonLabels       map[string]string `yaml:"commonLabels,omitempty"`
59	Releases           []ReleaseSpec     `yaml:"releases,omitempty"`
60	Selectors          []string          `yaml:"-"`
61	ApiVersions        []string          `yaml:"apiVersions,omitempty"`
62
63	// Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile
64	Hooks []event.Hook `yaml:"hooks,omitempty"`
65
66	Templates map[string]TemplateSpec `yaml:"templates"`
67
68	Env environment.Environment `yaml:"-"`
69
70	// If set to "Error", return an error when a subhelmfile points to a
71	// non-existent path. The default behavior is to print a warning. Note the
72	// differing default compared to other MissingFileHandlers.
73	MissingFileHandler string `yaml:"missingFileHandler,omitempty"`
74}
75
76type PullCommand struct {
77	ChartRef     string
78	responseChan chan error
79}
80
81// HelmState structure for the helmfile
82type HelmState struct {
83	basePath string
84	FilePath string
85
86	ReleaseSetSpec `yaml:",inline"`
87
88	logger *zap.SugaredLogger
89
90	readFile          func(string) ([]byte, error)
91	removeFile        func(string) error
92	fileExists        func(string) (bool, error)
93	glob              func(string) ([]string, error)
94	tempDir           func(string, string) (string, error)
95	directoryExistsAt func(string) bool
96
97	runner      helmexec.Runner
98	valsRuntime vals.Evaluator
99
100	// RenderedValues is the helmfile-wide values that is `.Values`
101	// which is accessible from within the whole helmfile go template.
102	// Note that this is usually computed by DesiredStateLoader from ReleaseSetSpec.Env
103	RenderedValues map[string]interface{}
104}
105
106// SubHelmfileSpec defines the subhelmfile path and options
107type SubHelmfileSpec struct {
108	//path or glob pattern for the sub helmfiles
109	Path string `yaml:"path,omitempty"`
110	//chosen selectors for the sub helmfiles
111	Selectors []string `yaml:"selectors,omitempty"`
112	//do the sub helmfiles inherits from parent selectors
113	SelectorsInherited bool `yaml:"selectorsInherited,omitempty"`
114
115	Environment SubhelmfileEnvironmentSpec
116}
117
118type SubhelmfileEnvironmentSpec struct {
119	OverrideValues []interface{} `yaml:"values,omitempty"`
120}
121
122// HelmSpec to defines helmDefault values
123type HelmSpec struct {
124	KubeContext     string   `yaml:"kubeContext,omitempty"`
125	TillerNamespace string   `yaml:"tillerNamespace,omitempty"`
126	Tillerless      bool     `yaml:"tillerless"`
127	Args            []string `yaml:"args,omitempty"`
128	Verify          bool     `yaml:"verify"`
129	// Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0'
130	Devel bool `yaml:"devel"`
131	// Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful
132	Wait bool `yaml:"wait"`
133	// Timeout is the time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
134	Timeout int `yaml:"timeout"`
135	// RecreatePods, when set to true, instruct helmfile to perform pods restart for the resource if applicable
136	RecreatePods bool `yaml:"recreatePods"`
137	// Force, when set to true, forces resource update through delete/recreate if needed
138	Force bool `yaml:"force"`
139	// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
140	Atomic bool `yaml:"atomic"`
141	// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
142	CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"`
143	// HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10)
144	HistoryMax *int `yaml:"historyMax,omitempty"`
145	// CreateNamespace, when set to true (default), --create-namespace is passed to helm3 on install/upgrade (ignored for helm2)
146	CreateNamespace *bool `yaml:"createNamespace,omitempty"`
147	// SkipDeps disables running `helm dependency up` and `helm dependency build` on this release's chart.
148	// This is relevant only when your release uses a local chart or a directory containing K8s manifests or a Kustomization
149	// as a Helm chart.
150	SkipDeps bool `yaml:"skipDeps"`
151
152	TLS                      bool   `yaml:"tls"`
153	TLSCACert                string `yaml:"tlsCACert,omitempty"`
154	TLSKey                   string `yaml:"tlsKey,omitempty"`
155	TLSCert                  string `yaml:"tlsCert,omitempty"`
156	DisableValidation        *bool  `yaml:"disableValidation,omitempty"`
157	DisableOpenAPIValidation *bool  `yaml:"disableOpenAPIValidation,omitempty"`
158}
159
160// RepositorySpec that defines values for a helm repo
161type RepositorySpec struct {
162	Name     string `yaml:"name,omitempty"`
163	URL      string `yaml:"url,omitempty"`
164	CaFile   string `yaml:"caFile,omitempty"`
165	CertFile string `yaml:"certFile,omitempty"`
166	KeyFile  string `yaml:"keyFile,omitempty"`
167	Username string `yaml:"username,omitempty"`
168	Password string `yaml:"password,omitempty"`
169	Managed  string `yaml:"managed,omitempty"`
170	OCI      bool   `yaml:"oci,omitempty"`
171}
172
173// ReleaseSpec defines the structure of a helm release
174type ReleaseSpec struct {
175	// Chart is the name of the chart being installed to create this release
176	Chart string `yaml:"chart,omitempty"`
177	// Directory is an alias to Chart which may be of more fit when you want to use a local/remote directory containing
178	// K8s manifests or Kustomization as a chart
179	Directory string `yaml:"directory,omitempty"`
180	// Version is the semver version or version constraint for the chart
181	Version string `yaml:"version,omitempty"`
182	// Verify enables signature verification on fetched chart.
183	// Beware some (or many?) chart repositories and charts don't seem to support it.
184	Verify *bool `yaml:"verify,omitempty"`
185	// Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0'
186	Devel *bool `yaml:"devel,omitempty"`
187	// Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful
188	Wait *bool `yaml:"wait,omitempty"`
189	// Timeout is the time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300)
190	Timeout *int `yaml:"timeout,omitempty"`
191	// RecreatePods, when set to true, instruct helmfile to perform pods restart for the resource if applicable
192	RecreatePods *bool `yaml:"recreatePods,omitempty"`
193	// Force, when set to true, forces resource update through delete/recreate if needed
194	Force *bool `yaml:"force,omitempty"`
195	// Installed, when set to true, `delete --purge` the release
196	Installed *bool `yaml:"installed,omitempty"`
197	// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
198	Atomic *bool `yaml:"atomic,omitempty"`
199	// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
200	CleanupOnFail *bool `yaml:"cleanupOnFail,omitempty"`
201	// HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10)
202	HistoryMax *int `yaml:"historyMax,omitempty"`
203	// Condition, when set, evaluate the mapping specified in this string to a boolean which decides whether or not to process the release
204	Condition string `yaml:"condition,omitempty"`
205	// CreateNamespace, when set to true (default), --create-namespace is passed to helm3 on install (ignored for helm2)
206	CreateNamespace *bool `yaml:"createNamespace,omitempty"`
207
208	// DisableOpenAPIValidation is rarely used to bypass OpenAPI validations only that is used for e.g.
209	// work-around against broken CRs
210	// See also:
211	// - https://github.com/helm/helm/pull/6819
212	// - https://github.com/roboll/helmfile/issues/1167
213	DisableOpenAPIValidation *bool `yaml:"disableOpenAPIValidation,omitempty"`
214
215	// DisableValidation is rarely used to bypass the whole validation of manifests against the Kubernetes cluster
216	// so that `helm diff` can be run containing a chart that installs both CRD and CRs on first install.
217	// FYI, such diff without `--disable-validation` fails on first install because the K8s cluster doesn't have CRDs registered yet.
218	DisableValidation *bool `yaml:"disableValidation,omitempty"`
219
220	// DisableValidationOnInstall disables the K8s API validation while running helm-diff on the release being newly installed on helmfile-apply.
221	// It is useful when any release contains custom resources for CRDs that is not yet installed onto the cluster.
222	DisableValidationOnInstall *bool `yaml:"disableValidationOnInstall,omitempty"`
223
224	// MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues.
225	// The default value for MissingFileHandler is "Error".
226	MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
227	// Needs is the [TILLER_NS/][NS/]NAME representations of releases that this release depends on.
228	Needs []string `yaml:"needs,omitempty"`
229
230	// Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile
231	Hooks []event.Hook `yaml:"hooks,omitempty"`
232
233	// Name is the name of this release
234	Name      string            `yaml:"name,omitempty"`
235	Namespace string            `yaml:"namespace,omitempty"`
236	Labels    map[string]string `yaml:"labels,omitempty"`
237	Values    []interface{}     `yaml:"values,omitempty"`
238	Secrets   []interface{}     `yaml:"secrets,omitempty"`
239	SetValues []SetValue        `yaml:"set,omitempty"`
240
241	ValuesTemplate    []interface{} `yaml:"valuesTemplate,omitempty"`
242	SetValuesTemplate []SetValue    `yaml:"setTemplate,omitempty"`
243
244	// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
245	EnvValues []SetValue `yaml:"env,omitempty"`
246
247	ValuesPathPrefix string `yaml:"valuesPathPrefix,omitempty"`
248
249	TillerNamespace string `yaml:"tillerNamespace,omitempty"`
250	Tillerless      *bool  `yaml:"tillerless,omitempty"`
251
252	KubeContext string `yaml:"kubeContext,omitempty"`
253
254	TLS       *bool  `yaml:"tls,omitempty"`
255	TLSCACert string `yaml:"tlsCACert,omitempty"`
256	TLSKey    string `yaml:"tlsKey,omitempty"`
257	TLSCert   string `yaml:"tlsCert,omitempty"`
258
259	// These values are used in templating
260	TillerlessTemplate *string `yaml:"tillerlessTemplate,omitempty"`
261	VerifyTemplate     *string `yaml:"verifyTemplate,omitempty"`
262	WaitTemplate       *string `yaml:"waitTemplate,omitempty"`
263	InstalledTemplate  *string `yaml:"installedTemplate,omitempty"`
264
265	// These settings requires helm-x integration to work
266	Dependencies          []Dependency  `yaml:"dependencies,omitempty"`
267	JSONPatches           []interface{} `yaml:"jsonPatches,omitempty"`
268	StrategicMergePatches []interface{} `yaml:"strategicMergePatches,omitempty"`
269
270	// Transformers is the list of Kustomize transformers
271	//
272	// Each item can be a path to a YAML or go template file, or an embedded transformer declaration as a YAML hash.
273	// It's often used to add common labels and annotations to your resources.
274	// See https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead for more information.
275	Transformers []interface{} `yaml:"transformers,omitempty"`
276	Adopt        []string      `yaml:"adopt,omitempty"`
277
278	//version of the chart that has really been installed cause desired version may be fuzzy (~2.0.0)
279	installedVersion string
280
281	// ForceGoGetter forces the use of go-getter for fetching remote directory as maniefsts/chart/kustomization
282	// by parsing the url from `chart` field of the release.
283	// This is handy when getting the go-getter url parsing error when it doesn't work as expected.
284	// Without this, any error in url parsing result in silently falling-back to normal process of treating `chart:` as the regular
285	// helm chart name.
286	ForceGoGetter bool `yaml:"forceGoGetter,omitempty"`
287
288	// ForceNamespace is an experimental feature to set metadata.namespace in every K8s resource rendered by the chart,
289	// regardless of the template, even when it doesn't have `namespace: {{ .Namespace | quote }}`.
290	// This is only needed when you can't FIX your chart to have `namespace: {{ .Namespace }}` AND you're using `helmfile template`.
291	// In standard use-cases, `Namespace` should be sufficient.
292	// Use this only when you know what you want to do!
293	ForceNamespace string `yaml:"forceNamespace,omitempty"`
294
295	// SkipDeps disables running `helm dependency up` and `helm dependency build` on this release's chart.
296	// This is relevant only when your release uses a local chart or a directory containing K8s manifests or a Kustomization
297	// as a Helm chart.
298	SkipDeps *bool `yaml:"skipDeps,omitempty"`
299}
300
301type Release struct {
302	ReleaseSpec
303
304	Filtered bool
305}
306
307// SetValue are the key values to set on a helm release
308type SetValue struct {
309	Name   string   `yaml:"name,omitempty"`
310	Value  string   `yaml:"value,omitempty"`
311	File   string   `yaml:"file,omitempty"`
312	Values []string `yaml:"values,omitempty"`
313}
314
315// AffectedReleases hold the list of released that where updated, deleted, or in error
316type AffectedReleases struct {
317	Upgraded []*ReleaseSpec
318	Deleted  []*ReleaseSpec
319	Failed   []*ReleaseSpec
320}
321
322const DefaultEnv = "default"
323
324const MissingFileHandlerError = "Error"
325const MissingFileHandlerInfo = "Info"
326const MissingFileHandlerWarn = "Warn"
327const MissingFileHandlerDebug = "Debug"
328
329func (st *HelmState) ApplyOverrides(spec *ReleaseSpec) {
330	if st.OverrideNamespace != "" {
331		spec.Namespace = st.OverrideNamespace
332
333		for i := 0; i < len(spec.Needs); i++ {
334			n := spec.Needs[i]
335			if len(strings.Split(n, "/")) == 1 {
336				spec.Needs[i] = st.OverrideNamespace + "/" + n
337			}
338		}
339	}
340}
341
342type RepoUpdater interface {
343	AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error
344	UpdateRepo() error
345	RegistryLogin(name string, username string, password string) error
346}
347
348// getRepositoriesToSync returns the names of repositories to be updated
349func (st *HelmState) getRepositoriesToSync() (map[string]bool, error) {
350	releases, err := st.GetSelectedReleasesWithOverrides()
351	if err != nil {
352		return nil, err
353	}
354
355	repositoriesToUpdate := map[string]bool{}
356
357	if len(releases) == 0 {
358		for _, repo := range st.Repositories {
359			repositoriesToUpdate[repo.Name] = true
360		}
361
362		return repositoriesToUpdate, nil
363	}
364
365	for _, release := range releases {
366		if release.Installed == nil || *release.Installed {
367			chart := strings.Split(release.Chart, "/")
368			if len(chart) == 1 {
369				continue
370			}
371			repositoriesToUpdate[chart[0]] = true
372		}
373	}
374
375	return repositoriesToUpdate, nil
376}
377
378func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]string, error) {
379	var updated []string
380
381	for _, repo := range st.Repositories {
382		if shouldSkip[repo.Name] {
383			continue
384		}
385		var err error
386		if repo.OCI {
387			username, password := gatherOCIUsernamePassword(repo.Name, repo.Username, repo.Password)
388			if username != "" && password != "" {
389				err = helm.RegistryLogin(repo.URL, username, password)
390			}
391		} else {
392			err = helm.AddRepo(repo.Name, repo.URL, repo.CaFile, repo.CertFile, repo.KeyFile, repo.Username, repo.Password, repo.Managed)
393		}
394
395		if err != nil {
396			return nil, err
397		}
398
399		updated = append(updated, repo.Name)
400	}
401
402	return updated, nil
403}
404
405func gatherOCIUsernamePassword(repoName string, username string, password string) (string, string) {
406	var user, pass string
407
408	if username != "" {
409		user = username
410	} else if u := os.Getenv(fmt.Sprintf("%s_USERNAME", strings.ToUpper(repoName))); u != "" {
411		user = u
412	}
413
414	if password != "" {
415		pass = password
416	} else if p := os.Getenv(fmt.Sprintf("%s_PASSWORD", strings.ToUpper(repoName))); p != "" {
417		pass = p
418	}
419
420	return user, pass
421}
422
423type syncResult struct {
424	errors []*ReleaseError
425}
426
427type syncPrepareResult struct {
428	release *ReleaseSpec
429	flags   []string
430	errors  []*ReleaseError
431	files   []string
432}
433
434// SyncReleases wrapper for executing helm upgrade on the releases
435func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int, opt ...SyncOpt) ([]syncPrepareResult, []error) {
436	opts := &SyncOpts{}
437	for _, o := range opt {
438		o.Apply(opts)
439	}
440
441	releases := []*ReleaseSpec{}
442	for i, _ := range st.Releases {
443		releases = append(releases, &st.Releases[i])
444	}
445
446	numReleases := len(releases)
447	jobs := make(chan *ReleaseSpec, numReleases)
448	results := make(chan syncPrepareResult, numReleases)
449
450	res := []syncPrepareResult{}
451	errs := []error{}
452
453	mut := sync.Mutex{}
454
455	st.scatterGather(
456		concurrency,
457		numReleases,
458		func() {
459			for i := 0; i < numReleases; i++ {
460				jobs <- releases[i]
461			}
462			close(jobs)
463		},
464		func(workerIndex int) {
465			for release := range jobs {
466				st.ApplyOverrides(release)
467
468				// If `installed: false`, the only potential operation on this release would be uninstalling.
469				// We skip generating values files in that case, because for an uninstall with `helm delete`, we don't need to those.
470				// The values files are for `helm upgrade -f values.yaml` calls that happens when the release has `installed: true`.
471				// This logic addresses:
472				// - https://github.com/roboll/helmfile/issues/519
473				// - https://github.com/roboll/helmfile/issues/616
474				if !release.Desired() {
475					results <- syncPrepareResult{release: release, flags: []string{}, errors: []*ReleaseError{}}
476					continue
477				}
478
479				// TODO We need a long-term fix for this :)
480				// See https://github.com/roboll/helmfile/issues/737
481				mut.Lock()
482				flags, files, flagsErr := st.flagsForUpgrade(helm, release, workerIndex)
483				mut.Unlock()
484				if flagsErr != nil {
485					results <- syncPrepareResult{errors: []*ReleaseError{newReleaseFailedError(release, flagsErr)}, files: files}
486					continue
487				}
488
489				errs := []*ReleaseError{}
490				for _, value := range additionalValues {
491					valfile, err := filepath.Abs(value)
492					if err != nil {
493						errs = append(errs, newReleaseFailedError(release, err))
494					}
495
496					ok, err := st.fileExists(valfile)
497					if err != nil {
498						errs = append(errs, newReleaseFailedError(release, err))
499					} else if !ok {
500						errs = append(errs, newReleaseFailedError(release, fmt.Errorf("file does not exist: %s", valfile)))
501					}
502					flags = append(flags, "--values", valfile)
503				}
504
505				if opts.Set != nil {
506					for _, s := range opts.Set {
507						flags = append(flags, "--set", s)
508					}
509				}
510
511				if opts.Wait {
512					flags = append(flags, "--wait")
513				}
514
515				if len(errs) > 0 {
516					results <- syncPrepareResult{errors: errs, files: files}
517					continue
518				}
519
520				results <- syncPrepareResult{release: release, flags: flags, errors: []*ReleaseError{}, files: files}
521			}
522		},
523		func() {
524			for i := 0; i < numReleases; {
525				select {
526				case r := <-results:
527					for _, e := range r.errors {
528						errs = append(errs, e)
529					}
530					res = append(res, r)
531					i++
532				}
533			}
534		},
535	)
536
537	return res, errs
538}
539
540func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helmexec.Interface, release ReleaseSpec) (bool, error) {
541	out, err := st.listReleases(context, helm, &release)
542	if err != nil {
543		return false, err
544	} else if out != "" {
545		return true, nil
546	}
547	return false, nil
548}
549
550func (st *HelmState) DetectReleasesToBeDeletedForSync(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {
551	detected := []ReleaseSpec{}
552	for i := range releases {
553		release := releases[i]
554
555		if !release.Desired() {
556			installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
557			if err != nil {
558				return nil, err
559			} else if installed {
560				// Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554)
561				r := release
562				detected = append(detected, r)
563			}
564		}
565	}
566	return detected, nil
567}
568
569func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {
570	detected := []ReleaseSpec{}
571	for i := range releases {
572		release := releases[i]
573
574		installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release)
575		if err != nil {
576			return nil, err
577		} else if installed {
578			// Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554)
579			r := release
580			detected = append(detected, r)
581		}
582	}
583	return detected, nil
584}
585
586type SyncOpts struct {
587	Set         []string
588	SkipCleanup bool
589	Wait        bool
590}
591
592type SyncOpt interface{ Apply(*SyncOpts) }
593
594func (o *SyncOpts) Apply(opts *SyncOpts) {
595	*opts = *o
596}
597
598func ReleaseToID(r *ReleaseSpec) string {
599	var id string
600
601	kc := r.KubeContext
602	if kc != "" {
603		id += kc + "/"
604	}
605
606	tns := r.TillerNamespace
607	if tns != "" {
608		id += tns + "/"
609	}
610
611	ns := r.Namespace
612	if ns != "" {
613		id += ns + "/"
614	}
615
616	id += r.Name
617
618	return id
619}
620
621// DeleteReleasesForSync deletes releases that are marked for deletion
622func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, helm helmexec.Interface, workerLimit int) []error {
623	errs := []error{}
624
625	releases := st.Releases
626
627	jobQueue := make(chan *ReleaseSpec, len(releases))
628	results := make(chan syncResult, len(releases))
629	if workerLimit == 0 {
630		workerLimit = len(releases)
631	}
632
633	m := new(sync.Mutex)
634
635	st.scatterGather(
636		workerLimit,
637		len(releases),
638		func() {
639			for i := 0; i < len(releases); i++ {
640				jobQueue <- &releases[i]
641			}
642			close(jobQueue)
643		},
644		func(workerIndex int) {
645			for release := range jobQueue {
646				var relErr *ReleaseError
647				context := st.createHelmContext(release, workerIndex)
648
649				if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
650					relErr = newReleaseFailedError(release, err)
651				} else {
652					var args []string
653					if helm.IsHelm3() {
654						args = []string{}
655						if release.Namespace != "" {
656							args = append(args, "--namespace", release.Namespace)
657						}
658					} else {
659						args = []string{"--purge"}
660					}
661					deletionFlags := st.appendConnectionFlags(args, helm, release)
662					m.Lock()
663					if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil {
664						affectedReleases.Failed = append(affectedReleases.Failed, release)
665						relErr = newReleaseFailedError(release, err)
666					} else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
667						affectedReleases.Failed = append(affectedReleases.Failed, release)
668						relErr = newReleaseFailedError(release, err)
669					} else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil {
670						affectedReleases.Failed = append(affectedReleases.Failed, release)
671						relErr = newReleaseFailedError(release, err)
672					} else {
673						affectedReleases.Deleted = append(affectedReleases.Deleted, release)
674					}
675					m.Unlock()
676				}
677
678				if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil {
679					st.logger.Warnf("warn: %v\n", err)
680				}
681
682				if _, err := st.TriggerCleanupEvent(release, "sync"); err != nil {
683					st.logger.Warnf("warn: %v\n", err)
684				}
685
686				if relErr == nil {
687					results <- syncResult{}
688				} else {
689					results <- syncResult{errors: []*ReleaseError{relErr}}
690				}
691			}
692		},
693		func() {
694			for i := 0; i < len(releases); {
695				select {
696				case res := <-results:
697					if len(res.errors) > 0 {
698						for _, e := range res.errors {
699							errs = append(errs, e)
700						}
701					}
702				}
703				i++
704			}
705		},
706	)
707	if len(errs) > 0 {
708		return errs
709	}
710	return nil
711}
712
713// SyncReleases wrapper for executing helm upgrade on the releases
714func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int, opt ...SyncOpt) []error {
715	opts := &SyncOpts{}
716	for _, o := range opt {
717		o.Apply(opts)
718	}
719
720	preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit, opts)
721
722	defer func() {
723		if opts.SkipCleanup {
724			return
725		}
726
727		for _, p := range preps {
728			st.removeFiles(p.files)
729		}
730	}()
731
732	if len(prepErrs) > 0 {
733		return prepErrs
734	}
735
736	errs := []error{}
737	jobQueue := make(chan *syncPrepareResult, len(preps))
738	results := make(chan syncResult, len(preps))
739	if workerLimit == 0 {
740		workerLimit = len(preps)
741	}
742
743	m := new(sync.Mutex)
744
745	st.scatterGather(
746		workerLimit,
747		len(preps),
748		func() {
749			for i := 0; i < len(preps); i++ {
750				jobQueue <- &preps[i]
751			}
752			close(jobQueue)
753		},
754		func(workerIndex int) {
755			for prep := range jobQueue {
756				release := prep.release
757				flags := prep.flags
758				chart := normalizeChart(st.basePath, release.Chart)
759				var relErr *ReleaseError
760				context := st.createHelmContext(release, workerIndex)
761
762				if _, err := st.triggerPresyncEvent(release, "sync"); err != nil {
763					relErr = newReleaseFailedError(release, err)
764				} else if !release.Desired() {
765					installed, err := st.isReleaseInstalled(context, helm, *release)
766					if err != nil {
767						relErr = newReleaseFailedError(release, err)
768					} else if installed {
769						var args []string
770						if helm.IsHelm3() {
771							args = []string{}
772						} else {
773							args = []string{"--purge"}
774						}
775						deletionFlags := st.appendConnectionFlags(args, helm, release)
776						m.Lock()
777						if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil {
778							affectedReleases.Failed = append(affectedReleases.Failed, release)
779							relErr = newReleaseFailedError(release, err)
780						} else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
781							affectedReleases.Failed = append(affectedReleases.Failed, release)
782							relErr = newReleaseFailedError(release, err)
783						} else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil {
784							affectedReleases.Failed = append(affectedReleases.Failed, release)
785							relErr = newReleaseFailedError(release, err)
786						} else {
787							affectedReleases.Deleted = append(affectedReleases.Deleted, release)
788						}
789						m.Unlock()
790					}
791				} else if err := helm.SyncRelease(context, release.Name, chart, flags...); err != nil {
792					m.Lock()
793					affectedReleases.Failed = append(affectedReleases.Failed, release)
794					m.Unlock()
795					relErr = newReleaseFailedError(release, err)
796				} else {
797					m.Lock()
798					affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
799					m.Unlock()
800					installedVersion, err := st.getDeployedVersion(context, helm, release)
801					if err != nil { //err is not really impacting so just log it
802						st.logger.Debugf("getting deployed release version failed:%v", err)
803					} else {
804						release.installedVersion = installedVersion
805					}
806				}
807
808				if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil {
809					st.logger.Warnf("warn: %v\n", err)
810				}
811
812				if _, err := st.TriggerCleanupEvent(release, "sync"); err != nil {
813					st.logger.Warnf("warn: %v\n", err)
814				}
815
816				if relErr == nil {
817					results <- syncResult{}
818				} else {
819					results <- syncResult{errors: []*ReleaseError{relErr}}
820				}
821			}
822		},
823		func() {
824			for i := 0; i < len(preps); {
825				select {
826				case res := <-results:
827					if len(res.errors) > 0 {
828						for _, e := range res.errors {
829							errs = append(errs, e)
830						}
831					}
832				}
833				i++
834			}
835		},
836	)
837	if len(errs) > 0 {
838		return errs
839	}
840	return nil
841}
842
843func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
844	flags := st.connectionFlags(helm, release)
845	if helm.IsHelm3() && release.Namespace != "" {
846		flags = append(flags, "--namespace", release.Namespace)
847	}
848	flags = append(flags, "--deployed", "--failed", "--pending")
849	return helm.List(context, "^"+release.Name+"$", flags...)
850}
851
852func (st *HelmState) getDeployedVersion(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
853	//retrieve the version
854	if out, err := st.listReleases(context, helm, release); err == nil {
855		chartName := filepath.Base(release.Chart)
856		//the regexp without escapes : .*\s.*\s.*\s.*\schartName-(.*?)\s
857		pat := regexp.MustCompile(".*\\s.*\\s.*\\s.*\\s" + chartName + "-(.*?)\\s")
858		versions := pat.FindStringSubmatch(out)
859		if len(versions) > 0 {
860			return versions[1], nil
861		} else {
862			//fails to find the version
863			return "failed to get version", errors.New("Failed to get the version for:" + chartName)
864		}
865	} else {
866		return "failed to get version", err
867	}
868}
869
870func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec {
871	var result []ReleaseSpec
872
873	for _, r := range releases {
874		if r.Installed != nil && !*r.Installed {
875			continue
876		}
877		result = append(result, r)
878	}
879
880	return result
881}
882
883type ChartPrepareOptions struct {
884	ForceDownload bool
885	SkipRepos     bool
886	SkipDeps      bool
887	SkipResolve   bool
888	Wait          bool
889}
890
891type chartPrepareResult struct {
892	releaseName            string
893	releaseNamespace       string
894	releaseContext         string
895	chartName              string
896	chartPath              string
897	err                    error
898	buildDeps              bool
899	chartFetchedByGoGetter bool
900}
901
902func (st *HelmState) GetRepositoryAndNameFromChartName(chartName string) (*RepositorySpec, string) {
903	chart := strings.Split(chartName, "/")
904	if len(chart) == 1 {
905		return nil, chartName
906	}
907	repo := chart[0]
908	for _, r := range st.Repositories {
909		if r.Name == repo {
910			return &r, strings.Join(chart[1:], "/")
911		}
912	}
913	return nil, chartName
914}
915
916type PrepareChartKey struct {
917	Namespace, Name, KubeContext string
918}
919
920// PrepareCharts creates temporary directories of charts.
921//
922// Each resulting "chart" can be one of the followings:
923//
924// (1) local chart
925// (2) temporary local chart generated from kustomization or manifests
926// (3) remote chart
927//
928// When running `helmfile template` on helm v2, or `helmfile lint` on both helm v2 and v3,
929// PrepareCharts will download and untar charts for linting and templating.
930//
931// Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart.
932//
933// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors.
934func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[PrepareChartKey]string, []error) {
935	var selected []ReleaseSpec
936
937	if len(st.Selectors) > 0 {
938		var err error
939
940		// This and releasesNeedCharts ensures that we run operations like helm-dep-build and prepare-hook calls only on
941		// releases that are (1) selected by the selectors and (2) to be installed.
942		selected, err = st.GetSelectedReleasesWithOverrides()
943		if err != nil {
944			return nil, []error{err}
945		}
946	} else {
947		selected = st.Releases
948	}
949
950	releases := releasesNeedCharts(selected)
951
952	temp := make(map[PrepareChartKey]string, len(releases))
953
954	errs := []error{}
955
956	jobQueue := make(chan *ReleaseSpec, len(releases))
957	results := make(chan *chartPrepareResult, len(releases))
958
959	var helm3 bool
960
961	if helm != nil {
962		helm3 = helm.IsHelm3()
963	}
964
965	if !opts.SkipResolve {
966		updated, err := st.ResolveDeps()
967		if err != nil {
968			return nil, []error{err}
969		}
970		*st = *updated
971	}
972
973	var builds []*chartPrepareResult
974	pullChan := make(chan PullCommand)
975	defer func() {
976		close(pullChan)
977	}()
978	go st.pullChartWorker(pullChan, helm)
979
980	st.scatterGather(
981		concurrency,
982		len(releases),
983		func() {
984			for i := 0; i < len(releases); i++ {
985				jobQueue <- &releases[i]
986			}
987			close(jobQueue)
988		},
989		func(workerIndex int) {
990			for release := range jobQueue {
991				// Call user-defined `prepare` hooks to create/modify local charts to be used by
992				// the later process.
993				//
994				// If it wasn't called here, Helmfile can end up an issue like
995				// https://github.com/roboll/helmfile/issues/1328
996				if _, err := st.triggerPrepareEvent(release, helmfileCommand); err != nil {
997					results <- &chartPrepareResult{err: err}
998					return
999				}
1000
1001				chartName := release.Chart
1002
1003				chartPath, err := st.downloadChartWithGoGetter(release)
1004				if err != nil {
1005					results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
1006					return
1007				}
1008				chartFetchedByGoGetter := chartPath != chartName
1009
1010				if !chartFetchedByGoGetter {
1011					ociChartPath, err := st.getOCIChart(pullChan, release, dir, helm)
1012					if err != nil {
1013						results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
1014
1015						return
1016					}
1017
1018					if ociChartPath != nil {
1019						chartPath = *ociChartPath
1020					}
1021				}
1022
1023				isLocal := st.directoryExistsAt(normalizeChart(st.basePath, chartName))
1024
1025				chartification, clean, err := st.PrepareChartify(helm, release, chartPath, workerIndex)
1026				defer clean()
1027				if err != nil {
1028					results <- &chartPrepareResult{err: err}
1029					return
1030				}
1031
1032				var buildDeps bool
1033
1034				skipDepsGlobal := opts.SkipDeps
1035				skipDepsRelease := release.SkipDeps != nil && *release.SkipDeps
1036				skipDepsDefault := release.SkipDeps == nil && st.HelmDefaults.SkipDeps
1037				skipDeps := !isLocal || skipDepsGlobal || skipDepsRelease || skipDepsDefault
1038
1039				if chartification != nil {
1040					c := chartify.New(
1041						chartify.HelmBin(st.DefaultHelmBinary),
1042						chartify.UseHelm3(helm3),
1043					)
1044
1045					chartifyOpts := chartification.Opts
1046
1047					if skipDeps {
1048						chartifyOpts.SkipDeps = true
1049					}
1050
1051					out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts))
1052					if err != nil {
1053						results <- &chartPrepareResult{err: err}
1054						return
1055					} else {
1056						chartPath = out
1057					}
1058
1059					// Skip `helm dep build` and `helm dep up` altogether when the chart is from remote or the dep is
1060					// explicitly skipped.
1061					buildDeps = !skipDeps
1062				} else if normalizedChart := normalizeChart(st.basePath, chartPath); st.directoryExistsAt(normalizedChart) {
1063					// At this point, we are sure that chartPath is a local directory containing either:
1064					// - A remote chart fetched by go-getter or
1065					// - A local chart
1066					//
1067					// The chart may have Chart.yaml(and requirements.yaml for Helm 2), and optionally Chart.lock/requirements.lock,
1068					// but no `charts/` directory populated at all, or a subet of chart dependencies are missing in the directory.
1069					//
1070					// In such situation, Helm fails with an error like:
1071					//   Error: found in Chart.yaml, but missing in charts/ directory: cert-manager, prometheus, postgresql, gitlab-runner, grafana, redis
1072					//
1073					// (See also https://github.com/roboll/helmfile/issues/1401#issuecomment-670854495)
1074					//
1075					// To avoid it, we need to call a `helm dep build` command on the chart.
1076					// But the command may consistently fail when an outdated Chart.lock exists.
1077					//
1078					// (I've mentioned about such case in https://github.com/roboll/helmfile/pull/1400.)
1079					//
1080					// Trying to run `helm dep build` on the chart regardless of if it's from local or remote is
1081					// problematic, as usually the user would have no way to fix the remote chart on their own.
1082					//
1083					// Given that, we always run `helm dep build` on the chart here, but tolerate any error caused by it
1084					// for a remote chart, so that the user can notice/fix the issue in a local chart while
1085					// a broken remote chart won't completely block their job.
1086					chartPath = normalizedChart
1087
1088					buildDeps = !skipDeps
1089				} else if !opts.ForceDownload {
1090					// At this point, we are sure that either:
1091					// 1. It is a local chart and we can use it in later process (helm upgrade/template/lint/etc)
1092					//    without any modification, or
1093					// 2. It is a remote chart which can be safely handed over to helm,
1094					//    because the version of Helm used in this transaction (helm v3 or greater) support downloading
1095					//    the chart instead, AND we don't need any modification to the chart
1096					//
1097					//    Also see HelmState.chartVersionFlags(). For `helmfile template`, it's called before `helm template`
1098					//    only on helm v3.
1099					//    For helm 2, we `helm fetch` with the version flags and call `helm template`
1100					//    WITHOUT the version flags.
1101				} else {
1102					pathElems := []string{
1103						dir,
1104					}
1105
1106					if release.TillerNamespace != "" {
1107						pathElems = append(pathElems, release.TillerNamespace)
1108					}
1109
1110					if release.Namespace != "" {
1111						pathElems = append(pathElems, release.Namespace)
1112					}
1113
1114					if release.KubeContext != "" {
1115						pathElems = append(pathElems, release.KubeContext)
1116					}
1117
1118					chartVersion := "latest"
1119					if release.Version != "" {
1120						chartVersion = release.Version
1121					}
1122
1123					pathElems = append(pathElems, release.Name, chartName, chartVersion)
1124
1125					chartPath = path.Join(pathElems...)
1126
1127					// only fetch chart if it is not already fetched
1128					if _, err := os.Stat(chartPath); os.IsNotExist(err) {
1129						fetchFlags := st.chartVersionFlags(release)
1130						fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
1131						if err := helm.Fetch(chartName, fetchFlags...); err != nil {
1132							results <- &chartPrepareResult{err: err}
1133							return
1134						}
1135					}
1136
1137					// Set chartPath to be the path containing Chart.yaml, if found
1138					fullChartPath, err := findChartDirectory(chartPath)
1139					if err == nil {
1140						chartPath = filepath.Dir(fullChartPath)
1141					}
1142				}
1143
1144				results <- &chartPrepareResult{
1145					releaseName:            release.Name,
1146					chartName:              chartName,
1147					releaseNamespace:       release.Namespace,
1148					releaseContext:         release.KubeContext,
1149					chartPath:              chartPath,
1150					buildDeps:              buildDeps,
1151					chartFetchedByGoGetter: chartFetchedByGoGetter,
1152				}
1153			}
1154		},
1155		func() {
1156			for i := 0; i < len(releases); i++ {
1157				downloadRes := <-results
1158
1159				if downloadRes.err != nil {
1160					errs = append(errs, downloadRes.err)
1161
1162					return
1163				}
1164				temp[PrepareChartKey{
1165					Namespace:   downloadRes.releaseNamespace,
1166					KubeContext: downloadRes.releaseContext,
1167					Name:        downloadRes.releaseName,
1168				}] = downloadRes.chartPath
1169
1170				if downloadRes.buildDeps {
1171					builds = append(builds, downloadRes)
1172				}
1173			}
1174		},
1175	)
1176
1177	if len(errs) > 0 {
1178		return nil, errs
1179	}
1180
1181	if len(builds) > 0 {
1182		if err := st.runHelmDepBuilds(helm, concurrency, builds); err != nil {
1183			return nil, []error{err}
1184		}
1185	}
1186
1187	return temp, nil
1188}
1189
1190func (st *HelmState) runHelmDepBuilds(helm helmexec.Interface, concurrency int, builds []*chartPrepareResult) error {
1191	// NOTES:
1192	// 1. `helm dep build` fails when it was run concurrency on the same chart.
1193	//    To avoid that, we run `helm dep build` only once per each local chart.
1194	//
1195	//    See https://github.com/roboll/helmfile/issues/1438
1196	// 2. Even if it isn't on the same local chart, `helm dep build` intermittently fails when run concurrentl
1197	//    So we shouldn't use goroutines like we do for other helm operations here.
1198	//
1199	//    See https://github.com/roboll/helmfile/issues/1521
1200	for _, r := range builds {
1201		if err := helm.BuildDeps(r.releaseName, r.chartPath); err != nil {
1202			if r.chartFetchedByGoGetter {
1203				diagnostic := fmt.Sprintf(
1204					"WARN: `helm dep build` failed. While processing release %q, Helmfile observed that remote chart %q fetched by go-getter is seemingly broken. "+
1205						"One of well-known causes of this is that the chart has outdated Chart.lock, which needs the chart maintainer needs to run `helm dep up`. "+
1206						"Helmfile is tolerating the error to avoid blocking you until the remote chart gets fixed. "+
1207						"But this may result in any failure later if the chart is broken badly. FYI, the tolerated error was: %v",
1208					r.releaseName,
1209					r.chartName,
1210					err.Error(),
1211				)
1212
1213				st.logger.Warn(diagnostic)
1214
1215				continue
1216			}
1217
1218			return fmt.Errorf("building dependencies of local chart: %w", err)
1219		}
1220	}
1221
1222	return nil
1223}
1224
1225type TemplateOpts struct {
1226	Set               []string
1227	SkipCleanup       bool
1228	OutputDirTemplate string
1229	IncludeCRDs       bool
1230}
1231
1232type TemplateOpt interface{ Apply(*TemplateOpts) }
1233
1234func (o *TemplateOpts) Apply(opts *TemplateOpts) {
1235	*opts = *o
1236}
1237
1238// TemplateReleases wrapper for executing helm template on the releases
1239func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, additionalValues []string, args []string, workerLimit int,
1240	validate bool, opt ...TemplateOpt) []error {
1241
1242	opts := &TemplateOpts{}
1243	for _, o := range opt {
1244		o.Apply(opts)
1245	}
1246
1247	errs := []error{}
1248
1249	for i := range st.Releases {
1250		release := &st.Releases[i]
1251
1252		if !release.Desired() {
1253			continue
1254		}
1255
1256		st.ApplyOverrides(release)
1257
1258		flags, files, err := st.flagsForTemplate(helm, release, 0)
1259
1260		defer func() {
1261			if opts.SkipCleanup {
1262				return
1263			}
1264
1265			st.removeFiles(files)
1266		}()
1267
1268		if err != nil {
1269			errs = append(errs, err)
1270		}
1271
1272		for _, value := range additionalValues {
1273			valfile, err := filepath.Abs(value)
1274			if err != nil {
1275				errs = append(errs, err)
1276			}
1277
1278			if _, err := os.Stat(valfile); os.IsNotExist(err) {
1279				errs = append(errs, err)
1280			}
1281			flags = append(flags, "--values", valfile)
1282		}
1283
1284		if opts.Set != nil {
1285			for _, s := range opts.Set {
1286				flags = append(flags, "--set", s)
1287			}
1288		}
1289
1290		if len(outputDir) > 0 || len(opts.OutputDirTemplate) > 0 {
1291			releaseOutputDir, err := st.GenerateOutputDir(outputDir, release, opts.OutputDirTemplate)
1292			if err != nil {
1293				errs = append(errs, err)
1294			}
1295
1296			flags = append(flags, "--output-dir", releaseOutputDir)
1297			st.logger.Debugf("Generating templates to : %s\n", releaseOutputDir)
1298			err = os.MkdirAll(releaseOutputDir, 0755)
1299			if err != nil {
1300				errs = append(errs, err)
1301			}
1302		}
1303
1304		if validate {
1305			flags = append(flags, "--validate")
1306		}
1307
1308		if opts.IncludeCRDs {
1309			flags = append(flags, "--include-crds")
1310		}
1311
1312		if len(errs) == 0 {
1313			if err := helm.TemplateRelease(release.Name, release.Chart, flags...); err != nil {
1314				errs = append(errs, err)
1315			}
1316		}
1317
1318		if _, err := st.TriggerCleanupEvent(release, "template"); err != nil {
1319			st.logger.Warnf("warn: %v\n", err)
1320		}
1321	}
1322
1323	if len(errs) != 0 {
1324		return errs
1325	}
1326
1327	return nil
1328}
1329
1330type WriteValuesOpts struct {
1331	Set                []string
1332	OutputFileTemplate string
1333}
1334
1335type WriteValuesOpt interface{ Apply(*WriteValuesOpts) }
1336
1337func (o *WriteValuesOpts) Apply(opts *WriteValuesOpts) {
1338	*opts = *o
1339}
1340
1341// WriteReleasesValues writes values files for releases
1342func (st *HelmState) WriteReleasesValues(helm helmexec.Interface, additionalValues []string, opt ...WriteValuesOpt) []error {
1343	opts := &WriteValuesOpts{}
1344	for _, o := range opt {
1345		o.Apply(opts)
1346	}
1347
1348	for i := range st.Releases {
1349		release := &st.Releases[i]
1350
1351		if !release.Desired() {
1352			continue
1353		}
1354
1355		st.ApplyOverrides(release)
1356
1357		generatedFiles, err := st.generateValuesFiles(helm, release, i)
1358		if err != nil {
1359			return []error{err}
1360		}
1361
1362		defer func() {
1363			st.removeFiles(generatedFiles)
1364		}()
1365
1366		for _, value := range additionalValues {
1367			valfile, err := filepath.Abs(value)
1368			if err != nil {
1369				return []error{err}
1370			}
1371
1372			if _, err := os.Stat(valfile); os.IsNotExist(err) {
1373				return []error{err}
1374			}
1375			generatedFiles = append(generatedFiles, valfile)
1376		}
1377
1378		outputValuesFile, err := st.GenerateOutputFilePath(release, opts.OutputFileTemplate)
1379		if err != nil {
1380			return []error{err}
1381		}
1382
1383		if err := os.MkdirAll(filepath.Dir(outputValuesFile), 0755); err != nil {
1384			return []error{err}
1385		}
1386
1387		st.logger.Infof("Writing values file %s", outputValuesFile)
1388
1389		merged := map[string]interface{}{}
1390
1391		for _, f := range generatedFiles {
1392			src := map[string]interface{}{}
1393
1394			srcBytes, err := st.readFile(f)
1395			if err != nil {
1396				return []error{fmt.Errorf("reading %s: %w", f, err)}
1397			}
1398
1399			if err := yaml.Unmarshal(srcBytes, &src); err != nil {
1400				return []error{fmt.Errorf("unmarshalling yaml %s: %w", f, err)}
1401			}
1402
1403			if err := mergo.Merge(&merged, &src, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil {
1404				return []error{fmt.Errorf("merging %s: %w", f, err)}
1405			}
1406		}
1407
1408		var buf bytes.Buffer
1409
1410		y := yaml.NewEncoder(&buf)
1411		if err := y.Encode(merged); err != nil {
1412			return []error{err}
1413		}
1414
1415		if err := ioutil.WriteFile(outputValuesFile, buf.Bytes(), 0644); err != nil {
1416			return []error{fmt.Errorf("writing values file %s: %w", outputValuesFile, err)}
1417		}
1418
1419		if _, err := st.TriggerCleanupEvent(release, "write-values"); err != nil {
1420			st.logger.Warnf("warn: %v\n", err)
1421		}
1422	}
1423
1424	return nil
1425}
1426
1427type LintOpts struct {
1428	Set []string
1429}
1430
1431type LintOpt interface{ Apply(*LintOpts) }
1432
1433func (o *LintOpts) Apply(opts *LintOpts) {
1434	*opts = *o
1435}
1436
1437// LintReleases wrapper for executing helm lint on the releases
1438func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int, opt ...LintOpt) []error {
1439	opts := &LintOpts{}
1440	for _, o := range opt {
1441		o.Apply(opts)
1442	}
1443
1444	// Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint`
1445	helm.SetExtraArgs()
1446
1447	errs := []error{}
1448
1449	if len(args) > 0 {
1450		helm.SetExtraArgs(args...)
1451	}
1452
1453	for i := range st.Releases {
1454		release := st.Releases[i]
1455
1456		if !release.Desired() {
1457			continue
1458		}
1459
1460		flags, files, err := st.flagsForLint(helm, &release, 0)
1461
1462		defer st.removeFiles(files)
1463
1464		if err != nil {
1465			errs = append(errs, err)
1466		}
1467		for _, value := range additionalValues {
1468			valfile, err := filepath.Abs(value)
1469			if err != nil {
1470				errs = append(errs, err)
1471			}
1472
1473			if _, err := os.Stat(valfile); os.IsNotExist(err) {
1474				errs = append(errs, err)
1475			}
1476			flags = append(flags, "--values", valfile)
1477		}
1478
1479		if opts.Set != nil {
1480			for _, s := range opts.Set {
1481				flags = append(flags, "--set", s)
1482			}
1483		}
1484
1485		if len(errs) == 0 {
1486			if err := helm.Lint(release.Name, release.Chart, flags...); err != nil {
1487				errs = append(errs, err)
1488			}
1489		}
1490
1491		if _, err := st.TriggerCleanupEvent(&release, "lint"); err != nil {
1492			st.logger.Warnf("warn: %v\n", err)
1493		}
1494	}
1495
1496	if len(errs) != 0 {
1497		return errs
1498	}
1499
1500	return nil
1501}
1502
1503type diffResult struct {
1504	release *ReleaseSpec
1505	err     *ReleaseError
1506	buf     *bytes.Buffer
1507}
1508
1509type diffPrepareResult struct {
1510	release                 *ReleaseSpec
1511	flags                   []string
1512	errors                  []*ReleaseError
1513	files                   []string
1514	upgradeDueToSkippedDiff bool
1515}
1516
1517func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, includeTests, suppressSecrets bool, opt ...DiffOpt) ([]diffPrepareResult, []error) {
1518	opts := &DiffOpts{}
1519	for _, o := range opt {
1520		o.Apply(opts)
1521	}
1522
1523	mu := &sync.Mutex{}
1524	installedReleases := map[string]bool{}
1525
1526	isInstalled := func(r *ReleaseSpec) bool {
1527		mu.Lock()
1528		defer mu.Unlock()
1529
1530		id := ReleaseToID(r)
1531
1532		if v, ok := installedReleases[id]; ok {
1533			return v
1534		}
1535
1536		v, err := st.isReleaseInstalled(st.createHelmContext(r, 0), helm, *r)
1537		if err != nil {
1538			st.logger.Warnf("confirming if the release is already installed or not: %v", err)
1539		} else {
1540			installedReleases[id] = v
1541		}
1542
1543		return v
1544	}
1545
1546	releases := []*ReleaseSpec{}
1547	for i, _ := range st.Releases {
1548		if !st.Releases[i].Desired() {
1549			continue
1550		}
1551		if st.Releases[i].Installed != nil && !*(st.Releases[i].Installed) {
1552			continue
1553		}
1554		releases = append(releases, &st.Releases[i])
1555	}
1556
1557	numReleases := len(releases)
1558	jobs := make(chan *ReleaseSpec, numReleases)
1559	results := make(chan diffPrepareResult, numReleases)
1560	resultsMap := map[string]diffPrepareResult{}
1561
1562	rs := []diffPrepareResult{}
1563	errs := []error{}
1564
1565	mut := sync.Mutex{}
1566
1567	st.scatterGather(
1568		concurrency,
1569		numReleases,
1570		func() {
1571			for i := 0; i < numReleases; i++ {
1572				jobs <- releases[i]
1573			}
1574			close(jobs)
1575		},
1576		func(workerIndex int) {
1577			for release := range jobs {
1578				errs := []error{}
1579
1580				st.ApplyOverrides(release)
1581
1582				if opts.SkipDiffOnInstall && !isInstalled(release) {
1583					results <- diffPrepareResult{release: release, upgradeDueToSkippedDiff: true}
1584					continue
1585				}
1586
1587				disableValidation := release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall && !isInstalled(release)
1588
1589				// TODO We need a long-term fix for this :)
1590				// See https://github.com/roboll/helmfile/issues/737
1591				mut.Lock()
1592				flags, files, err := st.flagsForDiff(helm, release, disableValidation, workerIndex)
1593				mut.Unlock()
1594				if err != nil {
1595					errs = append(errs, err)
1596				}
1597
1598				for _, value := range additionalValues {
1599					valfile, err := filepath.Abs(value)
1600					if err != nil {
1601						errs = append(errs, err)
1602					}
1603
1604					if _, err := os.Stat(valfile); os.IsNotExist(err) {
1605						errs = append(errs, err)
1606					}
1607					flags = append(flags, "--values", valfile)
1608				}
1609
1610				if detailedExitCode {
1611					flags = append(flags, "--detailed-exitcode")
1612				}
1613
1614				if includeTests {
1615					flags = append(flags, "--include-tests")
1616				}
1617
1618				if suppressSecrets {
1619					flags = append(flags, "--suppress-secrets")
1620				}
1621
1622				if opts.NoColor {
1623					flags = append(flags, "--no-color")
1624				}
1625
1626				if opts.Context > 0 {
1627					flags = append(flags, "--context", fmt.Sprintf("%d", opts.Context))
1628				}
1629
1630				if opts.Set != nil {
1631					for _, s := range opts.Set {
1632						flags = append(flags, "--set", s)
1633					}
1634				}
1635
1636				if len(errs) > 0 {
1637					rsErrs := make([]*ReleaseError, len(errs))
1638					for i, e := range errs {
1639						rsErrs[i] = newReleaseFailedError(release, e)
1640					}
1641					results <- diffPrepareResult{errors: rsErrs, files: files}
1642				} else {
1643					results <- diffPrepareResult{release: release, flags: flags, errors: []*ReleaseError{}, files: files}
1644				}
1645			}
1646		},
1647		func() {
1648			for i := 0; i < numReleases; i++ {
1649				res := <-results
1650				if res.errors != nil && len(res.errors) > 0 {
1651					for _, e := range res.errors {
1652						errs = append(errs, e)
1653					}
1654				} else if res.release != nil {
1655					resultsMap[ReleaseToID(res.release)] = res
1656				}
1657			}
1658		},
1659	)
1660
1661	for _, r := range releases {
1662		if p, ok := resultsMap[ReleaseToID(r)]; ok {
1663			rs = append(rs, p)
1664		}
1665	}
1666
1667	return rs, errs
1668}
1669
1670func (st *HelmState) createHelmContext(spec *ReleaseSpec, workerIndex int) helmexec.HelmContext {
1671	namespace := st.HelmDefaults.TillerNamespace
1672	if spec.TillerNamespace != "" {
1673		namespace = spec.TillerNamespace
1674	}
1675	tillerless := st.HelmDefaults.Tillerless
1676	if spec.Tillerless != nil {
1677		tillerless = *spec.Tillerless
1678	}
1679	historyMax := 10
1680	if st.HelmDefaults.HistoryMax != nil {
1681		historyMax = *st.HelmDefaults.HistoryMax
1682	}
1683	if spec.HistoryMax != nil {
1684		historyMax = *spec.HistoryMax
1685	}
1686
1687	return helmexec.HelmContext{
1688		Tillerless:      tillerless,
1689		TillerNamespace: namespace,
1690		WorkerIndex:     workerIndex,
1691		HistoryMax:      historyMax,
1692	}
1693}
1694
1695func (st *HelmState) createHelmContextWithWriter(spec *ReleaseSpec, w io.Writer) helmexec.HelmContext {
1696	ctx := st.createHelmContext(spec, 0)
1697
1698	ctx.Writer = w
1699
1700	return ctx
1701}
1702
1703type DiffOpts struct {
1704	Context int
1705	NoColor bool
1706	Set     []string
1707
1708	SkipCleanup bool
1709
1710	SkipDiffOnInstall bool
1711}
1712
1713func (o *DiffOpts) Apply(opts *DiffOpts) {
1714	*opts = *o
1715}
1716
1717type DiffOpt interface{ Apply(*DiffOpts) }
1718
1719// DiffReleases wrapper for executing helm diff on the releases
1720// It returns releases that had any changes, and errors if any.
1721//
1722// This function has responsibility to stabilize the order of writes to stdout from multiple concurrent helm-diff runs.
1723// It's required to use the stdout from helmfile-diff to detect if there was another change(s) between 2 points in time.
1724// For example, terraform-provider-helmfile runs a helmfile-diff on `terraform plan` and another on `terraform apply`.
1725// `terraform`, by design, fails when helmfile-diff outputs were not equivalent.
1726// Stabilized helmfile-diff output rescues that.
1727func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, includeTests, suppressSecrets, suppressDiff, triggerCleanupEvents bool, opt ...DiffOpt) ([]ReleaseSpec, []error) {
1728	opts := &DiffOpts{}
1729	for _, o := range opt {
1730		o.Apply(opts)
1731	}
1732
1733	preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, includeTests, suppressSecrets, opts)
1734
1735	defer func() {
1736		if opts.SkipCleanup {
1737			return
1738		}
1739
1740		for _, p := range preps {
1741			st.removeFiles(p.files)
1742		}
1743	}()
1744
1745	if len(prepErrs) > 0 {
1746		return []ReleaseSpec{}, prepErrs
1747	}
1748
1749	jobQueue := make(chan *diffPrepareResult, len(preps))
1750	results := make(chan diffResult, len(preps))
1751
1752	rs := []ReleaseSpec{}
1753	outputs := map[string]*bytes.Buffer{}
1754	errs := []error{}
1755
1756	// The exit code returned by helm-diff when it detected any changes
1757	HelmDiffExitCodeChanged := 2
1758
1759	st.scatterGather(
1760		workerLimit,
1761		len(preps),
1762		func() {
1763			for i := 0; i < len(preps); i++ {
1764				jobQueue <- &preps[i]
1765			}
1766			close(jobQueue)
1767		},
1768		func(workerIndex int) {
1769			for prep := range jobQueue {
1770				flags := prep.flags
1771				release := prep.release
1772				buf := &bytes.Buffer{}
1773				if prep.upgradeDueToSkippedDiff {
1774					results <- diffResult{release, &ReleaseError{ReleaseSpec: release, err: nil, Code: HelmDiffExitCodeChanged}, buf}
1775				} else if err := helm.DiffRelease(st.createHelmContextWithWriter(release, buf), release.Name, normalizeChart(st.basePath, release.Chart), suppressDiff, flags...); err != nil {
1776					switch e := err.(type) {
1777					case helmexec.ExitError:
1778						// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
1779						results <- diffResult{release, &ReleaseError{release, err, e.ExitStatus()}, buf}
1780					default:
1781						results <- diffResult{release, &ReleaseError{release, err, 0}, buf}
1782					}
1783				} else {
1784					// diff succeeded, found no changes
1785					results <- diffResult{release, nil, buf}
1786				}
1787
1788				if triggerCleanupEvents {
1789					if _, err := st.TriggerCleanupEvent(prep.release, "diff"); err != nil {
1790						st.logger.Warnf("warn: %v\n", err)
1791					}
1792				}
1793			}
1794		},
1795		func() {
1796			for i := 0; i < len(preps); i++ {
1797				res := <-results
1798				if res.err != nil {
1799					errs = append(errs, res.err)
1800					if res.err.Code == HelmDiffExitCodeChanged {
1801						rs = append(rs, *res.err.ReleaseSpec)
1802					}
1803				}
1804
1805				outputs[ReleaseToID(res.release)] = res.buf
1806			}
1807		},
1808	)
1809
1810	for _, p := range preps {
1811		if stdout, ok := outputs[ReleaseToID(p.release)]; ok {
1812			fmt.Print(stdout.String())
1813		}
1814	}
1815
1816	return rs, errs
1817}
1818
1819func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error {
1820	return st.scatterGatherReleases(helm, workerLimit, func(release ReleaseSpec, workerIndex int) error {
1821		if !release.Desired() {
1822			return nil
1823		}
1824
1825		st.ApplyOverrides(&release)
1826
1827		flags := []string{}
1828		if helm.IsHelm3() && release.Namespace != "" {
1829			flags = append(flags, "--namespace", release.Namespace)
1830		}
1831		flags = st.appendConnectionFlags(flags, helm, &release)
1832
1833		return helm.ReleaseStatus(st.createHelmContext(&release, workerIndex), release.Name, flags...)
1834	})
1835}
1836
1837// DeleteReleases wrapper for executing helm delete on the releases
1838func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool) []error {
1839	return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
1840		st.ApplyOverrides(&release)
1841
1842		flags := []string{}
1843		if purge && !helm.IsHelm3() {
1844			flags = append(flags, "--purge")
1845		}
1846		flags = st.appendConnectionFlags(flags, helm, &release)
1847		if helm.IsHelm3() && release.Namespace != "" {
1848			flags = append(flags, "--namespace", release.Namespace)
1849		}
1850		context := st.createHelmContext(&release, workerIndex)
1851
1852		if _, err := st.triggerReleaseEvent("preuninstall", nil, &release, "delete"); err != nil {
1853			affectedReleases.Failed = append(affectedReleases.Failed, &release)
1854
1855			return err
1856		}
1857
1858		if err := helm.DeleteRelease(context, release.Name, flags...); err != nil {
1859			affectedReleases.Failed = append(affectedReleases.Failed, &release)
1860			return err
1861		}
1862
1863		if _, err := st.triggerReleaseEvent("postuninstall", nil, &release, "delete"); err != nil {
1864			affectedReleases.Failed = append(affectedReleases.Failed, &release)
1865			return err
1866		}
1867
1868		affectedReleases.Deleted = append(affectedReleases.Deleted, &release)
1869		return nil
1870	})
1871}
1872
1873type TestOpts struct {
1874	Logs bool
1875}
1876
1877type TestOption func(*TestOpts)
1878
1879func Logs(v bool) func(*TestOpts) {
1880	return func(o *TestOpts) {
1881		o.Logs = v
1882	}
1883}
1884
1885// TestReleases wrapper for executing helm test on the releases
1886func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int, concurrency int, options ...TestOption) []error {
1887	var opts TestOpts
1888
1889	for _, o := range options {
1890		o(&opts)
1891	}
1892
1893	return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error {
1894		if !release.Desired() {
1895			return nil
1896		}
1897
1898		flags := []string{}
1899		if helm.IsHelm3() && release.Namespace != "" {
1900			flags = append(flags, "--namespace", release.Namespace)
1901		}
1902		if cleanup && !helm.IsHelm3() {
1903			flags = append(flags, "--cleanup")
1904		}
1905		if opts.Logs {
1906			flags = append(flags, "--logs")
1907		}
1908
1909		if timeout == EmptyTimeout {
1910			flags = append(flags, st.timeoutFlags(helm, &release)...)
1911		} else {
1912			duration := strconv.Itoa(timeout)
1913			if helm.IsHelm3() {
1914				duration += "s"
1915			}
1916			flags = append(flags, "--timeout", duration)
1917		}
1918
1919		flags = st.appendConnectionFlags(flags, helm, &release)
1920
1921		return helm.TestRelease(st.createHelmContext(&release, workerIndex), release.Name, flags...)
1922	})
1923}
1924
1925// Clean will remove any generated secrets
1926func (st *HelmState) Clean() []error {
1927	return nil
1928}
1929
1930func (st *HelmState) GetReleasesWithOverrides() []ReleaseSpec {
1931	var rs []ReleaseSpec
1932	for _, r := range st.Releases {
1933		var spec ReleaseSpec
1934		spec = r
1935		st.ApplyOverrides(&spec)
1936		rs = append(rs, spec)
1937	}
1938	return rs
1939}
1940
1941func (st *HelmState) SelectReleasesWithOverrides() ([]Release, error) {
1942	values := st.Values()
1943	rs, err := markExcludedReleases(st.GetReleasesWithOverrides(), st.Selectors, st.CommonLabels, values)
1944	if err != nil {
1945		return nil, err
1946	}
1947	return rs, nil
1948}
1949
1950func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabels map[string]string, values map[string]interface{}) ([]Release, error) {
1951	var filteredReleases []Release
1952	filters := []ReleaseFilter{}
1953	for _, label := range selectors {
1954		f, err := ParseLabels(label)
1955		if err != nil {
1956			return nil, err
1957		}
1958		filters = append(filters, f)
1959	}
1960	for _, r := range releases {
1961		if r.Labels == nil {
1962			r.Labels = map[string]string{}
1963		}
1964		// Let the release name, namespace, and chart be used as a tag
1965		r.Labels["name"] = r.Name
1966		r.Labels["namespace"] = r.Namespace
1967		// Strip off just the last portion for the name stable/newrelic would give newrelic
1968		chartSplit := strings.Split(r.Chart, "/")
1969		r.Labels["chart"] = chartSplit[len(chartSplit)-1]
1970		//Merge CommonLabels into release labels
1971		for k, v := range commonLabels {
1972			r.Labels[k] = v
1973		}
1974		var filterMatch bool
1975		for _, f := range filters {
1976			if r.Labels == nil {
1977				r.Labels = map[string]string{}
1978			}
1979			if f.Match(r) {
1980				filterMatch = true
1981				break
1982			}
1983		}
1984		var conditionMatch bool
1985		if len(r.Condition) > 0 {
1986			conditionSplit := strings.Split(r.Condition, ".")
1987			if len(conditionSplit) != 2 {
1988				return nil, fmt.Errorf("Condition value must be in the form 'foo.enabled' where 'foo' can be modified as necessary")
1989			}
1990			if v, ok := values[conditionSplit[0]]; ok {
1991				if v == nil {
1992					panic(fmt.Sprintf("environment values field '%s' is nil", conditionSplit[0]))
1993				}
1994				if v.(map[string]interface{})["enabled"] == true {
1995					conditionMatch = true
1996				}
1997			} else {
1998				panic(fmt.Sprintf("environment values does not contain field '%s'", conditionSplit[0]))
1999			}
2000		}
2001		res := Release{
2002			ReleaseSpec: r,
2003			Filtered:    (len(filters) > 0 && !filterMatch) || (len(r.Condition) > 0 && !conditionMatch),
2004		}
2005		filteredReleases = append(filteredReleases, res)
2006	}
2007
2008	return filteredReleases, nil
2009}
2010
2011func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) {
2012	filteredReleases, err := st.SelectReleasesWithOverrides()
2013	if err != nil {
2014		return nil, err
2015	}
2016	var releases []ReleaseSpec
2017	for _, r := range filteredReleases {
2018		if !r.Filtered {
2019			releases = append(releases, r.ReleaseSpec)
2020		}
2021	}
2022
2023	return releases, nil
2024}
2025
2026// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
2027func (st *HelmState) FilterReleases() error {
2028	releases, err := st.GetSelectedReleasesWithOverrides()
2029	if err != nil {
2030		return err
2031	}
2032	st.Releases = releases
2033	return nil
2034}
2035
2036func (st *HelmState) TriggerGlobalPrepareEvent(helmfileCommand string) (bool, error) {
2037	return st.triggerGlobalReleaseEvent("prepare", nil, helmfileCommand)
2038}
2039
2040func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string) (bool, error) {
2041	return st.triggerGlobalReleaseEvent("cleanup", nil, helmfileCommand)
2042}
2043
2044func (st *HelmState) triggerGlobalReleaseEvent(evt string, evtErr error, helmfileCmd string) (bool, error) {
2045	bus := &event.Bus{
2046		Hooks:         st.Hooks,
2047		StateFilePath: st.FilePath,
2048		BasePath:      st.basePath,
2049		Namespace:     st.OverrideNamespace,
2050		Env:           st.Env,
2051		Logger:        st.logger,
2052		ReadFile:      st.readFile,
2053	}
2054	data := map[string]interface{}{
2055		"HelmfileCommand": helmfileCmd,
2056	}
2057	return bus.Trigger(evt, evtErr, data)
2058}
2059
2060func (st *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
2061	return st.triggerReleaseEvent("prepare", nil, r, helmfileCommand)
2062}
2063
2064func (st *HelmState) TriggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
2065	return st.triggerReleaseEvent("cleanup", nil, r, helmfileCommand)
2066}
2067
2068func (st *HelmState) triggerPresyncEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) {
2069	return st.triggerReleaseEvent("presync", nil, r, helmfileCommand)
2070}
2071
2072func (st *HelmState) triggerPostsyncEvent(r *ReleaseSpec, evtErr error, helmfileCommand string) (bool, error) {
2073	return st.triggerReleaseEvent("postsync", evtErr, r, helmfileCommand)
2074}
2075
2076func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpec, helmfileCmd string) (bool, error) {
2077	bus := &event.Bus{
2078		Hooks:         r.Hooks,
2079		StateFilePath: st.FilePath,
2080		BasePath:      st.basePath,
2081		Namespace:     st.OverrideNamespace,
2082		Env:           st.Env,
2083		Logger:        st.logger,
2084		ReadFile:      st.readFile,
2085	}
2086	vals := st.Values()
2087	data := map[string]interface{}{
2088		"Values":          vals,
2089		"Release":         r,
2090		"HelmfileCommand": helmfileCmd,
2091	}
2092	return bus.Trigger(evt, evtErr, data)
2093}
2094
2095// ResolveDeps returns a copy of this helmfile state with the concrete chart version numbers filled in for remote chart dependencies
2096func (st *HelmState) ResolveDeps() (*HelmState, error) {
2097	return st.mergeLockedDependencies()
2098}
2099
2100// UpdateDeps wrapper for updating dependencies on the releases
2101func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error {
2102	var errs []error
2103
2104	for _, release := range st.Releases {
2105		if st.directoryExistsAt(release.Chart) {
2106			if err := helm.UpdateDeps(release.Chart); err != nil {
2107				errs = append(errs, err)
2108			}
2109		} else {
2110			st.logger.Debugf("skipped updating dependencies for remote chart %s", release.Chart)
2111		}
2112	}
2113
2114	if len(errs) == 0 {
2115		tempDir := st.tempDir
2116		if tempDir == nil {
2117			tempDir = ioutil.TempDir
2118		}
2119		_, err := st.updateDependenciesInTempDir(helm, tempDir)
2120		if err != nil {
2121			errs = append(errs, fmt.Errorf("unable to update deps: %v", err))
2122		}
2123	}
2124
2125	if len(errs) != 0 {
2126		return errs
2127	}
2128	return nil
2129}
2130
2131func chartNameWithoutRepository(chart string) string {
2132	chartSplit := strings.Split(chart, "/")
2133	return chartSplit[len(chartSplit)-1]
2134}
2135
2136// find "Chart.yaml"
2137func findChartDirectory(topLevelDir string) (string, error) {
2138	var files []string
2139	filepath.Walk(topLevelDir, func(path string, f os.FileInfo, err error) error {
2140		if err != nil {
2141			return fmt.Errorf("error walking through %s: %v", path, err)
2142		}
2143		if !f.IsDir() {
2144			r, err := regexp.MatchString("Chart.yaml", f.Name())
2145			if err == nil && r {
2146				files = append(files, path)
2147			}
2148		}
2149		return nil
2150	})
2151	// Sort to get the shortest path
2152	sort.Strings(files)
2153	if len(files) > 0 {
2154		first := files[0]
2155		return first, nil
2156	}
2157
2158	return topLevelDir, errors.New("No Chart.yaml found")
2159}
2160
2161// appendConnectionFlags append all the helm command-line flags related to K8s API and Tiller connection including the kubecontext
2162func (st *HelmState) appendConnectionFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec) []string {
2163	adds := st.connectionFlags(helm, release)
2164	for _, a := range adds {
2165		flags = append(flags, a)
2166	}
2167	return flags
2168}
2169
2170func (st *HelmState) connectionFlags(helm helmexec.Interface, release *ReleaseSpec) []string {
2171	flags := []string{}
2172	tillerless := st.HelmDefaults.Tillerless
2173	if release.Tillerless != nil {
2174		tillerless = *release.Tillerless
2175	}
2176	if !tillerless {
2177		if !helm.IsHelm3() {
2178			if release.TillerNamespace != "" {
2179				flags = append(flags, "--tiller-namespace", release.TillerNamespace)
2180			} else if st.HelmDefaults.TillerNamespace != "" {
2181				flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
2182			}
2183		}
2184
2185		if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS {
2186			flags = append(flags, "--tls")
2187		}
2188
2189		if release.TLSKey != "" {
2190			flags = append(flags, "--tls-key", release.TLSKey)
2191		} else if st.HelmDefaults.TLSKey != "" {
2192			flags = append(flags, "--tls-key", st.HelmDefaults.TLSKey)
2193		}
2194
2195		if release.TLSCert != "" {
2196			flags = append(flags, "--tls-cert", release.TLSCert)
2197		} else if st.HelmDefaults.TLSCert != "" {
2198			flags = append(flags, "--tls-cert", st.HelmDefaults.TLSCert)
2199		}
2200
2201		if release.TLSCACert != "" {
2202			flags = append(flags, "--tls-ca-cert", release.TLSCACert)
2203		} else if st.HelmDefaults.TLSCACert != "" {
2204			flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert)
2205		}
2206
2207		if release.KubeContext != "" {
2208			flags = append(flags, "--kube-context", release.KubeContext)
2209		} else if st.HelmDefaults.KubeContext != "" {
2210			flags = append(flags, "--kube-context", st.HelmDefaults.KubeContext)
2211		}
2212	}
2213
2214	return flags
2215}
2216
2217func (st *HelmState) timeoutFlags(helm helmexec.Interface, release *ReleaseSpec) []string {
2218	var flags []string
2219
2220	timeout := st.HelmDefaults.Timeout
2221	if release.Timeout != nil {
2222		timeout = *release.Timeout
2223	}
2224	if timeout != 0 {
2225		duration := strconv.Itoa(timeout)
2226		if helm.IsHelm3() {
2227			duration += "s"
2228		}
2229		flags = append(flags, "--timeout", duration)
2230	}
2231
2232	return flags
2233}
2234
2235func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
2236	flags := st.chartVersionFlags(release)
2237
2238	if release.Verify != nil && *release.Verify || release.Verify == nil && st.HelmDefaults.Verify {
2239		flags = append(flags, "--verify")
2240	}
2241
2242	if release.Wait != nil && *release.Wait || release.Wait == nil && st.HelmDefaults.Wait {
2243		flags = append(flags, "--wait")
2244	}
2245
2246	flags = append(flags, st.timeoutFlags(helm, release)...)
2247
2248	if release.Force != nil && *release.Force || release.Force == nil && st.HelmDefaults.Force {
2249		flags = append(flags, "--force")
2250	}
2251
2252	if release.RecreatePods != nil && *release.RecreatePods || release.RecreatePods == nil && st.HelmDefaults.RecreatePods {
2253		flags = append(flags, "--recreate-pods")
2254	}
2255
2256	if release.Atomic != nil && *release.Atomic || release.Atomic == nil && st.HelmDefaults.Atomic {
2257		flags = append(flags, "--atomic")
2258	}
2259
2260	if release.CleanupOnFail != nil && *release.CleanupOnFail || release.CleanupOnFail == nil && st.HelmDefaults.CleanupOnFail {
2261		flags = append(flags, "--cleanup-on-fail")
2262	}
2263
2264	if release.CreateNamespace != nil && *release.CreateNamespace ||
2265		release.CreateNamespace == nil && (st.HelmDefaults.CreateNamespace == nil || *st.HelmDefaults.CreateNamespace) {
2266		if helm.IsVersionAtLeast("3.2.0") {
2267			flags = append(flags, "--create-namespace")
2268		} else if release.CreateNamespace != nil || st.HelmDefaults.CreateNamespace != nil {
2269			// createNamespace was set explicitly, but not running supported version of helm - error
2270			return nil, nil, fmt.Errorf("releases[].createNamespace requires Helm 3.2.0 or greater")
2271		}
2272	}
2273
2274	if release.DisableOpenAPIValidation != nil && *release.DisableOpenAPIValidation ||
2275		release.DisableOpenAPIValidation == nil && st.HelmDefaults.DisableOpenAPIValidation != nil && *st.HelmDefaults.DisableOpenAPIValidation {
2276		flags = append(flags, "--disable-openapi-validation")
2277	}
2278
2279	flags = st.appendConnectionFlags(flags, helm, release)
2280
2281	var err error
2282	flags, err = st.appendHelmXFlags(flags, release)
2283	if err != nil {
2284		return nil, nil, err
2285	}
2286
2287	common, clean, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
2288	if err != nil {
2289		return nil, clean, err
2290	}
2291	return append(flags, common...), clean, nil
2292}
2293
2294func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
2295	var flags []string
2296
2297	// `helm template` in helm v2 does not support `--version` flag. So we fetch with the version flag and then template
2298	// without the flag. See PrepareCharts function to see the Helmfile implementation of chart fetching.
2299	//
2300	// `helm template` in helm v3 supports `--version` and it automatically fetches the remote chart to template,
2301	// so we skip fetching on helmfile-side and let helm fetch it.
2302	if helm.IsHelm3() {
2303		flags = st.chartVersionFlags(release)
2304	}
2305
2306	var err error
2307	flags, err = st.appendHelmXFlags(flags, release)
2308	if err != nil {
2309		return nil, nil, err
2310	}
2311
2312	flags = st.appendApiVersionsFlags(flags)
2313
2314	common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
2315	if err != nil {
2316		return nil, files, err
2317	}
2318	return append(flags, common...), files, nil
2319}
2320
2321func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, disableValidation bool, workerIndex int) ([]string, []string, error) {
2322	flags := st.chartVersionFlags(release)
2323
2324	disableOpenAPIValidation := false
2325	if release.DisableOpenAPIValidation != nil {
2326		disableOpenAPIValidation = *release.DisableOpenAPIValidation
2327	} else if st.HelmDefaults.DisableOpenAPIValidation != nil {
2328		disableOpenAPIValidation = *st.HelmDefaults.DisableOpenAPIValidation
2329	}
2330
2331	if disableOpenAPIValidation {
2332		flags = append(flags, "--disable-openapi-validation")
2333	}
2334
2335	if release.DisableValidation != nil {
2336		disableValidation = *release.DisableValidation
2337	} else if st.HelmDefaults.DisableValidation != nil {
2338		disableValidation = *st.HelmDefaults.DisableValidation
2339	}
2340
2341	if disableValidation {
2342		flags = append(flags, "--disable-validation")
2343	}
2344
2345	flags = st.appendConnectionFlags(flags, helm, release)
2346
2347	var err error
2348	flags, err = st.appendHelmXFlags(flags, release)
2349	if err != nil {
2350		return nil, nil, err
2351	}
2352
2353	common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
2354	if err != nil {
2355		return nil, files, err
2356	}
2357	return append(flags, common...), files, nil
2358}
2359
2360func (st *HelmState) chartVersionFlags(release *ReleaseSpec) []string {
2361	flags := []string{}
2362
2363	if release.Version != "" {
2364		flags = append(flags, "--version", release.Version)
2365	}
2366
2367	if st.isDevelopment(release) {
2368		flags = append(flags, "--devel")
2369	}
2370
2371	return flags
2372}
2373
2374func (st *HelmState) appendApiVersionsFlags(flags []string) []string {
2375	for _, a := range st.ApiVersions {
2376		flags = append(flags, "--api-versions", a)
2377	}
2378	return flags
2379}
2380
2381func (st *HelmState) isDevelopment(release *ReleaseSpec) bool {
2382	result := st.HelmDefaults.Devel
2383	if release.Devel != nil {
2384		result = *release.Devel
2385	}
2386
2387	return result
2388}
2389
2390func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
2391	flags, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex)
2392	if err != nil {
2393		return nil, files, err
2394	}
2395
2396	flags, err = st.appendHelmXFlags(flags, release)
2397	if err != nil {
2398		return nil, files, err
2399	}
2400
2401	return flags, files, nil
2402}
2403
2404func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) {
2405	vals := st.Values()
2406	templateData := st.createReleaseTemplateData(release, vals)
2407
2408	r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), templateData)
2409	rawBytes, err := r.RenderToBytes(path)
2410	if err != nil {
2411		return nil, err
2412	}
2413
2414	// If 'ref+.*' exists in file, run vals against the file
2415	match, err := regexp.Match("ref\\+.*", rawBytes)
2416	if err != nil {
2417		return nil, err
2418	}
2419
2420	if match {
2421		var rawYaml map[string]interface{}
2422
2423		if err := yaml.Unmarshal(rawBytes, &rawYaml); err != nil {
2424			return nil, err
2425		}
2426
2427		parsedYaml, err := st.valsRuntime.Eval(rawYaml)
2428		if err != nil {
2429			return nil, err
2430		}
2431
2432		return yaml.Marshal(parsedYaml)
2433	}
2434
2435	return rawBytes, nil
2436}
2437
2438func (st *HelmState) storage() *Storage {
2439	return &Storage{
2440		FilePath: st.FilePath,
2441		basePath: st.basePath,
2442		glob:     st.glob,
2443		logger:   st.logger,
2444	}
2445}
2446
2447func (st *HelmState) ExpandedHelmfiles() ([]SubHelmfileSpec, error) {
2448	helmfiles := []SubHelmfileSpec{}
2449	for _, hf := range st.Helmfiles {
2450		if remote.IsRemote(hf.Path) {
2451			helmfiles = append(helmfiles, hf)
2452			continue
2453		}
2454
2455		matches, err := st.storage().ExpandPaths(hf.Path)
2456		if err != nil {
2457			return nil, err
2458		}
2459		if len(matches) == 0 {
2460			err := fmt.Errorf("no matches for path: %s", hf.Path)
2461			if st.MissingFileHandler == "Error" {
2462				return nil, err
2463			}
2464			st.logger.Warnf("no matches for path: %s", hf.Path)
2465			continue
2466		}
2467		for _, match := range matches {
2468			newHelmfile := hf
2469			newHelmfile.Path = match
2470			helmfiles = append(helmfiles, newHelmfile)
2471		}
2472	}
2473
2474	return helmfiles, nil
2475}
2476
2477func (st *HelmState) removeFiles(files []string) {
2478	for _, f := range files {
2479		if err := st.removeFile(f); err != nil {
2480			st.logger.Warnf("Removing %s: %v", err)
2481		} else {
2482			st.logger.Debugf("Removed %s", f)
2483		}
2484	}
2485}
2486
2487func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []interface{}, missingFileHandler *string) ([]string, error) {
2488	generatedFiles := []string{}
2489
2490	for _, value := range values {
2491		switch typedValue := value.(type) {
2492		case string:
2493			paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", typedValue)
2494			if err != nil {
2495				return generatedFiles, err
2496			}
2497			if skip {
2498				continue
2499			}
2500
2501			if len(paths) > 1 {
2502				return generatedFiles, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary")
2503			}
2504			path := paths[0]
2505
2506			yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, path)
2507			if err != nil {
2508				return generatedFiles, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err)
2509			}
2510
2511			valfile, err := createTempValuesFile(release, yamlBytes)
2512			if err != nil {
2513				return generatedFiles, err
2514			}
2515			defer valfile.Close()
2516
2517			if _, err := valfile.Write(yamlBytes); err != nil {
2518				return generatedFiles, fmt.Errorf("failed to write %s: %v", valfile.Name(), err)
2519			}
2520
2521			st.logger.Debugf("Successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes))
2522
2523			generatedFiles = append(generatedFiles, valfile.Name())
2524		case map[interface{}]interface{}, map[string]interface{}:
2525			valfile, err := createTempValuesFile(release, typedValue)
2526			if err != nil {
2527				return generatedFiles, err
2528			}
2529			defer valfile.Close()
2530
2531			encoder := yaml.NewEncoder(valfile)
2532			defer encoder.Close()
2533
2534			if err := encoder.Encode(typedValue); err != nil {
2535				return generatedFiles, err
2536			}
2537
2538			generatedFiles = append(generatedFiles, valfile.Name())
2539		default:
2540			return generatedFiles, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue)
2541		}
2542	}
2543	return generatedFiles, nil
2544}
2545
2546func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string, error) {
2547	values := []interface{}{}
2548	for _, v := range release.Values {
2549		switch typedValue := v.(type) {
2550		case string:
2551			path := st.storage().normalizePath(release.ValuesPathPrefix + typedValue)
2552			values = append(values, path)
2553		default:
2554			values = append(values, v)
2555		}
2556	}
2557
2558	valuesMapSecretsRendered, err := st.valsRuntime.Eval(map[string]interface{}{"values": values})
2559	if err != nil {
2560		return nil, err
2561	}
2562
2563	valuesSecretsRendered, ok := valuesMapSecretsRendered["values"].([]interface{})
2564	if !ok {
2565		return nil, fmt.Errorf("Failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"])
2566	}
2567
2568	generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered, release.MissingFileHandler)
2569	if err != nil {
2570		return nil, err
2571	}
2572
2573	return generatedFiles, nil
2574}
2575
2576func (st *HelmState) generateSecretValuesFiles(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
2577	var generatedFiles []string
2578
2579	for _, v := range release.Secrets {
2580		var (
2581			paths []string
2582			skip  bool
2583			err   error
2584		)
2585
2586		switch value := v.(type) {
2587		case string:
2588			paths, skip, err = st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value)
2589			if err != nil {
2590				return nil, err
2591			}
2592		default:
2593			bs, err := yaml.Marshal(value)
2594			if err != nil {
2595				return nil, err
2596			}
2597
2598			path, err := ioutil.TempFile(os.TempDir(), "helmfile-embdedded-secrets-*.yaml.enc")
2599			if err != nil {
2600				return nil, err
2601			}
2602			_ = path.Close()
2603			defer func() {
2604				_ = os.Remove(path.Name())
2605			}()
2606
2607			if err := ioutil.WriteFile(path.Name(), bs, 0644); err != nil {
2608				return nil, err
2609			}
2610
2611			paths = []string{path.Name()}
2612		}
2613
2614		if skip {
2615			continue
2616		}
2617
2618		if len(paths) > 1 {
2619			return nil, fmt.Errorf("glob patterns in release secret file is not supported yet. please submit a feature request if necessary")
2620		}
2621		path := paths[0]
2622
2623		decryptFlags := st.appendConnectionFlags([]string{}, helm, release)
2624		valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...)
2625		if err != nil {
2626			return nil, err
2627		}
2628
2629		generatedFiles = append(generatedFiles, valfile)
2630	}
2631
2632	return generatedFiles, nil
2633}
2634
2635func (st *HelmState) generateValuesFiles(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) {
2636	valuesFiles, err := st.generateVanillaValuesFiles(release)
2637	if err != nil {
2638		return nil, err
2639	}
2640
2641	secretValuesFiles, err := st.generateSecretValuesFiles(helm, release, workerIndex)
2642	if err != nil {
2643		return nil, err
2644	}
2645
2646	files := append(valuesFiles, secretValuesFiles...)
2647
2648	return files, nil
2649}
2650
2651func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
2652	flags := []string{}
2653	if release.Namespace != "" {
2654		flags = append(flags, "--namespace", release.Namespace)
2655	}
2656
2657	var files []string
2658
2659	generatedFiles, err := st.generateValuesFiles(helm, release, workerIndex)
2660	if err != nil {
2661		return nil, files, err
2662	}
2663
2664	files = generatedFiles
2665
2666	for _, f := range generatedFiles {
2667		flags = append(flags, "--values", f)
2668	}
2669
2670	if len(release.SetValues) > 0 {
2671		for _, set := range release.SetValues {
2672			if set.Value != "" {
2673				renderedValue, err := renderValsSecrets(st.valsRuntime, set.Value)
2674				if err != nil {
2675					return nil, files, fmt.Errorf("Failed to render set value entry in %s for release %s: %v", st.FilePath, release.Name, err)
2676				}
2677				flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(renderedValue[0])))
2678			} else if set.File != "" {
2679				flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.storage().normalizePath(set.File)))
2680			} else if len(set.Values) > 0 {
2681				renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...)
2682				if err != nil {
2683					return nil, files, fmt.Errorf("Failed to render set values entry in %s for release %s: %v", st.FilePath, release.Name, err)
2684				}
2685				items := make([]string, len(renderedValues))
2686				for i, raw := range renderedValues {
2687					items[i] = escape(raw)
2688				}
2689				v := strings.Join(items, ",")
2690				flags = append(flags, "--set", fmt.Sprintf("%s={%s}", escape(set.Name), v))
2691			}
2692		}
2693	}
2694
2695	/***********
2696	 * START 'env' section for backwards compatibility
2697	 ***********/
2698	// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
2699	if len(release.EnvValues) > 0 {
2700		val := []string{}
2701		envValErrs := []string{}
2702		for _, set := range release.EnvValues {
2703			value, isSet := os.LookupEnv(set.Value)
2704			if isSet {
2705				val = append(val, fmt.Sprintf("%s=%s", escape(set.Name), escape(value)))
2706			} else {
2707				errMsg := fmt.Sprintf("\t%s", set.Value)
2708				envValErrs = append(envValErrs, errMsg)
2709			}
2710		}
2711		if len(envValErrs) != 0 {
2712			joinedEnvVals := strings.Join(envValErrs, "\n")
2713			errMsg := fmt.Sprintf("Environment Variables not found. Please make sure they are set and try again:\n%s", joinedEnvVals)
2714			return nil, files, errors.New(errMsg)
2715		}
2716		flags = append(flags, "--set", strings.Join(val, ","))
2717	}
2718	/**************
2719	 * END 'env' section for backwards compatibility
2720	 **************/
2721
2722	return flags, files, nil
2723}
2724
2725// renderValsSecrets helper function which renders 'ref+.*' secrets
2726func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) {
2727	output := make([]string, len(input))
2728	if len(input) > 0 {
2729		mapRendered, err := e.Eval(map[string]interface{}{"values": input})
2730		if err != nil {
2731			return nil, err
2732		}
2733
2734		rendered, ok := mapRendered["values"].([]interface{})
2735		if !ok {
2736			return nil, fmt.Errorf("type %T isn't supported", mapRendered["values"])
2737		}
2738
2739		for i := 0; i < len(rendered); i++ {
2740			output[i] = fmt.Sprintf("%v", rendered[i])
2741		}
2742	}
2743	return output, nil
2744}
2745
2746// DisplayAffectedReleases logs the upgraded, deleted and in error releases
2747func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
2748	if ar.Upgraded != nil && len(ar.Upgraded) > 0 {
2749		logger.Info("\nUPDATED RELEASES:")
2750		tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
2751			prettytable.Column{Header: "CHART", MinWidth: 6},
2752			prettytable.Column{Header: "VERSION", AlignRight: true},
2753		)
2754		tbl.Separator = "   "
2755		for _, release := range ar.Upgraded {
2756			tbl.AddRow(release.Name, release.Chart, release.installedVersion)
2757		}
2758		logger.Info(tbl.String())
2759	}
2760	if ar.Deleted != nil && len(ar.Deleted) > 0 {
2761		logger.Info("\nDELETED RELEASES:")
2762		logger.Info("NAME")
2763		for _, release := range ar.Deleted {
2764			logger.Info(release.Name)
2765		}
2766	}
2767	if ar.Failed != nil && len(ar.Failed) > 0 {
2768		logger.Info("\nFAILED RELEASES:")
2769		logger.Info("NAME")
2770		for _, release := range ar.Failed {
2771			logger.Info(release.Name)
2772		}
2773	}
2774}
2775
2776func escape(value string) string {
2777	intermediate := strings.Replace(value, "{", "\\{", -1)
2778	intermediate = strings.Replace(intermediate, "}", "\\}", -1)
2779	return strings.Replace(intermediate, ",", "\\,", -1)
2780}
2781
2782//MarshalYAML will ensure we correctly marshal SubHelmfileSpec structure correctly so it can be unmarshalled at some
2783//future time
2784func (p SubHelmfileSpec) MarshalYAML() (interface{}, error) {
2785	type SubHelmfileSpecTmp struct {
2786		Path               string        `yaml:"path,omitempty"`
2787		Selectors          []string      `yaml:"selectors,omitempty"`
2788		SelectorsInherited bool          `yaml:"selectorsInherited,omitempty"`
2789		OverrideValues     []interface{} `yaml:"values,omitempty"`
2790	}
2791	return &SubHelmfileSpecTmp{
2792		Path:               p.Path,
2793		Selectors:          p.Selectors,
2794		SelectorsInherited: p.SelectorsInherited,
2795		OverrideValues:     p.Environment.OverrideValues,
2796	}, nil
2797}
2798
2799//UnmarshalYAML will unmarshal the helmfile yaml section and fill the SubHelmfileSpec structure
2800//this is required to keep allowing string scalar for defining helmfile
2801func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) error {
2802
2803	var tmp interface{}
2804	if err := unmarshal(&tmp); err != nil {
2805		return err
2806	}
2807
2808	switch i := tmp.(type) {
2809	case string: // single path definition without sub items, legacy sub helmfile definition
2810		hf.Path = i
2811	case map[interface{}]interface{}: // helmfile path with sub section
2812		var subHelmfileSpecTmp struct {
2813			Path               string   `yaml:"path"`
2814			Selectors          []string `yaml:"selectors"`
2815			SelectorsInherited bool     `yaml:"selectorsInherited"`
2816
2817			Environment SubhelmfileEnvironmentSpec `yaml:",inline"`
2818		}
2819		if err := unmarshal(&subHelmfileSpecTmp); err != nil {
2820			return err
2821		}
2822		hf.Path = subHelmfileSpecTmp.Path
2823		hf.Selectors = subHelmfileSpecTmp.Selectors
2824		hf.SelectorsInherited = subHelmfileSpecTmp.SelectorsInherited
2825		hf.Environment = subHelmfileSpecTmp.Environment
2826	}
2827	//since we cannot make sur the "console" string can be red after the "path" we must check we don't have
2828	//a SubHelmfileSpec with only selector and no path
2829	if hf.Selectors != nil && hf.Path == "" {
2830		return fmt.Errorf("found 'selectors' definition without path: %v", hf.Selectors)
2831	}
2832	//also exclude SelectorsInherited to true and explicit selectors
2833	if hf.SelectorsInherited && len(hf.Selectors) > 0 {
2834		return fmt.Errorf("You cannot use 'SelectorsInherited: true' along with and explicit selector for path: %v", hf.Path)
2835	}
2836	return nil
2837}
2838
2839func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec, outputDirTemplate string) (string, error) {
2840	// get absolute path of state file to generate a hash
2841	// use this hash to write helm output in a specific directory by state file and release name
2842	// ie. in a directory named stateFileName-stateFileHash-releaseName
2843	stateAbsPath, err := filepath.Abs(st.FilePath)
2844	if err != nil {
2845		return stateAbsPath, err
2846	}
2847
2848	hasher := sha1.New()
2849	io.WriteString(hasher, stateAbsPath)
2850
2851	var stateFileExtension = filepath.Ext(st.FilePath)
2852	var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)]
2853
2854	sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8]
2855
2856	var sb strings.Builder
2857	sb.WriteString(stateFileName)
2858	sb.WriteString("-")
2859	sb.WriteString(sha1sum)
2860	sb.WriteString("-")
2861	sb.WriteString(release.Name)
2862
2863	if outputDirTemplate == "" {
2864		outputDirTemplate = filepath.Join("{{ .OutputDir }}", "{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}")
2865	}
2866
2867	t, err := template.New("output-dir").Parse(outputDirTemplate)
2868	if err != nil {
2869		return "", fmt.Errorf("parsing output-dir templmate")
2870	}
2871
2872	buf := &bytes.Buffer{}
2873
2874	type state struct {
2875		BaseName    string
2876		Path        string
2877		AbsPath     string
2878		AbsPathSHA1 string
2879	}
2880
2881	data := struct {
2882		OutputDir string
2883		State     state
2884		Release   *ReleaseSpec
2885	}{
2886		OutputDir: outputDir,
2887		State: state{
2888			BaseName:    stateFileName,
2889			Path:        st.FilePath,
2890			AbsPath:     stateAbsPath,
2891			AbsPathSHA1: sha1sum,
2892		},
2893		Release: release,
2894	}
2895
2896	if err := t.Execute(buf, data); err != nil {
2897		return "", fmt.Errorf("executing output-dir template: %w", err)
2898	}
2899
2900	return buf.String(), nil
2901}
2902
2903func (st *HelmState) GenerateOutputFilePath(release *ReleaseSpec, outputFileTemplate string) (string, error) {
2904	// get absolute path of state file to generate a hash
2905	// use this hash to write helm output in a specific directory by state file and release name
2906	// ie. in a directory named stateFileName-stateFileHash-releaseName
2907	stateAbsPath, err := filepath.Abs(st.FilePath)
2908	if err != nil {
2909		return stateAbsPath, err
2910	}
2911
2912	hasher := sha1.New()
2913	io.WriteString(hasher, stateAbsPath)
2914
2915	var stateFileExtension = filepath.Ext(st.FilePath)
2916	var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)]
2917
2918	sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8]
2919
2920	var sb strings.Builder
2921	sb.WriteString(stateFileName)
2922	sb.WriteString("-")
2923	sb.WriteString(sha1sum)
2924	sb.WriteString("-")
2925	sb.WriteString(release.Name)
2926
2927	if outputFileTemplate == "" {
2928		outputFileTemplate = filepath.Join("{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}", "{{ .Release.Name }}.yaml")
2929	}
2930
2931	t, err := template.New("output-file").Parse(outputFileTemplate)
2932	if err != nil {
2933		return "", fmt.Errorf("parsing output-file templmate")
2934	}
2935
2936	buf := &bytes.Buffer{}
2937
2938	type state struct {
2939		BaseName    string
2940		Path        string
2941		AbsPath     string
2942		AbsPathSHA1 string
2943	}
2944
2945	data := struct {
2946		State   state
2947		Release *ReleaseSpec
2948	}{
2949		State: state{
2950			BaseName:    stateFileName,
2951			Path:        st.FilePath,
2952			AbsPath:     stateAbsPath,
2953			AbsPathSHA1: sha1sum,
2954		},
2955		Release: release,
2956	}
2957
2958	if err := t.Execute(buf, data); err != nil {
2959		return "", fmt.Errorf("executing output-file template: %w", err)
2960	}
2961
2962	return buf.String(), nil
2963}
2964
2965func (st *HelmState) ToYaml() (string, error) {
2966	if result, err := yaml.Marshal(st); err != nil {
2967		return "", err
2968	} else {
2969		return string(result), nil
2970	}
2971}
2972
2973func (st *HelmState) LoadYAMLForEmbedding(release *ReleaseSpec, entries []interface{}, missingFileHandler *string, pathPrefix string) ([]interface{}, error) {
2974	var result []interface{}
2975
2976	for _, v := range entries {
2977		switch t := v.(type) {
2978		case string:
2979			var values map[string]interface{}
2980
2981			paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", pathPrefix+t)
2982			if err != nil {
2983				return nil, err
2984			}
2985			if skip {
2986				continue
2987			}
2988
2989			if len(paths) > 1 {
2990				return nil, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary")
2991			}
2992			yamlOrTemplatePath := paths[0]
2993
2994			yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, yamlOrTemplatePath)
2995			if err != nil {
2996				return nil, fmt.Errorf("failed to render values files \"%s\": %v", t, err)
2997			}
2998
2999			if err := yaml.Unmarshal(yamlBytes, &values); err != nil {
3000				return nil, err
3001			}
3002
3003			result = append(result, values)
3004		default:
3005			result = append(result, v)
3006		}
3007	}
3008
3009	return result, nil
3010}
3011
3012func (st *HelmState) Reverse() {
3013	for i, j := 0, len(st.Releases)-1; i < j; i, j = i+1, j-1 {
3014		st.Releases[i], st.Releases[j] = st.Releases[j], st.Releases[i]
3015	}
3016
3017	for i, j := 0, len(st.Helmfiles)-1; i < j; i, j = i+1, j-1 {
3018		st.Helmfiles[i], st.Helmfiles[j] = st.Helmfiles[j], st.Helmfiles[i]
3019	}
3020}
3021
3022func (st *HelmState) getOCIChart(pullChan chan PullCommand, release *ReleaseSpec, tempDir string, helm helmexec.Interface) (*string, error) {
3023	repo, name := st.GetRepositoryAndNameFromChartName(release.Chart)
3024	if repo == nil {
3025		return nil, nil
3026	}
3027
3028	if !repo.OCI {
3029		return nil, nil
3030	}
3031
3032	chartVersion := "latest"
3033	if release.Version != "" {
3034		chartVersion = release.Version
3035	}
3036
3037	qualifiedChartName := fmt.Sprintf("%s/%s:%s", repo.URL, name, chartVersion)
3038
3039	err := st.pullChart(pullChan, qualifiedChartName)
3040	if err != nil {
3041		return nil, err
3042	}
3043
3044	pathElems := []string{
3045		tempDir,
3046	}
3047
3048	if release.Namespace != "" {
3049		pathElems = append(pathElems, release.Namespace)
3050	}
3051
3052	if release.KubeContext != "" {
3053		pathElems = append(pathElems, release.KubeContext)
3054	}
3055
3056	pathElems = append(pathElems, release.Name, name, chartVersion)
3057
3058	chartPath := path.Join(pathElems...)
3059	err = helm.ChartExport(qualifiedChartName, chartPath)
3060
3061	fullChartPath, err := findChartDirectory(chartPath)
3062	if err != nil {
3063		return nil, err
3064	}
3065
3066	chartPath = filepath.Dir(fullChartPath)
3067
3068	return &chartPath, nil
3069}
3070
3071// Pull charts one by one to prevent concurrent pull problems with Helm
3072func (st *HelmState) pullChartWorker(pullChan chan PullCommand, helm helmexec.Interface) {
3073	for pullCmd := range pullChan {
3074		err := helm.ChartPull(pullCmd.ChartRef)
3075		pullCmd.responseChan <- err
3076	}
3077}
3078
3079// Send a pull command to the pull worker
3080func (st *HelmState) pullChart(pullChan chan PullCommand, chartRef string) error {
3081	response := make(chan error, 1)
3082	cmd := PullCommand{
3083		responseChan: response,
3084		ChartRef:     chartRef,
3085	}
3086	pullChan <- cmd
3087	return <-response
3088}
3089