Repository: roboll/helmfile
Pull request #1594 (2020-11-20 13:34:58)

Don't append --tiller-namespace flag when running helm3
When `helmDefaults.tillerNamespace` is set, helmfile will append `--tiller-namespace` to the helm flags regardless of the helm version.


Patch:
diff --git a/Dockerfile b/Dockerfile
index 51f67f2..324fa3d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -46,4 +46,4 @@ RUN helm plugin install https://github.com/databus23/helm-diff && \
 
 COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
 
-CMD ["/usr/local/bin/helmfile"]
+CMD ["/usr/local/bin/helmfile", "--help"]
diff --git a/Dockerfile.helm3 b/Dockerfile.helm3
index e236b95..7e177f2 100644
--- a/Dockerfile.helm3
+++ b/Dockerfile.helm3
@@ -43,4 +43,4 @@ RUN helm plugin install https://github.com/databus23/helm-diff --version v3.1.3
 
 COPY --from=builder /workspace/helmfile/dist/helmfile_linux_amd64 /usr/local/bin/helmfile
 
-CMD ["/usr/local/bin/helmfile"]
+CMD ["/usr/local/bin/helmfile", "--help"]
diff --git a/Makefile b/Makefile
index 6aeb898..1f7874b 100644
--- a/Makefile
+++ b/Makefile
@@ -30,7 +30,7 @@ integration:
 .PHONY: integration
 
 cross:
-	env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!mips !mipsle !mips64 !mips64le !s390x' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
+	env CGO_ENABLED=0 gox -os '!openbsd !freebsd !netbsd' -arch '!mips !mipsle !mips64 !mips64le !s390x !arm arm64' -output "dist/{{.Dir}}_{{.OS}}_{{.Arch}}" -ldflags '-X github.com/roboll/helmfile/pkg/app/version.Version=${TAG}' ${TARGETS}
 .PHONY: cross
 
 static-linux:
