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
This commit is contained in:
KUOKA Yusuke 2018-08-31 10:15:02 +09:00 committed by GitHub
parent bb3b44e511
commit 3840605e04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 158 additions and 52 deletions

View File

@ -185,25 +185,28 @@ USAGE:
COMMANDS: COMMANDS:
repos sync repositories from state file (helm repo add && helm repo update) repos sync repositories from state file (helm repo add && helm repo update)
charts sync charts from state file (helm upgrade --install) charts sync releases from state file (helm upgrade --install)
diff diff charts from state file against env (helm diff) diff diff releases from state file against env (helm diff)
lint lint charts from state file (helm lint) 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 status retrieve status of releases in state file
delete delete charts from state file (helm delete) delete delete releases from state file (helm delete)
test tets releases from state file (helm test) test test releases from state file (helm test)
GLOBAL OPTIONS: GLOBAL OPTIONS:
--file FILE, -f FILE load config from FILE (default: "helmfile.yaml") --helm-binary value, -b value path to helm binary
--quiet, -q silence output --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
--namespace value, -n value Set namespace. Uses the namespace set in the context by default --quiet, -q Silence output. Equivalent to log-level warn
--selector,l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar. --kube-context value Set kubectl context. Uses current context by default
A release must match all labels in a group in order to be used. Multiple groups can be specified at once. --log-level value Set log level, default info
--selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases. --namespace value, -n value Set namespace. Uses the namespace set in the context by default
The name of a release can be used as a label. --selector name=myrelease --selector value, -l value Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar.
--kube-context value Set kubectl context. Uses current context by default A release must match all labels in a group in order to be used. Multiple groups can be specified at once.
--help, -h show help --selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases.
--version, -v print the version The name of a release can be used as a label. --selector name=myrelease
--help, -h show help
--version, -v print the version
``` ```
### sync ### 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 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). 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 ### delete
The `helmfile delete` sub-command deletes all the releases defined in the manfiests The `helmfile delete` sub-command deletes all the releases defined in the manfiests

33
ask.go Normal file
View File

@ -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
}
}
}

138
main.go
View File

@ -184,25 +184,7 @@ func main() {
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error { return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
args := args.GetArgs(c.String("args"), state) return executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"))
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)
}) })
}, },
}, },
@ -263,26 +245,64 @@ func main() {
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error { return eachDesiredStateDo(c, func(state *state.HelmState, helm helmexec.Interface) []error {
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 { return executeSyncCommand(c, state, helm)
return errs })
},
},
{
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
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)
}) })
}, },
}, },
@ -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 { func eachDesiredStateDo(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
fileOrDirPath := c.GlobalString("file") fileOrDirPath := c.GlobalString("file")
desiredStateFiles, err := findDesiredStateFiles(fileOrDirPath) desiredStateFiles, err := findDesiredStateFiles(fileOrDirPath)