Repository: roboll/helmfile
Pull request #1425 (2020-08-22 09:53:45)

HELMFILE_ENVIRONMENT variable
A simple change to allow the 'environment name' to be set by an environment variable. Precedence have command line option.


Patch:
diff --git a/Dockerfile.helm3 b/Dockerfile.helm3
index 27049de..68f56ce 100644
--- a/Dockerfile.helm3
+++ b/Dockerfile.helm3
@@ -32,7 +32,7 @@ RUN curl --retry 3 --retry-connrefused -LO "https://storage.googleapis.com/kuber
     chmod +x kubectl && \
     mv kubectl /usr/local/bin/kubectl
 
-RUN helm plugin install https://github.com/databus23/helm-diff --version v3.0.0-rc.7 && \
+RUN helm plugin install https://github.com/databus23/helm-diff --version v3.1.3 && \
     helm plugin install https://github.com/futuresimple/helm-secrets && \
     helm plugin install https://github.com/hypnoglow/helm-s3.git && \
     helm plugin install https://github.com/aslafy-z/helm-git.git
diff --git a/README.md b/README.md
index 6bd0231..2bfc3c3 100644
--- a/README.md
+++ b/README.md
@@ -420,6 +420,9 @@ GLOBAL OPTIONS:
    --interactive, -i                       Request confirmation before attempting to modify clusters
    --help, -h                              show help
    --version, -v                           print the version