diff --git a/README.md b/README.md
index 70cfc2f..9fac06f 100644
--- a/README.md
+++ b/README.md
@@ -365,12 +365,40 @@ If you wish to treat your enviroment variables as strings always, even if they a
 ## Installation
 
 - download one of [releases](https://github.com/roboll/helmfile/releases) or
-- run as a [container](https://quay.io/roboll/helmfile) or
+- [run as a container](#running-as-a-container) or
 - Archlinux: install via `pacman -S helmfile` or from [AUR](https://aur.archlinux.org/packages/kubernetes-helmfile-bin/) or
 - openSUSE: install via `zypper in helmfile` assuming you are on Tumbleweed; if you are on Leap you must add the [kubic](https://download.opensuse.org/repositories/devel:/kubic/) repo for your distribution version once before that command, e.g. `zypper ar https://download.opensuse.org/repositories/devel:/kubic/openSUSE_Leap_\$releasever kubic`, or
 - Windows (using [scoop](https://scoop.sh/)): `scoop install helmfile`
 - macOS (using [homebrew](https://brew.sh/)): `brew install helmfile`
 
+### Running as a container
+
+The [Helmfile Docker images are available in Quay](https://quay.io/roboll/helmfile). There is no `latest` tag, since the `0.x` versions can contain breaking changes, so make sure you pick the right tag. Example using `helmfile 0.135.0`:
+
+```sh-session
+# helm 2
+$ docker run --rm --net=host -v "${HOME}/.kube:/root/.kube" -v "${HOME}/.helm:/root/.helm" -v "${PWD}:/wd" --workdir /wd quay.io/roboll/helmfile:v0.135.0 helmfile sync
+
+# helm 3
+$ docker run --rm --net=host -v "${HOME}/.kube:/root/.kube" -v "${HOME}/.config/helm:/root/.config/helm" -v "${PWD}:/wd" --workdir /wd quay.io/roboll/helmfile:helm3-v0.135.0 helmfile sync
+```
+
+You can also use shims to make calling the binaries easier:
+
+```sh-session
+# helm 2
+$ printf '%s\n' '#!/bin/sh' 'docker run --rm --net=host -v "${HOME}/.kube:/root/.kube" -v "${HOME}/.helm:/root/.helm" -v "${PWD}:/wd" --workdir /wd quay.io/roboll/helmfile:v0.135.0 helmfile "$@"' |
+    tee helmfile
+$ chmod +x helmfile
+$ ./helmfile sync
+
+# helm 3
+$ printf '%s\n' '#!/bin/sh' 'docker run --rm --net=host -v "${HOME}/.kube:/root/.kube" -v "${HOME}/.config/helm:/root/.config/helm" -v "${PWD}:/wd" --workdir /wd quay.io/roboll/helmfile:helm3-v0.135.0 helmfile "$@"' |
+    tee helmfile
+$ chmod +x helmfile
+$ ./helmfile sync
+```
+
 ## Getting Started
 
 Let's start with a simple `helmfile` and gradually improve it to fit your use-case!
diff --git a/examples/README.md b/examples/README.md
index f6372ce..1a74322 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -26,7 +26,7 @@ You would then start a database migration job by executing:
 
 ```console
 # Start a database migration for the prod environment
-$ RAILS_ENV=prod helmfile sync --selector job=dbmigrator
+$ RAILS_ENV=prod helmfile --selector job=dbmigrator sync
 
 # Tail log until you are satisfied
 $ kubectl logs -l job=dbmigrator
diff --git a/main.go b/main.go
index ebb8c74..7e04e0c 100644
--- a/main.go
+++ b/main.go
@@ -397,6 +397,10 @@ func main() {
 					Name:  "skip-cleanup",
 					Usage: "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security",
 				},
+				cli.BoolFlag{
+					Name:  "skip-diff-on-install",
+					Usage: "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all",
+				},
 				cli.BoolFlag{
 					Name:  "include-tests",
 					Usage: "enable the diffing of the helm test hooks",
@@ -738,6 +742,10 @@ func (c configImpl) SkipCleanup() bool {
 	return c.c.Bool("skip-cleanup")
 }
 
+func (c configImpl) SkipDiffOnInstall() bool {
+	return c.c.Bool("skip-diff-on-install")
+}
+
 func (c configImpl) EmbedValues() bool {
 	return c.c.Bool("embed-values")
 }
diff --git a/pkg/app/app.go b/pkg/app/app.go
index 64eb8b4..65bc998 100644
--- a/pkg/app/app.go
+++ b/pkg/app/app.go
@@ -1084,10 +1084,11 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
 	detailedExitCode := true
 
 	diffOpts := &state.DiffOpts{
-		NoColor:     c.NoColor(),
-		Context:     c.Context(),
-		Set:         c.Set(),
-		SkipCleanup: c.RetainValuesFiles() || c.SkipCleanup(),
+		NoColor:           c.NoColor(),
+		Context:           c.Context(),
+		Set:               c.Set(),
+		SkipCleanup:       c.RetainValuesFiles() || c.SkipCleanup(),
+		SkipDiffOnInstall: c.SkipDiffOnInstall(),
 	}
 
 	infoMsg, releasesToBeUpdated, releasesToBeDeleted, errs := r.diff(false, detailedExitCode, c, diffOpts)
diff --git a/pkg/app/config.go b/pkg/app/config.go
index 9ba448e..853507d 100644
--- a/pkg/app/config.go
+++ b/pkg/app/config.go
@@ -52,6 +52,7 @@ type ApplyConfigProvider interface {
 
 	RetainValuesFiles() bool
 	SkipCleanup() bool
+	SkipDiffOnInstall() bool
 
 	concurrencyConfig
 	interactive
diff --git a/pkg/helmexec/context.go b/pkg/helmexec/context.go
index 4dfa715..12a4849 100644
--- a/pkg/helmexec/context.go
+++ b/pkg/helmexec/context.go
@@ -1,6 +1,7 @@
 package helmexec
 
 import (
+	"io"
 	"os"
 	"path/filepath"
 )
@@ -10,6 +11,7 @@ type HelmContext struct {
 	TillerNamespace string
 	HistoryMax      int
 	WorkerIndex     int
+	Writer          io.Writer
 }
 
 func (context *HelmContext) GetTillerlessArgs(helm *execer) []string {
diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go
index 31a9fcf..e7e8b1a 100644
--- a/pkg/helmexec/exec.go
+++ b/pkg/helmexec/exec.go
@@ -183,7 +183,7 @@ func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags .
 	}
 
 	out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env)
-	helm.write(out)
+	helm.write(nil, out)
 	return err
 }
 
@@ -192,7 +192,7 @@ func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...str
 	preArgs := context.GetTillerlessArgs(helm)
 	env := context.getTillerlessEnv()
 	out, err := helm.exec(append(append(preArgs, "status", name), flags...), env)
-	helm.write(out)
+	helm.write(nil, out)
 	return err
 }
 
@@ -219,7 +219,7 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s
 		lines = lines[1:]
 		out = []byte(strings.Join(lines, "\n"))
 	}
-	helm.write(out)
+	helm.write(nil, out)
 	return string(out), err
 }
 
@@ -317,12 +317,16 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string)
 	}
 
 	out, err := helm.exec(append(args, flags...), map[string]string{})
-	helm.write(out)
+	helm.write(nil, out)
 	return err
 }
 
 func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppressDiff bool, flags ...string) error {
-	helm.logger.Infof("Comparing release=%v, chart=%v", name, chart)
+	if context.Writer != nil {
+		fmt.Fprintf(context.Writer, "Comparing release=%v, chart=%v\n", name, chart)
+	} else {
+		helm.logger.Infof("Comparing release=%v, chart=%v", name, chart)
+	}
 	preArgs := context.GetTillerlessArgs(helm)
 	env := context.getTillerlessEnv()
 	out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--reset-values", "--allow-unreleased", name, chart), flags...), env)
@@ -340,13 +344,13 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppres
 		case ExitError:
 			if e.ExitStatus() == 2 {
 				if !(suppressDiff) {
-					helm.write(out)
+					helm.write(context.Writer, out)
 				}
 				return err
 			}
 		}
 	} else if !(suppressDiff) {
-		helm.write(out)
+		helm.write(context.Writer, out)
 	}
 	return err
 }
@@ -354,7 +358,7 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppres
 func (helm *execer) Lint(name, chart string, flags ...string) error {
 	helm.logger.Infof("Linting release=%v, chart=%v", name, chart)
 	out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{})
-	helm.write(out)
+	helm.write(nil, out)
 	return err
 }
 
@@ -370,7 +374,7 @@ func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...str
 	preArgs := context.GetTillerlessArgs(helm)
 	env := context.getTillerlessEnv()
 	out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env)
-	helm.write(out)
+	helm.write(nil, out)
 	return err
 }
 
@@ -380,7 +384,7 @@ func (helm *execer) TestRelease(context HelmContext, name string, flags ...strin
 	env := context.getTillerlessEnv()
 	args := []string{"test", name}
 	out, err := helm.exec(append(append(preArgs, args...), flags...), env)
-	helm.write(out)
+	helm.write(nil, out)
 	return err
 }
 
@@ -413,9 +417,12 @@ func (helm *execer) info(out []byte) {
 	}
 }
 
-func (helm *execer) write(out []byte) {
+func (helm *execer) write(w io.Writer, out []byte) {
 	if len(out) > 0 {
-		fmt.Printf("%s\n", out)
+		if w == nil {
+			w = os.Stdout
+		}
+		fmt.Fprintf(w, "%s\n", out)
 	}
 }
 
diff --git a/pkg/state/create.go b/pkg/state/create.go
index 2706ab3..8db31fe 100644
--- a/pkg/state/create.go
+++ b/pkg/state/create.go
@@ -272,6 +272,7 @@ func (c *StateCreator) loadEnvValues(st *HelmState, name string, failOnMissingEn
 func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles []string, envVals map[string]interface{}, readFile func(string) ([]byte, error)) error {
 	var errs []error
 
+	helm := c.getHelm(st)
 	inputs := envSecretFiles
 	inputsSize := len(inputs)
 
@@ -294,8 +295,8 @@ func (c *StateCreator) scatterGatherEnvSecretFiles(st *HelmState, envSecretFiles
 		func(id int) {
 			for path := range secrets {
 				release := &ReleaseSpec{}
-				flags := st.appendConnectionFlags([]string{}, release)
-				decFile, err := c.getHelm(st).DecryptSecret(st.createHelmContext(release, 0), path, flags...)
+				flags := st.appendConnectionFlags([]string{}, helm, release)
+				decFile, err := helm.DecryptSecret(st.createHelmContext(release, 0), path, flags...)
 				if err != nil {
 					results <- secretResult{nil, err, path}
 					continue
diff --git a/pkg/state/state.go b/pkg/state/state.go
index 24b9949..7bbbbd4 100644
--- a/pkg/state/state.go
+++ b/pkg/state/state.go
@@ -70,7 +70,7 @@ type ReleaseSetSpec struct {
 	// If set to "Error", return an error when a subhelmfile points to a
 	// non-existent path. The default behavior is to print a warning. Note the
 	// differing default compared to other MissingFileHandlers.
-	MissingFileHandler string `yaml:"missingFileHandler"`
+	MissingFileHandler string `yaml:"missingFileHandler,omitempty"`
 }
 
 // HelmState structure for the helmfile
@@ -211,6 +211,10 @@ type ReleaseSpec struct {
 	// FYI, such diff without `--disable-validation` fails on first install because the K8s cluster doesn't have CRDs registered yet.
 	DisableValidation *bool `yaml:"disableValidation,omitempty"`
 
+	// DisableValidationOnInstall disables the K8s API validation while running helm-diff on the release being newly installed on helmfile-apply.
+	// It is useful when any release contains custom resources for CRDs that is not yet installed onto the cluster.
+	DisableValidationOnInstall *bool `yaml:"disableValidationOnInstall,omitempty"`
+
 	// 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.
 	// The default value for MissingFileHandler is "Error".
 	MissingFileHandler *string `yaml:"missingFileHandler,omitempty"`
@@ -280,12 +284,12 @@ type ReleaseSpec struct {
 	// This is only needed when you can't FIX your chart to have `namespace: {{ .Namespace }}` AND you're using `helmfile template`.
 	// In standard use-cases, `Namespace` should be sufficient.
 	// Use this only when you know what you want to do!
-	ForceNamespace string `yaml:"forceNamespace"`
+	ForceNamespace string `yaml:"forceNamespace,omitempty"`
 
 	// SkipDeps disables running `helm dependency up` and `helm dependency build` on this release's chart.
 	// This is relevant only when your release uses a local chart or a directory containing K8s manifests or a Kustomization
 	// as a Helm chart.
-	SkipDeps *bool `yaml:"skipDeps"`
+	SkipDeps *bool `yaml:"skipDeps,omitempty"`
 }
 
 type Release struct {
@@ -615,7 +619,7 @@ func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, h
 					} else {
 						args = []string{"--purge"}
 					}
-					deletionFlags := st.appendConnectionFlags(args, release)
+					deletionFlags := st.appendConnectionFlags(args, helm, release)
 					m.Lock()
 					if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil {
 						affectedReleases.Failed = append(affectedReleases.Failed, release)
@@ -729,7 +733,7 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
 						} else {
 							args = []string{"--purge"}
 						}
-						deletionFlags := st.appendConnectionFlags(args, release)
+						deletionFlags := st.appendConnectionFlags(args, helm, release)
 						m.Lock()
 						if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil {
 							affectedReleases.Failed = append(affectedReleases.Failed, release)
@@ -798,7 +802,7 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
 }
 
 func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
-	flags := st.connectionFlags(release)
+	flags := st.connectionFlags(helm, release)
 	if helm.IsHelm3() && release.Namespace != "" {
 		flags = append(flags, "--namespace", release.Namespace)
 	}
@@ -1414,14 +1418,17 @@ func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []st
 }
 
 type diffResult struct {
-	err *ReleaseError
+	release *ReleaseSpec
+	err     *ReleaseError
+	buf     *bytes.Buffer
 }
 
 type diffPrepareResult struct {
-	release *ReleaseSpec
-	flags   []string
-	errors  []*ReleaseError
-	files   []string
+	release                 *ReleaseSpec
+	flags                   []string
+	errors                  []*ReleaseError
+	files                   []string
+	upgradeDueToSkippedDiff bool
 }
 
 func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, includeTests, suppressSecrets bool, opt ...DiffOpt) ([]diffPrepareResult, []error) {
@@ -1430,6 +1437,29 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
 		o.Apply(opts)
 	}
 
+	mu := &sync.Mutex{}
+	installedReleases := map[string]bool{}
+
+	isInstalled := func(r *ReleaseSpec) bool {
+		mu.Lock()
+		defer mu.Unlock()
+
+		id := ReleaseToID(r)
+
+		if v, ok := installedReleases[id]; ok {
+			return v
+		}
+
+		v, err := st.isReleaseInstalled(st.createHelmContext(r, 0), helm, *r)
+		if err != nil {
+			st.logger.Warnf("confirming if the release is already installed or not: %v", err)
+		} else {
+			installedReleases[id] = v
+		}
+
+		return v
+	}
+
 	releases := []*ReleaseSpec{}
 	for i, _ := range st.Releases {
 		if !st.Releases[i].Desired() {
@@ -1465,10 +1495,17 @@ func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValu
 
 				st.ApplyOverrides(release)
 
+				if opts.SkipDiffOnInstall && !isInstalled(release) {
+					results <- diffPrepareResult{release: release, upgradeDueToSkippedDiff: true}
+					continue
+				}
+
+				disableValidation := release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall && !isInstalled(release)
+
 				// TODO We need a long-term fix for this :)
 				// See https://github.com/roboll/helmfile/issues/737
 				mut.Lock()
-				flags, files, err := st.flagsForDiff(helm, release, workerIndex)
+				flags, files, err := st.flagsForDiff(helm, release, disableValidation, workerIndex)
 				mut.Unlock()
 				if err != nil {
 					errs = append(errs, err)
@@ -1565,12 +1602,22 @@ func (st *HelmState) createHelmContext(spec *ReleaseSpec, workerIndex int) helme
 	}
 }
 
+func (st *HelmState) createHelmContextWithWriter(spec *ReleaseSpec, w io.Writer) helmexec.HelmContext {
+	ctx := st.createHelmContext(spec, 0)
+
+	ctx.Writer = w
+
+	return ctx
+}
+
 type DiffOpts struct {
 	Context int
 	NoColor bool
 	Set     []string
 
 	SkipCleanup bool
+
+	SkipDiffOnInstall bool
 }
 
 func (o *DiffOpts) Apply(opts *DiffOpts) {
@@ -1580,7 +1627,13 @@ func (o *DiffOpts) Apply(opts *DiffOpts) {
 type DiffOpt interface{ Apply(*DiffOpts) }
 
 // DiffReleases wrapper for executing helm diff on the releases
-// It returns releases that had any changes
+// It returns releases that had any changes, and errors if any.
+//
+// This function has responsibility to stabilize the order of writes to stdout from multiple concurrent helm-diff runs.
+// It's required to use the stdout from helmfile-diff to detect if there was another change(s) between 2 points in time.
+// For example, terraform-provider-helmfile runs a helmfile-diff on `terraform plan` and another on `terraform apply`.
+// `terraform`, by design, fails when helmfile-diff outputs were not equivalent.
+// Stabilized helmfile-diff output rescues that.
 func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, includeTests, suppressSecrets, suppressDiff, triggerCleanupEvents bool, opt ...DiffOpt) ([]ReleaseSpec, []error) {
 	opts := &DiffOpts{}
 	for _, o := range opt {
@@ -1607,8 +1660,12 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
 	results := make(chan diffResult, len(preps))
 
 	rs := []ReleaseSpec{}
+	outputs := map[string]*bytes.Buffer{}
 	errs := []error{}
 
+	// The exit code returned by helm-diff when it detected any changes
+	HelmDiffExitCodeChanged := 2
+
 	st.scatterGather(
 		workerLimit,
 		len(preps),
@@ -1622,17 +1679,20 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
 			for prep := range jobQueue {
 				flags := prep.flags
 				release := prep.release
-				if err := helm.DiffRelease(st.createHelmContext(release, workerIndex), release.Name, normalizeChart(st.basePath, release.Chart), suppressDiff, flags...); err != nil {
+				buf := &bytes.Buffer{}
+				if prep.upgradeDueToSkippedDiff {
+					results <- diffResult{release, &ReleaseError{ReleaseSpec: release, err: nil, Code: HelmDiffExitCodeChanged}, buf}
+				} else if err := helm.DiffRelease(st.createHelmContextWithWriter(release, buf), release.Name, normalizeChart(st.basePath, release.Chart), suppressDiff, flags...); err != nil {
 					switch e := err.(type) {
 					case helmexec.ExitError:
 						// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
-						results <- diffResult{&ReleaseError{release, err, e.ExitStatus()}}
+						results <- diffResult{release, &ReleaseError{release, err, e.ExitStatus()}, buf}
 					default:
-						results <- diffResult{&ReleaseError{release, err, 0}}
+						results <- diffResult{release, &ReleaseError{release, err, 0}, buf}
 					}
 				} else {
 					// diff succeeded, found no changes
-					results <- diffResult{}
+					results <- diffResult{release, nil, buf}
 				}
 
 				if triggerCleanupEvents {
@@ -1647,14 +1707,22 @@ func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []st
 				res := <-results
 				if res.err != nil {
 					errs = append(errs, res.err)
-					if res.err.Code == 2 {
+					if res.err.Code == HelmDiffExitCodeChanged {
 						rs = append(rs, *res.err.ReleaseSpec)
 					}
 				}
+
+				outputs[ReleaseToID(res.release)] = res.buf
 			}
 		},
 	)
 
+	for _, p := range preps {
+		if stdout, ok := outputs[ReleaseToID(p.release)]; ok {
+			fmt.Print(stdout.String())
+		}
+	}
+
 	return rs, errs
 }
 
@@ -1670,7 +1738,7 @@ func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) [
 		if helm.IsHelm3() && release.Namespace != "" {
 			flags = append(flags, "--namespace", release.Namespace)
 		}
-		flags = st.appendConnectionFlags(flags, &release)
+		flags = st.appendConnectionFlags(flags, helm, &release)
 
 		return helm.ReleaseStatus(st.createHelmContext(&release, workerIndex), release.Name, flags...)
 	})
@@ -1685,7 +1753,7 @@ func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm hel
 		if purge && !helm.IsHelm3() {
 			flags = append(flags, "--purge")
 		}
-		flags = st.appendConnectionFlags(flags, &release)
+		flags = st.appendConnectionFlags(flags, helm, &release)
 		if helm.IsHelm3() && release.Namespace != "" {
 			flags = append(flags, "--namespace", release.Namespace)
 		}
@@ -1758,7 +1826,7 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
 			flags = append(flags, "--timeout", duration)
 		}
 
-		flags = st.appendConnectionFlags(flags, &release)
+		flags = st.appendConnectionFlags(flags, helm, &release)
 
 		return helm.TestRelease(st.createHelmContext(&release, workerIndex), release.Name, flags...)
 	})
@@ -1994,25 +2062,27 @@ func findChartDirectory(topLevelDir string) (string, error) {
 }
 
 // appendConnectionFlags append all the helm command-line flags related to K8s API and Tiller connection including the kubecontext
-func (st *HelmState) appendConnectionFlags(flags []string, release *ReleaseSpec) []string {
-	adds := st.connectionFlags(release)
+func (st *HelmState) appendConnectionFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec) []string {
+	adds := st.connectionFlags(helm, release)
 	for _, a := range adds {
 		flags = append(flags, a)
 	}
 	return flags
 }
 
-func (st *HelmState) connectionFlags(release *ReleaseSpec) []string {
+func (st *HelmState) connectionFlags(helm helmexec.Interface, release *ReleaseSpec) []string {
 	flags := []string{}
 	tillerless := st.HelmDefaults.Tillerless
 	if release.Tillerless != nil {
 		tillerless = *release.Tillerless
 	}
 	if !tillerless {
-		if release.TillerNamespace != "" {
-			flags = append(flags, "--tiller-namespace", release.TillerNamespace)
-		} else if st.HelmDefaults.TillerNamespace != "" {
-			flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
+		if !helm.IsHelm3() {
+			if release.TillerNamespace != "" {
+				flags = append(flags, "--tiller-namespace", release.TillerNamespace)
+			} else if st.HelmDefaults.TillerNamespace != "" {
+				flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace)
+			}
 		}
 
 		if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS {
@@ -2109,7 +2179,7 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
 		flags = append(flags, "--disable-openapi-validation")
 	}
 
-	flags = st.appendConnectionFlags(flags, release)
+	flags = st.appendConnectionFlags(flags, helm, release)
 
 	var err error
 	flags, err = st.appendHelmXFlags(flags, release)
@@ -2151,7 +2221,7 @@ func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseS
 	return append(flags, common...), files, nil
 }
 
-func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) {
+func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, disableValidation bool, workerIndex int) ([]string, []string, error) {
 	flags := st.chartVersionFlags(release)
 
 	disableOpenAPIValidation := false
@@ -2165,7 +2235,6 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
 		flags = append(flags, "--disable-openapi-validation")
 	}
 
-	disableValidation := false
 	if release.DisableValidation != nil {
 		disableValidation = *release.DisableValidation
 	} else if st.HelmDefaults.DisableValidation != nil {
@@ -2176,7 +2245,7 @@ func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec,
 		flags = append(flags, "--disable-validation")
 	}
 
-	flags = st.appendConnectionFlags(flags, release)
+	flags = st.appendConnectionFlags(flags, helm, release)
 
 	var err error
 	flags, err = st.appendHelmXFlags(flags, release)
@@ -2449,7 +2518,7 @@ func (st *HelmState) generateSecretValuesFiles(helm helmexec.Interface, release
 		}
 		path := paths[0]
 
-		decryptFlags := st.appendConnectionFlags([]string{}, release)
+		decryptFlags := st.appendConnectionFlags([]string{}, helm, release)
 		valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...)
 		if err != nil {
 			return nil, err
@@ -2608,6 +2677,23 @@ func escape(value string) string {
 	return strings.Replace(intermediate, ",", "\\,", -1)
 }
 
+//MarshalYAML will ensure we correctly marshal SubHelmfileSpec structure correctly so it can be unmarshalled at some
+//future time
+func (p SubHelmfileSpec) MarshalYAML() (interface{}, error) {
+	type SubHelmfileSpecTmp struct {
+		Path               string        `yaml:"path,omitempty"`
+		Selectors          []string      `yaml:"selectors,omitempty"`
+		SelectorsInherited bool          `yaml:"selectorsInherited,omitempty"`
+		OverrideValues     []interface{} `yaml:"values,omitempty"`
+	}
+	return &SubHelmfileSpecTmp{
+		Path:               p.Path,
+		Selectors:          p.Selectors,
+		SelectorsInherited: p.SelectorsInherited,
+		OverrideValues:     p.Environment.OverrideValues,
+	}, nil
+}
+
 //UnmarshalYAML will unmarshal the helmfile yaml section and fill the SubHelmfileSpec structure
 //this is required to keep allowing string scalar for defining helmfile
 func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) error {


Test patch:
diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go
index 13dd3ca..af215a4 100644
--- a/pkg/app/app_test.go
+++ b/pkg/app/app_test.go
@@ -2313,6 +2313,7 @@ type applyConfig struct {
 	concurrency       int
 	detailedExitcode  bool
 	interactive       bool
+	skipDiffOnInstall bool
 	logger            *zap.SugaredLogger
 }
 
@@ -2380,6 +2381,10 @@ func (a applyConfig) RetainValuesFiles() bool {
 	return a.retainValuesFiles
 }
 
+func (a applyConfig) SkipDiffOnInstall() bool {
+	return a.skipDiffOnInstall
+}
+
 type depsConfig struct {
 	skipRepos bool
 }
@@ -2651,18 +2656,19 @@ releases:
 
 func TestApply(t *testing.T) {
 	testcases := []struct {
-		name        string
-		loc         string
-		ns          string
-		concurrency int
-		error       string
-		files       map[string]string
-		selectors   []string
-		lists       map[exectest.ListKey]string
-		diffs       map[exectest.DiffKey]error
-		upgraded    []exectest.Release
-		deleted     []exectest.Release
-		log         string
+		name              string
+		loc               string
+		ns                string
+		concurrency       int
+		skipDiffOnInstall bool
+		error             string
+		files             map[string]string
+		selectors         []string
+		lists             map[exectest.ListKey]string
+		diffs             map[exectest.DiffKey]error
+		upgraded          []exectest.Release
+		deleted           []exectest.Release
+		log               string
 	}{
 		//
 		// complex test cases for smoke testing
@@ -3071,6 +3077,222 @@ baz    stable/mychart3
 bar    stable/mychart2          
 foo    stable/mychart1          
 
+`,
+		},
+		//
+		// install with upgrade
+		//
+		{
+			name: "install-with-upgrade-with-validation-control",
+			loc:  location(),
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: baz
+  chart: stable/mychart3
+  disableValidationOnInstall: true
+- name: foo
+  chart: stable/mychart1
+  disableValidationOnInstall: true
+  needs:
+  - bar
+- name: bar
+  chart: stable/mychart2
+  disableValidation: true
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}:                     helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "foo", Chart: "stable/mychart1", Flags: "--disable-validation--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: ``,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^baz$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+baz 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart3-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{
+				{Name: "baz", Flags: []string{"--kube-context", "default"}},
+				{Name: "bar", Flags: []string{"--kube-context", "default"}},
+				{Name: "foo", Flags: []string{"--kube-context", "default"}},
+			},
+			deleted:     []exectest.Release{},
+			concurrency: 1,
+			log: `processing file "helmfile.yaml" in directory "."
+first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
+first-pass uses: &{default map[] map[]}
+first-pass rendering output of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   chart: stable/mychart3
+ 4:   disableValidationOnInstall: true
+ 5: - name: foo
+ 6:   chart: stable/mychart1
+ 7:   disableValidationOnInstall: true
+ 8:   needs:
+ 9:   - bar
+10: - name: bar
+11:   chart: stable/mychart2
+12:   disableValidation: true
+13: 
+
+first-pass produced: &{default map[] map[]}
+first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
+vals:
+map[]
+defaultVals:[]
+second-pass rendering result of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   chart: stable/mychart3
+ 4:   disableValidationOnInstall: true
+ 5: - name: foo
+ 6:   chart: stable/mychart1
+ 7:   disableValidationOnInstall: true
+ 8:   needs:
+ 9:   - bar
+10: - name: bar
+11:   chart: stable/mychart2
+12:   disableValidation: true
+13: 
+
+merged environment: &{default map[] map[]}
+3 release(s) found in helmfile.yaml
+
+Affected releases are:
+  bar (stable/mychart2) UPDATED
+  baz (stable/mychart3) UPDATED
+  foo (stable/mychart1) UPDATED
+
+processing 2 groups of releases in this order:
+GROUP RELEASES
+1     baz, bar
+2     foo
+
+processing releases in group 1/2: baz, bar
+processing releases in group 2/2: foo
+getting deployed release version failed:Failed to get the version for:mychart1
+
+UPDATED RELEASES:
+NAME   CHART             VERSION
+baz    stable/mychart3     3.1.0
+bar    stable/mychart2     3.1.0
+foo    stable/mychart1          
+
+`,
+		},
+		//
+		// install with upgrade and --skip-diff-on-install
+		//
+		{
+			name:              "install-with-upgrade-with-skip-diff-on-install",
+			loc:               location(),
+			skipDiffOnInstall: true,
+			files: map[string]string{
+				"/path/to/helmfile.yaml": `
+releases:
+- name: baz
+  chart: stable/mychart3
+  disableValidationOnInstall: true
+- name: foo
+  chart: stable/mychart1
+  disableValidationOnInstall: true
+  needs:
+  - bar
+- name: bar
+  chart: stable/mychart2
+  disableValidation: true
+`,
+			},
+			diffs: map[exectest.DiffKey]error{
+				exectest.DiffKey{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-contextdefault--detailed-exitcode"}:                     helmexec.ExitError{Code: 2},
+				exectest.DiffKey{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2},
+			},
+			lists: map[exectest.ListKey]string{
+				exectest.ListKey{Filter: "^foo$", Flags: "--kube-contextdefault--deployed--failed--pending"}: ``,
+				exectest.ListKey{Filter: "^bar$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+bar 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart2-3.1.0	3.1.0      	default
+`,
+				exectest.ListKey{Filter: "^baz$", Flags: "--kube-contextdefault--deployed--failed--pending"}: `NAME	REVISION	UPDATED                 	STATUS  	CHART        	APP VERSION	NAMESPACE
+baz 	4       	Fri Nov  1 08:40:07 2019	DEPLOYED	mychart3-3.1.0	3.1.0      	default
+`,
+			},
+			upgraded: []exectest.Release{
+				{Name: "baz", Flags: []string{"--kube-context", "default"}},
+				{Name: "bar", Flags: []string{"--kube-context", "default"}},
+				{Name: "foo", Flags: []string{"--kube-context", "default"}},
+			},
+			deleted:     []exectest.Release{},
+			concurrency: 1,
+			log: `processing file "helmfile.yaml" in directory "."
+first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
+first-pass uses: &{default map[] map[]}
+first-pass rendering output of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   chart: stable/mychart3
+ 4:   disableValidationOnInstall: true
+ 5: - name: foo
+ 6:   chart: stable/mychart1
+ 7:   disableValidationOnInstall: true
+ 8:   needs:
+ 9:   - bar
+10: - name: bar
+11:   chart: stable/mychart2
+12:   disableValidation: true
+13: 
+
+first-pass produced: &{default map[] map[]}
+first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
+vals:
+map[]
+defaultVals:[]
+second-pass rendering result of "helmfile.yaml.part.0":
+ 0: 
+ 1: releases:
+ 2: - name: baz
+ 3:   chart: stable/mychart3
+ 4:   disableValidationOnInstall: true
+ 5: - name: foo
+ 6:   chart: stable/mychart1
+ 7:   disableValidationOnInstall: true
+ 8:   needs:
+ 9:   - bar
+10: - name: bar
+11:   chart: stable/mychart2
+12:   disableValidation: true
+13: 
+
+merged environment: &{default map[] map[]}
+3 release(s) found in helmfile.yaml
+
+Affected releases are:
+  bar (stable/mychart2) UPDATED
+  baz (stable/mychart3) UPDATED
+  foo (stable/mychart1) UPDATED
+
+processing 2 groups of releases in this order:
+GROUP RELEASES
+1     baz, bar
+2     foo
+
+processing releases in group 1/2: baz, bar
+processing releases in group 2/2: foo
+getting deployed release version failed:Failed to get the version for:mychart1
+
+UPDATED RELEASES:
+NAME   CHART             VERSION
+baz    stable/mychart3     3.1.0
+bar    stable/mychart2     3.1.0
+foo    stable/mychart1          
+
 `,
 		},
 		//
@@ -3919,8 +4141,9 @@ err: "foo" depends on nonexistent release "bar"
 
 				applyErr := app.Apply(applyConfig{
 					// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
-					concurrency: tc.concurrency,
-					logger:      logger,
+					concurrency:       tc.concurrency,
+					logger:            logger,
+					skipDiffOnInstall: tc.skipDiffOnInstall,
 				})
 				if tc.error == "" && applyErr != nil {
 					t.Fatalf("unexpected error for data defined at %s: %v", tc.loc, applyErr)