Merge pull request #963 from frontapp/auto-helm3

feat: Automatically enable Helm v3 mode
This commit is contained in:
KUOKA Yusuke 2019-11-15 08:40:37 +09:00 committed by GitHub
commit 990d31c667
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 69 additions and 38 deletions

View File

@ -1994,6 +1994,9 @@ func (helm *mockHelmExec) Fetch(chart string, flags ...string) error {
func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error { func (helm *mockHelmExec) Lint(name, chart string, flags ...string) error {
return nil return nil
} }
func (helm *mockHelmExec) IsHelm3() bool {
return false
}
func TestTemplate_SingleStateFile(t *testing.T) { func TestTemplate_SingleStateFile(t *testing.T) {
files := map[string]string{ files := map[string]string{

View File

@ -157,6 +157,10 @@ func (helm *Helm) TemplateRelease(name, chart string, flags ...string) error {
return nil return nil
} }
func (helm *Helm) IsHelm3() bool {
return false
}
func (helm *Helm) sync(m *sync.Mutex, f func()) { func (helm *Helm) sync(m *sync.Mutex, f func()) {
if m != nil { if m != nil {
m.Lock() m.Lock()

View File

@ -20,6 +20,7 @@ type decryptedSecret struct {
type execer struct { type execer struct {
helmBinary string helmBinary string
isHelm3 bool
runner Runner runner Runner
logger *zap.SugaredLogger logger *zap.SugaredLogger
kubeContext string kubeContext string
@ -45,15 +46,31 @@ func NewLogger(writer io.Writer, logLevel string) *zap.SugaredLogger {
return zap.New(core).Sugar() 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 // New for running helm commands
func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer { func New(helmBinary string, logger *zap.SugaredLogger, kubeContext string, runner Runner) *execer {
return &execer{ return &execer{
helmBinary: helmBinary, helmBinary: helmBinary,
isHelm3: detectHelm3(helmBinary, logger, runner),
logger: logger, logger: logger,
kubeContext: kubeContext, kubeContext: kubeContext,
runner: runner, runner: runner,
decryptedSecrets: make(map[string]*decryptedSecret), decryptedSecrets: make(map[string]*decryptedSecret),
} }
} }
func (helm *execer) SetExtraArgs(args ...string) { 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) preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv() env := context.getTillerlessEnv()
var args []string var args []string
if helm.isHelm3() { if helm.IsHelm3() {
args = []string{"list", "--filter", filter} args = []string{"list", "--filter", filter}
} else { } else {
args = []string{"list", filter} args = []string{"list", filter}
@ -139,7 +156,7 @@ func (helm *execer) List(context HelmContext, filter string, flags ...string) (s
// of the release to exist. // 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. // 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 := strings.Split(string(out), "\n")
lines = lines[1:] lines = lines[1:]
out = []byte(strings.Join(lines, "\n")) 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 { func (helm *execer) TemplateRelease(name string, chart string, flags ...string) error {
helm.logger.Infof("Templating release=%v, chart=%v", name, chart) helm.logger.Infof("Templating release=%v, chart=%v", name, chart)
var args []string var args []string
if helm.isHelm3() { if helm.IsHelm3() {
args = []string{"template", name, chart} args = []string{"template", name, chart}
} else { } else {
args = []string{"template", chart, "--name", name} 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) preArgs := context.GetTillerlessArgs(helm.helmBinary)
env := context.getTillerlessEnv() env := context.getTillerlessEnv()
var args []string var args []string
if helm.isHelm3() { if helm.IsHelm3() {
args = []string{"test", "run", name} args = []string{"test", "run", name}
} else { } else {
args = []string{"test", name} args = []string{"test", name}
@ -323,6 +340,6 @@ func (helm *execer) write(out []byte) {
} }
} }
func (helm *execer) isHelm3() bool { func (helm *execer) IsHelm3() bool {
return os.Getenv("HELMFILE_HELM3") != "" return helm.isHelm3
} }

View File

@ -20,7 +20,7 @@ type mockRunner struct {
} }
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) ([]byte, error) {
return []byte{}, nil return mock.output, mock.err
} }
func MockExecer(logger *zap.SugaredLogger, kubeContext string) *execer { 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) { func TestNewHelmExec(t *testing.T) {
buffer := bytes.NewBufferString("something") buffer := bytes.NewBufferString("something")
logger := NewLogger(buffer, "debug") helm := MockExecer(NewLogger(buffer, "debug"), "dev")
helm := New("helm", logger, "dev", &ShellRunner{
Logger: logger,
})
if helm.kubeContext != "dev" { if helm.kubeContext != "dev" {
t.Error("helmexec.New() - kubeContext") t.Error("helmexec.New() - kubeContext")
} }
@ -48,11 +45,7 @@ func TestNewHelmExec(t *testing.T) {
} }
func Test_SetExtraArgs(t *testing.T) { func Test_SetExtraArgs(t *testing.T) {
buffer := bytes.NewBufferString("something") helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
logger := NewLogger(buffer, "debug")
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
Logger: logger,
})
helm.SetExtraArgs() helm.SetExtraArgs()
if len(helm.extra) != 0 { if len(helm.extra) != 0 {
t.Error("helmexec.SetExtraArgs() - passing no arguments should not change extra field") 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) { func Test_SetHelmBinary(t *testing.T) {
buffer := bytes.NewBufferString("something") helm := MockExecer(NewLogger(os.Stdout, "info"), "dev")
logger := NewLogger(buffer, "debug")
helm := New("helm", NewLogger(os.Stdout, "info"), "dev", &ShellRunner{
Logger: logger,
})
if helm.helmBinary != "helm" { if helm.helmBinary != "helm" {
t.Error("helmexec.command - default command is not 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) 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")
}
}

View File

@ -19,8 +19,10 @@ type Interface interface {
TestRelease(context HelmContext, name string, flags ...string) error TestRelease(context HelmContext, name string, flags ...string) error
List(context HelmContext, filter string, flags ...string) (string, error) List(context HelmContext, filter string, flags ...string) (string, error)
DecryptSecret(context HelmContext, name string, flags ...string) (string, error) DecryptSecret(context HelmContext, name string, flags ...string) (string, error)
IsHelm3() bool
} }
type DependencyUpdater interface { type DependencyUpdater interface {
UpdateDeps(chart string) error UpdateDeps(chart string) error
IsHelm3() bool
} }

View File

@ -280,7 +280,7 @@ func (m *chartDependencyManager) lockFileName() string {
} }
func (m *chartDependencyManager) Update(shell helmexec.DependencyUpdater, wd string, unresolved *UnresolvedDependencies) (*ResolvedDependencies, error) { 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.updateHelm3(shell, wd, unresolved)
} }
return m.updateHelm2(shell, wd, unresolved) return m.updateHelm2(shell, wd, unresolved)
@ -330,7 +330,7 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre
return nil, err return nil, err
} }
if isHelm3() && originalLockFileContent != nil { if shell.IsHelm3() && originalLockFileContent != nil {
if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil { if err := m.writeBytes(filepath.Join(wd, chartLockFile), originalLockFileContent); err != nil {
return nil, err return nil, err
} }
@ -357,7 +357,7 @@ func (m *chartDependencyManager) doUpdate(chartLockFile string, unresolved *Unre
}) })
// Don't update lock file if no dependency updated. // Don't update lock file if no dependency updated.
if !isHelm3() && originalLockFileContent != nil { if !shell.IsHelm3() && originalLockFileContent != nil {
originalLockedReqs := &ChartLockedRequirements{} originalLockedReqs := &ChartLockedRequirements{}
if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil { if err := yaml.Unmarshal(originalLockFileContent, originalLockedReqs); err != nil {
return nil, err return nil, err

View File

@ -477,7 +477,7 @@ func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, h
relErr = newReleaseError(release, err) relErr = newReleaseError(release, err)
} else { } else {
var args []string var args []string
if isHelm3() { if helm.IsHelm3() {
args = []string{} args = []string{}
if release.Namespace != "" { if release.Namespace != "" {
args = append(args, "--namespace", release.Namespace) args = append(args, "--namespace", release.Namespace)
@ -577,7 +577,7 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
relErr = newReleaseError(release, err) relErr = newReleaseError(release, err)
} else if installed { } else if installed {
var args []string var args []string
if isHelm3() { if helm.IsHelm3() {
args = []string{} args = []string{}
} else { } else {
args = []string{"--purge"} 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) { func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
flags := st.connectionFlags(release) flags := st.connectionFlags(release)
if isHelm3() && release.Namespace != "" { if helm.IsHelm3() && release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace) flags = append(flags, "--namespace", release.Namespace)
} }
return helm.List(context, "^"+release.Name+"$", flags...) return helm.List(context, "^"+release.Name+"$", flags...)
@ -1161,11 +1161,11 @@ func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm hel
st.ApplyOverrides(&release) st.ApplyOverrides(&release)
flags := []string{} flags := []string{}
if purge && !isHelm3() { if purge && !helm.IsHelm3() {
flags = append(flags, "--purge") flags = append(flags, "--purge")
} }
flags = st.appendConnectionFlags(flags, &release) flags = st.appendConnectionFlags(flags, &release)
if isHelm3() && release.Namespace != "" { if helm.IsHelm3() && release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace) flags = append(flags, "--namespace", release.Namespace)
} }
context := st.createHelmContext(&release, workerIndex) context := st.createHelmContext(&release, workerIndex)
@ -1192,7 +1192,7 @@ func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout
flags = append(flags, "--cleanup") flags = append(flags, "--cleanup")
} }
duration := strconv.Itoa(timeout) duration := strconv.Itoa(timeout)
if isHelm3() { if helm.IsHelm3() {
duration += "s" duration += "s"
} }
flags = append(flags, "--timeout", duration) 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 // Clean will remove any generated secrets
func (st *HelmState) Clean() []error { func (st *HelmState) Clean() []error {
errs := []error{} errs := []error{}
@ -1532,7 +1528,7 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
} }
if timeout != 0 { if timeout != 0 {
duration := strconv.Itoa(timeout) duration := strconv.Itoa(timeout)
if isHelm3() { if helm.IsHelm3() {
duration += "s" duration += "s"
} }
flags = append(flags, "--timeout", duration) flags = append(flags, "--timeout", duration)

View File

@ -145,6 +145,14 @@ func boolValue(v bool) *bool {
return &v 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) { func TestHelmState_flagsForUpgrade(t *testing.T) {
enable := true enable := true
disable := false disable := false
@ -524,9 +532,7 @@ func TestHelmState_flagsForUpgrade(t *testing.T) {
HelmDefaults: tt.defaults, HelmDefaults: tt.defaults,
valsRuntime: valsRuntime, valsRuntime: valsRuntime,
} }
helm := helmexec.New("helm", logger, "default", &helmexec.ShellRunner{ helm := helmexec.New("helm", logger, "default", &mockRunner{})
Logger: logger,
})
args, err := state.flagsForUpgrade(helm, tt.release, 0) args, err := state.flagsForUpgrade(helm, tt.release, 0)
if err != nil { if err != nil {
t.Errorf("unexpected error flagsForUpgade: %v", err) t.Errorf("unexpected error flagsForUpgade: %v", err)