From 3840605e04e7acb8eeb89df9fbd7b515b3cf693a Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Fri, 31 Aug 2018 10:15:02 +0900 Subject: [PATCH] feat: helmfile apply [--auto-approve] (#263) 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 --- README.md | 39 +++++++++------ ask.go | 33 +++++++++++++ main.go | 138 +++++++++++++++++++++++++++++++++++++++--------------- 3 files changed, 158 insertions(+), 52 deletions(-) create mode 100644 ask.go diff --git a/README.md b/README.md index 2624e400..10ea23f9 100644 --- a/README.md +++ b/README.md @@ -185,25 +185,28 @@ USAGE: COMMANDS: repos sync repositories from state file (helm repo add && helm repo update) - charts sync charts from state file (helm upgrade --install) - diff diff charts from state file against env (helm diff) + charts sync releases from state file (helm upgrade --install) + diff diff releases from state file against env (helm diff) lint lint charts from state file (helm lint) - sync sync all resources from state file (repos, charts and local chart deps) + sync sync all resources from state file (repos, releases and chart deps) + apply apply all resources from state file only when there are changes status retrieve status of releases in state file - delete delete charts from state file (helm delete) - test tets releases from state file (helm test) + delete delete releases from state file (helm delete) + test test releases from state file (helm test) GLOBAL OPTIONS: - --file FILE, -f FILE load config from FILE (default: "helmfile.yaml") - --quiet, -q silence output - --namespace value, -n value Set namespace. Uses the namespace set in the context by default - --selector,l value 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 - --kube-context value Set kubectl context. Uses current context by default - --help, -h show help - --version, -v print the version + --helm-binary value, -b value path to helm binary + --file helmfile.yaml, -f helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference + --quiet, -q Silence output. Equivalent to log-level warn + --kube-context value Set kubectl context. Uses current context by default + --log-level value Set log level, default info + --namespace value, -n value Set namespace. Uses the namespace set in the context by default + --selector value, -l value 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 + --help, -h show help + --version, -v print the version ``` ### sync @@ -224,6 +227,12 @@ To supply the diff functionality Helmfile needs the [helm-diff](https://github.c you should be able to simply execute `helm plugin install https://github.com/databus23/helm-diff`. For more details please look at their [documentation](https://github.com/databus23/helm-diff#helm-diff-plugin). +### apply + +The `helmfile apply` sub-command begins by executing `diff`. If `diff` finds that there is any changes, `sync` is executed after prompting you for an confirmation. `--auto-approve` skips the confirmation. + +An expected use-case of `apply` is to schedule it to run periodically, so that you can auto-fix skews between the desired and the current state of your apps running on Kubernetes clusters. + ### delete The `helmfile delete` sub-command deletes all the releases defined in the manfiests diff --git a/ask.go b/ask.go new file mode 100644 index 00000000..8fd38f9c --- /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 ca3b333d..acdfc4ed 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 + } + } + + msg := `Do you really want to apply? + Helmfile will apply all your changes, as shown above. + +` + if allErrsIndicateChanges { + autoApprove := c.Bool("auto-approve") + if autoApprove || !autoApprove && askForConfirmation(msg) { + return executeSyncCommand(c, state, helm) + } + } } - 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) + 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)