feat: --detailed-exitcode for `helmfile apply` (#1120)

Resolves #1113
This commit is contained in:
KUOKA Yusuke 2020-02-26 21:09:05 +09:00 committed by GitHub
parent 8acbbc596d
commit 0186254e79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 70 additions and 37 deletions

View File

@ -329,6 +329,10 @@ func main() {
Value: 0, Value: 0,
Usage: "output NUM lines of context around changes", Usage: "output NUM lines of context around changes",
}, },
cli.BoolFlag{
Name: "detailed-exitcode",
Usage: "return a non-zero exit code 2 instead of 0 when there were changes detected AND the changes are synced successfully",
},
cli.StringFlag{ cli.StringFlag{
Name: "args", Name: "args",
Value: "", Value: "",
@ -625,11 +629,11 @@ func action(do func(*app.App, configImpl) error) func(*cli.Context) error {
a := app.New(conf) a := app.New(conf)
a.ErrorHandler = func(err error) error { if err := do(a, conf); err != nil {
return toCliError(implCtx, err) return toCliError(implCtx, err)
} }
return do(a, conf) return nil
} }
} }

View File

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"syscall" "syscall"
"text/tabwriter" "text/tabwriter"
@ -40,8 +41,6 @@ type App struct {
FileOrDir string FileOrDir string
ErrorHandler func(error) error
readFile func(string) ([]byte, error) readFile func(string) ([]byte, error)
fileExists func(string) (bool, error) fileExists func(string) (bool, error)
glob func(string) ([]string, error) glob func(string) ([]string, error)
@ -144,9 +143,31 @@ func (a *App) Sync(c SyncConfigProvider) error {
} }
func (a *App) Apply(c ApplyConfigProvider) error { func (a *App) Apply(c ApplyConfigProvider) error {
return a.ForEachState(func(run *Run) (bool, []error) { var any bool
return a.apply(run, c)
mut := &sync.Mutex{}
err := a.ForEachState(func(run *Run) (bool, []error) {
matched, updated, errs := a.apply(run, c)
mut.Lock()
any = any || updated
mut.Unlock()
return matched, errs
}) })
if err != nil {
return err
}
if c.DetailedExitcode() && any {
code := 2
return &Error{msg: "", Errors: nil, code: &code}
}
return nil
} }
func (a *App) Status(c StatusesConfigProvider) error { func (a *App) Status(c StatusesConfigProvider) error {
@ -410,10 +431,6 @@ func (a *App) ForEachStateFiltered(do func(*Run) []error) error {
return do(run) return do(run)
}) })
if err != nil && a.ErrorHandler != nil {
return a.ErrorHandler(err)
}
return err return err
} }
@ -424,10 +441,6 @@ func (a *App) ForEachState(do func(*Run) (bool, []error)) error {
return do(run) return do(run)
}) })
if err != nil && a.ErrorHandler != nil {
return a.ErrorHandler(err)
}
return err return err
} }
@ -661,7 +674,7 @@ func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) {
return files, nil return files, nil
} }
func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) { func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) {
st := r.state st := r.state
helm := r.helm helm := r.helm
ctx := r.ctx ctx := r.ctx
@ -670,10 +683,10 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) {
toApply, err := st.GetSelectedReleasesWithOverrides() toApply, err := st.GetSelectedReleasesWithOverrides()
if err != nil { if err != nil {
return false, []error{err} return false, false, []error{err}
} }
if len(toApply) == 0 { if len(toApply) == 0 {
return false, nil return false, false, nil
} }
// Do build deps and prepare only on selected releases so that we won't waste time // Do build deps and prepare only on selected releases so that we won't waste time
@ -682,14 +695,14 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) {
if !c.SkipDeps() { if !c.SkipDeps() {
if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 { if errs := ctx.SyncReposOnce(st, helm); errs != nil && len(errs) > 0 {
return false, errs return false, false, errs
} }
if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 { if errs := st.BuildDeps(helm); errs != nil && len(errs) > 0 {
return false, errs return false, false, errs
} }
} }
if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 { if errs := st.PrepareReleases(helm, "apply"); errs != nil && len(errs) > 0 {
return false, errs return false, false, errs
} }
// helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly // helm must be 2.11+ and helm-diff should be provided `--detailed-exitcode` in order for `helmfile apply` to work properly
@ -730,7 +743,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) {
} }
if len(fatalErrs) > 0 { if len(fatalErrs) > 0 {
return false, fatalErrs return false, false, fatalErrs
} }
releasesToBeDeleted := map[string]state.ReleaseSpec{} releasesToBeDeleted := map[string]state.ReleaseSpec{}
@ -755,7 +768,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) {
logger := c.Logger() logger := c.Logger()
logger.Infof("") logger.Infof("")
logger.Infof("No affected releases") logger.Infof("No affected releases")
return true, nil return true, false, nil
} }
names := []string{} names := []string{}
@ -838,7 +851,7 @@ Do you really want to apply?
} }
affectedReleases.DisplayAffectedReleases(c.Logger()) affectedReleases.DisplayAffectedReleases(c.Logger())
return true, syncErrs return true, true, syncErrs
} }
func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) { func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) {
@ -1133,6 +1146,8 @@ type Error struct {
msg string msg string
Errors []error Errors []error
code *int
} }
func (e *Error) Error() string { func (e *Error) Error() string {
@ -1165,6 +1180,10 @@ func (e *Error) Error() string {
} }
func (e *Error) Code() int { func (e *Error) Code() int {
if e.code != nil {
return *e.code
}
allDiff := false allDiff := false
anyNonZero := false anyNonZero := false
for _, err := range e.Errors { for _, err := range e.Errors {
@ -1195,7 +1214,7 @@ func (e *Error) Code() int {
} }
func appError(msg string, err error) error { func appError(msg string, err error) error {
return &Error{msg, []error{err}} return &Error{msg: msg, Errors: []error{err}}
} }
func (c context) clean(errs []error) error { func (c context) clean(errs []error) error {

View File

@ -1899,6 +1899,7 @@ type applyConfig struct {
noColor bool noColor bool
context int context int
concurrency int concurrency int
detailedExitcode bool
interactive bool interactive bool
logger *zap.SugaredLogger logger *zap.SugaredLogger
} }
@ -1939,6 +1940,10 @@ func (a applyConfig) Concurrency() int {
return a.concurrency return a.concurrency
} }
func (a applyConfig) DetailedExitcode() bool {
return a.detailedExitcode
}
func (a applyConfig) Interactive() bool { func (a applyConfig) Interactive() bool {
return a.interactive return a.interactive
} }

View File

@ -43,6 +43,8 @@ type ApplyConfigProvider interface {
SuppressSecrets() bool SuppressSecrets() bool
SuppressDiff() bool SuppressDiff() bool
DetailedExitcode() bool
NoColor() bool NoColor() bool
Context() int Context() int

View File

@ -77,6 +77,9 @@ ${helmfile} -f ${dir}/happypath.yaml template
code=$? code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}" [ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}"
info "Applying ${dir}/happypath.yaml"
bash -c "${helmfile} -f ${dir}/happypath.yaml apply --detailed-exitcode; code="'$?'"; echo Code: "'$code'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile apply"
info "Syncing ${dir}/happypath.yaml" info "Syncing ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml sync ${helmfile} -f ${dir}/happypath.yaml sync
wait_deploy_ready httpbin-httpbin wait_deploy_ready httpbin-httpbin
@ -84,9 +87,9 @@ retry 5 "curl --fail $(minikube service --url --namespace=${test_ns} httpbin-htt
[ ${retry_result} -eq 0 ] || fail "httpbin failed to return 200 OK" [ ${retry_result} -eq 0 ] || fail "httpbin failed to return 200 OK"
info "Applying ${dir}/happypath.yaml" info "Applying ${dir}/happypath.yaml"
${helmfile} -f ${dir}/happypath.yaml apply ${helmfile} -f ${dir}/happypath.yaml apply --detailed-exitcode
code=$? code=$?
[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: ${code}" [ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: want 0, got ${code}"
info "Locking dependencies" info "Locking dependencies"
${helmfile} -f ${dir}/happypath.yaml deps ${helmfile} -f ${dir}/happypath.yaml deps