diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index fbf94adf..add8351c 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -1994,6 +1994,9 @@ func (helm *mockHelmExec) Fetch(chart string, flags ...string) error { func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error { return nil } +func (helm *mockHelmExec) IsHelm3() bool { + return false +} func TestTemplate_SingleStateFile(t *testing.T) { files := map[string]string{ diff --git a/pkg/exectest/helm.go b/pkg/exectest/helm.go index ebcf1fa2..3a759c50 100644 --- a/pkg/exectest/helm.go +++ b/pkg/exectest/helm.go @@ -157,6 +157,10 @@ func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error { return nil } +func (helm *Helm) IsHelm3() bool { + return false +} + func (helm *Helm) sync(m *sync.Mutex, f func()) { if m != nil { m.Lock() diff --git a/pkg/helmexec/exec.go b/pkg/helmexec/exec.go index 79b41208..67d10290 100644 --- a/pkg/helmexec/exec.go +++ b/pkg/helmexec/exec.go @@ -20,6 +20,7 @@ type decryptedSecret struct { type execer struct { helmBinary string + isHelm3 bool runner Runner logger *zap.SugaredLogger kubeContext string @@ -45,15 +46,31 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger { return zap.New(core).Sugar() } +func detectHelm3(helmBinary string, logger *zap.SugaredLogger, runner Runner) bool { + // Support explicit opt-in via environment variable + if os.Getenv("HELMFILE_HELM3") != "" { + return true + } + + // Autodetect from `helm verison` + bytes, err := runner.Execute(helmBinary, []string{"version", "--client", "--short"}, nil) + if err != nil { + panic(err) + } + return strings.HasPrefix(string(bytes), "v3.") +} + // New for running helm commands func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer { return &execer{ helmBinary: helmBinary, + isHelm3: detectHelm3(helmBinary, logger, runner), logger: logger, kubeContext: kubeContext, runner: runner, decryptedSecrets: make(map[string]*decryptedSecret), } + } func (helm *execer) SetExtraArgs(args ...string) { @@ -126,7 +143,7 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s preArgs := context.GetTillerlessArgs(helm.helmBinary) env := context.getTillerlessEnv() var args []string - if helm.isHelm3() { + if helm.IsHelm3() { args = []string{"list", "--filter", filter} } else { args = []string{"list", filter} @@ -139,7 +156,7 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s // of the release to exist. // // This fixes it by removing the header from the v3 output, so that the output is formatted the same as that of v2. - if helm.isHelm3() { + if helm.IsHelm3() { lines := strings.Split(string(out), "\n") lines = lines[1:] out = []byte(strings.Join(lines, "\n")) @@ -219,7 +236,7 @@ func (helm *execer) DecryptSecret(context HelmContext, name string, flags ...str func (helm *execer) TemplateRelease(name string, chart string, flags ...string) error { helm.logger.Infof("Templating release=%v, chart=%v", name, chart) var args []string - if helm.isHelm3() { + if helm.IsHelm3() { args = []string{"template", name, chart} } else { args = []string{"template", chart, "--name", name} @@ -286,7 +303,7 @@ func (helm *execer) TestRelease(context HelmContext, name string, flags ...strin preArgs := context.GetTillerlessArgs(helm.helmBinary) env := context.getTillerlessEnv() var args []string - if helm.isHelm3() { + if helm.IsHelm3() { args = []string{"test", "run", name} } else { args = []string{"test", name} @@ -323,6 +340,6 @@ func (helm *execer) write(out []byte) { } } -func (helm *execer) isHelm3() bool { - return os.Getenv("HELMFILE_HELM3") != "" +func (helm *execer) IsHelm3() bool { + return helm.isHelm3 } diff --git a/pkg/helmexec/exec_test.go b/pkg/helmexec/exec_test.go index 0bb32ff4..91f4f642 100644 --- a/pkg/helmexec/exec_test.go +++ b/pkg/helmexec/exec_test.go @@ -20,7 +20,7 @@ type mockRunner struct { } func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) { - return []byte{}, nil + return mock.output, mock.err } func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer { @@ -32,10 +32,7 @@ func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer { func TestNewHelmExec(t *testing.T) { buffer := bytes.NewBufferString("something") - logger := NewLogger(buffer, "debug") - helm := New("helm", logger, "dev", &ShellRunner{ - Logger: logger, - }) + helm := MockExecer(NewLogger(buffer, "debug"), "dev") if helm.kubeContext != "dev" { t.Error("helmexec.New() - kubeContext") } @@ -48,11 +45,7 @@ func TestNewHelmExec(t *testing.T) { } func Test_SetExtraArgs(t *testing.T) { - buffer := bytes.NewBufferString("something") - logger := NewLogger(buffer, "debug") - helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &ShellRunner{ - Logger: logger, - }) + helm := MockExecer(NewLogger(os.Stdout, "info"), "dev") helm.SetExtraArgs() if len(helm.extra) != 0 { t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field") @@ -68,11 +61,7 @@ func Test_SetExtraArgs(t *testing.T) { } func Test_SetHelmBinary(t *testing.T) { - buffer := bytes.NewBufferString("something") - logger := NewLogger(buffer, "debug") - helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &ShellRunner{ - Logger: logger, - }) + helm := MockExecer(NewLogger(os.Stdout, "info"), "dev") if helm.helmBinary != "helm" { t.Error("helmexec.command - default command is not helm") } @@ -517,3 +506,17 @@ exec: helm template path/to/chart --name release --values file.yml --kube-contex t.Errorf("helmexec.Template()\nactual = %v\nexpect = %v", buffer.String(), expected) } } + +func Test_IsHelm3(t *testing.T) { + helm2Runner := mockRunner{output: []byte("Client: v2.16.0+ge13bc94\n")} + helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &helm2Runner) + if helm.IsHelm3() { + t.Error("helmexec.IsHelm3() - Detected Helm 3 with Helm 2 version") + } + + helm3Runner := mockRunner{output: []byte("v3.0.0+ge29ce2a\n")} + helm = New("helm", NewLogger(os.Stdout, "info"), "dev", &helm3Runner) + if !helm.IsHelm3() { + t.Error("helmexec.IsHelm3() - Failed to detect Helm 3") + } +} diff --git a/pkg/helmexec/helmexec.go b/pkg/helmexec/helmexec.go index 54c3feae..74a40d3b 100644 --- a/pkg/helmexec/helmexec.go +++ b/pkg/helmexec/helmexec.go @@ -19,8 +19,10 @@ type Interface interface { TestRelease(context HelmContext, name string, flags ...string) error List(context HelmContext, filter string, flags ...string) (string, error) DecryptSecret(context HelmContext, name string, flags ...string) (string, error) + IsHelm3() bool } type DependencyUpdater interface { UpdateDeps(chart string) error + IsHelm3() bool } diff --git a/pkg/state/chart_dependency.go b/pkg/state/chart_dependency.go index 467e4795..ccd649a6 100644 --- a/pkg/state/chart_dependency.go +++ b/pkg/state/chart_dependency.go @@ -280,7 +280,7 @@ func (m *chartDependencyManager) lockFileName() string { } func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) { - if isHelm3() { + if shell.IsHelm3() { return m.updateHelm3(shell, wd, unresolved) } return m.updateHelm2(shell, wd, unresolved) @@ -330,7 +330,7 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre return nil, err } - if isHelm3() && originalLockFileContent != nil { + if shell.IsHelm3() && originalLockFileContent != nil { if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil { return nil, err } @@ -357,7 +357,7 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre }) // Don't update lock file if no dependency updated. - if !isHelm3() && originalLockFileContent != nil { + if !shell.IsHelm3() && originalLockFileContent != nil { originalLockedReqs := &ChartLockedRequirements{} if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil { return nil, err diff --git a/pkg/state/state.go b/pkg/state/state.go index f6dd690f..fbc22584 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -477,7 +477,7 @@ func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, h relErr = newReleaseError(release, err) } else { var args []string - if isHelm3() { + if helm.IsHelm3() { args = []string{} if release.Namespace != "" { args = append(args, "--namespace", release.Namespace) @@ -577,7 +577,7 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme relErr = newReleaseError(release, err) } else if installed { var args []string - if isHelm3() { + if helm.IsHelm3() { args = []string{} } else { args = []string{"--purge"} @@ -646,7 +646,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) - if isHelm3() && release.Namespace != "" { + if helm.IsHelm3() && release.Namespace != "" { flags = append(flags, "--namespace", release.Namespace) } return helm.List(context, "^"+release.Name+"$", flags...) @@ -1161,11 +1161,11 @@ func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm hel st.ApplyOverrides(&release) flags := []string{} - if purge && !isHelm3() { + if purge && !helm.IsHelm3() { flags = append(flags, "--purge") } flags = st.appendConnectionFlags(flags, &release) - if isHelm3() && release.Namespace != "" { + if helm.IsHelm3() && release.Namespace != "" { flags = append(flags, "--namespace", release.Namespace) } context := st.createHelmContext(&release, workerIndex) @@ -1192,7 +1192,7 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout flags = append(flags, "--cleanup") } duration := strconv.Itoa(timeout) - if isHelm3() { + if helm.IsHelm3() { duration += "s" } flags = append(flags, "--timeout", duration) @@ -1202,10 +1202,6 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout }) } -func isHelm3() bool { - return os.Getenv("HELMFILE_HELM3") != "" -} - // Clean will remove any generated secrets func (st *HelmState) Clean() []error { errs := []error{} @@ -1532,7 +1528,7 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp } if timeout != 0 { duration := strconv.Itoa(timeout) - if isHelm3() { + if helm.IsHelm3() { duration += "s" } flags = append(flags, "--timeout", duration) diff --git a/pkg/state/state_test.go b/pkg/state/state_test.go index caa2c6fb..eb865ce5 100644 --- a/pkg/state/state_test.go +++ b/pkg/state/state_test.go @@ -145,6 +145,14 @@ func boolValue(v bool) *bool { return &v } +// Mocking the command-line runner + +type mockRunner struct{} + +func (mock *mockRunner) Execute(cmd string, args []string, env map[string]string) ([]byte, error) { + return []byte{}, nil +} + func TestHelmState_flagsForUpgrade(t *testing.T) { enable := true disable := false @@ -524,9 +532,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) { HelmDefaults: tt.defaults, valsRuntime: valsRuntime, } - helm := helmexec.New("helm", logger, "default", &helmexec.ShellRunner{ - Logger: logger, - }) + helm := helmexec.New("helm", logger, "default", &mockRunner{}) args, err := state.flagsForUpgrade(helm, tt.release, 0) if err != nil { t.Errorf("unexpected error flagsForUpgade: %v", err)