+
+Environment variables:
+  HELMFILE_ENVIRONMENT                     specify the environment name, the command line option '-e' have precedence
 ```
 
 ### sync
diff --git a/go.mod b/go.mod
index 5675560..04560ba 100644
--- a/go.mod
+++ b/go.mod
@@ -24,7 +24,7 @@ require (
 	github.com/spf13/cobra v0.0.3
 	github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939
 	github.com/urfave/cli v1.20.0
-	github.com/variantdev/chartify v0.4.1
+	github.com/variantdev/chartify v0.4.2
 	github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363
 	github.com/variantdev/vals v0.10.0
 	go.uber.org/multierr v1.1.0
diff --git a/go.sum b/go.sum
index e9bd427..4453ab6 100644
--- a/go.sum
+++ b/go.sum
@@ -833,6 +833,8 @@ github.com/variantdev/chartify v0.4.0 h1:Ze+gfykTqFuQgfH2w6/ZqMwsrGrRU8yIsW8M+e7
 github.com/variantdev/chartify v0.4.0/go.mod h1:Fr4oPNJ4b19knlovn0wBoU2+MZxQdBacmBs30wwgAFk=
 github.com/variantdev/chartify v0.4.1 h1:0a8Ipf7n65mbQaAAfI9JEbdDauMnwKs0Gdq/yu5jI7I=
 github.com/variantdev/chartify v0.4.1/go.mod h1:Fr4oPNJ4b19knlovn0wBoU2+MZxQdBacmBs30wwgAFk=
+github.com/variantdev/chartify v0.4.2 h1:Vw647JysxJ4kz+6vOxOmrctRoTTf5qmpleB6hTQEjUk=
+github.com/variantdev/chartify v0.4.2/go.mod h1:Fr4oPNJ4b19knlovn0wBoU2+MZxQdBacmBs30wwgAFk=
 github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363 h1:KrfQBEUn+wEOQ/6UIfoqRDvn+Q/wZridQ7t0G1vQqKE=
 github.com/variantdev/dag v0.0.0-20191028002400-bb0b3c785363/go.mod h1:pH1TQsNSLj2uxMo9NNl9zdGy01Wtn+/2MT96BrKmVyE=
 github.com/variantdev/vals v0.4.0 h1:O1O7/sWhlvozcY2DjZBzlE1notxwVo6UBT1+w7HsO/k=
diff --git a/main.go b/main.go
index 369ad95..6c0e1c5 100644
--- a/main.go
+++ b/main.go
@@ -681,7 +681,10 @@ func (c configImpl) Logger() *zap.SugaredLogger {
 func (c configImpl) Env() string {
 	env := c.c.GlobalString("environment")
 	if env == "" {
-		env = state.DefaultEnv
+		env = os.Getenv("HELMFILE_ENVIRONMENT")
+		if env == "" {
+			env = state.DefaultEnv
+		}
 	}
 	return env
 }
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 4e627b6..d04b458 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -103,7 +103,10 @@ func Init(app *App) *App {
 
 func (a *App) Deps(c DepsConfigProvider) error {
 	return a.ForEachState(func(run *Run) (_ bool, errs []error) {
-		prepErr := run.withPreparedCharts(false, c.SkipRepos(), "deps", func() {
+		prepErr := run.withPreparedCharts("deps", state.ChartPrepareOptions{
+			SkipRepos:   c.SkipRepos(),
+			SkipResolve: true,
+		}, func() {
 			errs = run.Deps(c)
 		})
 
@@ -119,7 +122,7 @@ func (a *App) Repos(c ReposConfigProvider) error {
 	return a.ForEachState(func(run *Run) (_ bool, errs []error) {
 		var reposErr error
 
-		err := run.withPreparedCharts(false, true, "repos", func() {
+		err := run.withPreparedCharts("repos", state.ChartPrepareOptions{SkipRepos: true}, func() {
 			reposErr = run.Repos(c)
 		})
 
@@ -137,7 +140,7 @@ func (a *App) Repos(c ReposConfigProvider) error {
 
 func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error {
 	return a.ForEachState(func(run *Run) (_ bool, errs []error) {
-		err := run.withPreparedCharts(false, true, "charts", func() {
+		err := run.withPreparedCharts("charts", state.ChartPrepareOptions{SkipRepos: true}, func() {
 			errs = run.DeprecatedSyncCharts(c)
 		})
 
@@ -163,7 +166,7 @@ func (a *App) Diff(c DiffConfigProvider) error {
 
 		var errs []error
 
-		prepErr := run.withPreparedCharts(false, c.SkipDeps(), "diff", func() {
+		prepErr := run.withPreparedCharts("diff", state.ChartPrepareOptions{SkipRepos: c.SkipDeps()}, func() {
 			msg, matched, affected, errs = a.diff(run, c)
 		})
 
@@ -218,7 +221,10 @@ func (a *App) Template(c TemplateConfigProvider) error {
 	return a.ForEachState(func(run *Run) (ok bool, errs []error) {
 		// `helm template` in helm v2 does not support local chart.
 		// So, we set forceDownload=true for helm v2 only
-		prepErr := run.withPreparedCharts(!run.helm.IsHelm3(), c.SkipDeps(), "template", func() {
+		prepErr := run.withPreparedCharts("template", state.ChartPrepareOptions{
+			ForceDownload: !run.helm.IsHelm3(),
+			SkipRepos:     c.SkipDeps(),
+		}, func() {
 			ok, errs = a.template(run, c)
 		})
 
@@ -233,7 +239,10 @@ func (a *App) Template(c TemplateConfigProvider) error {
 func (a *App) Lint(c LintConfigProvider) error {
 	return a.ForEachState(func(run *Run) (_ bool, errs []error) {
 		// `helm lint` on helm v2 and v3 does not support remote charts, that we need to set `forceDownload=true` here
-		prepErr := run.withPreparedCharts(true, c.SkipDeps(), "lint", func() {
+		prepErr := run.withPreparedCharts("lint", state.ChartPrepareOptions{
+			ForceDownload: true,
+			SkipRepos:     c.SkipDeps(),
+		}, func() {
 			errs = run.Lint(c)
 		})
 
@@ -247,7 +256,9 @@ func (a *App) Lint(c LintConfigProvider) error {
 
 func (a *App) Sync(c SyncConfigProvider) error {
 	return a.ForEachState(func(run *Run) (ok bool, errs []error) {
-		prepErr := run.withPreparedCharts(false, c.SkipDeps(), "sync", func() {
+		prepErr := run.withPreparedCharts("sync", state.ChartPrepareOptions{
+			SkipRepos: c.SkipDeps(),
+		}, func() {
 			ok, errs = a.sync(run, c)
 		})
 
@@ -269,7 +280,9 @@ func (a *App) Apply(c ApplyConfigProvider) error {
 	opts = append(opts, SetRetainValuesFiles(c.RetainValuesFiles()))
 
 	err := a.ForEachState(func(run *Run) (ok bool, errs []error) {
-		prepErr := run.withPreparedCharts(false, c.SkipDeps(), "apply", func() {
+		prepErr := run.withPreparedCharts("apply", state.ChartPrepareOptions{
+			SkipRepos: c.SkipDeps(),
+		}, func() {
 			matched, updated, es := a.apply(run, c)
 
 			mut.Lock()
@@ -301,7 +314,9 @@ func (a *App) Apply(c ApplyConfigProvider) error {
 
 func (a *App) Status(c StatusesConfigProvider) error {
 	return a.ForEachState(func(run *Run) (_ bool, errs []error) {
-		err := run.withPreparedCharts(false, true, "status", func() {
+		err := run.withPreparedCharts("status", state.ChartPrepareOptions{
+			SkipRepos: true,
+		}, func() {
 			errs = run.Status(c)
 		})
 
@@ -315,7 +330,9 @@ func (a *App) Status(c StatusesConfigProvider) error {
 
 func (a *App) Delete(c DeleteConfigProvider) error {
 	return a.ForEachState(func(run *Run) (ok bool, errs []error) {
-		err := run.withPreparedCharts(false, true, "delete", func() {
+		err := run.withPreparedCharts("delete", state.ChartPrepareOptions{
+			SkipRepos: true,
+		}, func() {
 			ok, errs = a.delete(run, c.Purge(), c)
 		})
 
@@ -329,7 +346,9 @@ func (a *App) Delete(c DeleteConfigProvider) error {
 
 func (a *App) Destroy(c DestroyConfigProvider) error {
 	return a.ForEachState(func(run *Run) (ok bool, errs []error) {
-		err := run.withPreparedCharts(false, true, "destroy", func() {
+		err := run.withPreparedCharts("destroy", state.ChartPrepareOptions{
+			SkipRepos: true,
+		}, func() {
 			ok, errs = a.delete(run, true, c)
 		})
 
@@ -349,7 +368,9 @@ func (a *App) Test(c TestConfigProvider) error {
 				"or set helm.sh/hook-delete-policy\n")
 		}
 
-		err := run.withPreparedCharts(false, true, "test", func() {
+		err := run.withPreparedCharts("test", state.ChartPrepareOptions{
+			SkipRepos: true,
+		}, func() {
 			errs = run.Test(c)
 		})
 
@@ -363,7 +384,9 @@ func (a *App) Test(c TestConfigProvider) error {
 
 func (a *App) PrintState(c StateConfigProvider) error {
 	return a.ForEachState(func(run *Run) (_ bool, errs []error) {
-		err := run.withPreparedCharts(false, true, "build", func() {
+		err := run.withPreparedCharts("build", state.ChartPrepareOptions{
+			SkipRepos: true,
+		}, func() {
 			state, err := run.state.ToYaml()
 			if err != nil {
 				errs = []error{err}
@@ -386,7 +409,9 @@ func (a *App) ListReleases(c ListConfigProvider) error {
 	var releases []*HelmRelease
 
 	err := a.ForEachState(func(run *Run) (_ bool, errs []error) {
-		err := run.withPreparedCharts(false, true, "list", func() {
+		err := run.withPreparedCharts("list", state.ChartPrepareOptions{
+			SkipRepos: true,
+		}, func() {
 
 			//var releases m
 			for _, r := range run.state.Releases {
diff --git a/pkg/app/run.go b/pkg/app/run.go
index ae01649..539d24a 100644
--- a/pkg/app/run.go
+++ b/pkg/app/run.go
@@ -36,12 +36,12 @@ func (r *Run) askForConfirmation(msg string) bool {
 	return AskForConfirmation(msg)
 }
 
-func (r *Run) withPreparedCharts(forceDownload, skipRepos bool, helmfileCommand string, f func()) error {
+func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepareOptions, f func()) error {
 	if r.ReleaseToChart != nil {
 		panic("Run.PrepareCharts can be called only once")
 	}
 
-	if !skipRepos {
+	if !opts.SkipRepos {
 		ctx := r.ctx
 		if err := ctx.SyncReposOnce(r.state, r.helm); err != nil {
 			return err
@@ -59,7 +59,7 @@ func (r *Run) withPreparedCharts(forceDownload, skipRepos bool, helmfileCommand
 		return err
 	}
 
-	releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, 2, helmfileCommand, forceDownload, skipRepos)
+	releaseToChart, errs := r.state.PrepareCharts(r.helm, dir, 2, helmfileCommand, opts)
 
 	if len(errs) > 0 {
 		return fmt.Errorf("%v", errs)
diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go
index 6338c69..e45bbc0 100644
--- a/pkg/helmexec/exec.go
+++ b/pkg/helmexec/exec.go
@@ -48,40 +48,28 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
 	return zap.New(core).Sugar()
 }
 
-func getHelmVersion(helmBinary string, logger *zap.SugaredLogger, runner Runner) Version {
+// versionRegex matches versions like v1.1.1 and v1.1
+var versionRegex = regexp.MustCompile("v(?P<major>\\d+)\\.(?P<minor>\\d+)(?:\\.(?P<patch>\\d+))?")
 
-	// Autodetect from `helm verison`
-	bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
-	if err != nil {
-		panic(err)
+func parseHelmVersion(versionStr string) (Version, error) {
+	if len(versionStr) == 0 {
+		return Version{}, nil
 	}
 
-	if bytes == nil || len(bytes) == 0 {
-		return Version{}
+	matches := versionRegex.FindStringSubmatch(versionStr)
+	if len(matches) == 0 {
+		return Version{}, fmt.Errorf("error parsing helm verion '%s'", versionStr)
 	}
-
-	re := regexp.MustCompile("v(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)")
-	matches := re.FindStringSubmatch(string(bytes))
-
 	result := make(map[string]string)
-	for i, name := range re.SubexpNames() {
+	for i, name := range versionRegex.SubexpNames() {
 		result[name] = matches[i]
 	}
 
-	major, err := strconv.Atoi(result["major"])
-	if err != nil {
-		panic(err)
-	}
-
-	minor, err := strconv.Atoi(result["minor"])
-	if err != nil {
-		panic(err)
-	}
-
-	patch, err := strconv.Atoi(result["patch"])
-	if err != nil {
-		panic(err)
-	}
+	// We ignore errors because regex matches only integers
+	// If any of the parts does not exist - default "0" will be used
+	major, _ := strconv.Atoi(result["major"])
+	minor, _ := strconv.Atoi(result["minor"])
+	patch, _ := strconv.Atoi(result["patch"])
 
 	// Support explicit helm3 opt-in via environment variable
 	if os.Getenv("HELMFILE_HELM3") != "" && major < 3 {
@@ -89,21 +77,37 @@ func getHelmVersion(helmBinary string, logger *zap.SugaredLogger, runner Runner)
 			Major: 3,
 			Minor: 0,
 			Patch: 0,
-		}
+		}, nil
 	}
 
 	return Version{
 		Major: major,
 		Minor: minor,
 		Patch: patch,
+	}, nil
+}
+
+func getHelmVersion(helmBinary string, runner Runner) (Version, error) {
+
+	// Autodetect from `helm verison`
+	bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil)
+	if err != nil {
+		return Version{}, fmt.Errorf("error determining helm version: %w", err)
 	}
+
+	return parseHelmVersion(string(bytes))
 }
 
 // New for running helm commands
 func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
+	// TODO: proper error handling
+	version, err := getHelmVersion(helmBinary, runner)
+	if err != nil {
+		panic(err)
+	}
 	return &execer{
 		helmBinary:       helmBinary,
-		version:          getHelmVersion(helmBinary, logger, runner),
+		version:          version,
 		logger:           logger,
 		kubeContext:      kubeContext,
 		runner:           runner,
diff --git a/pkg/state/helmx.go b/pkg/state/helmx.go
index 611a6fe..d0a2b39 100644
--- a/pkg/state/helmx.go
+++ b/pkg/state/helmx.go
@@ -114,7 +114,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
 
 	jsonPatches := release.JSONPatches
 	if len(jsonPatches) > 0 {
-		generatedFiles, err := st.generateTemporaryValuesFiles(jsonPatches, release.MissingFileHandler)
+		generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, jsonPatches, release.MissingFileHandler)
 		if err != nil {
 			return nil, clean, err
 		}
@@ -130,7 +130,7 @@ func (st *HelmState) PrepareChartify(helm helmexec.Interface, release *ReleaseSp
 
 	strategicMergePatches := release.StrategicMergePatches
 	if len(strategicMergePatches) > 0 {
-		generatedFiles, err := st.generateTemporaryValuesFiles(strategicMergePatches, release.MissingFileHandler)
+		generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, strategicMergePatches, release.MissingFileHandler)
 		if err != nil {
 			return nil, clean, err
 		}
diff --git a/pkg/state/state.go b/pkg/state/state.go
index ee3229e..a2d02c4 100644
--- a/pkg/state/state.go
+++ b/pkg/state/state.go
@@ -6,6 +6,7 @@ import (
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"golang.org/x/sync/errgroup"
 	"io"
 	"io/ioutil"
 	"os"
@@ -519,6 +520,11 @@ func (o *SyncOpts) Apply(opts *SyncOpts) {
 func ReleaseToID(r *ReleaseSpec) string {
 	var id string
 
+	kc := r.KubeContext
+	if kc != "" {
+		id += kc + "/"
+	}
+
 	tns := r.TillerNamespace
 	if tns != "" {
 		id += tns + "/"
@@ -792,6 +798,21 @@ func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec {
 	return result
 }
 
+type ChartPrepareOptions struct {
+	ForceDownload bool
+	SkipRepos     bool
+	SkipResolve   bool
+}
+
+type chartPrepareResult struct {
+	releaseName            string
+	chartName              string
+	chartPath              string
+	err                    error
+	buildDeps              bool
+	chartFetchedByGoGetter bool
+}
+
 // PrepareCharts creates temporary directories of charts.
 //
 // Each resulting "chart" can be one of the followings:
@@ -806,21 +827,15 @@ func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec {
 // Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart.
 //
 // If exists, it will also patch resources by json patches, strategic-merge patches, and injectors.
-func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, forceDownload, skipDeps bool) (map[string]string, []error) {
+func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[string]string, []error) {
 	releases := releasesNeedCharts(st.Releases)
 
 	temp := make(map[string]string, len(releases))
-	type downloadResults struct {
-		releaseName string
-		chartPath   string
-		err         error
-		diagnostic  string
-	}
 
 	errs := []error{}
 
 	jobQueue := make(chan *ReleaseSpec, len(releases))
-	results := make(chan *downloadResults, len(releases))
+	results := make(chan *chartPrepareResult, len(releases))
 
 	var helm3 bool
 
@@ -828,13 +843,15 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 		helm3 = helm.IsHelm3()
 	}
 
-	updated, err := st.ResolveDeps()
-	if err != nil {
-		return nil, []error{err}
+	if !opts.SkipResolve {
+		updated, err := st.ResolveDeps()
+		if err != nil {
+			return nil, []error{err}
+		}
+		*st = *updated
 	}
-	*st = *updated
 
-	var diagnostics []string
+	var builds []*chartPrepareResult
 
 	st.scatterGather(
 		concurrency,
@@ -853,7 +870,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 				// If it wasn't called here, Helmfile can end up an issue like
 				// https://github.com/roboll/helmfile/issues/1328
 				if _, err := st.triggerPrepareEvent(release, helmfileCommand); err != nil {
-					results <- &downloadResults{err: err}
+					results <- &chartPrepareResult{err: err}
 					return
 				}
 
@@ -861,7 +878,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 
 				chartPath, err := st.goGetterChart(chartName, release.Directory, release.ForceGoGetter)
 				if err != nil {
-					results <- &downloadResults{err: fmt.Errorf("release %q: %w", release.Name, err)}
+					results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)}
 					return
 				}
 
@@ -870,11 +887,11 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 				chartification, clean, err := st.PrepareChartify(helm, release, chartPath, workerIndex)
 				defer clean()
 				if err != nil {
-					results <- &downloadResults{err: err}
+					results <- &chartPrepareResult{err: err}
 					return
 				}
 
-				var diagnostic string
+				var buildDeps bool
 
 				if chartification != nil {
 					c := chartify.New(
@@ -884,7 +901,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 
 					out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartification.Opts))
 					if err != nil {
-						results <- &downloadResults{err: err}
+						results <- &chartPrepareResult{err: err}
 						return
 					} else {
 						// TODO Chartify
@@ -916,25 +933,8 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 					// a broken remote chart won't completely block their job.
 					chartPath = normalizeChart(st.basePath, chartPath)
 
-					if !skipDeps {
-						if err := helm.BuildDeps(release.Name, chartPath); err != nil {
-							if chartFetchedByGoGetter {
-								diagnostic = fmt.Sprintf(
-									"WARN: `helm dep build` failed. While processing release %q, Helmfile observed that remote chart %q fetched by go-getter is seemingly broken. "+
-										"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`. "+
-										"Helmfile is tolerating the error to avoid blocking you until the remote chart gets fixed. "+
-										"But this may result in any failure later if the chart is broken badly. FYI, the tolerated error was: %v",
-									release.Name,
-									chartName,
-									err.Error(),
-								)
-							} else {
-								results <- &downloadResults{err: err}
-								return
-							}
-						}
-					}
-				} else if !forceDownload {
+					buildDeps = !opts.SkipRepos
+				} else if !opts.ForceDownload {
 					// At this point, we are sure that either:
 					// 1. It is a local chart and we can use it in later process (helm upgrade/template/lint/etc)
 					//    without any modification, or
@@ -973,7 +973,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 						fetchFlags := st.chartVersionFlags(release)
 						fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath)
 						if err := helm.Fetch(chartName, fetchFlags...); err != nil {
-							results <- &downloadResults{err: err}
+							results <- &chartPrepareResult{err: err}
 							return
 						}
 					}
