diff --git a/.gitignore b/.gitignore index f6ef0b1d..3a9fcd48 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ vendor/ .vagrant/ *.lock test/integration/.gnupg/ +.vscode/launch.json diff --git a/cmd/helmfile/deps.go b/cmd/helmfile/deps.go new file mode 100644 index 00000000..1f3a564f --- /dev/null +++ b/cmd/helmfile/deps.go @@ -0,0 +1,28 @@ +package helmfile + +import ( + "github.com/helmfile/helmfile/pkg/app" + "github.com/helmfile/helmfile/pkg/config" + "github.com/urfave/cli" +) + +func addDepsSubcommand(cliApp *cli.App) { + cliApp.Commands = append(cliApp.Commands, cli.Command{ + Name: "deps", + Usage: "update charts based on their requirements", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "args", + Value: "", + Usage: "pass args to helm exec", + }, + cli.BoolFlag{ + Name: "skip-repos", + Usage: `skip running "helm repo update" before running "helm dependency build"`, + }, + }, + Action: Action(func(a *app.App, c config.ConfigImpl) error { + return a.Deps(c) + }), + }) +} diff --git a/cmd/helmfile/root.go b/cmd/helmfile/root.go new file mode 100644 index 00000000..2dfb0b64 --- /dev/null +++ b/cmd/helmfile/root.go @@ -0,0 +1,166 @@ +package helmfile + +import ( + "fmt" + "os" + + "github.com/helmfile/helmfile/pkg/app" + "github.com/helmfile/helmfile/pkg/app/version" + "github.com/helmfile/helmfile/pkg/config" + "github.com/helmfile/helmfile/pkg/helmexec" + "github.com/urfave/cli" + "go.uber.org/zap" +) + +var logger *zap.SugaredLogger + +// RootCmd is the root command for helmfile. +func RootCommand() *cli.App { + cliApp := cli.NewApp() + cliApp.Name = "helmfile" + cliApp.Usage = "" + cliApp.Version = version.Version + cliApp.EnableBashCompletion = true + cliApp.Before = configureLogging + setRootCommandFlags(cliApp) + + // add subcommands + addDepsSubcommand(cliApp) + + return cliApp +} + +// setRootCommandFlags sets the flags for the root command. +func setRootCommandFlags(cliApp *cli.App) { + cliApp.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "helm-binary, b", + Usage: "path to helm binary", + Value: app.DefaultHelmBinary, + }, + 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.StringSliceFlag{ + Name: "state-values-set", + Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)", + }, + cli.StringSliceFlag{ + Name: "state-values-file", + Usage: "specify state values in a YAML file", + }, + 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.BoolFlag{ + Name: "debug", + Usage: "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect", + }, + cli.BoolFlag{ + Name: "color", + Usage: "Output with color", + }, + cli.BoolFlag{ + Name: "no-color", + Usage: "Output without color", + }, + 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.StringFlag{ + Name: "chart, c", + Usage: "Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}", + }, + 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: "allow-no-matching-release", + Usage: `Do not exit with an error code if the provided selector has no matching releases.`, + }, + cli.BoolFlag{ + Name: "interactive, i", + Usage: "Request confirmation before attempting to modify clusters", + }, + } + +} + +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.MultiError: + return cli.NewExitError(e.Error(), 1) + 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 +} + +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("debug") { + logLevel = "debug" + } else if c.GlobalBool("quiet") { + logLevel = "warn" + } + logger = helmexec.NewLogger(os.Stderr, 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 Action(do func(*app.App, config.ConfigImpl) error) func(*cli.Context) error { + return func(implCtx *cli.Context) error { + conf, err := config.NewUrfaveCliConfigImpl(implCtx) + if err != nil { + return err + } + + if err := app.ValidateConfig(conf); err != nil { + return err + } + + a := app.New(conf) + + if err := do(a, conf); err != nil { + return toCliError(implCtx, err) + } + + return nil + } +} diff --git a/main.go b/main.go index fbb5f222..f417b242 100644 --- a/main.go +++ b/main.go @@ -3,137 +3,17 @@ package main import ( "fmt" "os" - "strings" + "github.com/helmfile/helmfile/cmd/helmfile" "github.com/helmfile/helmfile/pkg/app" - "github.com/helmfile/helmfile/pkg/app/version" - "github.com/helmfile/helmfile/pkg/helmexec" - "github.com/helmfile/helmfile/pkg/maputil" - "github.com/helmfile/helmfile/pkg/state" + "github.com/helmfile/helmfile/pkg/config" "github.com/urfave/cli" - "go.uber.org/zap" - "golang.org/x/crypto/ssh/terminal" ) -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("debug") { - logLevel = "debug" - } else if c.GlobalBool("quiet") { - logLevel = "warn" - } - logger = helmexec.NewLogger(os.Stderr, 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.Version - cliApp.EnableBashCompletion = true - cliApp.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "helm-binary, b", - Usage: "path to helm binary", - Value: app.DefaultHelmBinary, - }, - 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.StringSliceFlag{ - Name: "state-values-set", - Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)", - }, - cli.StringSliceFlag{ - Name: "state-values-file", - Usage: "specify state values in a YAML file", - }, - 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.BoolFlag{ - Name: "debug", - Usage: "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect", - }, - cli.BoolFlag{ - Name: "color", - Usage: "Output with color", - }, - cli.BoolFlag{ - Name: "no-color", - Usage: "Output without color", - }, - 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.StringFlag{ - Name: "chart, c", - Usage: "Set chart. Uses the chart set in release by default, and is available in template as {{ .Chart }}", - }, - 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: "allow-no-matching-release", - Usage: `Do not exit with an error code if the provided selector has no matching releases.`, - }, - cli.BoolFlag{ - Name: "interactive, i", - Usage: "Request confirmation before attempting to modify clusters", - }, - } - - cliApp.Before = configureLogging - cliApp.Commands = []cli.Command{ - { - Name: "deps", - Usage: "update charts based on their requirements", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "args", - Value: "", - Usage: "pass args to helm exec", - }, - cli.BoolFlag{ - Name: "skip-repos", - Usage: `skip running "helm repo update" before running "helm dependency build"`, - }, - }, - Action: action(func(a *app.App, c configImpl) error { - return a.Deps(c) - }), - }, + cmd := helmfile.RootCommand() + subCommands := []cli.Command{ { Name: "repos", Usage: "sync repositories from state file (helm repo add && helm repo update)", @@ -144,7 +24,7 @@ func main() { Usage: "pass args to helm exec", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Repos(c) }), }, @@ -171,7 +51,7 @@ func main() { Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.DeprecatedSyncCharts(c) }), }, @@ -249,7 +129,7 @@ func main() { Usage: "output format for diff plugin", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Diff(c) }), }, @@ -312,7 +192,7 @@ func main() { Usage: "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Template(c) }), }, @@ -342,7 +222,7 @@ func main() { Usage: `skip running "helm repo update" and "helm dependency build"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.WriteValues(c) }), }, @@ -373,7 +253,7 @@ func main() { Usage: `skip running "helm repo update" and "helm dependency build"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Lint(c) }), }, @@ -395,7 +275,7 @@ func main() { Usage: "directory to store charts (default: temporary directory which is deleted when the command terminates)", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Fetch(c) }), }, @@ -454,7 +334,7 @@ func main() { Usage: `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Sync(c) }), }, @@ -559,7 +439,7 @@ func main() { Usage: `Override helmDefaults.waitForJobs setting "helm upgrade --install --wait-for-jobs"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Apply(c) }), }, @@ -578,7 +458,7 @@ func main() { Usage: "pass args to helm exec", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Status(c) }), }, @@ -605,7 +485,7 @@ func main() { Usage: `skip running "helm repo update" and "helm dependency build"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Delete(c) }), }, @@ -628,7 +508,7 @@ func main() { Usage: `skip running "helm repo update" and "helm dependency build"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Destroy(c) }), }, @@ -664,7 +544,7 @@ func main() { Usage: `skip running "helm repo update" and "helm dependency build"`, }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.Test(c) }), }, @@ -677,7 +557,7 @@ func main() { Usage: "Read all the values files for every release and embed into the output helmfile.yaml", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.PrintState(c) }), }, @@ -695,7 +575,7 @@ func main() { Usage: "Keep temporary directory", }, }, - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.ListReleases(c) }), }, @@ -707,14 +587,14 @@ func main() { { Name: "info", Usage: "cache info", - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.ShowCacheDir(c) }), }, { Name: "cleanup", Usage: "clean up cache directory", - Action: action(func(a *app.App, c configImpl) error { + Action: helmfile.Action(func(a *app.App, c config.ConfigImpl) error { return a.CleanCacheDir(c) }), }, @@ -730,335 +610,11 @@ func main() { }, }, } + cmd.Commands = append(cmd.Commands, subCommands...) - err := cliApp.Run(os.Args) + err := cmd.Run(os.Args) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(3) } } - -type configImpl struct { - c *cli.Context - - set map[string]interface{} -} - -func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { - if c.NArg() > 0 { - err := cli.ShowAppHelp(c) - if err != nil { - return configImpl{}, err - } - return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) - } - - conf := configImpl{ - c: c, - } - - optsSet := c.GlobalStringSlice("state-values-set") - if len(optsSet) > 0 { - set := map[string]interface{}{} - for i := range optsSet { - ops := strings.Split(optsSet[i], ",") - for j := range ops { - op := strings.SplitN(ops[j], "=", 2) - k := maputil.ParseKey(op[0]) - v := op[1] - - maputil.Set(set, k, v) - } - } - conf.set = set - } - - return conf, nil -} - -func (c configImpl) Set() []string { - return c.c.StringSlice("set") -} - -func (c configImpl) SkipRepos() bool { - return c.c.Bool("skip-repos") -} - -func (c configImpl) Wait() bool { - return c.c.Bool("wait") -} - -func (c configImpl) WaitForJobs() bool { - return c.c.Bool("wait-for-jobs") -} - -func (c configImpl) Values() []string { - return c.c.StringSlice("values") -} - -func (c configImpl) Args() string { - args := c.c.String("args") - enableHelmDebug := c.c.GlobalBool("debug") - - if enableHelmDebug { - args = fmt.Sprintf("%s %s", args, "--debug") - } - return args -} - -func (c configImpl) OutputDir() string { - return strings.TrimRight(c.c.String("output-dir"), fmt.Sprintf("%c", os.PathSeparator)) -} - -func (c configImpl) OutputDirTemplate() string { - return c.c.String("output-dir-template") -} - -func (c configImpl) OutputFileTemplate() string { - return c.c.String("output-file-template") -} - -func (c configImpl) Validate() bool { - return c.c.Bool("validate") -} - -func (c configImpl) Concurrency() int { - return c.c.Int("concurrency") -} - -func (c configImpl) HasCommandName(name string) bool { - return c.c.Command.HasName(name) -} - -func (c configImpl) SkipNeeds() bool { - if !c.IncludeNeeds() { - return c.c.Bool("skip-needs") - } - - return false -} - -func (c configImpl) IncludeNeeds() bool { - return c.c.Bool("include-needs") || c.IncludeTransitiveNeeds() -} - -func (c configImpl) IncludeTransitiveNeeds() bool { - return c.c.Bool("include-transitive-needs") -} - -// 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) RetainValuesFiles() bool { - return c.c.Bool("retain-values-files") -} - -func (c configImpl) IncludeTests() bool { - return c.c.Bool("include-tests") -} - -func (c configImpl) Suppress() []string { - return c.c.StringSlice("suppress") -} - -func (c configImpl) SuppressSecrets() bool { - return c.c.Bool("suppress-secrets") -} - -func (c configImpl) ShowSecrets() bool { - return c.c.Bool("show-secrets") -} - -func (c configImpl) SuppressDiff() bool { - return c.c.Bool("suppress-diff") -} - -// 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) Logs() bool { - return c.c.Bool("logs") -} - -func (c configImpl) Timeout() int { - if !c.c.IsSet("timeout") { - return state.EmptyTimeout - } - return c.c.Int("timeout") -} - -// ListConfig - -func (c configImpl) Output() string { - return c.c.String("output") -} - -func (c configImpl) KeepTempDir() bool { - return c.c.Bool("keep-temp-dir") -} - -// 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) Chart() string { - return c.c.GlobalString("chart") -} - -func (c configImpl) FileOrDir() string { - return c.c.GlobalString("file") -} - -func (c configImpl) Selectors() []string { - return c.c.GlobalStringSlice("selector") -} - -func (c configImpl) StateValuesSet() map[string]interface{} { - return c.set -} - -func (c configImpl) StateValuesFiles() []string { - return c.c.GlobalStringSlice("state-values-file") -} - -func (c configImpl) Interactive() bool { - return c.c.GlobalBool("interactive") -} - -func (c configImpl) Color() bool { - if c := c.c.GlobalBool("color"); c { - return c - } - - if c.NoColor() { - return false - } - - // We replicate the helm-diff behavior in helmfile - // because when when helmfile calls helm-diff, helm-diff has no access to term and therefore - // we can't rely on helm-diff's ability to auto-detect term for color output. - // See https://github.com/roboll/helmfile/issues/2043 - - term := terminal.IsTerminal(int(os.Stdout.Fd())) - // https://github.com/databus23/helm-diff/issues/281 - dumb := os.Getenv("TERM") == "dumb" - return term && !dumb -} - -func (c configImpl) NoColor() bool { - return c.c.GlobalBool("no-color") -} - -func (c configImpl) Context() int { - return c.c.Int("context") -} - -func (c configImpl) DiffOutput() string { - return c.c.String("output") -} - -func (c configImpl) SkipCleanup() bool { - return c.c.Bool("skip-cleanup") -} - -func (c configImpl) SkipCRDs() bool { - return c.c.Bool("skip-crds") -} - -func (c configImpl) SkipDiffOnInstall() bool { - return c.c.Bool("skip-diff-on-install") -} - -func (c configImpl) EmbedValues() bool { - return c.c.Bool("embed-values") -} - -func (c configImpl) IncludeCRDs() bool { - return c.c.Bool("include-crds") -} - -func (c configImpl) SkipTests() bool { - return c.c.Bool("skip-tests") -} - -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 = os.Getenv("HELMFILE_ENVIRONMENT") - if env == "" { - env = state.DefaultEnv - } - } - return env -} - -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 - } - - if err := app.ValidateConfig(conf); err != nil { - return err - } - - a := app.New(conf) - - if err := do(a, conf); err != nil { - return toCliError(implCtx, err) - } - - return nil - } -} - -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.MultiError: - return cli.NewExitError(e.Error(), 1) - 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/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..fce36e46 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,297 @@ +package config + +import ( + "fmt" + "os" + "strings" + + "github.com/helmfile/helmfile/pkg/maputil" + "github.com/helmfile/helmfile/pkg/state" + "github.com/urfave/cli" + "go.uber.org/zap" + "golang.org/x/crypto/ssh/terminal" +) + +type ConfigImpl struct { + c *cli.Context + + set map[string]interface{} +} + +func NewUrfaveCliConfigImpl(c *cli.Context) (ConfigImpl, error) { + if c.NArg() > 0 { + err := cli.ShowAppHelp(c) + if err != nil { + return ConfigImpl{}, err + } + return ConfigImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) + } + + conf := ConfigImpl{ + c: c, + } + + optsSet := c.GlobalStringSlice("state-values-set") + if len(optsSet) > 0 { + set := map[string]interface{}{} + for i := range optsSet { + ops := strings.Split(optsSet[i], ",") + for j := range ops { + op := strings.SplitN(ops[j], "=", 2) + k := maputil.ParseKey(op[0]) + v := op[1] + + maputil.Set(set, k, v) + } + } + conf.set = set + } + + return conf, nil +} + +func (c ConfigImpl) Set() []string { + return c.c.StringSlice("set") +} + +func (c ConfigImpl) SkipRepos() bool { + return c.c.Bool("skip-repos") +} + +func (c ConfigImpl) Wait() bool { + return c.c.Bool("wait") +} + +func (c ConfigImpl) WaitForJobs() bool { + return c.c.Bool("wait-for-jobs") +} + +func (c ConfigImpl) Values() []string { + return c.c.StringSlice("values") +} + +func (c ConfigImpl) Args() string { + args := c.c.String("args") + enableHelmDebug := c.c.GlobalBool("debug") + + if enableHelmDebug { + args = fmt.Sprintf("%s %s", args, "--debug") + } + return args +} + +func (c ConfigImpl) OutputDir() string { + return strings.TrimRight(c.c.String("output-dir"), fmt.Sprintf("%c", os.PathSeparator)) +} + +func (c ConfigImpl) OutputDirTemplate() string { + return c.c.String("output-dir-template") +} + +func (c ConfigImpl) OutputFileTemplate() string { + return c.c.String("output-file-template") +} + +func (c ConfigImpl) Validate() bool { + return c.c.Bool("validate") +} + +func (c ConfigImpl) Concurrency() int { + return c.c.Int("concurrency") +} + +func (c ConfigImpl) HasCommandName(name string) bool { + return c.c.Command.HasName(name) +} + +func (c ConfigImpl) SkipNeeds() bool { + if !c.IncludeNeeds() { + return c.c.Bool("skip-needs") + } + + return false +} + +func (c ConfigImpl) IncludeNeeds() bool { + return c.c.Bool("include-needs") || c.IncludeTransitiveNeeds() +} + +func (c ConfigImpl) IncludeTransitiveNeeds() bool { + return c.c.Bool("include-transitive-needs") +} + +// 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) RetainValuesFiles() bool { + return c.c.Bool("retain-values-files") +} + +func (c ConfigImpl) IncludeTests() bool { + return c.c.Bool("include-tests") +} + +func (c ConfigImpl) Suppress() []string { + return c.c.StringSlice("suppress") +} + +func (c ConfigImpl) SuppressSecrets() bool { + return c.c.Bool("suppress-secrets") +} + +func (c ConfigImpl) ShowSecrets() bool { + return c.c.Bool("show-secrets") +} + +func (c ConfigImpl) SuppressDiff() bool { + return c.c.Bool("suppress-diff") +} + +// 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) Logs() bool { + return c.c.Bool("logs") +} + +func (c ConfigImpl) Timeout() int { + if !c.c.IsSet("timeout") { + return state.EmptyTimeout + } + return c.c.Int("timeout") +} + +// ListConfig + +func (c ConfigImpl) Output() string { + return c.c.String("output") +} + +func (c ConfigImpl) KeepTempDir() bool { + return c.c.Bool("keep-temp-dir") +} + +// 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) Chart() string { + return c.c.GlobalString("chart") +} + +func (c ConfigImpl) FileOrDir() string { + return c.c.GlobalString("file") +} + +func (c ConfigImpl) Selectors() []string { + return c.c.GlobalStringSlice("selector") +} + +func (c ConfigImpl) StateValuesSet() map[string]interface{} { + return c.set +} + +func (c ConfigImpl) StateValuesFiles() []string { + return c.c.GlobalStringSlice("state-values-file") +} + +func (c ConfigImpl) Interactive() bool { + return c.c.GlobalBool("interactive") +} + +func (c ConfigImpl) Color() bool { + if c := c.c.GlobalBool("color"); c { + return c + } + + if c.NoColor() { + return false + } + + // We replicate the helm-diff behavior in helmfile + // because when when helmfile calls helm-diff, helm-diff has no access to term and therefore + // we can't rely on helm-diff's ability to auto-detect term for color output. + // See https://github.com/roboll/helmfile/issues/2043 + + term := terminal.IsTerminal(int(os.Stdout.Fd())) + // https://github.com/databus23/helm-diff/issues/281 + dumb := os.Getenv("TERM") == "dumb" + return term && !dumb +} + +func (c ConfigImpl) NoColor() bool { + return c.c.GlobalBool("no-color") +} + +func (c ConfigImpl) Context() int { + return c.c.Int("context") +} + +func (c ConfigImpl) DiffOutput() string { + return c.c.String("output") +} + +func (c ConfigImpl) SkipCleanup() bool { + return c.c.Bool("skip-cleanup") +} + +func (c ConfigImpl) SkipCRDs() bool { + return c.c.Bool("skip-crds") +} + +func (c ConfigImpl) SkipDiffOnInstall() bool { + return c.c.Bool("skip-diff-on-install") +} + +func (c ConfigImpl) EmbedValues() bool { + return c.c.Bool("embed-values") +} + +func (c ConfigImpl) IncludeCRDs() bool { + return c.c.Bool("include-crds") +} + +func (c ConfigImpl) SkipTests() bool { + return c.c.Bool("skip-tests") +} + +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 = os.Getenv("HELMFILE_ENVIRONMENT") + if env == "" { + env = state.DefaultEnv + } + } + return env +}