diff --git a/cmd/root.go b/cmd/root.go index 8829a5d9..587d3e77 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,12 +6,12 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/urfave/cli" "go.uber.org/zap" "github.com/helmfile/helmfile/pkg/app" "github.com/helmfile/helmfile/pkg/app/version" "github.com/helmfile/helmfile/pkg/config" + "github.com/helmfile/helmfile/pkg/errors" "github.com/helmfile/helmfile/pkg/helmexec" ) @@ -26,11 +26,11 @@ func toCLIError(g *config.GlobalImpl, err error) error { if g.AllowNoMatchingRelease { noMatchingExitCode = 0 } - return cli.NewExitError(e.Error(), noMatchingExitCode) + return errors.NewExitError(e.Error(), noMatchingExitCode) case *app.MultiError: - return cli.NewExitError(e.Error(), 1) + return errors.NewExitError(e.Error(), 1) case *app.Error: - return cli.NewExitError(e.Error(), e.Code()) + return errors.NewExitError(e.Error(), e.Code()) default: panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e)) } @@ -41,12 +41,13 @@ func toCLIError(g *config.GlobalImpl, err error) error { // NewRootCmd creates the root command for the CLI. func NewRootCmd(globalConfig *config.GlobalOptions, args []string) (*cobra.Command, error) { cmd := &cobra.Command{ - Use: "helmfile", - Short: globalUsage, - Long: globalUsage, - Args: cobra.MinimumNArgs(1), - Version: version.GetVersion(), - SilenceUsage: true, + Use: "helmfile", + Short: globalUsage, + Long: globalUsage, + Args: cobra.MinimumNArgs(1), + Version: version.GetVersion(), + SilenceUsage: true, + SilenceErrors: true, PersistentPreRunE: func(c *cobra.Command, args []string) error { // Valid levels: // https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126 diff --git a/go.mod b/go.mod index 3159b4d1..9b442157 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,6 @@ require ( github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.0 github.com/tatsushid/go-prettytable v0.0.0-20141013043238-ed2d14c29939 - github.com/urfave/cli v1.22.10 github.com/variantdev/chartify v0.10.2 github.com/variantdev/dag v1.1.0 github.com/variantdev/vals v0.18.0 @@ -62,7 +61,6 @@ require ( github.com/aws/aws-sdk-go v1.40.28 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/blang/semver v3.5.1+incompatible // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/fujiwara/tfstate-lookup v0.4.4 // indirect @@ -107,7 +105,6 @@ require ( github.com/pbnjay/strptime v0.0.0-20140226051138-5c05b0d668c9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect diff --git a/go.sum b/go.sum index 7bc31c6a..f30e358a 100644 --- a/go.sum +++ b/go.sum @@ -394,7 +394,6 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1144,7 +1143,6 @@ github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTE github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc/go.mod h1:HFLT6i9iR4QBOF5rdCyjddC9t59ArqWJV2xx+jwcCMo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -1245,8 +1243,6 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= -github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/variantdev/chartify v0.10.2 h1:CAMlSE4kBl8ft/Xl4ob+eyFZ2KV5evkiMEI43M4CaKs= github.com/variantdev/chartify v0.10.2/go.mod h1:A0nQmb+ihiBJrrbgofs1t7QVeit+/llT0vJhvkj7U0Q= github.com/variantdev/dag v1.1.0 h1:xodYlSng33KWGvIGMpKUyLcIZRXKiNUx612mZJqYrDg= diff --git a/main.go b/main.go index e9b86d51..4cfae4da 100644 --- a/main.go +++ b/main.go @@ -1,28 +1,19 @@ package main import ( - "fmt" "os" - "github.com/urfave/cli" - "github.com/helmfile/helmfile/cmd" "github.com/helmfile/helmfile/pkg/config" + "github.com/helmfile/helmfile/pkg/errors" ) -func warning(format string, v ...interface{}) { - format = fmt.Sprintf("WARNING: %s\n", format) - fmt.Fprintf(os.Stderr, format, v...) -} - func main() { globalConfig := new(config.GlobalOptions) rootCmd, err := cmd.NewRootCmd(globalConfig, os.Args[1:]) - if err != nil { - warning("%+v", err) - os.Exit(1) - } + errors.HandleExitCoder(err) + if err := rootCmd.Execute(); err != nil { - cli.HandleExitCoder(err) + errors.HandleExitCoder(err) } } diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 00000000..0b4aea77 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,97 @@ +/* +* MIT License +* +* Copyright (c) 2022 urfave/cli maintainers +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ + +package errors + +import ( + "fmt" + "io" + "os" +) + +// OsExiter is the function used when the app exits. If not set defaults to os.Exit. +var OsExiter = os.Exit + +// ErrWriter is used to write errors to the user. This can be anything +// implementing the io.Writer interface and defaults to os.Stderr. +var ErrWriter io.Writer = os.Stderr + +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + +// ExitCoder is the interface checked by `App` and `Command` for a custom exit +// code +type ExitCoder interface { + error + ExitCode() int +} + +// ExitError fulfills both the builtin `error` interface and `ExitCoder` +type ExitError struct { + exitCode int + message interface{} +} + +// NewExitError makes a new *ExitError +func NewExitError(message interface{}, exitCode int) *ExitError { + return &ExitError{ + exitCode: exitCode, + message: message, + } +} + +// Error returns the string message, fulfilling the interface required by +// `error` +func (ee *ExitError) Error() string { + return fmt.Sprintf("%v", ee.message) +} + +// ExitCode returns the exit code, fulfilling the interface required by +// `ExitCoder` +func (ee *ExitError) ExitCode() int { + return ee.exitCode +} + +// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if +// so prints the error to stderr (if it is non-empty) and calls OsExiter with the +// given exit code. If the given error is a MultiError, then this func is +// called on all members of the Errors slice and calls OsExiter with the last exit code. +func HandleExitCoder(err error) { + if err == nil { + return + } + + if exitErr, ok := err.(ExitCoder); ok { + if err.Error() != "" { + fmt.Fprintln(ErrWriter, err) + } + OsExiter(exitErr.ExitCode()) + return + } + // unknown error exit with code 3 + fmt.Fprintln(ErrWriter, err) + + OsExiter(3) +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 00000000..89605e96 --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,87 @@ +/* +* MIT License +* +* Copyright (c) 2022 urfave/cli maintainers +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. + */ +package errors + +import ( + "os" + "reflect" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +var ( + wd, _ = os.Getwd() + lastExitCode = 0 + fakeOsExiter = func(rc int) { + lastExitCode = rc + } +) + +func expect(t *testing.T, a interface{}, b interface{}) { + _, fn, line, _ := runtime.Caller(1) + fn = strings.Replace(fn, wd+"/", "", -1) + + require.Equalf(t, a, b, "(%s:%d) Expected %v (type %v) - Got %v (type %v)", fn, line, b, reflect.TypeOf(b), a, reflect.TypeOf(a)) +} + +func TestHandleExitCoder_nil(t *testing.T) { + exitCode := 0 + called := false + + OsExiter = func(rc int) { + if !called { + exitCode = rc + called = true + } + } + + defer func() { OsExiter = fakeOsExiter }() + + HandleExitCoder(nil) + + expect(t, exitCode, 0) + expect(t, called, false) +} + +func TestHandleExitCoder_ExitCoder(t *testing.T) { + exitCode := 0 + called := false + + OsExiter = func(rc int) { + if !called { + exitCode = rc + called = true + } + } + + defer func() { OsExiter = fakeOsExiter }() + + HandleExitCoder(NewExitError("galactic perimeter breach", 9)) + + expect(t, exitCode, 9) + expect(t, called, true) +}