From 255518b44f325cb187563d2bb4e05ed7651405ac Mon Sep 17 00:00:00 2001 From: Yusuke KUOKA Date: Thu, 30 Aug 2018 22:33:50 +0900 Subject: [PATCH] feat: `helmfile apply [--auto-approve] This command syncs releases only if there is any difference between the desired and the current state. It asks for an confirmation by default. Provide `--auto-approve` flag after the `apply` command to skip it. Resolves #205 --- ask.go | 33 ++++++++++++++ main.go | 134 +++++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 132 insertions(+), 35 deletions(-) create mode 100644 ask.go diff --git a/ask.go b/ask.go new file mode 100644 index 000000000..8fd38f9c4 --- /dev/null +++ b/ask.go @@ -0,0 +1,33 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "strings" +) + +// 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 { + reader := bufio.NewReader(os.Stdin) + + for { + fmt.Printf("%s [y/n]: ", s) + + response, err := reader.ReadString('\n') + if err != nil { + log.Fatal(err) + } + + response = strings.ToLower(strings.TrimSpace(response)) + + if response == "y" || response == "yes" { + return true + } else if response == "n" || response == "no" { + return false + } + } +} diff --git a/main.go b/main.go index ca3b333d0..acdfc4edf 100644 --- a/main.go +++ b/main.go @@ -184,25 +184,7 @@ func main() { }, Action: func(c *cli.Context) error { return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error { - args := args.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - if c.GlobalString("helm-binary") != "" { - helm.SetHelmBinary(c.GlobalString("helm-binary")) - } - - if c.Bool("sync-repos") { - if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 { - return errs - } - } - - values := c.StringSlice("values") - workers := c.Int("concurrency") - detailedExitCode := c.Bool("detailed-exitcode") - - return state.DiffReleases(helm, values, workers, detailedExitCode) + return executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode")) }) }, }, @@ -263,26 +245,64 @@ func main() { }, Action: func(c *cli.Context) error { return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error { - if errs := state.SyncRepos(helm); 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: "auto-approve", + Usage: "Skip interactive approval before applying", + }, + }, + Action: func(c *cli.Context) error { + return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error { + errs := executeDiffCommand(c, state, helm, true) + + // sync only when there are changes + if len(errs) > 0 { + allErrsIndicateChanges := true + for _, err := range errs { + switch e := err.(type) { + case *exec.ExitError: + status := e.Sys().(syscall.WaitStatus) + // `helm diff --detailed-exitcode` returns 2 when there are changes + allErrsIndicateChanges = allErrsIndicateChanges && status.ExitStatus() == 2 + default: + allErrsIndicateChanges = false + } + } - if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { - return errs - } + msg := `Do you really want to apply? + Helmfile will apply all your changes, as shown above. - args := args.GetArgs(c.String("args"), state) - if len(args) > 0 { - helm.SetExtraArgs(args...) - } - if c.GlobalString("helm-binary") != "" { - helm.SetHelmBinary(c.GlobalString("helm-binary")) +` + if allErrsIndicateChanges { + autoApprove := c.Bool("auto-approve") + if autoApprove || !autoApprove && askForConfirmation(msg) { + return executeSyncCommand(c, state, helm) + } + } } - values := c.StringSlice("values") - workers := c.Int("concurrency") - - return state.SyncReleases(helm, values, workers) + return errs }) }, }, @@ -393,6 +413,50 @@ func main() { } } +func executeSyncCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface) []error { + if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 { + return errs + } + + if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 { + return errs + } + + args := args.GetArgs(c.String("args"), state) + if len(args) > 0 { + helm.SetExtraArgs(args...) + } + if c.GlobalString("helm-binary") != "" { + helm.SetHelmBinary(c.GlobalString("helm-binary")) + } + + values := c.StringSlice("values") + workers := c.Int("concurrency") + + return state.SyncReleases(helm, values, workers) +} + +func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode bool) []error { + args := args.GetArgs(c.String("args"), state) + if len(args) > 0 { + helm.SetExtraArgs(args...) + } + if c.GlobalString("helm-binary") != "" { + helm.SetHelmBinary(c.GlobalString("helm-binary")) + } + + if c.Bool("sync-repos") { + if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 { + return errs + } + } + + values := c.StringSlice("values") + workers := c.Int("concurrency") + + return state.DiffReleases(helm, values, workers, detailedExitCode) +} + func eachDesiredStateDo(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error { fileOrDirPath := c.GlobalString("file") desiredStateFiles, err := findDesiredStateFiles(fileOrDirPath)