@@ -985,38 +985,134 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre
 					}
 				}
 
-				results <- &downloadResults{releaseName: release.Name, chartPath: chartPath, diagnostic: diagnostic}
+				results <- &chartPrepareResult{
+					releaseName:            release.Name,
+					chartName:              chartName,
+					chartPath:              chartPath,
+					buildDeps:              buildDeps,
+					chartFetchedByGoGetter: chartFetchedByGoGetter,
+				}
 			}
 		},
 		func() {
 			for i := 0; i < len(releases); i++ {
 				downloadRes := <-results
 
-				if d := downloadRes.diagnostic; d != "" {
-					diagnostics = append(diagnostics, d)
-				}
-
 				if downloadRes.err != nil {
 					errs = append(errs, downloadRes.err)
 
 					return
 				}
 				temp[downloadRes.releaseName] = downloadRes.chartPath
+
+				if downloadRes.buildDeps {
+					builds = append(builds, downloadRes)
+				}
 			}
 		},
 	)
 
-	sort.Strings(diagnostics)
-	for _, d := range diagnostics {
-		st.logger.Warn(d)
-	}
-
 	if len(errs) > 0 {
 		return nil, errs
 	}
+
+	if len(builds) > 0 {
+		if err := st.runHelmDepBuilds(helm, concurrency, builds); err != nil {
+			return nil, []error{err}
+		}
+	}
+
 	return temp, nil
 }
 
