From e2d6dc4afaec36efa9246fbd91dbe0bed6e573a7 Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Tue, 4 Jun 2019 09:12:00 +0900 Subject: [PATCH] feat: helmfile as a go library (#639) * feat: helmfile as a go library This removes almost all the dependencies from the helmfile core logic to urfave/cli. `main.go` is now a thin wrapper around the core logic implemented in `pkg/app`. --- cmd/cmd.go | 98 --------- cmd/deps.go | 37 ---- main.go | 456 +++++++++++++-------------------------- pkg/app/app.go | 123 ++++++++++- ask.go => pkg/app/ask.go | 4 +- pkg/app/config.go | 128 +++++++++++ pkg/app/run.go | 302 ++++++++++++++++++++++++++ pkg/state/state.go | 6 + 8 files changed, 710 insertions(+), 444 deletions(-) delete mode 100644 cmd/cmd.go delete mode 100644 cmd/deps.go rename ask.go => pkg/app/ask.go (91%) create mode 100644 pkg/app/config.go create mode 100644 pkg/app/run.go diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index 883da6a0..00000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,98 +0,0 @@ -package cmd - -import ( - "fmt" - "github.com/roboll/helmfile/pkg/app" - "github.com/roboll/helmfile/pkg/helmexec" - "github.com/roboll/helmfile/pkg/state" - "github.com/urfave/cli" - "go.uber.org/zap" - "strings" -) - -func VisitAllDesiredStates(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) (bool, []error)) error { - a, fileOrDir, err := InitAppEntry(c, false) - if err != nil { - return err - } - - ctx := app.NewContext() - - convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { - if c.GlobalString("helm-binary") != "" { - helm.SetHelmBinary(c.GlobalString("helm-binary")) - } - return converge(st, helm, ctx) - } - - err = a.VisitDesiredStates(fileOrDir, app.LoadOpts{Selectors: a.Selectors}, convergeWithHelmBinary) - - return toCliError(c, err) -} - -func InitAppEntry(c *cli.Context, reverse bool) (*app.App, string, error) { - if c.NArg() > 0 { - cli.ShowAppHelp(c) - return nil, "", fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) - } - - fileOrDir := c.GlobalString("file") - kubeContext := c.GlobalString("kube-context") - namespace := c.GlobalString("namespace") - selectors := c.GlobalStringSlice("selector") - logger := c.App.Metadata["logger"].(*zap.SugaredLogger) - - env := c.GlobalString("environment") - if env == "" { - env = state.DefaultEnv - } - - app := app.Init(&app.App{ - KubeContext: kubeContext, - Logger: logger, - Reverse: reverse, - Env: env, - Namespace: namespace, - Selectors: selectors, - }) - - return app, fileOrDir, nil -} - -func FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c *cli.Context, reverse bool, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error { - a, fileOrDir, err := InitAppEntry(c, reverse) - if err != nil { - return err - } - - ctx := app.NewContext() - - convergeWithHelmBinary := func(st *state.HelmState, helm helmexec.Interface) []error { - if c.GlobalString("helm-binary") != "" { - helm.SetHelmBinary(c.GlobalString("helm-binary")) - } - return converge(st, helm, ctx) - } - - err = a.VisitDesiredStatesWithReleasesFiltered(fileOrDir, convergeWithHelmBinary) - - return toCliError(c, err) -} - -func toCliError(c *cli.Context, err error) error { - if err != nil { - switch e := err.(type) { - case *app.NoMatchingHelmfileError: - noMatchingExitCode := 3 - if c.GlobalBool("allow-no-matching-release") { - noMatchingExitCode = 0 - } - return cli.NewExitError(e.Error(), noMatchingExitCode) - case *app.Error: - return cli.NewExitError(e.Error(), e.Code()) - default: - panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e)) - } - } - return err -} diff --git a/cmd/deps.go b/cmd/deps.go deleted file mode 100644 index ac40568d..00000000 --- a/cmd/deps.go +++ /dev/null @@ -1,37 +0,0 @@ -package cmd - -import ( - "github.com/roboll/helmfile/pkg/app" - "github.com/roboll/helmfile/pkg/argparser" - "github.com/roboll/helmfile/pkg/helmexec" - "github.com/roboll/helmfile/pkg/state" - "github.com/urfave/cli" -) - -func Deps() cli.Command { - return cli.Command{ - Name: "deps", - Usage: "update charts based on the contents of requirements.yaml", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "args", - Value: "", - Usage: "pass args to helm exec", - }, - }, - Action: func(c *cli.Context) error { - return VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) { - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - errs := state.UpdateDeps(helm) - - ok := len(errs) == 0 - - return ok, errs - }) - }, - } -} diff --git a/main.go b/main.go index d679f413..6f499303 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,7 @@ package main import ( "fmt" - "github.com/roboll/helmfile/cmd" "github.com/roboll/helmfile/pkg/app" - "github.com/roboll/helmfile/pkg/argparser" "github.com/roboll/helmfile/pkg/helmexec" "github.com/roboll/helmfile/pkg/state" "github.com/urfave/cli" @@ -94,7 +92,20 @@ func main() { cliApp.Before = configureLogging cliApp.Commands = []cli.Command{ - cmd.Deps(), + { + Name: "deps", + Usage: "update charts based on the contents of requirements.yaml", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass args to helm exec", + }, + }, + Action: action(func(run *app.App, c configImpl) error { + return run.Deps(c) + }), + }, { Name: "repos", Usage: "sync repositories from state file (helm repo add && helm repo update)", @@ -105,20 +116,9 @@ func main() { Usage: "pass args to helm exec", }, }, - Action: func(c *cli.Context) error { - return cmd.VisitAllDesiredStates(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) (bool, []error) { - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - errs := ctx.SyncReposOnce(state, helm) - - ok := len(state.Repositories) > 0 && len(errs) == 0 - - return ok, errs - }) - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Repos(c) + }), }, { Name: "charts", @@ -139,14 +139,9 @@ func main() { Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, }, - Action: func(c *cli.Context) error { - affectedReleases := state.AffectedReleases{} - errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, _ app.Context) []error { - return executeSyncCommand(c, &affectedReleases, st, helm) - }) - affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) - return errs - }, + Action: action(func(run *app.App, c configImpl) error { + return run.DeprecatedSyncCharts(c) + }), }, { Name: "diff", @@ -179,24 +174,9 @@ func main() { Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, }, - Action: func(c *cli.Context) error { - return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { - if !c.Bool("skip-deps") { - if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { - return errs - } - if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { - return errs - } - } - if errs := state.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 { - return errs - } - - _, errs := ExecuteDiffCommand(NewUrfaveCliConfigImpl(c), state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets")) - return errs - }) - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Diff(c) + }), }, { Name: "template", @@ -221,22 +201,9 @@ func main() { Usage: "skip running `helm repo update` and `helm dependency build`", }, }, - Action: func(c *cli.Context) error { - return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { - if !c.Bool("skip-deps") { - if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { - return errs - } - if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { - return errs - } - } - if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 { - return errs - } - return executeTemplateCommand(c, state, helm) - }) - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Template(c) + }), }, { Name: "lint", @@ -261,25 +228,9 @@ func main() { Usage: "skip running `helm repo update` and `helm dependency build`", }, }, - Action: func(c *cli.Context) error { - return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { - values := c.StringSlice("values") - args := argparser.GetArgs(c.String("args"), state) - workers := c.Int("concurrency") - if !c.Bool("skip-deps") { - if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { - return errs - } - if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { - return errs - } - } - if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 { - return errs - } - return state.LintReleases(helm, values, args, workers) - }) - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Lint(c) + }), }, { Name: "sync", @@ -304,25 +255,9 @@ func main() { Usage: "skip running `helm repo update` and `helm dependency build`", }, }, - Action: func(c *cli.Context) error { - affectedReleases := state.AffectedReleases{} - errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { - if !c.Bool("skip-deps") { - if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { - return errs - } - if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { - return errs - } - } - if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 { - return errs - } - return executeSyncCommand(c, &affectedReleases, st, helm) - }) - affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) - return errs - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Sync(c) + }), }, { Name: "apply", @@ -351,88 +286,9 @@ func main() { Usage: "skip running `helm repo update` and `helm dependency build`", }, }, - Action: func(c *cli.Context) error { - affectedReleases := state.AffectedReleases{} - errs := findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { - if !c.Bool("skip-deps") { - if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { - return errs - } - if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { - return errs - } - } - if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 { - return errs - } - - releases, errs := ExecuteDiffCommand(NewUrfaveCliConfigImpl(c), st, helm, true, c.Bool("suppress-secrets")) - - releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm) - if err != nil { - errs = append(errs, err) - } - - fatalErrs := []error{} - - noError := true - for _, e := range errs { - switch err := e.(type) { - case *state.ReleaseError: - if err.Code != 2 { - noError = false - fatalErrs = append(fatalErrs, e) - } - default: - noError = false - fatalErrs = append(fatalErrs, e) - } - } - - // sync only when there are changes - if noError { - if len(releases) == 0 && len(releasesToBeDeleted) == 0 { - // TODO better way to get the logger - logger := c.App.Metadata["logger"].(*zap.SugaredLogger) - logger.Infof("") - logger.Infof("No affected releases") - } else { - names := []string{} - for _, r := range releases { - names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart)) - } - for _, r := range releasesToBeDeleted { - names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart)) - } - - msg := fmt.Sprintf(`Affected releases are: -%s - -Do you really want to apply? - Helmfile will apply all your changes, as shown above. - -`, strings.Join(names, "\n")) - interactive := c.GlobalBool("interactive") - if !interactive || interactive && askForConfirmation(msg) { - rs := []state.ReleaseSpec{} - for _, r := range releases { - rs = append(rs, *r) - } - for _, r := range releasesToBeDeleted { - rs = append(rs, *r) - } - - st.Releases = rs - return executeSyncCommand(c, &affectedReleases, st, helm) - } - } - } - - return fatalErrs - }) - affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) - return errs - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Apply(c) + }), }, { Name: "status", @@ -449,18 +305,9 @@ Do you really want to apply? Usage: "pass args to helm exec", }, }, - Action: func(c *cli.Context) error { - return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { - workers := c.Int("concurrency") - - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - return state.ReleaseStatuses(helm, workers) - }) - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Status(c) + }), }, { Name: "delete", @@ -476,37 +323,9 @@ Do you really want to apply? Usage: "purge releases i.e. free release names and histories", }, }, - Action: func(c *cli.Context) error { - affectedReleases := state.AffectedReleases{} - errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { - purge := c.Bool("purge") - - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - names := make([]string, len(state.Releases)) - for i, r := range state.Releases { - names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart) - } - - msg := fmt.Sprintf(`Affected releases are: -%s - -Do you really want to delete? - Helmfile will delete all your releases, as shown above. - -`, strings.Join(names, "\n")) - interactive := c.GlobalBool("interactive") - if !interactive || interactive && askForConfirmation(msg) { - return state.DeleteReleases(&affectedReleases, helm, purge) - } - return nil - }) - affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) - return errs - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Delete(c) + }), }, { Name: "destroy", @@ -518,35 +337,9 @@ Do you really want to delete? Usage: "pass args to helm exec", }, }, - Action: func(c *cli.Context) error { - affectedReleases := state.AffectedReleases{} - errs := cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - names := make([]string, len(state.Releases)) - for i, r := range state.Releases { - names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart) - } - - msg := fmt.Sprintf(`Affected releases are: -%s - -Do you really want to delete? - Helmfile will delete all your releases, as shown above. - -`, strings.Join(names, "\n")) - interactive := c.GlobalBool("interactive") - if !interactive || interactive && askForConfirmation(msg) { - return state.DeleteReleases(&affectedReleases, helm, true) - } - return nil - }) - affectedReleases.DisplayAffectedReleases(c.App.Metadata["logger"].(*zap.SugaredLogger)) - return errs - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Destroy(c) + }), }, { Name: "test", @@ -572,20 +365,9 @@ Do you really want to delete? Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, }, - Action: func(c *cli.Context) error { - return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { - cleanup := c.Bool("cleanup") - timeout := c.Int("timeout") - concurrency := c.Int("concurrency") - - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - return state.TestReleases(helm, cleanup, timeout, concurrency) - }) - }, + Action: action(func(run *app.App, c configImpl) error { + return run.Test(c) + }), }, } @@ -596,41 +378,19 @@ Do you really want to delete? } } -func executeSyncCommand(c *cli.Context, affectedReleases *state.AffectedReleases, state *state.HelmState, helm helmexec.Interface) []error { - args := argparser.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - - values := c.StringSlice("values") - workers := c.Int("concurrency") - - return state.SyncReleases(affectedReleases, helm, values, workers) -} - -func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { - args := argparser.GetArgs(c.String("args"), state) - values := c.StringSlice("values") - workers := c.Int("concurrency") - - return state.TemplateReleases(helm, values, args, workers) -} - -type Config interface { - HasCommandName(string) bool - Values() []string - Concurrency() int - Args() string -} - type configImpl struct { c *cli.Context } -func NewUrfaveCliConfigImpl(c *cli.Context) configImpl { +func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { + if c.NArg() > 0 { + cli.ShowAppHelp(c) + return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) + } + return configImpl{ c: c, - } + }, nil } func (c configImpl) Values() []string { @@ -649,17 +409,105 @@ func (c configImpl) HasCommandName(name string) bool { return c.c.Command.HasName(name) } -func ExecuteDiffCommand(c Config, st *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) ([]*state.ReleaseSpec, []error) { - args := argparser.GetArgs(c.Args(), st) - if len(args) > 0 { - helm.SetExtraArgs(args...) +// DiffConfig + +func (c configImpl) SkipDeps() bool { + return c.c.Bool("skip-deps") +} + +func (c configImpl) DetailedExitcode() bool { + return c.c.Bool("detailed-exitcode") +} + +func (c configImpl) SuppressSecrets() bool { + return c.c.Bool("suppress-secrets") +} + +// DeleteConfig + +func (c configImpl) Purge() bool { + return c.c.Bool("purge") +} + +// TestConfig + +func (c configImpl) Cleanup() bool { + return c.c.Bool("cleanup") +} + +func (c configImpl) Timeout() int { + return c.c.Int("timeout") +} + +// GlobalConfig + +func (c configImpl) HelmBinary() string { + return c.c.GlobalString("helm-binary") +} + +func (c configImpl) KubeContext() string { + return c.c.GlobalString("kube-context") +} + +func (c configImpl) Namespace() string { + return c.c.GlobalString("namespace") +} + +func (c configImpl) FileOrDir() string { + return c.c.GlobalString("file") +} + +func (c configImpl) Selectors() []string { + return c.c.GlobalStringSlice("selector") +} + +func (c configImpl) Interactive() bool { + return c.c.GlobalBool("interactive") +} + +func (c configImpl) Logger() *zap.SugaredLogger { + return c.c.App.Metadata["logger"].(*zap.SugaredLogger) +} + +func (c configImpl) Env() string { + env := c.c.GlobalString("environment") + if env == "" { + env = state.DefaultEnv } - - triggerCleanupEvents := c.HasCommandName("diff") - - return st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, suppressSecrets, triggerCleanupEvents) + return env } -func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error { - return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge) +func action(do func(*app.App, configImpl) error) func(*cli.Context) error { + return func(implCtx *cli.Context) error { + conf, err := NewUrfaveCliConfigImpl(implCtx) + if err != nil { + return err + } + + a := app.New(conf) + + a.ErrorHandler = func(err error) error { + return toCliError(implCtx, err) + } + + return do(a, conf) + } +} + +func toCliError(c *cli.Context, err error) error { + if err != nil { + switch e := err.(type) { + case *app.NoMatchingHelmfileError: + noMatchingExitCode := 3 + if c.GlobalBool("allow-no-matching-release") { + noMatchingExitCode = 0 + } + return cli.NewExitError(e.Error(), noMatchingExitCode) + case *app.Error: + return cli.NewExitError(e.Error(), e.Code()) + default: + panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e)) + } + } + return err } diff --git a/pkg/app/app.go b/pkg/app/app.go index 610ab58b..ae0b7233 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -24,6 +24,12 @@ type App struct { Env string Namespace string Selectors []string + HelmBinary string + Args string + + FileOrDir string + + ErrorHandler func(error) error readFile func(string) ([]byte, error) fileExists func(string) (bool, error) @@ -36,6 +42,19 @@ type App struct { chdir func(string) error } +func New(conf ConfigProvider) *App { + return Init(&App{ + KubeContext: conf.KubeContext(), + Logger: conf.Logger(), + Env: conf.Env(), + Namespace: conf.Namespace(), + Selectors: conf.Selectors(), + HelmBinary: conf.HelmBinary(), + Args: conf.Args(), + FileOrDir: conf.FileOrDir(), + }) +} + func Init(app *App) *App { app.readFile = ioutil.ReadFile app.glob = filepath.Glob @@ -48,6 +67,84 @@ func Init(app *App) *App { return app } +func (a *App) Deps(c DepsConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Deps(c) + }) +} + +func (a *App) Repos(c ReposConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Repos(c) + }) +} + +func (a *App) reverse() *App { + new := *a + new.Reverse = true + return &new +} + +func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.DeprecatedSyncCharts(c) + }) +} + +func (a *App) Diff(c DiffConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Diff(c) + }) +} + +func (a *App) Template(c TemplateConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Template(c) + }) +} + +func (a *App) Lint(c LintConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Lint(c) + }) +} + +func (a *App) Sync(c SyncConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Sync(c) + }) +} + +func (a *App) Apply(c ApplyConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Apply(c) + }) +} + +func (a *App) Status(c StatusesConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Status(c) + }) +} + +func (a *App) Delete(c DeleteConfigProvider) error { + return a.reverse().ForEachState(func(run *Run) []error { + return run.Delete(c) + }) +} + +func (a *App) Destroy(c DestroyConfigProvider) error { + return a.reverse().ForEachState(func(run *Run) []error { + return run.Destroy(c) + }) +} + +func (a *App) Test(c TestConfigProvider) error { + return a.ForEachState(func(run *Run) []error { + return run.Test(c) + }) +} + func (a *App) within(dir string, do func() error) error { if dir == "." { return do() @@ -140,7 +237,7 @@ func (a *App) loadDesiredStateFromYaml(file string, opts ...LoadOpts) (*state.He return ld.Load(file, op) } -func (a *App) VisitDesiredStates(fileOrDir string, opts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { +func (a *App) visitStates(fileOrDir string, opts LoadOpts, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { noMatchInHelmfiles := true err := a.visitStateFiles(fileOrDir, func(f, d string) error { @@ -195,7 +292,7 @@ func (a *App) VisitDesiredStates(fileOrDir string, opts LoadOpts, converge func( } else { optsForNestedState.Selectors = m.Selectors } - if err := a.VisitDesiredStates(m.Path, optsForNestedState, converge); err != nil { + if err := a.visitStates(m.Path, optsForNestedState, converge); err != nil { switch err.(type) { case *NoMatchingHelmfileError: @@ -229,10 +326,26 @@ func (a *App) VisitDesiredStates(fileOrDir string, opts LoadOpts, converge func( return nil } +func (a *App) ForEachState(do func(*Run) []error) error { + err := a.VisitDesiredStatesWithReleasesFiltered(a.FileOrDir, func(st *state.HelmState, helm helmexec.Interface) []error { + ctx := NewContext() + + run := NewRun(st, helm, ctx) + + return do(run) + }) + + if err != nil && a.ErrorHandler != nil { + return a.ErrorHandler(err) + } + + return err +} + func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error { opts := LoadOpts{Selectors: a.Selectors} - err := a.VisitDesiredStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { + err := a.visitStates(fileOrDir, opts, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { if len(st.Selectors) > 0 { err := st.FilterReleases() if err != nil { @@ -240,6 +353,10 @@ func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge } } + if a.HelmBinary != "" { + helm.SetHelmBinary(a.HelmBinary) + } + type Key struct { TillerNamespace, Name string } diff --git a/ask.go b/pkg/app/ask.go similarity index 91% rename from ask.go rename to pkg/app/ask.go index 8fd38f9c..ad3425d9 100644 --- a/ask.go +++ b/pkg/app/ask.go @@ -1,4 +1,4 @@ -package main +package app import ( "bufio" @@ -11,7 +11,7 @@ import ( // Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com] // // Shamelessly borrowed from @r0l1's awesome work that is available at https://gist.github.com/r0l1/3dcbb0c8f6cfe9c66ab8008f55f8f28b -func askForConfirmation(s string) bool { +func AskForConfirmation(s string) bool { reader := bufio.NewReader(os.Stdin) for { diff --git a/pkg/app/config.go b/pkg/app/config.go new file mode 100644 index 00000000..e24833ca --- /dev/null +++ b/pkg/app/config.go @@ -0,0 +1,128 @@ +package app + +import "go.uber.org/zap" + +type ConfigProvider interface { + Args() string + HelmBinary() string + + FileOrDir() string + KubeContext() string + Namespace() string + Selectors() []string + Env() string + + loggingConfig +} + +type DeprecatedChartsConfigProvider interface { + Values() []string + + concurrencyConfig + loggingConfig +} + +type DepsConfigProvider interface { + Args() string +} + +type ReposConfigProvider interface { + Args() string +} + +type ApplyConfigProvider interface { + Args() string + + Values() []string + SkipDeps() bool + + SuppressSecrets() bool + + concurrencyConfig + interactive + loggingConfig +} + +type SyncConfigProvider interface { + Args() string + + Values() []string + SkipDeps() bool + + concurrencyConfig + loggingConfig +} + +type DiffConfigProvider interface { + Args() string + + Values() []string + SkipDeps() bool + + SuppressSecrets() bool + + DetailedExitcode() bool + + concurrencyConfig +} + +type DeleteConfigProvider interface { + Args() string + + Purge() bool + + interactive + loggingConfig +} + +type DestroyConfigProvider interface { + Args() string + + interactive + loggingConfig +} + +type TestConfigProvider interface { + Args() string + + Timeout() int + Cleanup() bool + + concurrencyConfig +} + +type LintConfigProvider interface { + Args() string + + Values() []string + SkipDeps() bool + + concurrencyConfig +} + +type TemplateConfigProvider interface { + Args() string + + Values() []string + SkipDeps() bool + + concurrencyConfig +} + +type StatusesConfigProvider interface { + Args() string + + concurrencyConfig +} + +type concurrencyConfig interface { + Concurrency() int +} + +type loggingConfig interface { + Logger() *zap.SugaredLogger +} + +type interactive interface { + Interactive() bool +} diff --git a/pkg/app/run.go b/pkg/app/run.go new file mode 100644 index 00000000..51338af9 --- /dev/null +++ b/pkg/app/run.go @@ -0,0 +1,302 @@ +package app + +import ( + "fmt" + "github.com/roboll/helmfile/pkg/argparser" + "github.com/roboll/helmfile/pkg/helmexec" + "github.com/roboll/helmfile/pkg/state" + "strings" +) + +type Run struct { + state *state.HelmState + helm helmexec.Interface + ctx Context + + Ask func(string) bool +} + +func NewRun(st *state.HelmState, helm helmexec.Interface, ctx Context) *Run { + return &Run{state: st, helm: helm, ctx: ctx} +} + +func (r *Run) askForConfirmation(msg string) bool { + if r.Ask != nil { + return r.Ask(msg) + } + return AskForConfirmation(msg) +} + +func (r *Run) Deps(c DepsConfigProvider) []error { + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + return r.state.UpdateDeps(r.helm) +} + +func (r *Run) Repos(c ReposConfigProvider) []error { + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + return r.ctx.SyncReposOnce(r.state, r.helm) +} + +func (r *Run) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) []error { + st := r.state + helm := r.helm + + affectedReleases := state.AffectedReleases{} + errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency()) + affectedReleases.DisplayAffectedReleases(c.Logger()) + return errs +} + +func (r *Run) Status(c StatusesConfigProvider) []error { + workers := c.Concurrency() + + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + return r.state.ReleaseStatuses(r.helm, workers) +} + +func (r *Run) Delete(c DeleteConfigProvider) []error { + affectedReleases := state.AffectedReleases{} + purge := c.Purge() + + errs := []error{} + + names := make([]string, len(r.state.Releases)) + for i, r := range r.state.Releases { + names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart) + } + + msg := fmt.Sprintf(`Affected releases are: +%s + +Do you really want to delete? + Helmfile will delete all your releases, as shown above. + +`, strings.Join(names, "\n")) + interactive := c.Interactive() + if !interactive || interactive && r.askForConfirmation(msg) { + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + errs = r.state.DeleteReleases(&affectedReleases, r.helm, purge) + } + affectedReleases.DisplayAffectedReleases(c.Logger()) + return errs +} + +func (r *Run) Destroy(c DestroyConfigProvider) []error { + errs := []error{} + affectedReleases := state.AffectedReleases{} + + names := make([]string, len(r.state.Releases)) + for i, r := range r.state.Releases { + names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart) + } + + msg := fmt.Sprintf(`Affected releases are: +%s + +Do you really want to delete? + Helmfile will delete all your releases, as shown above. + +`, strings.Join(names, "\n")) + interactive := c.Interactive() + if !interactive || interactive && r.askForConfirmation(msg) { + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + errs = r.state.DeleteReleases(&affectedReleases, r.helm, true) + } + affectedReleases.DisplayAffectedReleases(c.Logger()) + return errs +} + +func (r *Run) Apply(c ApplyConfigProvider) []error { + st := r.state + helm := r.helm + ctx := r.ctx + + affectedReleases := state.AffectedReleases{} + if !c.SkipDeps() { + if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { + return errs + } + if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + } + if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 { + return errs + } + + // helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly + detailedExitCode := true + + releases, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), detailedExitCode, c.SuppressSecrets(), false) + + releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm) + if err != nil { + errs = append(errs, err) + } + + fatalErrs := []error{} + + noError := true + for _, e := range errs { + switch err := e.(type) { + case *state.ReleaseError: + if err.Code != 2 { + noError = false + fatalErrs = append(fatalErrs, e) + } + default: + noError = false + fatalErrs = append(fatalErrs, e) + } + } + + // sync only when there are changes + if noError { + if len(releases) == 0 && len(releasesToBeDeleted) == 0 { + // TODO better way to get the logger + logger := c.Logger() + logger.Infof("") + logger.Infof("No affected releases") + } else { + names := []string{} + for _, r := range releases { + names = append(names, fmt.Sprintf(" %s (%s) UPDATED", r.Name, r.Chart)) + } + for _, r := range releasesToBeDeleted { + names = append(names, fmt.Sprintf(" %s (%s) DELETED", r.Name, r.Chart)) + } + + msg := fmt.Sprintf(`Affected releases are: +%s + +Do you really want to apply? + Helmfile will apply all your changes, as shown above. + +`, strings.Join(names, "\n")) + interactive := c.Interactive() + if !interactive || interactive && r.askForConfirmation(msg) { + rs := []state.ReleaseSpec{} + for _, r := range releases { + rs = append(rs, *r) + } + for _, r := range releasesToBeDeleted { + rs = append(rs, *r) + } + + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + st.Releases = rs + return st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency()) + } + } + } + + affectedReleases.DisplayAffectedReleases(c.Logger()) + return fatalErrs +} + +func (r *Run) Diff(c DiffConfigProvider) []error { + st := r.state + helm := r.helm + ctx := r.ctx + + if !c.SkipDeps() { + if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { + return errs + } + if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + } + if errs := st.PrepareReleases(helm, "diff"); errs != nil && len(errs) > 0 { + return errs + } + + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + _, errs := st.DiffReleases(helm, c.Values(), c.Concurrency(), c.DetailedExitcode(), c.SuppressSecrets(), true) + return errs +} + +func (r *Run) Sync(c SyncConfigProvider) []error { + st := r.state + helm := r.helm + ctx := r.ctx + + affectedReleases := state.AffectedReleases{} + if !c.SkipDeps() { + if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { + return errs + } + if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + } + if errs := st.PrepareReleases(helm, "sync"); errs != nil && len(errs) > 0 { + return errs + } + + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + errs := st.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency()) + affectedReleases.DisplayAffectedReleases(c.Logger()) + return errs +} + +func (r *Run) Template(c TemplateConfigProvider) []error { + state := r.state + helm := r.helm + ctx := r.ctx + + if !c.SkipDeps() { + if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { + return errs + } + if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + } + if errs := state.PrepareReleases(helm, "template"); errs != nil && len(errs) > 0 { + return errs + } + + args := argparser.GetArgs(c.Args(), state) + return state.TemplateReleases(helm, c.Values(), args, c.Concurrency()) +} + +func (r *Run) Test(c TestConfigProvider) []error { + cleanup := c.Cleanup() + timeout := c.Timeout() + concurrency := c.Concurrency() + + r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) + + return r.state.TestReleases(r.helm, cleanup, timeout, concurrency) +} + +func (r *Run) Lint(c LintConfigProvider) []error { + state := r.state + helm := r.helm + ctx := r.ctx + + values := c.Values() + args := argparser.GetArgs(c.Args(), state) + workers := c.Concurrency() + if !c.SkipDeps() { + if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { + return errs + } + if errs := state.BuildDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + } + if errs := state.PrepareReleases(helm, "lint"); errs != nil && len(errs) > 0 { + return errs + } + return state.LintReleases(helm, values, args, workers) +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 755032f5..62524bb8 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -514,6 +514,9 @@ func (st *HelmState) downloadCharts(helm helmexec.Interface, dir string, concurr // TemplateReleases wrapper for executing helm template on the releases func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { + // Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint` + helm.SetExtraArgs() + errs := []error{} // Create tmp directory and bail immediately if it fails dir, err := ioutil.TempDir("", "") @@ -577,6 +580,9 @@ func (st *HelmState) TemplateReleases(helm helmexec.Interface, additionalValues // LintReleases wrapper for executing helm lint on the releases func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int) []error { + // Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint` + helm.SetExtraArgs() + errs := []error{} // Create tmp directory and bail immediately if it fails dir, err := ioutil.TempDir("", "")