package main import ( "fmt" "github.com/roboll/helmfile/args" "github.com/roboll/helmfile/cmd" "github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/pkg/app" "github.com/roboll/helmfile/state" "github.com/urfave/cli" "go.uber.org/zap" "go.uber.org/zap/zapcore" "os" "strings" ) var Version string var logger *zap.SugaredLogger func configureLogging(c *cli.Context) error { // Valid levels: // https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126 logLevel := c.GlobalString("log-level") if c.GlobalBool("quiet") { logLevel = "warn" } var level zapcore.Level err := level.Set(logLevel) if err != nil { return err } logger = helmexec.NewLogger(os.Stdout, logLevel) if c.App.Metadata == nil { // Auto-initialised in 1.19.0 // https://github.com/urfave/cli/blob/master/CHANGELOG.md#1190---2016-11-19 c.App.Metadata = make(map[string]interface{}) } c.App.Metadata["logger"] = logger return nil } func main() { cliApp := cli.NewApp() cliApp.Name = "helmfile" cliApp.Usage = "" cliApp.Version = Version cliApp.Flags = []cli.Flag{ cli.StringFlag{ Name: "helm-binary, b", Usage: "path to helm binary", }, cli.StringFlag{ Name: "file, f", Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference", }, cli.StringFlag{ Name: "environment, e", Usage: "specify the environment name. defaults to `default`", }, cli.BoolFlag{ Name: "quiet, q", Usage: "Silence output. Equivalent to log-level warn", }, cli.StringFlag{ Name: "kube-context", Usage: "Set kubectl context. Uses current context by default", }, cli.StringFlag{ Name: "log-level", Usage: "Set log level, default info", }, cli.StringFlag{ Name: "namespace, n", Usage: "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}", }, cli.StringSliceFlag{ Name: "selector, l", Usage: `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar. A release must match all labels in a group in order to be used. Multiple groups can be specified at once. --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`, }, cli.BoolFlag{ Name: "interactive, i", Usage: "Request confirmation before attempting to modify clusters", }, } cliApp.Before = configureLogging cliApp.Commands = []cli.Command{ { Name: "repos", Usage: "sync repositories from state file (helm repo add && helm repo update)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", Value: "", 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 := args.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 }) }, }, { Name: "charts", Usage: "sync releases from state file (helm upgrade --install)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm exec", }, cli.StringSliceFlag{ Name: "values", Usage: "additional value files to be merged into the command", }, cli.IntFlag{ Name: "concurrency", Value: 0, 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 { return executeSyncCommand(c, state, helm) }) }, }, { Name: "diff", Usage: "diff releases from state file against env (helm diff)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm exec", }, cli.StringSliceFlag{ Name: "values", Usage: "additional value files to be merged into the command", }, cli.BoolFlag{ Name: "sync-repos", Usage: "DEPRECATED", }, cli.BoolFlag{ Name: "skip-deps", Usage: "skip running `helm repo update` and `helm dependency build`", }, cli.BoolFlag{ Name: "detailed-exitcode", Usage: "return a non-zero exit code when there are changes", }, cli.BoolFlag{ Name: "suppress-secrets", Usage: "suppress secrets in the output. highly recommended to specify on CI/CD use-cases", }, cli.IntFlag{ Name: "concurrency", Value: 0, 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 c.Bool("sync-repos") { logger.Warnf("--sync-repos has been removed and `helmfile diff` updates repositories by default. Provide `--skip-repo-update` to opt-out.") } 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.PrepareRelease(helm, "diff"); errs != nil && len(errs) > 0 { return errs } _, errs := executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets")) return errs }) }, }, { Name: "template", Usage: "template releases from state file against env (helm template)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm template", }, cli.StringSliceFlag{ Name: "values", Usage: "additional value files to be merged into the command", }, cli.IntFlag{ Name: "concurrency", Value: 0, Usage: "maximum number of concurrent downloads of release charts", }, cli.BoolFlag{ Name: "skip-deps", 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.PrepareRelease(helm, "template"); errs != nil && len(errs) > 0 { return errs } return executeTemplateCommand(c, state, helm) }) }, }, { Name: "lint", Usage: "lint charts from state file (helm lint)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm exec", }, cli.StringSliceFlag{ Name: "values", Usage: "additional value files to be merged into the command", }, cli.IntFlag{ Name: "concurrency", Value: 0, Usage: "maximum number of concurrent downloads of release charts", }, }, 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 := args.GetArgs(c.String("args"), state) workers := c.Int("concurrency") if errs := ctx.SyncReposOnce(state, helm); errs != nil && len(errs) > 0 { return errs } if errs := state.PrepareRelease(helm, "lint"); errs != nil && len(errs) > 0 { return errs } return state.LintReleases(helm, values, args, workers) }) }, }, { Name: "sync", Usage: "sync all resources from state file (repos, releases and chart deps)", Flags: []cli.Flag{ cli.StringSliceFlag{ Name: "values", Usage: "additional value files to be merged into the command", }, cli.IntFlag{ Name: "concurrency", Value: 0, Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm exec", }, cli.BoolFlag{ Name: "skip-deps", 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.PrepareRelease(helm, "sync"); errs != nil && len(errs) > 0 { return errs } return executeSyncCommand(c, state, helm) }) }, }, { Name: "apply", Usage: "apply all resources from state file only when there are changes", Flags: []cli.Flag{ cli.StringSliceFlag{ Name: "values", Usage: "additional value files to be merged into the command", }, cli.IntFlag{ Name: "concurrency", Value: 0, Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm exec", }, cli.BoolFlag{ Name: "suppress-secrets", Usage: "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases", }, cli.BoolFlag{ Name: "skip-repo-update", Usage: "skip running `helm repo update` on repositories declared in helmfile", }, cli.BoolFlag{ Name: "skip-deps", Usage: "skip running `helm repo update` and `helm dependency build`", }, }, Action: func(c *cli.Context) error { return findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface, ctx app.Context) []error { if !c.Bool("skip-deps") || !c.Bool("skip-repo-update") { if !c.Bool("skip-repo-update") { logger.Warn("--skip-repo-update has been deprecated. Provide --skip-deps instead.") } 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.PrepareRelease(helm, "apply"); errs != nil && len(errs) > 0 { return errs } releases, errs := executeDiffCommand(c, st, helm, true, c.Bool("suppress-secrets")) releasesToBeDeleted, err := st.DetectReleasesToBeDeleted(helm) if err != nil { errs = append(errs, err) } noError := true for _, e := range errs { switch err := e.(type) { case *state.DiffError: noError = noError && err.Code == 2 default: noError = false } } // 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, st, helm) } } } return errs }) }, }, { Name: "status", Usage: "retrieve status of releases in state file", Flags: []cli.Flag{ cli.IntFlag{ Name: "concurrency", Value: 0, Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, cli.StringFlag{ Name: "args", Value: "", 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 := args.GetArgs(c.String("args"), state) if len(args) > 0 { helm.SetExtraArgs(args...) } return state.ReleaseStatuses(helm, workers) }) }, }, { Name: "delete", Usage: "delete releases from state file (helm delete)", Flags: []cli.Flag{ cli.StringFlag{ Name: "args", Value: "", Usage: "pass args to helm exec", }, cli.BoolFlag{ Name: "purge", Usage: "purge releases i.e. free release names and histories", }, }, Action: func(c *cli.Context) error { return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, true, func(state *state.HelmState, helm helmexec.Interface, _ app.Context) []error { purge := c.Bool("purge") args := args.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(helm, purge) } return nil }) }, }, { Name: "test", Usage: "test releases from state file (helm test)", Flags: []cli.Flag{ cli.BoolFlag{ Name: "cleanup", Usage: "delete test pods upon completion", }, cli.StringFlag{ Name: "args", Value: "", Usage: "pass additional args to helm exec", }, cli.IntFlag{ Name: "timeout", Value: 300, Usage: "maximum time for tests to run before being considered failed", }, cli.IntFlag{ Name: "concurrency", Value: 0, 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 := args.GetArgs(c.String("args"), state) if len(args) > 0 { helm.SetExtraArgs(args...) } return state.TestReleases(helm, cleanup, timeout, concurrency) }) }, }, } err := cliApp.Run(os.Args) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(3) } } func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { args := args.GetArgs(c.String("args"), state) if len(args) > 0 { helm.SetExtraArgs(args...) } values := c.StringSlice("values") workers := c.Int("concurrency") return state.SyncReleases(helm, values, workers) } func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { args := args.GetArgs(c.String("args"), state) values := c.StringSlice("values") workers := c.Int("concurrency") return state.TemplateReleases(helm, values, args, workers) } func executeDiffCommand(c *cli.Context, st *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) ([]*state.ReleaseSpec, []error) { args := args.GetArgs(c.String("args"), st) if len(args) > 0 { helm.SetExtraArgs(args...) } values := c.StringSlice("values") workers := c.Int("concurrency") triggerCleanupEvents := c.Command.HasName("diff") return st.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets, triggerCleanupEvents) } func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface, app.Context) []error) error { return cmd.FindAndIterateOverDesiredStatesUsingFlagsWithReverse(c, false, converge) }