+func (st *HelmState) runHelmDepBuilds(helm helmexec.Interface, concurrency int, builds []*chartPrepareResult) error {
+	type buildOutput struct {
+		diagnostic string
+	}
+
+	buildQueue := make(chan *chartPrepareResult)
+
+	buildOutputQueue := make(chan *buildOutput)
+
+	buildWorkers := &errgroup.Group{}
+	for i := 0; i < concurrency; i++ {
+		buildWorkers.Go(func() error {
+			for r := range buildQueue {
+				out, err := func() (*buildOutput, error) {
+					if err := helm.BuildDeps(r.releaseName, r.chartPath); err != nil {
+						if r.chartFetchedByGoGetter {
+							return &buildOutput{
+								diagnostic: fmt.Sprintf(
+									"WARN: `helm dep build` failed. While processing release %q, Helmfile observed that remote chart %q fetched by go-getter is seemingly broken. "+
+										"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`. "+
+										"Helmfile is tolerating the error to avoid blocking you until the remote chart gets fixed. "+
+										"But this may result in any failure later if the chart is broken badly. FYI, the tolerated error was: %v",
+									r.releaseName,
+									r.chartName,
+									err.Error(),
+								),
+							}, nil
+						}
+						return nil, err
+					}
+					return nil, nil
+				}()
+
+				if err != nil {
+					return err
+				}
+
+				if out != nil {
+					buildOutputQueue <- out
+				}
+			}
+
+			return nil
+		})
+	}
+
+	diagnosticsAggregator := &sync.WaitGroup{}
+	diagnosticsAggregator.Add(1)
+	go func() {
+		var diagnostics []string
+		for r := range buildOutputQueue {
+			if d := r.diagnostic; d != "" {
+				diagnostics = append(diagnostics, d)
+			}
+		}
+
+		sort.Strings(diagnostics)
+		for _, d := range diagnostics {
+			st.logger.Warn(d)
+		}
+
+		diagnosticsAggregator.Done()
+	}()
+
+	built := map[string]bool{}
+
+	for _, b := range builds {
+		// `helm dep build` fails when it was run concurrency on the same chart.
+		// To avoid that, we run `helm dep build` only once per each local chart.
+		//
+		// See https://github.com/roboll/helmfile/issues/1438
+		if !built[b.chartPath] {
+			built[b.chartPath] = true
+			buildQueue <- b
+		}
+	}
+	close(buildQueue)
+
+	if err := buildWorkers.Wait(); err != nil {
+		return err
+	}
+	close(buildOutputQueue)
+
+	diagnosticsAggregator.Wait()
+
+	return nil
+}
+
 type TemplateOpts struct {
 	Set               []string
 	OutputDirTemplate string
@@ -1988,8 +2084,14 @@ func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec,
 	return flags, files, nil
 }
 
