diff --git a/main.go b/main.go index 5881fb0f..163fe6a5 100644 --- a/main.go +++ b/main.go @@ -329,6 +329,10 @@ func main() { Value: 0, 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{ Name: "args", Value: "", @@ -625,11 +629,11 @@ func action(do func(*app.App, configImpl) error) func(*cli.Context) error { a := app.New(conf) - a.ErrorHandler = func(err error) error { + if err := do(a, conf); err != nil { return toCliError(implCtx, err) } - return do(a, conf) + return nil } } diff --git a/pkg/app/app.go b/pkg/app/app.go index 15a7efdf..38636b49 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -10,6 +10,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "syscall" "text/tabwriter" @@ -40,8 +41,6 @@ type App struct { FileOrDir string - ErrorHandler func(error) error - readFile func(string) ([]byte, error) fileExists func(string) (bool, error) glob func(string) ([]string, error) @@ -144,9 +143,31 @@ func (a *App) Sync(c SyncConfigProvider) error { } func (a *App) Apply(c ApplyConfigProvider) error { - return a.ForEachState(func(run *Run) (bool, []error) { - return a.apply(run, c) + var any bool + + 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 { @@ -410,10 +431,6 @@ func (a *App) ForEachStateFiltered(do func(*Run) []error) error { return do(run) }) - if err != nil && a.ErrorHandler != nil { - return a.ErrorHandler(err) - } - return err } @@ -424,10 +441,6 @@ func (a *App) ForEachState(do func(*Run) (bool, []error)) error { return do(run) }) - if err != nil && a.ErrorHandler != nil { - return a.ErrorHandler(err) - } - return err } @@ -661,7 +674,7 @@ func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) { 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 helm := r.helm ctx := r.ctx @@ -670,10 +683,10 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) { toApply, err := st.GetSelectedReleasesWithOverrides() if err != nil { - return false, []error{err} + return false, false, []error{err} } 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 @@ -682,14 +695,14 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) { if !c.SkipDeps() { 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 { - return false, errs + return false, false, errs } } 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 @@ -730,7 +743,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) { } if len(fatalErrs) > 0 { - return false, fatalErrs + return false, false, fatalErrs } releasesToBeDeleted := map[string]state.ReleaseSpec{} @@ -755,7 +768,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, []error) { logger := c.Logger() logger.Infof("") logger.Infof("No affected releases") - return true, nil + return true, false, nil } names := []string{} @@ -838,7 +851,7 @@ Do you really want to apply? } affectedReleases.DisplayAffectedReleases(c.Logger()) - return true, syncErrs + return true, true, syncErrs } func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error) { @@ -1133,6 +1146,8 @@ type Error struct { msg string Errors []error + + code *int } func (e *Error) Error() string { @@ -1165,6 +1180,10 @@ func (e *Error) Error() string { } func (e *Error) Code() int { + if e.code != nil { + return *e.code + } + allDiff := false anyNonZero := false for _, err := range e.Errors { @@ -1195,7 +1214,7 @@ func (e *Error) Code() int { } 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 { diff --git a/pkg/app/app_test.go b/pkg/app/app_test.go index b74f9d33..343af86a 100644 --- a/pkg/app/app_test.go +++ b/pkg/app/app_test.go @@ -1890,17 +1890,18 @@ func (c configImpl) Concurrency() int { } type applyConfig struct { - args string - values []string - set []string - skipDeps bool - suppressSecrets bool - suppressDiff bool - noColor bool - context int - concurrency int - interactive bool - logger *zap.SugaredLogger + args string + values []string + set []string + skipDeps bool + suppressSecrets bool + suppressDiff bool + noColor bool + context int + concurrency int + detailedExitcode bool + interactive bool + logger *zap.SugaredLogger } func (a applyConfig) Args() string { @@ -1939,6 +1940,10 @@ func (a applyConfig) Concurrency() int { return a.concurrency } +func (a applyConfig) DetailedExitcode() bool { + return a.detailedExitcode +} + func (a applyConfig) Interactive() bool { return a.interactive } diff --git a/pkg/app/config.go b/pkg/app/config.go index 63e1c21f..377d325a 100644 --- a/pkg/app/config.go +++ b/pkg/app/config.go @@ -43,6 +43,8 @@ type ApplyConfigProvider interface { SuppressSecrets() bool SuppressDiff() bool + DetailedExitcode() bool + NoColor() bool Context() int diff --git a/test/integration/run.sh b/test/integration/run.sh index ec9605fb..14707b4c 100755 --- a/test/integration/run.sh +++ b/test/integration/run.sh @@ -77,6 +77,9 @@ ${helmfile} -f ${dir}/happypath.yaml 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" ${helmfile} -f ${dir}/happypath.yaml sync 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" info "Applying ${dir}/happypath.yaml" -${helmfile} -f ${dir}/happypath.yaml apply +${helmfile} -f ${dir}/happypath.yaml apply --detailed-exitcode 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" ${helmfile} -f ${dir}/happypath.yaml deps