feat: show live output from the Helm binary (#286)
* feat: show live output from the Helm binary Signed-off-by: Rodrigo Fior Kuntzer <rodrigo@miro.com> * fixup! Merge branch 'main' into enable-live-output Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>
This commit is contained in:
		
							parent
							
								
									c828d22a5c
								
							
						
					
					
						commit
						8408b021f0
					
				|  | @ -52,16 +52,32 @@ jobs: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     strategy: |     strategy: | ||||||
|       matrix: |       matrix: | ||||||
|         helm-version: |        include: | ||||||
|           - v3.4.2 |          # We intend to support 2 helm minor version at a time. | ||||||
|           - v3.5.4 |          # What's why we include only 2 helm minor versions in this matrix. | ||||||
|           - v3.6.3 |          # See https://github.com/helmfile/helmfile/pull/286#issuecomment-1250161182 for more context. | ||||||
|           - v3.7.2 |          - helm-version: v3.8.2 | ||||||
|           - v3.8.2 |            plugin-secrets-version: 3.15.0 | ||||||
|           - v3.9.4 |            extra-helmfile-flags: | ||||||
|         plugin-secrets-version: |          - helm-version: v3.8.2 | ||||||
|           - 3.15.0 |            # We assume that the helm-secrets plugin is supposed to | ||||||
|           - 4.0.0 |            # work with the two most recent helm minor versions. | ||||||
|  |            # Once it turned out to be not practically true, | ||||||
|  |            # we will mark this combination as failable, | ||||||
|  |            # and instruct users to upgrade helm and helm-secrets at once. | ||||||
|  |            plugin-secrets-version: 4.0.0 | ||||||
|  |            extra-helmfile-flags: | ||||||
|  |          - helm-version: v3.9.4 | ||||||
|  |            plugin-secrets-version: 3.15.0 | ||||||
|  |            extra-helmfile-flags: | ||||||
|  |          - helm-version: v3.9.4 | ||||||
|  |            plugin-secrets-version: 4.0.0 | ||||||
|  |            extra-helmfile-flags: | ||||||
|  |          # In case you need to test some optional helmfile features, | ||||||
|  |          # enable it via extra-helmfile-flags below. | ||||||
|  |          - helm-version: v3.9.4 | ||||||
|  |            plugin-secrets-version: 4.0.0 | ||||||
|  |            extra-helmfile-flags: "--enable-live-output" | ||||||
|     steps: |     steps: | ||||||
|     - uses: actions/checkout@v2 |     - uses: actions/checkout@v2 | ||||||
|     - name: Cache libraries |     - name: Cache libraries | ||||||
|  | @ -102,4 +118,5 @@ jobs: | ||||||
|         HELM_SECRETS_VERSION: ${{ matrix.plugin-secrets-version }} |         HELM_SECRETS_VERSION: ${{ matrix.plugin-secrets-version }} | ||||||
|         HELMFILE_HELM3: 1 |         HELMFILE_HELM3: 1 | ||||||
|         TERM: xterm |         TERM: xterm | ||||||
|  |         EXTRA_HELMFILE_FLAGS: ${{ matrix.extra-helmfile-flags }} | ||||||
|       run: make integration |       run: make integration | ||||||
|  |  | ||||||
|  | @ -122,6 +122,8 @@ A release must match all labels in a group in order to be used. Multiple groups | ||||||
| "--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases. | "--selector tier=frontend,tier!=proxy --selector tier=backend" will match all frontend, non-proxy releases AND all backend releases. | ||||||
| The name of a release can be used as a label: "--selector name=myrelease"`) | The name of a release can be used as a label: "--selector name=myrelease"`) | ||||||
| 	fs.BoolVar(&globalOptions.AllowNoMatchingRelease, "allow-no-matching-release", false, `Do not exit with an error code if the provided selector has no matching releases.`) | 	fs.BoolVar(&globalOptions.AllowNoMatchingRelease, "allow-no-matching-release", false, `Do not exit with an error code if the provided selector has no matching releases.`) | ||||||
|  | 	fs.BoolVar(&globalOptions.EnableLiveOutput, "enable-live-output", globalOptions.EnableLiveOutput, `Show live output from the Helm binary Stdout/Stderr into Helmfile own Stdout/Stderr. | ||||||
|  | 	It only applies for the Helm CLI commands, Stdout/Stderr for Hooks are still displayed only when it's execution finishes.`) | ||||||
| 	// avoid 'pflag: help requested' error (#251)
 | 	// avoid 'pflag: help requested' error (#251)
 | ||||||