-func (st *HelmState) RenderValuesFileToBytes(path string) ([]byte, error) {
-	r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), st.valuesFileTemplateData())
+func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) {
+	vals, err := st.Values()
+	if err != nil {
+		return nil, err
+	}
+	templateData := st.createReleaseTemplateData(release, vals)
+
+	r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), templateData)
 	rawBytes, err := r.RenderToBytes(path)
 	if err != nil {
 		return nil, err
@@ -2068,7 +2170,7 @@ func (st *HelmState) removeFiles(files []string) {
 	}
 }
 
-func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingFileHandler *string) ([]string, error) {
+func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []interface{}, missingFileHandler *string) ([]string, error) {
 	generatedFiles := []string{}
 
 	for _, value := range values {
@@ -2087,7 +2189,7 @@ func (st *HelmState) generateTemporaryValuesFiles(values []interface{}, missingF
 			}
 			path := paths[0]
 
-			yamlBytes, err := st.RenderValuesFileToBytes(path)
+			yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, path)
 			if err != nil {
 				return generatedFiles, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err)
 			}
@@ -2144,7 +2246,7 @@ func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string,
 		return nil, fmt.Errorf("Failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"])
 	}
 
-	generatedFiles, err := st.generateTemporaryValuesFiles(valuesSecretsRendered, release.MissingFileHandler)
+	generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered, release.MissingFileHandler)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/state/state_exec_tmpl.go b/pkg/state/state_exec_tmpl.go
index 68f0af7..c1b627d 100644
--- a/pkg/state/state_exec_tmpl.go
+++ b/pkg/state/state_exec_tmpl.go
@@ -12,19 +12,16 @@ func (st *HelmState) Values() (map[string]interface{}, error) {
 	return st.Env.GetMergedValues()
 }
 
-func (st *HelmState) mustLoadVals() map[string]interface{} {
-	vals, err := st.Values()
-	if err != nil {
-		panic(err)
-	}
-	return vals
-}
-
-func (st *HelmState) valuesFileTemplateData() EnvironmentTemplateData {
-	return EnvironmentTemplateData{
+func (st *HelmState) createReleaseTemplateData(release *ReleaseSpec, vals map[string]interface{}) releaseTemplateData {
+	return releaseTemplateData{
 		Environment: st.Env,
-		Namespace:   st.OverrideNamespace,
-		Values:      st.mustLoadVals(),
+		Values:      vals,
+		Release: releaseTemplateDataRelease{
+			Name:      release.Name,
+			Chart:     release.Chart,
+			Namespace: release.Namespace,
+			Labels:    release.Labels,
+		},
 	}
 }
 
@@ -88,11 +85,7 @@ func (st *HelmState) ExecuteTemplates() (*HelmState, error) {
 	for i, rt := range st.Releases {
 		successFlag := false
 		for it, prev := 0, &rt; it < 6; it++ {
-			tmplData := releaseTemplateData{
-				Environment: st.Env,
-				Release:     *prev,
-				Values:      vals,
-			}
+			tmplData := st.createReleaseTemplateData(prev, vals)
 			renderer := tmpl.NewFileRenderer(st.readFile, st.basePath, tmplData)
 			r, err := rt.ExecuteTemplateExpressions(renderer)
 			if err != nil {
diff --git a/pkg/state/types.go b/pkg/state/types.go
index fed1068..06a4eec 100644
--- a/pkg/state/types.go
+++ b/pkg/state/types.go
@@ -19,12 +19,28 @@ type EnvironmentTemplateData struct {
 	Values map[string]interface{}
 }
 
-// releaseTemplateData provides variables accessible while executing golang text/template expressions in releases of a helmfile YAML file
+// releaseTemplateData provides variables accessible while executing golang text/template expressions in release templates
+// and release values templates within a Helmfile YAML file.
 type releaseTemplateData struct {
 	// Environment is accessible as `.Environment` from any template expression executed by the renderer
 	Environment environment.Environment
-	// Release is accessible as `.Release` from any template expression executed by the renderer
-	Release ReleaseSpec
+	// Release is accessible as `.Release` from any template expression executed by the renderer.
+	// It contains a subset of ReleaseSpec that is known to be useful to dynamically render values.
+	Release releaseTemplateDataRelease
 	// Values is accessible as `.Values` and it contains default state values overrode by environment values and override values.
 	Values map[string]interface{}
 }
+
+type releaseTemplateDataRelease struct {
+	// Name is basically ReleaseSpec.Name exposed to the template
+	Name string
+
+	// Namespace is HelmState.OverrideNamespace, or if it's empty, ReleaseSpec.Namespace.
+	Namespace string
+
+	// Labels is ReleaseSpec.Labels
+	Labels map[string]string
+
+	// Chart is ReleaseSpec.Chart
+	Chart string
+}


Test patch:
diff --git a/pkg/helmexec/exec_test.go b/pkg/helmexec/exec_test.go
index 966a4c6..832f793 100644
--- a/pkg/helmexec/exec_test.go
+++ b/pkg/helmexec/exec_test.go
@@ -570,3 +570,50 @@ func Test_IsVersionAtLeast(t *testing.T) {
 	}
 
 }
+
+func Test_parseHelmVersion(t *testing.T) {
+	tests := []struct {
+		ver     string
+		want    Version
+		wantErr bool
+	}{
+		{
+			ver: "v1.2.3",
+			want: Version{
+				Major: 1,
+				Minor: 2,
+				Patch: 3,
+			},
+			wantErr: false,
+		},
+		{
+			ver: "v1.2",
+			want: Version{
+				Major: 1,
+				Minor: 2,
+				Patch: 0,
+			},
+			wantErr: false,
+		},
+		{
+			ver:     "v1",
+			wantErr: true,
+		},
+		{
+			ver:     "1.1.1",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.ver, func(t *testing.T) {
+			got, err := parseHelmVersion(tt.ver)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("parseHelmVersion() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("parseHelmVersion() got = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/state/create_test.go b/pkg/state/create_test.go
index 9c0303f..224472e 100644
--- a/pkg/state/create_test.go
+++ b/pkg/state/create_test.go
@@ -100,9 +100,11 @@ bar: {{ readFile "bar.txt" }}
 	}
 
 	valuesFile := "/example/path/to/values.yaml.gotmpl"
-	valuesContent := []byte(`env: {{ .Environment.Name }}`)
+	valuesContent := []byte(`env: {{ .Environment.Name }}
+releaseName: {{ .Release.Name }}`)
 
-	expectedValues := `env: production`
+	expectedValues := `env: production
+releaseName: myrelease`
 
 	testFs := testhelper.NewTestFs(map[string]string{
 		fooYamlFile: string(fooYamlContent),
@@ -124,7 +126,11 @@ bar: {{ readFile "bar.txt" }}
 		t.Errorf("unexpected environment values: expected=%v, actual=%v", expected, actual)
 	}
 
-	actualValuesData, err := state.RenderValuesFileToBytes(valuesFile)
+	var release = ReleaseSpec{
+		Name: "myrelease",
+	}
+
+	actualValuesData, err := state.RenderReleaseValuesFileToBytes(&release, valuesFile)
 	if err != nil {
 		t.Errorf("unexpected error: %v", err)
 	}