| 	fs.BoolP("help", "h", false, "help for helmfile") | 	fs.BoolP("help", "h", false, "help for helmfile") | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ import ( | ||||||
| type App struct { | type App struct { | ||||||
| 	OverrideKubeContext string | 	OverrideKubeContext string | ||||||
| 	OverrideHelmBinary  string | 	OverrideHelmBinary  string | ||||||
|  | 	EnableLiveOutput    bool | ||||||
| 
 | 
 | ||||||
| 	Logger      *zap.SugaredLogger | 	Logger      *zap.SugaredLogger | ||||||
| 	Env         string | 	Env         string | ||||||
|  | @ -64,6 +65,7 @@ func New(conf ConfigProvider) *App { | ||||||
| 	return Init(&App{ | 	return Init(&App{ | ||||||
| 		OverrideKubeContext: conf.KubeContext(), | 		OverrideKubeContext: conf.KubeContext(), | ||||||
| 		OverrideHelmBinary:  conf.HelmBinary(), | 		OverrideHelmBinary:  conf.HelmBinary(), | ||||||
|  | 		EnableLiveOutput:    conf.EnableLiveOutput(), | ||||||
| 		Logger:              conf.Logger(), | 		Logger:              conf.Logger(), | ||||||
| 		Env:                 conf.Env(), | 		Env:                 conf.Env(), | ||||||
| 		Namespace:           conf.Namespace(), | 		Namespace:           conf.Namespace(), | ||||||
|  | @ -84,6 +86,10 @@ func Init(app *App) *App { | ||||||
| 		panic(fmt.Sprintf("Failed to initialize vals runtime: %v", err)) | 		panic(fmt.Sprintf("Failed to initialize vals runtime: %v", err)) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if app.EnableLiveOutput { | ||||||
|  | 		app.Logger.Info("Live output is enabled") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return app | 	return app | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -213,6 +219,9 @@ func (a *App) Template(c TemplateConfigProvider) error { | ||||||
| 	return a.ForEachState(func(run *Run) (ok bool, errs []error) { | 	return a.ForEachState(func(run *Run) (ok bool, errs []error) { | ||||||
| 		includeCRDs := c.IncludeCRDs() | 		includeCRDs := c.IncludeCRDs() | ||||||
| 
 | 
 | ||||||
|  | 		// Live output should never be enabled for the "template" subcommand to avoid breaking `helmfile template | kubectl apply -f -`
 | ||||||
|  | 		run.helm.SetEnableLiveOutput(false) | ||||||
|  | 
 | ||||||
| 		// `helm template` in helm v2 does not support local chart.
 | 		// `helm template` in helm v2 does not support local chart.
 | ||||||
| 		// So, we set forceDownload=true for helm v2 only
 | 		// So, we set forceDownload=true for helm v2 only
 | ||||||
| 		prepErr := run.withPreparedCharts("template", state.ChartPrepareOptions{ | 		prepErr := run.withPreparedCharts("template", state.ChartPrepareOptions{ | ||||||
|  | @ -717,6 +726,7 @@ func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.He | ||||||
| 
 | 
 | ||||||
| 		overrideKubeContext: a.OverrideKubeContext, | 		overrideKubeContext: a.OverrideKubeContext, | ||||||
| 		overrideHelmBinary:  a.OverrideHelmBinary, | 		overrideHelmBinary:  a.OverrideHelmBinary, | ||||||
|  | 		enableLiveOutput:    a.EnableLiveOutput, | ||||||
| 		getHelm:             a.getHelm, | 		getHelm:             a.getHelm, | ||||||
| 		valsRuntime:         a.valsRuntime, | 		valsRuntime:         a.valsRuntime, | ||||||
| 	} | 	} | ||||||
|  | @ -755,7 +765,7 @@ func (a *App) getHelm(st *state.HelmState) helmexec.Interface { | ||||||
| 	key := createHelmKey(bin, kubectx) | 	key := createHelmKey(bin, kubectx) | ||||||
| 
 | 
 | ||||||
| 	if _, ok := a.helms[key]; !ok { | 	if _, ok := a.helms[key]; !ok { | ||||||
| 		a.helms[key] = helmexec.New(bin, a.Logger, kubectx, &helmexec.ShellRunner{ | 		a.helms[key] = helmexec.New(bin, a.EnableLiveOutput, a.Logger, kubectx, &helmexec.ShellRunner{ | ||||||
| 			Logger: a.Logger, | 			Logger: a.Logger, | ||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -2488,12 +2488,12 @@ func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]s | ||||||
| 	return []byte{}, nil | 	return []byte{}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) { | func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) { | ||||||
| 	return []byte{}, nil | 	return []byte{}, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface { | func MockExecer(logger *zap.SugaredLogger, kubeContext string) helmexec.Interface { | ||||||
| 	execer := helmexec.New("helm", logger, kubeContext, &mockRunner{}) | 	execer := helmexec.New("helm", false, logger, kubeContext, &mockRunner{}) | ||||||
| 	return execer | 	return execer | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -2538,6 +2538,8 @@ func (helm *mockHelmExec) SetExtraArgs(args ...string) { | ||||||
| } | } | ||||||
| func (helm *mockHelmExec) SetHelmBinary(bin string) { | func (helm *mockHelmExec) SetHelmBinary(bin string) { | ||||||
| } | } | ||||||
|  | func (helm *mockHelmExec) SetEnableLiveOutput(enableLiveOutput bool) { | ||||||
|  | } | ||||||
| func (helm *mockHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | func (helm *mockHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | ||||||
| 	helm.repos = append(helm.repos, mockRepo{Name: name}) | 	helm.repos = append(helm.repos, mockRepo{Name: name}) | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import "go.uber.org/zap" | ||||||
| type ConfigProvider interface { | type ConfigProvider interface { | ||||||
| 	Args() string | 	Args() string | ||||||
| 	HelmBinary() string | 	HelmBinary() string | ||||||
|  | 	EnableLiveOutput() bool | ||||||
| 
 | 
 | ||||||
| 	FileOrDir() string | 	FileOrDir() string | ||||||
| 	KubeContext() string | 	KubeContext() string | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ const ( | ||||||
| type desiredStateLoader struct { | type desiredStateLoader struct { | ||||||
| 	overrideKubeContext string | 	overrideKubeContext string | ||||||
| 	overrideHelmBinary  string | 	overrideHelmBinary  string | ||||||
|  | 	enableLiveOutput    bool | ||||||
| 
 | 
 | ||||||
| 	env       string | 	env       string | ||||||
| 	namespace string | 	namespace string | ||||||
|  | @ -162,7 +163,7 @@ func (ld *desiredStateLoader) loadFileWithOverrides(inheritedEnv, overrodeEnv *e | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *desiredStateLoader) underlying() *state.StateCreator { | func (a *desiredStateLoader) underlying() *state.StateCreator { | ||||||
| 	c := state.NewCreator(a.logger, a.fs, a.valsRuntime, a.getHelm, a.overrideHelmBinary, a.remote) | 	c := state.NewCreator(a.logger, a.fs, a.valsRuntime, a.getHelm, a.overrideHelmBinary, a.remote, a.enableLiveOutput) | ||||||
| 	c.LoadFile = a.loadFile | 	c.LoadFile = a.loadFile | ||||||
| 	return c | 	return c | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -46,6 +46,9 @@ func (helm *noCallHelmExec) SetExtraArgs(args ...string) { | ||||||
| func (helm *noCallHelmExec) SetHelmBinary(bin string) { | func (helm *noCallHelmExec) SetHelmBinary(bin string) { | ||||||
| 	helm.doPanic() | 	helm.doPanic() | ||||||
| } | } | ||||||
|  | func (helm *noCallHelmExec) SetEnableLiveOutput(enableLiveOutput bool) { | ||||||
|  | 	helm.doPanic() | ||||||
|  | } | ||||||
| func (helm *noCallHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | func (helm *noCallHelmExec) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | ||||||
| 	helm.doPanic() | 	helm.doPanic() | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
|  | @ -44,6 +44,8 @@ type GlobalOptions struct { | ||||||
| 	AllowNoMatchingRelease bool | 	AllowNoMatchingRelease bool | ||||||
| 	// logger is the logger to use.
 | 	// logger is the logger to use.
 | ||||||
| 	logger *zap.SugaredLogger | 	logger *zap.SugaredLogger | ||||||
|  | 	// EnableLiveOutput enables live output from the Helm binary stdout/stderr into Helmfile own stdout/stderr
 | ||||||
|  | 	EnableLiveOutput bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Logger returns the logger to use.
 | // Logger returns the logger to use.
 | ||||||
|  | @ -120,6 +122,11 @@ func (g *GlobalImpl) StateValuesFiles() []string { | ||||||
| 	return g.GlobalOptions.StateValuesFile | 	return g.GlobalOptions.StateValuesFile | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // EnableLiveOutput return when to pipe the stdout and stderr from Helm live to the helmfile stdout
 | ||||||
|  | func (g *GlobalImpl) EnableLiveOutput() bool { | ||||||
|  | 	return g.GlobalOptions.EnableLiveOutput | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Logger returns the logger
 | // Logger returns the logger
 | ||||||
| func (g *GlobalImpl) Logger() *zap.SugaredLogger { | func (g *GlobalImpl) Logger() *zap.SugaredLogger { | ||||||
| 	return g.GlobalOptions.logger | 	return g.GlobalOptions.logger | ||||||
|  |  | ||||||
|  | @ -118,7 +118,7 @@ func (bus *Bus) Trigger(evt string, evtErr error, context map[string]interface{} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		bytes, err := bus.Runner.Execute(command, args, map[string]string{}) | 		bytes, err := bus.Runner.Execute(command, args, map[string]string{}, false) | ||||||
| 		bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes)) | 		bus.Logger.Debugf("hook[%s]: %s\n", name, string(bytes)) | ||||||
| 		if hook.ShowLogs { | 		if hook.ShowLogs { | ||||||
| 			prefix := fmt.Sprintf("\nhook[%s] logs | ", evt) | 			prefix := fmt.Sprintf("\nhook[%s] logs | ", evt) | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ func (r *runner) ExecuteStdIn(cmd string, args []string, env map[string]string, | ||||||
| 	return []byte(""), nil | 	return []byte(""), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *runner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) { | func (r *runner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) { | ||||||
| 	if cmd == "ng" { | 	if cmd == "ng" { | ||||||
| 		return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd) | 		return nil, fmt.Errorf("cmd failed due to invalid cmd: %s", cmd) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -89,6 +89,8 @@ func (helm *Helm) SetExtraArgs(args ...string) { | ||||||
| } | } | ||||||
| func (helm *Helm) SetHelmBinary(bin string) { | func (helm *Helm) SetHelmBinary(bin string) { | ||||||
| } | } | ||||||
|  | func (helm *Helm) SetEnableLiveOutput(enableLiveOutput bool) { | ||||||
|  | } | ||||||
| func (helm *Helm) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | func (helm *Helm) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | ||||||
| 	helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password, managed, passCredentials, skipTLSVerify} | 	helm.Repo = []string{name, repository, cafile, certfile, keyfile, username, password, managed, passCredentials, skipTLSVerify} | ||||||
| 	return nil | 	return nil | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ type decryptedSecret struct { | ||||||
| 
 | 
 | ||||||
| type execer struct { | type execer struct { | ||||||
| 	helmBinary           string | 	helmBinary           string | ||||||
|  | 	enableLiveOutput     bool | ||||||
| 	version              semver.Version | 	version              semver.Version | ||||||
| 	runner               Runner | 	runner               Runner | ||||||
| 	logger               *zap.SugaredLogger | 	logger               *zap.SugaredLogger | ||||||
|  | @ -78,7 +79,7 @@ func parseHelmVersion(versionStr string) (semver.Version, error) { | ||||||
| 
 | 
 | ||||||
| func getHelmVersion(helmBinary string, runner Runner) (semver.Version, error) { | func getHelmVersion(helmBinary string, runner Runner) (semver.Version, error) { | ||||||
| 	// Autodetect from `helm version`
 | 	// Autodetect from `helm version`
 | ||||||
| 	outBytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil) | 	outBytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return semver.Version{}, fmt.Errorf("error determining helm version: %w", err) | 		return semver.Version{}, fmt.Errorf("error determining helm version: %w", err) | ||||||
| 	} | 	} | ||||||
|  | @ -110,7 +111,7 @@ func redactedURL(chart string) string { | ||||||
| 
 | 
 | ||||||
| // New for running helm commands
 | // New for running helm commands
 | ||||||
| // nolint: golint
 | // nolint: golint
 | ||||||
| func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer { | func New(helmBinary string, enableLiveOutput bool, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer { | ||||||
| 	// TODO: proper error handling
 | 	// TODO: proper error handling
 | ||||||
| 	version, err := getHelmVersion(helmBinary, runner) | 	version, err := getHelmVersion(helmBinary, runner) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -118,6 +119,7 @@ func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runne | ||||||
| 	} | 	} | ||||||
| 	return &execer{ | 	return &execer{ | ||||||
| 		helmBinary:       helmBinary, | 		helmBinary:       helmBinary, | ||||||
|  | 		enableLiveOutput: enableLiveOutput, | ||||||
| 		version:          version, | 		version:          version, | ||||||
| 		logger:           logger, | 		logger:           logger, | ||||||
| 		kubeContext:      kubeContext, | 		kubeContext:      kubeContext, | ||||||
|  | @ -134,6 +136,10 @@ func (helm *execer) SetHelmBinary(bin string) { | ||||||
| 	helm.helmBinary = bin | 	helm.helmBinary = bin | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (helm *execer) SetEnableLiveOutput(enableLiveOutput bool) { | ||||||
|  | 	helm.enableLiveOutput = enableLiveOutput | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error { | ||||||
| 	var args []string | 	var args []string | ||||||
| 	var out []byte | 	var out []byte | ||||||
|  | @ -174,7 +180,7 @@ func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, usernam | ||||||
| 			args = append(args, "--insecure-skip-tls-verify") | 			args = append(args, "--insecure-skip-tls-verify") | ||||||
| 		} | 		} | ||||||
| 		helm.logger.Infof("Adding repo %v %v", name, repository) | 		helm.logger.Infof("Adding repo %v %v", name, repository) | ||||||
| 		out, err = helm.exec(args, map[string]string{}) | 		out, err = helm.exec(args, map[string]string{}, nil) | ||||||
| 	default: | 	default: | ||||||
| 		helm.logger.Errorf("ERROR: unknown type '%v' for repository %v", managed, name) | 		helm.logger.Errorf("ERROR: unknown type '%v' for repository %v", managed, name) | ||||||
| 		out = nil | 		out = nil | ||||||
|  | @ -186,7 +192,7 @@ func (helm *execer) AddRepo(name, repository, cafile, certfile, keyfile, usernam | ||||||
| 
 | 
 | ||||||
| func (helm *execer) UpdateRepo() error { | func (helm *execer) UpdateRepo() error { | ||||||
| 	helm.logger.Info("Updating repo") | 	helm.logger.Info("Updating repo") | ||||||
| 	out, err := helm.exec([]string{"repo", "update"}, map[string]string{}) | 	out, err := helm.exec([]string{"repo", "update"}, map[string]string{}, nil) | ||||||
| 	helm.info(out) | 	helm.info(out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -210,14 +216,14 @@ func (helm *execer) RegistryLogin(repository string, username string, password s | ||||||
| 
 | 
 | ||||||
| func (helm *execer) BuildDeps(name, chart string) error { | func (helm *execer) BuildDeps(name, chart string) error { | ||||||
| 	helm.logger.Infof("Building dependency release=%v, chart=%v", name, chart) | 	helm.logger.Infof("Building dependency release=%v, chart=%v", name, chart) | ||||||
| 	out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{}) | 	out, err := helm.exec([]string{"dependency", "build", chart}, map[string]string{}, nil) | ||||||
| 	helm.info(out) | 	helm.info(out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (helm *execer) UpdateDeps(chart string) error { | func (helm *execer) UpdateDeps(chart string) error { | ||||||
| 	helm.logger.Infof("Updating dependency %v", chart) | 	helm.logger.Infof("Updating dependency %v", chart) | ||||||
| 	out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{}) | 	out, err := helm.exec([]string{"dependency", "update", chart}, map[string]string{}, nil) | ||||||
| 	helm.info(out) | 	helm.info(out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -233,7 +239,7 @@ func (helm *execer) SyncRelease(context HelmContext, name, chart string, flags . | ||||||
| 		env["HELM_TILLER_HISTORY_MAX"] = strconv.Itoa(context.HistoryMax) | 		env["HELM_TILLER_HISTORY_MAX"] = strconv.Itoa(context.HistoryMax) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env) | 	out, err := helm.exec(append(append(preArgs, "upgrade", "--install", "--reset-values", name, chart), flags...), env, nil) | ||||||
| 	helm.write(nil, out) | 	helm.write(nil, out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -242,7 +248,7 @@ func (helm *execer) ReleaseStatus(context HelmContext, name string, flags ...str | ||||||
| 	helm.logger.Infof("Getting status %v", name) | 	helm.logger.Infof("Getting status %v", name) | ||||||
| 	preArgs := context.GetTillerlessArgs(helm) | 	preArgs := context.GetTillerlessArgs(helm) | ||||||
| 	env := context.getTillerlessEnv() | 	env := context.getTillerlessEnv() | ||||||
| 	out, err := helm.exec(append(append(preArgs, "status", name), flags...), env) | 	out, err := helm.exec(append(append(preArgs, "status", name), flags...), env, nil) | ||||||
| 	helm.write(nil, out) | 	helm.write(nil, out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -258,7 +264,8 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s | ||||||
| 		args = []string{"list", filter} | 		args = []string{"list", filter} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	out, err := helm.exec(append(append(preArgs, args...), flags...), env) | 	enableLiveOutput := false | ||||||
|  | 	out, err := helm.exec(append(append(preArgs, args...), flags...), env, &enableLiveOutput) | ||||||
| 	// In v2 we have been expecting `helm list FILTER` prints nothing.
 | 	// In v2 we have been expecting `helm list FILTER` prints nothing.
 | ||||||
| 	// In v3 helm still prints the header like `NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION`,
 | 	// In v3 helm still prints the header like `NAME	NAMESPACE	REVISION	UPDATED	STATUS	CHART	APP VERSION`,
 | ||||||
| 	// which confuses helmfile's existing logic that treats any non-empty output from `helm list` is considered as the indication
 | 	// which confuses helmfile's existing logic that treats any non-empty output from `helm list` is considered as the indication
 | ||||||
|  | @ -308,7 +315,8 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str | ||||||
| 		if pluginVersion.Major() > 3 { | 		if pluginVersion.Major() > 3 { | ||||||
| 			secretArg = "decrypt" | 			secretArg = "decrypt" | ||||||
| 		} | 		} | ||||||
| 		secretBytes, err := helm.exec(append(append(preArgs, "secrets", secretArg, absPath), flags...), env) | 		enableLiveOutput := false | ||||||
|  | 		secretBytes, err := helm.exec(append(append(preArgs, "secrets", secretArg, absPath), flags...), env, &enableLiveOutput) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			secret.err = err | 			secret.err = err | ||||||
| 			return "", err | 			return "", err | ||||||
|  | @ -370,7 +378,7 @@ func (helm *execer) TemplateRelease(name string, chart string, flags ...string) | ||||||
| 		args = []string{"template", chart, "--name", name} | 		args = []string{"template", chart, "--name", name} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	out, err := helm.exec(append(args, flags...), map[string]string{}) | 	out, err := helm.exec(append(args, flags...), map[string]string{}, nil) | ||||||
| 
 | 
 | ||||||
| 	var outputToFile bool | 	var outputToFile bool | ||||||
| 
 | 
 | ||||||
|  | @ -407,7 +415,12 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppres | ||||||
| 	} | 	} | ||||||
| 	preArgs := context.GetTillerlessArgs(helm) | 	preArgs := context.GetTillerlessArgs(helm) | ||||||
| 	env := context.getTillerlessEnv() | 	env := context.getTillerlessEnv() | ||||||
| 	out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--reset-values", "--allow-unreleased", name, chart), flags...), env) | 	var overrideEnableLiveOutput *bool = nil | ||||||
|  | 	if suppressDiff { | ||||||
|  | 		enableLiveOutput := false | ||||||
|  | 		overrideEnableLiveOutput = &enableLiveOutput | ||||||
|  | 	} | ||||||
|  | 	out, err := helm.exec(append(append(preArgs, "diff", "upgrade", "--reset-values", "--allow-unreleased", name, chart), flags...), env, overrideEnableLiveOutput) | ||||||
| 	// Do our best to write STDOUT only when diff existed
 | 	// Do our best to write STDOUT only when diff existed
 | ||||||
| 	// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
 | 	// Unfortunately, this works only when you run helmfile with `--detailed-exitcode`
 | ||||||
| 	detailedExitcodeEnabled := false | 	detailedExitcodeEnabled := false | ||||||
|  | @ -433,14 +446,14 @@ func (helm *execer) DiffRelease(context HelmContext, name, chart string, suppres | ||||||
| 
 | 
 | ||||||
| func (helm *execer) Lint(name, chart string, flags ...string) error { | func (helm *execer) Lint(name, chart string, flags ...string) error { | ||||||
| 	helm.logger.Infof("Linting release=%v, chart=%v", name, chart) | 	helm.logger.Infof("Linting release=%v, chart=%v", name, chart) | ||||||
| 	out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{}) | 	out, err := helm.exec(append([]string{"lint", chart}, flags...), map[string]string{}, nil) | ||||||
| 	helm.write(nil, out) | 	helm.write(nil, out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (helm *execer) Fetch(chart string, flags ...string) error { | func (helm *execer) Fetch(chart string, flags ...string) error { | ||||||
| 	helm.logger.Infof("Fetching %v", redactedURL(chart)) | 	helm.logger.Infof("Fetching %v", redactedURL(chart)) | ||||||
| 	out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{}) | 	out, err := helm.exec(append([]string{"fetch", chart}, flags...), map[string]string{}, nil) | ||||||
| 	helm.info(out) | 	helm.info(out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -463,7 +476,7 @@ func (helm *execer) ChartPull(chart string, flags ...string) error { | ||||||
| 	} else { | 	} else { | ||||||
| 		helmArgs = []string{"chart", "pull", chart} | 		helmArgs = []string{"chart", "pull", chart} | ||||||
| 	} | 	} | ||||||
| 	out, err := helm.exec(append(helmArgs, flags...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}) | 	out, err := helm.exec(append(helmArgs, flags...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, nil) | ||||||
| 	helm.info(out) | 	helm.info(out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -478,7 +491,7 @@ func (helm *execer) ChartExport(chart string, path string, flags ...string) erro | ||||||
| 	} else { | 	} else { | ||||||
| 		helmArgs = []string{"chart", "export", chart} | 		helmArgs = []string{"chart", "export", chart} | ||||||
| 	} | 	} | ||||||
| 	out, err := helm.exec(append(append(helmArgs, "--destination", path), flags...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}) | 	out, err := helm.exec(append(append(helmArgs, "--destination", path), flags...), map[string]string{"HELM_EXPERIMENTAL_OCI": "1"}, nil) | ||||||
| 	helm.info(out) | 	helm.info(out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -487,7 +500,7 @@ func (helm *execer) DeleteRelease(context HelmContext, name string, flags ...str | ||||||
| 	helm.logger.Infof("Deleting %v", name) | 	helm.logger.Infof("Deleting %v", name) | ||||||
| 	preArgs := context.GetTillerlessArgs(helm) | 	preArgs := context.GetTillerlessArgs(helm) | ||||||
| 	env := context.getTillerlessEnv() | 	env := context.getTillerlessEnv() | ||||||
| 	out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env) | 	out, err := helm.exec(append(append(preArgs, "delete", name), flags...), env, nil) | ||||||
| 	helm.write(nil, out) | 	helm.write(nil, out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -497,12 +510,12 @@ func (helm *execer) TestRelease(context HelmContext, name string, flags ...strin | ||||||
| 	preArgs := context.GetTillerlessArgs(helm) | 	preArgs := context.GetTillerlessArgs(helm) | ||||||
| 	env := context.getTillerlessEnv() | 	env := context.getTillerlessEnv() | ||||||
| 	args := []string{"test", name} | 	args := []string{"test", name} | ||||||
| 	out, err := helm.exec(append(append(preArgs, args...), flags...), env) | 	out, err := helm.exec(append(append(preArgs, args...), flags...), env, nil) | ||||||
| 	helm.write(nil, out) | 	helm.write(nil, out) | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) { | func (helm *execer) exec(args []string, env map[string]string, overrideEnableLiveOutput *bool) ([]byte, error) { | ||||||
| 	cmdargs := args | 	cmdargs := args | ||||||
| 	if len(helm.extra) > 0 { | 	if len(helm.extra) > 0 { | ||||||
| 		cmdargs = append(cmdargs, helm.extra...) | 		cmdargs = append(cmdargs, helm.extra...) | ||||||
|  | @ -512,7 +525,11 @@ func (helm *execer) exec(args []string, env map[string]string) ([]byte, error) { | ||||||
| 	} | 	} | ||||||
| 	cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " ")) | 	cmd := fmt.Sprintf("exec: %s %s", helm.helmBinary, strings.Join(cmdargs, " ")) | ||||||
| 	helm.logger.Debug(cmd) | 	helm.logger.Debug(cmd) | ||||||
| 	outBytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env) | 	enableLiveOutput := helm.enableLiveOutput | ||||||
|  | 	if overrideEnableLiveOutput != nil { | ||||||
|  | 		enableLiveOutput = *overrideEnableLiveOutput | ||||||
|  | 	} | ||||||
|  | 	outBytes, err := helm.runner.Execute(helm.helmBinary, cmdargs, env, enableLiveOutput) | ||||||
| 	return outBytes, err | 	return outBytes, err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -534,7 +551,7 @@ func (helm *execer) azcli(name string) ([]byte, error) { | ||||||
| 	cmdargs := append(strings.Split("acr helm repo add --name", " "), name) | 	cmdargs := append(strings.Split("acr helm repo add --name", " "), name) | ||||||
| 	cmd := fmt.Sprintf("exec: az %s", strings.Join(cmdargs, " ")) | 	cmd := fmt.Sprintf("exec: az %s", strings.Join(cmdargs, " ")) | ||||||
| 	helm.logger.Debug(cmd) | 	helm.logger.Debug(cmd) | ||||||
| 	outBytes, err := helm.runner.Execute("az", cmdargs, map[string]string{}) | 	outBytes, err := helm.runner.Execute("az", cmdargs, map[string]string{}, false) | ||||||
| 	helm.logger.Debugf("%s: %s", cmd, outBytes) | 	helm.logger.Debugf("%s: %s", cmd, outBytes) | ||||||
| 	return outBytes, err | 	return outBytes, err | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -29,13 +29,13 @@ func (mock *mockRunner) ExecuteStdIn(cmd string, args []string, env map[string]s | ||||||
| 	return mock.output, mock.err | 	return mock.output, mock.err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) { | func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) { | ||||||
| 	return mock.output, mock.err | 	return mock.output, mock.err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // nolint: golint
 | // nolint: golint
 | ||||||
| func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer { | func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer { | ||||||
| 	execer := New("helm", logger, kubeContext, &mockRunner{}) | 	execer := New("helm", false, logger, kubeContext, &mockRunner{}) | ||||||
| 	return execer | 	return execer | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -82,6 +82,17 @@ func Test_SetHelmBinary(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func Test_SetEnableLiveOutput(t *testing.T) { | ||||||
|  | 	helm := MockExecer(NewLogger(os.Stdout, "info"), "dev") | ||||||
|  | 	if helm.enableLiveOutput { | ||||||
|  | 		t.Error("helmexec.enableLiveOutput should not be enabled by default") | ||||||
|  | 	} | ||||||
|  | 	helm.SetEnableLiveOutput(true) | ||||||
|  | 	if !helm.enableLiveOutput { | ||||||
|  | 		t.Errorf("helmexec.SetEnableLiveOutput() - actual = %t expect = true", helm.enableLiveOutput) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func Test_AddRepo_Helm_3_3_2(t *testing.T) { | func Test_AddRepo_Helm_3_3_2(t *testing.T) { | ||||||
| 	var buffer bytes.Buffer | 	var buffer bytes.Buffer | ||||||
| 	logger := NewLogger(&buffer, "debug") | 	logger := NewLogger(&buffer, "debug") | ||||||
|  | @ -582,7 +593,7 @@ func Test_exec(t *testing.T) { | ||||||
| 	logger := NewLogger(&buffer, "debug") | 	logger := NewLogger(&buffer, "debug") | ||||||
| 	helm := MockExecer(logger, "") | 	helm := MockExecer(logger, "") | ||||||
| 	env := map[string]string{} | 	env := map[string]string{} | ||||||
| 	_, err := helm.exec([]string{"version"}, env) | 	_, err := helm.exec([]string{"version"}, env, nil) | ||||||
| 	expected := `exec: helm version | 	expected := `exec: helm version | ||||||
| ` | ` | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -593,14 +604,14 @@ func Test_exec(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	helm = MockExecer(logger, "dev") | 	helm = MockExecer(logger, "dev") | ||||||
| 	ret, _ := helm.exec([]string{"diff"}, env) | 	ret, _ := helm.exec([]string{"diff"}, env, nil) | ||||||
| 	if len(ret) != 0 { | 	if len(ret) != 0 { | ||||||
| 		t.Error("helmexec.exec() - expected empty return value") | 		t.Error("helmexec.exec() - expected empty return value") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	buffer.Reset() | 	buffer.Reset() | ||||||
| 	helm = MockExecer(logger, "dev") | 	helm = MockExecer(logger, "dev") | ||||||
| 	_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env) | 	_, err = helm.exec([]string{"diff", "release", "chart", "--timeout 10", "--wait", "--wait-for-jobs"}, env, nil) | ||||||
| 	expected = `exec: helm --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs | 	expected = `exec: helm --kube-context dev diff release chart --timeout 10 --wait --wait-for-jobs | ||||||
| ` | ` | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -611,7 +622,7 @@ func Test_exec(t *testing.T) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	buffer.Reset() | 	buffer.Reset() | ||||||
| 	_, err = helm.exec([]string{"version"}, env) | 	_, err = helm.exec([]string{"version"}, env, nil) | ||||||
| 	expected = `exec: helm --kube-context dev version | 	expected = `exec: helm --kube-context dev version | ||||||
| ` | ` | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -623,7 +634,7 @@ func Test_exec(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 	buffer.Reset() | 	buffer.Reset() | ||||||
| 	helm.SetExtraArgs("foo") | 	helm.SetExtraArgs("foo") | ||||||
| 	_, err = helm.exec([]string{"version"}, env) | 	_, err = helm.exec([]string{"version"}, env, nil) | ||||||
| 	expected = `exec: helm --kube-context dev version foo | 	expected = `exec: helm --kube-context dev version foo | ||||||
| ` | ` | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -636,7 +647,7 @@ func Test_exec(t *testing.T) { | ||||||
| 	buffer.Reset() | 	buffer.Reset() | ||||||
| 	helm = MockExecer(logger, "") | 	helm = MockExecer(logger, "") | ||||||
| 	helm.SetHelmBinary("overwritten") | 	helm.SetHelmBinary("overwritten") | ||||||
| 	_, err = helm.exec([]string{"version"}, env) | 	_, err = helm.exec([]string{"version"}, env, nil) | ||||||
| 	expected = `exec: overwritten version | 	expected = `exec: overwritten version | ||||||
| ` | ` | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -896,20 +907,20 @@ exec: helm --kube-context dev template https://example_user:example_password@rep | ||||||
| 
 | 
 | ||||||
| func Test_IsHelm3(t *testing.T) { | func Test_IsHelm3(t *testing.T) { | ||||||
| 	helm2Runner := mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")} | 	helm2Runner := mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")} | ||||||
| 	helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | 	helm := New("helm", false, NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | ||||||
| 	if helm.IsHelm3() { | 	if helm.IsHelm3() { | ||||||
| 		t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 2 version") | 		t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 2 version") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")} | 	helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")} | ||||||
| 	helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner) | 	helm = New("helm", false, NewLogger(os.Stdout, "info"), "dev", &helm3Runner) | ||||||
| 	if !helm.IsHelm3() { | 	if !helm.IsHelm3() { | ||||||
| 		t.Error("helmexec.IsHelm3() - Failed to detect Helm 3") | 		t.Error("helmexec.IsHelm3() - Failed to detect Helm 3") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	t.Setenv(envvar.Helm3, "1") | 	t.Setenv(envvar.Helm3, "1") | ||||||
| 	helm2Runner = mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")} | 	helm2Runner = mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")} | ||||||
| 	helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | 	helm = New("helm", false, NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | ||||||
| 	if !helm.IsHelm3() { | 	if !helm.IsHelm3() { | ||||||
| 		t.Errorf("helmexec.IsHelm3() - Helm3 not detected when %s is set", envvar.Helm3) | 		t.Errorf("helmexec.IsHelm3() - Helm3 not detected when %s is set", envvar.Helm3) | ||||||
| 	} | 	} | ||||||
|  | @ -940,14 +951,14 @@ func Test_GetPluginVersion(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func Test_GetVersion(t *testing.T) { | func Test_GetVersion(t *testing.T) { | ||||||
| 	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")} | 	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")} | ||||||
| 	helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | 	helm := New("helm", false, NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | ||||||
| 	ver := helm.GetVersion() | 	ver := helm.GetVersion() | ||||||
| 	if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 { | 	if ver.Major != 2 || ver.Minor != 16 || ver.Patch != 1 { | ||||||
| 		t.Errorf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver) | 		t.Errorf("helmexec.GetVersion - did not detect correct Helm2 version; it was: %+v", ver) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")} | 	helm3Runner := mockRunner{output: []byte("v3.2.4+ge29ce2a\n")} | ||||||
| 	helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner) | 	helm = New("helm", false, NewLogger(os.Stdout, "info"), "dev", &helm3Runner) | ||||||
| 	ver = helm.GetVersion() | 	ver = helm.GetVersion() | ||||||
| 	if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 { | 	if ver.Major != 3 || ver.Minor != 2 || ver.Patch != 4 { | ||||||
| 		t.Errorf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver) | 		t.Errorf("helmexec.GetVersion - did not detect correct Helm3 version; it was: %+v", ver) | ||||||
|  | @ -956,7 +967,7 @@ func Test_GetVersion(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| func Test_IsVersionAtLeast(t *testing.T) { | func Test_IsVersionAtLeast(t *testing.T) { | ||||||
| 	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")} | 	helm2Runner := mockRunner{output: []byte("Client: v2.16.1+ge13bc94\n")} | ||||||
| 	helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | 	helm := New("helm", false, NewLogger(os.Stdout, "info"), "dev", &helm2Runner) | ||||||
| 	if !helm.IsVersionAtLeast("2.1.0") { | 	if !helm.IsVersionAtLeast("2.1.0") { | ||||||
| 		t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1") | 		t.Error("helmexec.IsVersionAtLeast - 2.16.1 not atleast 2.1") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -11,6 +11,7 @@ type Version struct { | ||||||
| type Interface interface { | type Interface interface { | ||||||
| 	SetExtraArgs(args ...string) | 	SetExtraArgs(args ...string) | ||||||
| 	SetHelmBinary(bin string) | 	SetHelmBinary(bin string) | ||||||
|  | 	SetEnableLiveOutput(enableLiveOutput bool) | ||||||
| 
 | 
 | ||||||
| 	AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error | 	AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string, passCredentials string, skipTLSVerify string) error | ||||||
| 	UpdateRepo() error | 	UpdateRepo() error | ||||||
|  |  | ||||||
|  | @ -1,6 +1,7 @@ | ||||||
| package helmexec | package helmexec | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"bufio" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | @ -16,7 +17,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| // Runner interface for shell commands
 | // Runner interface for shell commands
 | ||||||
| type Runner interface { | type Runner interface { | ||||||
| 	Execute(cmd string, args []string, env map[string]string) ([]byte, error) | 	Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) | ||||||
| 	ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) | 	ExecuteStdIn(cmd string, args []string, env map[string]string, stdin io.Reader) ([]byte, error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -28,13 +29,18 @@ type ShellRunner struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Execute a shell command
 | // Execute a shell command
 | ||||||
| func (shell ShellRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) { | func (shell ShellRunner) Execute(cmd string, args []string, env map[string]string, enableLiveOutput bool) ([]byte, error) { | ||||||
| 	preparedCmd := exec.Command(cmd, args...) | 	preparedCmd := exec.Command(cmd, args...) | ||||||
| 	preparedCmd.Dir = shell.Dir | 	preparedCmd.Dir = shell.Dir | ||||||
| 	preparedCmd.Env = mergeEnv(os.Environ(), env) | 	preparedCmd.Env = mergeEnv(os.Environ(), env) | ||||||
| 	return Output(preparedCmd, &logWriterGenerator{ | 
 | ||||||
| 		log: shell.Logger, | 	if !enableLiveOutput { | ||||||
| 	}) | 		return Output(preparedCmd, &logWriterGenerator{ | ||||||
|  | 			log: shell.Logger, | ||||||
|  | 		}) | ||||||
|  | 	} else { | ||||||
|  | 		return LiveOutput(preparedCmd, os.Stdout) | ||||||
|  | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Execute a shell command
 | // Execute a shell command
 | ||||||
|  | @ -94,6 +100,43 @@ func Output(c *exec.Cmd, logWriterGenerators ...*logWriterGenerator) ([]byte, er | ||||||
| 	return stdout.Bytes(), err | 	return stdout.Bytes(), err | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func LiveOutput(c *exec.Cmd, stdout io.Writer) ([]byte, error) { | ||||||
|  | 	reader, writer := io.Pipe() | ||||||
|  | 	scannerStopped := make(chan struct{}) | ||||||
|  | 
 | ||||||
|  | 	go func() { | ||||||
|  | 		defer close(scannerStopped) | ||||||
|  | 		scanner := bufio.NewScanner(reader) | ||||||
|  | 		for scanner.Scan() { | ||||||
|  | 			fmt.Fprintln(stdout, scanner.Text()) | ||||||
|  | 		} | ||||||
|  | 	}() | ||||||
|  | 
 | ||||||
|  | 	c.Stdout = writer | ||||||
|  | 	c.Stderr = writer | ||||||
|  | 	err := c.Start() | ||||||
|  | 	if err == nil { | ||||||
|  | 		err = c.Wait() | ||||||
|  | 		_ = writer.Close() | ||||||
|  | 		<-scannerStopped | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err != nil { | ||||||
|  | 		switch ee := err.(type) { | ||||||
|  | 		case *exec.ExitError: | ||||||
|  | 			// Propagate any non-zero exit status from the external command, rather than throwing it away,
 | ||||||
|  | 			// so that helmfile could return its own exit code accordingly
 | ||||||
|  | 			waitStatus := ee.Sys().(syscall.WaitStatus) | ||||||
|  | 			exitStatus := waitStatus.ExitStatus() | ||||||
|  | 			err = newExitError(c.Path, c.Args, exitStatus, ee, "", "") | ||||||
|  | 		default: | ||||||
|  | 			panic(fmt.Sprintf("unexpected error: %v", err)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func mergeEnv(orig []string, new map[string]string) []string { | func mergeEnv(orig []string, new map[string]string) []string { | ||||||
| 	wanted := env2map(orig) | 	wanted := env2map(orig) | ||||||
| 	for k, v := range new { | 	for k, v := range new { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,88 @@ | ||||||
|  | package helmexec | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"bytes" | ||||||
|  | 	"os/exec" | ||||||
|  | 	"reflect" | ||||||
|  | 	"strings" | ||||||
|  | 	"testing" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestShellRunner_Execute(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name             string | ||||||
|  | 		want             []byte | ||||||
|  | 		stdoutWant       string | ||||||
|  | 		enableLiveOutput bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:             "echo_template_no_live_output", | ||||||
|  | 			want:             []byte("template\n"), | ||||||
|  | 			enableLiveOutput: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name:             "echo_template_enable_live_output", | ||||||
|  | 			want:             nil, | ||||||
|  | 			enableLiveOutput: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			var buffer bytes.Buffer | ||||||
|  | 			shell := ShellRunner{ | ||||||
|  | 				Logger: NewLogger(&buffer, "debug"), | ||||||
|  | 			} | ||||||
|  | 			got, err := shell.Execute("echo", strings.Split("template", " "), map[string]string{}, tt.enableLiveOutput) | ||||||
|  | 
 | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Errorf("Execute() has produced an error = %v", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if !reflect.DeepEqual(got, tt.want) { | ||||||
|  | 				t.Errorf("ExecuteStdIn() got = %v, want %v", got, tt.want) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestLiveOutput(t *testing.T) { | ||||||
|  | 	tests := []struct { | ||||||
|  | 		name    string | ||||||
|  | 		cmd     *exec.Cmd | ||||||
|  | 		wantW   string | ||||||
|  | 		wantErr bool | ||||||
|  | 	}{ | ||||||
|  | 		{ | ||||||
|  | 			name:    "echo_template", | ||||||
|  | 			cmd:     exec.Command("echo", "template"), | ||||||
|  | 			wantW:   "template\n", | ||||||
|  | 			wantErr: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			name: "helm_template", | ||||||
|  | 			cmd:  exec.Command("helm", "template"), | ||||||
|  | 			wantW: `Error: "helm template" requires at least 1 argument | ||||||
|  | 
 | ||||||
|  | Usage:  helm template [NAME] [CHART] [flags] | ||||||
|  | `, | ||||||
|  | 			wantErr: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 	for _, tt := range tests { | ||||||
|  | 		t.Run(tt.name, func(t *testing.T) { | ||||||
|  | 			w := &bytes.Buffer{} | ||||||
|  | 			got, err := LiveOutput(tt.cmd, w) | ||||||
|  | 			if (err != nil) != tt.wantErr { | ||||||
|  | 				t.Errorf("LiveOutput() error = %v, wantErr %v", err, tt.wantErr) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if gotW := w.String(); gotW != tt.wantW { | ||||||
|  | 				t.Errorf("LiveOutput() gotW = %v, want %v", gotW, tt.wantW) | ||||||
|  | 			} | ||||||
|  | 			if got != nil { | ||||||
|  | 				t.Errorf("LiveOutput() got unespected %v", got) | ||||||
|  | 			} | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | @ -55,10 +55,12 @@ type StateCreator struct { | ||||||
| 
 | 
 | ||||||
| 	overrideHelmBinary string | 	overrideHelmBinary string | ||||||
| 
 | 
 | ||||||
|  | 	enableLiveOutput bool | ||||||
|  | 
 | ||||||
| 	remote *remote.Remote | 	remote *remote.Remote | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewCreator(logger *zap.SugaredLogger, fs *filesystem.FileSystem, valsRuntime vals.Evaluator, getHelm func(*HelmState) helmexec.Interface, overrideHelmBinary string, remote *remote.Remote) *StateCreator { | func NewCreator(logger *zap.SugaredLogger, fs *filesystem.FileSystem, valsRuntime vals.Evaluator, getHelm func(*HelmState) helmexec.Interface, overrideHelmBinary string, remote *remote.Remote, enableLiveOutput bool) *StateCreator { | ||||||
| 	return &StateCreator{ | 	return &StateCreator{ | ||||||
| 		logger: logger, | 		logger: logger, | ||||||
| 
 | 
 | ||||||
|  | @ -68,6 +70,7 @@ func NewCreator(logger *zap.SugaredLogger, fs *filesystem.FileSystem, valsRuntim | ||||||
| 		getHelm:     getHelm, | 		getHelm:     getHelm, | ||||||
| 
 | 
 | ||||||
| 		overrideHelmBinary: overrideHelmBinary, | 		overrideHelmBinary: overrideHelmBinary, | ||||||
|  | 		enableLiveOutput:   enableLiveOutput, | ||||||
| 
 | 
 | ||||||
| 		remote: remote, | 		remote: remote, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -65,6 +65,10 @@ type stateTestEnv struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *HelmState { | func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *HelmState { | ||||||
|  | 	return testEnv.MustLoadStateWithEnableLiveOutput(t, file, envName, false) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (testEnv stateTestEnv) MustLoadStateWithEnableLiveOutput(t *testing.T, file, envName string, enableLiveOutput bool) *HelmState { | ||||||
| 	t.Helper() | 	t.Helper() | ||||||
| 
 | 
 | ||||||
| 	testFs := testhelper.NewTestFs(testEnv.Files) | 	testFs := testhelper.NewTestFs(testEnv.Files) | ||||||
|  | @ -79,7 +83,7 @@ func (testEnv stateTestEnv) MustLoadState(t *testing.T, file, envName string) *H | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem()) | 	r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem()) | ||||||
| 	state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r). | 	state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r, enableLiveOutput). | ||||||
| 		ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil) | 		ParseAndLoad([]byte(yamlContent), filepath.Dir(file), file, envName, true, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
|  | @ -149,7 +153,7 @@ releaseNamespace: mynamespace | ||||||
| 	env := environment.Environment{ | 	env := environment.Environment{ | ||||||
| 		Name: "production", | 		Name: "production", | ||||||
| 	} | 	} | ||||||
| 	state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r). | 	state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r, false). | ||||||
| 		ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, &env) | 		ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, &env) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
|  | @ -236,7 +240,7 @@ overrideNamespace: myns | ||||||
| 	testFs.Cwd = "/example/path/to" | 	testFs.Cwd = "/example/path/to" | ||||||
| 
 | 
 | ||||||
| 	r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem()) | 	r := remote.NewRemote(logger, testFs.Cwd, testFs.ToFileSystem()) | ||||||
| 	state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r). | 	state, err := NewCreator(logger, testFs.ToFileSystem(), nil, nil, "", r, false). | ||||||
| 		ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil) | 		ParseAndLoad(yamlContent, filepath.Dir(yamlFile), yamlFile, "production", true, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		t.Fatalf("unexpected error: %v", err) | 		t.Fatalf("unexpected error: %v", err) | ||||||
|  |  | ||||||
							
								
								
									
										7
									
								
								test/e2e/template/helmfile/testdata/snapshot/chart_need_enable_live_output/config.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										7
									
								
								test/e2e/template/helmfile/testdata/snapshot/chart_need_enable_live_output/config.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | localChartRepoServer: | ||||||
|  |   enabled: true | ||||||
|  |   port: 18080 | ||||||
|  | chartifyTempDir: temp1 | ||||||
|  | helmfileArgs: | ||||||
|  | - --enable-live-output | ||||||
|  | - template | ||||||
							
								
								
									
										31
									
								
								test/e2e/template/helmfile/testdata/snapshot/chart_need_enable_live_output/input.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										31
									
								
								test/e2e/template/helmfile/testdata/snapshot/chart_need_enable_live_output/input.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,31 @@ | ||||||
|  | repositories: | ||||||
|  | - name: myrepo | ||||||
|  |   url: http://localhost:18080/ | ||||||
|  | 
 | ||||||
|  | releases: | ||||||
|  | - name: foo | ||||||
|  |   chart: ../../charts/raw | ||||||
|  |   values: | ||||||
|  |   - templates: | ||||||
|  |     - | | ||||||
|  |       apiVersion: v1 | ||||||
|  |       kind: ConfigMap | ||||||
|  |       metadata: | ||||||
|  |         name: {{`{{ .Release.Name }}`}}-1 | ||||||
|  |         namespace: {{`{{ .Release.Namespace }}`}} | ||||||
|  |       data: | ||||||
|  |         foo: FOO | ||||||
|  |     dep: | ||||||
|  |       templates: | ||||||
|  |       - | | ||||||
|  |         apiVersion: v1 | ||||||
|  |         kind: ConfigMap | ||||||
|  |         metadata: | ||||||
|  |           name: {{`{{ .Release.Name }}`}}-2 | ||||||
|  |           namespace: {{`{{ .Release.Namespace }}`}} | ||||||
|  |         data: | ||||||
|  |           bar: BAR | ||||||
|  |   dependencies: | ||||||
|  |   - alias: dep | ||||||
|  |     chart: myrepo/raw | ||||||
|  |     version: 0.1.0 | ||||||
							
								
								
									
										27
									
								
								test/e2e/template/helmfile/testdata/snapshot/chart_need_enable_live_output/output.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										27
									
								
								test/e2e/template/helmfile/testdata/snapshot/chart_need_enable_live_output/output.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,27 @@ | ||||||
|  | Live output is enabled | ||||||
|  | Adding repo myrepo http://localhost:18080/ | ||||||
|  | "myrepo" has been added to your repositories | ||||||
|  | 
 | ||||||
|  | Building dependency release=foo, chart=$WD/temp1/foo | ||||||
|  | Templating release=foo, chart=$WD/temp1/foo | ||||||
|  | --- | ||||||
|  | # Source: raw/templates/charts/dep/templates/resources.yaml | ||||||
|  | # Source: raw/charts/dep/templates/resources.yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: ConfigMap | ||||||
|  | metadata: | ||||||
|  |   name: foo-2 | ||||||
|  |   namespace: default | ||||||
|  | data: | ||||||
|  |   bar: BAR | ||||||
|  | --- | ||||||
|  | # Source: raw/templates/resources.yaml | ||||||
|  | # Source: raw/templates/resources.yaml | ||||||
|  | apiVersion: v1 | ||||||
|  | kind: ConfigMap | ||||||
|  | metadata: | ||||||
|  |   name: foo-1 | ||||||
|  |   namespace: default | ||||||
|  | data: | ||||||
|  |   foo: FOO | ||||||
|  | 
 | ||||||
|  | @ -17,7 +17,7 @@ if [[ ! -d "${dir}" ]]; then dir="${PWD}"; fi | ||||||
| # GLOBALS ----------------------------------------------------------------------------------------------------------- | # GLOBALS ----------------------------------------------------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| test_ns="helmfile-tests-$(date +"%Y%m%d-%H%M%S")" | test_ns="helmfile-tests-$(date +"%Y%m%d-%H%M%S")" | ||||||
| helmfile="./helmfile --namespace=${test_ns}" | helmfile="./helmfile ${EXTRA_HELMFILE_FLAGS} --namespace=${test_ns}" | ||||||
| helm="helm --kube-context=minikube" | helm="helm --kube-context=minikube" | ||||||
| kubectl="kubectl --context=minikube --namespace=${test_ns}" | kubectl="kubectl --context=minikube --namespace=${test_ns}" | ||||||
| helm_dir="${PWD}/${dir}/.helm" | helm_dir="${PWD}/${dir}/.helm" | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue