diff --git a/awshelper/config.go b/awshelper/config.go index 389956fcbe..334656cea5 100644 --- a/awshelper/config.go +++ b/awshelper/config.go @@ -17,7 +17,7 @@ import ( "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/sts" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" ) @@ -81,7 +81,7 @@ func CreateAwsSessionFromConfig(config *AwsSessionConfig, terragruntOptions *opt sess, err := session.NewSessionWithOptions(sessionOptions) if err != nil { - return nil, errors.WithStackTraceAndPrefix(err, "Error initializing session") + return nil, errors.Errorf("Error initializing session: %w", err) } sess.Handlers.Build.PushFrontNamed(addUserAgent) @@ -131,7 +131,7 @@ func (f tokenFetcher) FetchToken(ctx credentials.Context) ([]byte, error) { token, err := os.ReadFile(string(f)) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return token, nil @@ -204,7 +204,7 @@ func CreateAwsSession(config *AwsSessionConfig, terragruntOptions *options.Terra sess, err = session.NewSessionWithOptions(sessionOptions) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } sess.Handlers.Build.PushFrontNamed(addUserAgent) @@ -223,7 +223,7 @@ func CreateAwsSession(config *AwsSessionConfig, terragruntOptions *options.Terra } else { sess, err = CreateAwsSessionFromConfig(config, terragruntOptions) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } } @@ -234,7 +234,7 @@ func CreateAwsSession(config *AwsSessionConfig, terragruntOptions *options.Terra msg = fmt.Sprintf("Error finding AWS credentials in file '%s' (did you set the correct file name and/or profile?)", config.CredsFilename) } - return nil, errors.WithStackTraceAndPrefix(err, msg) //nolint:govet + return nil, errors.Errorf("%s: %w", msg, err) } return sess, nil @@ -246,7 +246,7 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error) sess, err := session.NewSessionWithOptions(sessionOptions) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } sess.Handlers.Build.PushFrontNamed(addUserAgent) @@ -257,7 +257,7 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error) _, err = sess.Config.Credentials.Get() if err != nil { - return nil, errors.WithStackTraceAndPrefix(err, "Error finding AWS credentials (did you set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables?)") + return nil, errors.Errorf("error finding AWS credentials (did you set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables?): %w", err) } stsClient := sts.New(sess) @@ -282,7 +282,7 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error) output, err := stsClient.AssumeRole(&input) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return output.Credentials, nil @@ -296,7 +296,7 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error) } else { tb, err := os.ReadFile(iamRoleOpts.WebIdentityToken) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } token = string(tb) @@ -314,7 +314,7 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error) // N.B: copied from SDK implementation req.RetryErrorCodes = append(req.RetryErrorCodes, sts.ErrCodeInvalidIdentityTokenException) if err := req.Send(); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return resp.Credentials, nil @@ -324,12 +324,12 @@ func AssumeIamRole(iamRoleOpts options.IAMRoleOptions) (*sts.Credentials, error) func GetAWSCallerIdentity(config *AwsSessionConfig, terragruntOptions *options.TerragruntOptions) (sts.GetCallerIdentityOutput, error) { sess, err := CreateAwsSession(config, terragruntOptions) if err != nil { - return sts.GetCallerIdentityOutput{}, errors.WithStackTrace(err) + return sts.GetCallerIdentityOutput{}, errors.New(err) } identity, err := sts.New(sess).GetCallerIdentity(nil) if err != nil { - return sts.GetCallerIdentityOutput{}, errors.WithStackTrace(err) + return sts.GetCallerIdentityOutput{}, errors.New(err) } return *identity, nil @@ -346,12 +346,12 @@ func ValidateAwsSession(config *AwsSessionConfig, terragruntOptions *options.Ter func GetAWSPartition(config *AwsSessionConfig, terragruntOptions *options.TerragruntOptions) (string, error) { identity, err := GetAWSCallerIdentity(config, terragruntOptions) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } arn, err := arn.Parse(*identity.Arn) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return arn.Partition, nil @@ -361,7 +361,7 @@ func GetAWSPartition(config *AwsSessionConfig, terragruntOptions *options.Terrag func GetAWSAccountID(config *AwsSessionConfig, terragruntOptions *options.TerragruntOptions) (string, error) { identity, err := GetAWSCallerIdentity(config, terragruntOptions) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return *identity.Account, nil @@ -371,7 +371,7 @@ func GetAWSAccountID(config *AwsSessionConfig, terragruntOptions *options.Terrag func GetAWSIdentityArn(config *AwsSessionConfig, terragruntOptions *options.TerragruntOptions) (string, error) { identity, err := GetAWSCallerIdentity(config, terragruntOptions) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return *identity.Arn, nil @@ -381,7 +381,7 @@ func GetAWSIdentityArn(config *AwsSessionConfig, terragruntOptions *options.Terr func GetAWSUserID(config *AwsSessionConfig, terragruntOptions *options.TerragruntOptions) (string, error) { identity, err := GetAWSCallerIdentity(config, terragruntOptions) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return *identity.UserId, nil diff --git a/cli/app.go b/cli/app.go index e72a7dd4d7..5c5ef060ca 100644 --- a/cli/app.go +++ b/cli/app.go @@ -3,14 +3,14 @@ package cli import ( "context" - goerrors "errors" "fmt" "os" "path/filepath" "sort" - "time" "github.com/gruntwork-io/terragrunt/engine" + "github.com/gruntwork-io/terragrunt/internal/os/exec" + "github.com/gruntwork-io/terragrunt/internal/os/signal" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" "github.com/gruntwork-io/terragrunt/pkg/log/hooks" @@ -27,9 +27,9 @@ import ( "github.com/gruntwork-io/terragrunt/shell" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/go-commons/version" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" hashicorpversion "github.com/hashicorp/go-version" @@ -49,9 +49,6 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/cli" ) -// forced shutdown interval after receiving an interrupt signal -const forceExitInterval = shell.SignalForwardingDelay * 2 - func init() { cli.AppVersionTemplate = AppVersionTemplate cli.AppHelpTemplate = AppHelpTemplate @@ -84,6 +81,7 @@ func NewApp(opts *options.TerragruntOptions) *App { app.Before = beforeAction(opts) app.DefaultCommand = terraformCmd.NewCommand(opts).WrapAction(WrapWithTelemetry(opts)) // by default, if no terragrunt command is specified, run the Terraform command app.OsExiter = OSExiter + app.ExitErrHandler = ExitErrHandler return &App{app, opts} } @@ -92,25 +90,26 @@ func (app *App) Run(args []string) error { return app.RunContext(context.Background(), args) } +func (app *App) registerGracefullyShutdown(ctx context.Context) context.Context { + ctx, cancel := context.WithCancelCause(ctx) + + signal.NotifierWithContext(ctx, func(sig os.Signal) { + // Carriage return helps prevent "^C" from being printed + fmt.Fprint(app.Writer, "\r") //nolint:errcheck + app.opts.Logger.Infof("%s signal received. Gracefully shutting down...", cases.Title(language.English).String(sig.String())) + + cancel(signal.NewContextCanceledError(sig)) + }, signal.InterruptSignals...) + + return ctx +} + func (app *App) RunContext(ctx context.Context, args []string) error { ctx, cancel := context.WithCancel(ctx) defer cancel() - shell.RegisterSignalHandler(func(signal os.Signal) { - app.opts.Logger.Infof("%s signal received. Gracefully shutting down... (it can take up to %v)", cases.Title(language.English).String(signal.String()), shell.SignalForwardingDelay) - cancel() + ctx = app.registerGracefullyShutdown(ctx) - shell.RegisterSignalHandler(func(signal os.Signal) { - app.opts.Logger.Infof("Second %s signal received, force shutting down...", cases.Title(language.English).String(signal.String())) - os.Exit(1) - }) - - time.Sleep(forceExitInterval) - app.opts.Logger.Infof("Failed to gracefully shutdown within %v, force shutting down...", forceExitInterval) - os.Exit(1) - }, shell.InterruptSignals...) - - // configure telemetry integration err := telemetry.InitTelemetry(ctx, &telemetry.TelemetryOptions{ Vars: env.Parse(os.Environ()), AppName: app.Name, @@ -137,7 +136,7 @@ func (app *App) RunContext(ctx context.Context, args []string) error { } }(ctx) - if err := app.App.RunContext(ctx, args); err != nil && !goerrors.Is(err, context.Canceled) { + if err := app.App.RunContext(ctx, args); err != nil && !errors.IsContextCanceled(err) { return err } @@ -282,7 +281,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { if opts.WorkingDir == "" { currentDir, err := os.Getwd() if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.WorkingDir = currentDir @@ -292,7 +291,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { workingDir, err := filepath.Abs(opts.WorkingDir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger = opts.Logger.WithField(format.PrefixKeyName, workingDir) @@ -315,7 +314,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { downloadDir, err := filepath.Abs(opts.DownloadDir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.DownloadDir = filepath.ToSlash(downloadDir) @@ -329,7 +328,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { opts.TerragruntConfigPath, err = filepath.Abs(opts.TerragruntConfigPath) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.TerraformPath = filepath.ToSlash(opts.TerraformPath) @@ -361,7 +360,7 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { if err != nil { // Malformed Terragrunt version; set the version to 0.0 if terragruntVersion, err = hashicorpversion.NewVersion("0.0"); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -381,12 +380,18 @@ func initialSetup(cliCtx *cli.Context, opts *options.TerragruntOptions) error { opts.RunTerragrunt = terraformCmd.Run - shell.PrepareConsole(opts) + exec.PrepareConsole(opts.Logger) return nil } +// OSExiter is an empty function that overrides the default behavior. func OSExiter(exitCode int) { // Do nothing. We just need to override this function, as the default value calls os.Exit, which // kills the app (or any automated test) dead in its tracks. } + +// ExitErrHandler is an empty function that overrides the default behavior. +func ExitErrHandler(_ *cli.Context, err error) error { + return err +} diff --git a/cli/app_test.go b/cli/app_test.go index 9b89b441a3..b24ecaee11 100644 --- a/cli/app_test.go +++ b/cli/app_test.go @@ -8,7 +8,6 @@ import ( "strings" "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli" "github.com/gruntwork-io/terragrunt/cli/commands" awsproviderpatch "github.com/gruntwork-io/terragrunt/cli/commands/aws-provider-patch" @@ -17,6 +16,7 @@ import ( runall "github.com/gruntwork-io/terragrunt/cli/commands/run-all" terraformcmd "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" cliPkg "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/pkg/log" @@ -225,7 +225,7 @@ func mockOptions(t *testing.T, terragruntConfigPath string, workingDir string, t opts, err := options.NewTerragruntOptionsForTest(terragruntConfigPath) if err != nil { - t.Fatalf("error: %v\n", errors.WithStackTrace(err)) + t.Fatalf("error: %v\n", errors.New(err)) } opts.WorkingDir = workingDir @@ -498,6 +498,7 @@ func runAppTest(args []string, opts *options.TerragruntOptions) (*options.Terrag terragruntCommands...).WrapAction(cli.WrapWithTelemetry(opts)) app.DefaultCommand = defaultCommand.WrapAction(cli.WrapWithTelemetry(opts)) app.OsExiter = cli.OSExiter + app.ExitErrHandler = cli.ExitErrHandler err := app.Run(append([]string{"--"}, args...)) return opts, err diff --git a/cli/commands/aws-provider-patch/action.go b/cli/commands/aws-provider-patch/action.go index 37255966d8..1a9e54f617 100644 --- a/cli/commands/aws-provider-patch/action.go +++ b/cli/commands/aws-provider-patch/action.go @@ -12,9 +12,9 @@ import ( "github.com/mattn/go-zglob" ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -29,7 +29,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions) error { func runAwsProviderPatch(ctx context.Context, opts *options.TerragruntOptions, cfg *config.TerragruntConfig) error { if len(opts.AwsProviderPatchOverrides) == 0 { - return errors.WithStackTrace(MissingOverrideAttrError(FlagNameTerragruntOverrideAttr)) + return errors.New(MissingOverrideAttrError(FlagNameTerragruntOverrideAttr)) } terraformFilesInModules, err := findAllTerraformFilesInModules(opts) @@ -95,12 +95,12 @@ func findAllTerraformFilesInModules(opts *options.TerragruntOptions) ([]string, modulesJSONContents, err := os.ReadFile(modulesJSONPath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } var terraformModulesJSON TerraformModulesJSON if err := json.Unmarshal(modulesJSONContents, &terraformModulesJSON); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } var terraformFiles []string @@ -117,7 +117,7 @@ func findAllTerraformFilesInModules(opts *options.TerragruntOptions) ([]string, // So we use a third-party library. matches, err := zglob.Glob(moduleAbsPath + "/**/*.tf") if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } terraformFiles = append(terraformFiles, matches...) @@ -153,7 +153,7 @@ func PatchAwsProviderInTerraformCode(terraformCode string, terraformFilePath str hclFile, err := hclwrite.ParseConfig([]byte(terraformCode), terraformFilePath, hcl.InitialPos) if err != nil { - return "", false, errors.WithStackTrace(err) + return "", false, errors.New(err) } codeWasUpdated := false @@ -237,14 +237,16 @@ func overrideAttributeInBlock(block *hclwrite.Block, key string, value string) ( if err != nil { // Wrap error in a custom error type that has better error messaging to the user. returnErr := TypeInferenceError{value: value, underlyingErr: err} - return false, errors.WithStackTrace(returnErr) + + return false, errors.New(returnErr) } ctyVal, err := ctyjson.Unmarshal(valueBytes, ctyType) if err != nil { // Wrap error in a custom error type that has better error messaging to the user. returnErr := MalformedJSONValError{value: value, underlyingErr: err} - return false, errors.WithStackTrace(returnErr) + + return false, errors.New(returnErr) } body.SetAttributeValue(attr, ctyVal) diff --git a/cli/commands/catalog/action.go b/cli/commands/catalog/action.go index 3125d99e85..d3a7aa5ead 100644 --- a/cli/commands/catalog/action.go +++ b/cli/commands/catalog/action.go @@ -6,10 +6,10 @@ import ( "os" "path/filepath" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/catalog/module" "github.com/gruntwork-io/terragrunt/cli/commands/catalog/tui" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) diff --git a/cli/commands/catalog/module/doc.go b/cli/commands/catalog/module/doc.go index 277c57ddd8..5e1ec4ee21 100644 --- a/cli/commands/catalog/module/doc.go +++ b/cli/commands/catalog/module/doc.go @@ -6,7 +6,7 @@ import ( "regexp" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" ) const ( @@ -138,7 +138,7 @@ func FindDoc(dir string) (*Doc, error) { files, err := os.ReadDir(dir) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } for _, file := range files { @@ -167,7 +167,7 @@ func FindDoc(dir string) (*Doc, error) { contentByte, err := os.ReadFile(filePath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } rawContent := string(contentByte) diff --git a/cli/commands/catalog/module/module.go b/cli/commands/catalog/module/module.go index 433b54199a..624c98db1d 100644 --- a/cli/commands/catalog/module/module.go +++ b/cli/commands/catalog/module/module.go @@ -6,7 +6,7 @@ import ( "path/filepath" "github.com/gruntwork-io/go-commons/collections" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" ) @@ -104,7 +104,7 @@ func (module *Module) TerraformSourcePath() string { func (module *Module) isValid() (bool, error) { files, err := os.ReadDir(filepath.Join(module.repoPath, module.moduleDir)) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } for _, file := range files { diff --git a/cli/commands/catalog/module/repo.go b/cli/commands/catalog/module/repo.go index 5bc9f29b81..e25efef3ae 100644 --- a/cli/commands/catalog/module/repo.go +++ b/cli/commands/catalog/module/repo.go @@ -10,8 +10,8 @@ import ( "strings" "github.com/gitsight/go-vcsurl" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/go-commons/files" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/terraform" "github.com/hashicorp/go-getter" @@ -94,7 +94,7 @@ func (repo *Repo) FindModules(ctx context.Context) (Modules, error) { moduleDir, err := filepath.Rel(repo.path, dir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if module, err := NewModule(repo, moduleDir); err != nil { @@ -121,7 +121,7 @@ func (repo *Repo) ModuleURL(moduleDir string) (string, error) { remote, err := vcsurl.Parse(repo.remoteURL) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } switch remote.Host { @@ -143,7 +143,7 @@ func (repo *Repo) clone(ctx context.Context) error { if repo.cloneURL == "" { currentDir, err := os.Getwd() if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } repo.cloneURL = currentDir @@ -153,7 +153,7 @@ func (repo *Repo) clone(ctx context.Context) error { if !filepath.IsAbs(repoPath) { absRepoPath, err := filepath.Abs(repoPath) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } repo.logger.Debugf("Converting relative path %q to absolute %q", repoPath, absRepoPath) @@ -165,7 +165,7 @@ func (repo *Repo) clone(ctx context.Context) error { } if err := os.MkdirAll(repo.path, os.ModePerm); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } repoName := "temp" @@ -182,7 +182,7 @@ func (repo *Repo) clone(ctx context.Context) error { repo.logger.Debugf("The repo dir exists but git file %q does not. Removing the repo dir for cloning from the remote source.", repo.gitHeadfile()) if err := os.RemoveAll(repo.path); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -201,7 +201,7 @@ func (repo *Repo) clone(ctx context.Context) error { sourceURL.RawQuery = (url.Values{"ref": []string{"HEAD"}}).Encode() if err := getter.Get(repo.path, strings.Trim(sourceURL.String(), "/"), getter.WithContext(ctx), getter.WithMode(getter.ClientModeDir)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -219,7 +219,7 @@ func (repo *Repo) parseRemoteURL() error { inidata, err := ini.Load(gitConfigPath) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } var sectionName string diff --git a/cli/commands/flags.go b/cli/commands/flags.go index 2227d965b5..6fc426415d 100644 --- a/cli/commands/flags.go +++ b/cli/commands/flags.go @@ -2,11 +2,10 @@ package commands import ( - goErrors "errors" "fmt" "github.com/gruntwork-io/go-commons/collections" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/pkg/log" @@ -561,7 +560,7 @@ func NewHelpFlag(opts *options.TerragruntOptions) cli.Flag { // If the command name is not found, it is most likely a terraform command, show Terraform help. var invalidCommandNameError cli.InvalidCommandNameError - if ok := goErrors.As(err, &invalidCommandNameError); ok { + if ok := errors.As(err, &invalidCommandNameError); ok { terraformHelpCmd := append([]string{cmdName, "-help"}, ctx.Args().Tail()...) return shell.RunTerraformCommand(ctx, opts, terraformHelpCmd...) } diff --git a/cli/commands/hclfmt/action.go b/cli/commands/hclfmt/action.go index 9ab276c42e..e28c2aedd8 100644 --- a/cli/commands/hclfmt/action.go +++ b/cli/commands/hclfmt/action.go @@ -14,9 +14,8 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/writer" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl/v2/hclwrite" "github.com/mattn/go-zglob" @@ -69,12 +68,12 @@ func Run(opts *options.TerragruntOptions) error { opts.Logger.Debugf("Found %d hcl files", len(filteredTgHclFiles)) - var formatErrors *multierror.Error + var formatErrors *errors.MultiError for _, tgHclFile := range filteredTgHclFiles { err := formatTgHCL(opts, tgHclFile) if err != nil { - formatErrors = multierror.Append(formatErrors, err) + formatErrors = formatErrors.Append(err) } } @@ -146,7 +145,7 @@ func checkErrors(logger log.Logger, disableColor bool, contents []byte, tgHclFil err := diagWriter.WriteDiagnostics(diags) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if diags.HasErrors() { diff --git a/cli/commands/render-json/action.go b/cli/commands/render-json/action.go index 4df5190976..001817cb9b 100644 --- a/cli/commands/render-json/action.go +++ b/cli/commands/render-json/action.go @@ -9,7 +9,6 @@ package renderjson import ( "context" "encoding/json" - goErrors "errors" "os" "path/filepath" @@ -18,9 +17,9 @@ import ( "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -33,7 +32,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions) error { func runRenderJSON(ctx context.Context, opts *options.TerragruntOptions, cfg *config.TerragruntConfig) error { if cfg == nil { - return goErrors.New("terragrunt was not able to render the config as json because it received no config. This is almost certainly a bug in Terragrunt. Please open an issue on github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl") + return errors.New("terragrunt was not able to render the config as json because it received no config. This is almost certainly a bug in Terragrunt. Please open an issue on github.com/gruntwork-io/terragrunt with this message and the contents of your terragrunt.hcl") } if !opts.JSONDisableDependentModules { @@ -85,7 +84,7 @@ func runRenderJSON(ctx context.Context, opts *options.TerragruntOptions, cfg *co const ownerWriteGlobalReadPerms = 0644 if err := os.WriteFile(jsonOutPath, jsonBytes, ownerWriteGlobalReadPerms); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -98,15 +97,15 @@ func runRenderJSON(ctx context.Context, opts *options.TerragruntOptions, cfg *co func marshalCtyValueJSONWithoutType(ctyVal cty.Value) ([]byte, error) { jsonBytesIntermediate, err := ctyjson.Marshal(ctyVal, cty.DynamicPseudoType) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } var ctyJSONOutput config.CtyJSONOutput if err := json.Unmarshal(jsonBytesIntermediate, &ctyJSONOutput); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } jsonBytes, err := json.Marshal(ctyJSONOutput.Value) - return jsonBytes, errors.WithStackTrace(err) + return jsonBytes, errors.New(err) } diff --git a/cli/commands/run-all/action.go b/cli/commands/run-all/action.go index 41f198f58a..65b800abdf 100644 --- a/cli/commands/run-all/action.go +++ b/cli/commands/run-all/action.go @@ -3,8 +3,8 @@ package runall import ( "context" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/configstack" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/telemetry" @@ -31,7 +31,7 @@ var runAllDisabledCommands = map[string]string{ func Run(ctx context.Context, opts *options.TerragruntOptions) error { if opts.TerraformCommand == "" { - return errors.WithStackTrace(MissingCommand{}) + return errors.New(MissingCommand{}) } reason, isDisabled := runAllDisabledCommands[opts.TerraformCommand] diff --git a/cli/commands/scaffold/action.go b/cli/commands/scaffold/action.go index 6a45bfa6d5..f115d39eb5 100644 --- a/cli/commands/scaffold/action.go +++ b/cli/commands/scaffold/action.go @@ -19,7 +19,7 @@ import ( boilerplate_options "github.com/gruntwork-io/boilerplate/options" "github.com/gruntwork-io/boilerplate/templates" "github.com/gruntwork-io/boilerplate/variables" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terratest/modules/files" "github.com/hashicorp/go-getter" @@ -117,13 +117,13 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, moduleURL, templa } if moduleURL == "" { - return errors.WithStackTrace(NoModuleURLPassed{}) + return errors.New(NoModuleURLPassed{}) } // create temporary directory where to download module tempDir, err := os.MkdirTemp("", "scaffold") if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } dirsToClean = append(dirsToClean, tempDir) @@ -131,25 +131,25 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, moduleURL, templa // prepare variables vars, err := variables.ParseVars(opts.ScaffoldVars, opts.ScaffoldVarFiles) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // parse module url moduleURL, err = parseModuleURL(ctx, opts, vars, moduleURL) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger.Infof("Scaffolding a new Terragrunt module %s to %s", moduleURL, opts.WorkingDir) if err := getter.GetAny(tempDir, moduleURL); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // extract variables from downloaded module requiredVariables, optionalVariables, err := parseVariables(opts, tempDir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger.Debugf("Parsed %d required variables and %d optional variables", len(requiredVariables), len(optionalVariables)) @@ -157,7 +157,7 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, moduleURL, templa // prepare boilerplate files to render Terragrunt files boilerplateDir, err := prepareBoilerplateFiles(ctx, opts, templateURL, tempDir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // add additional variables @@ -180,13 +180,13 @@ func Run(ctx context.Context, opts *options.TerragruntOptions, moduleURL, templa emptyDep := variables.Dependency{} if err := templates.ProcessTemplate(boilerplateOpts, boilerplateOpts, emptyDep); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger.Infof("Running fmt on generated code %s", opts.WorkingDir) if err := hclfmt.Run(opts); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger.Info("Scaffolding completed") @@ -203,12 +203,12 @@ func prepareBoilerplateFiles(ctx context.Context, opts *options.TerragruntOption // process template url if was passed parsedTemplateURL, err := terraform.ToSourceURL(templateURL, tempDir) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } parsedTemplateURL, err = rewriteTemplateURL(ctx, opts, parsedTemplateURL) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // regenerate template url with all changes templateURL = parsedTemplateURL.String() @@ -216,14 +216,14 @@ func prepareBoilerplateFiles(ctx context.Context, opts *options.TerragruntOption // prepare temporary directory for template templateDir, err = os.MkdirTemp("", "template") if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // downloading template opts.Logger.Infof("Using template from %s", templateURL) if err := getter.GetAny(templateDir, templateURL); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } // prepare boilerplate dir @@ -238,18 +238,18 @@ func prepareBoilerplateFiles(ctx context.Context, opts *options.TerragruntOption // no default boilerplate dir, create one defaultTempDir, err := os.MkdirTemp("", "boilerplate") if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } boilerplateDir = defaultTempDir const ownerWriteGlobalReadPerms = 0644 if err := os.WriteFile(util.JoinPath(boilerplateDir, "terragrunt.hcl"), []byte(DefaultTerragruntTemplate), ownerWriteGlobalReadPerms); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } if err := os.WriteFile(util.JoinPath(boilerplateDir, "boilerplate.yml"), []byte(DefaultBoilerplateConfig), ownerWriteGlobalReadPerms); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } @@ -260,7 +260,7 @@ func prepareBoilerplateFiles(ctx context.Context, opts *options.TerragruntOption func parseVariables(opts *options.TerragruntOptions, moduleDir string) ([]*config.ParsedVariable, []*config.ParsedVariable, error) { inputs, err := config.ParseVariables(opts, moduleDir) if err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } // separate variables that require value and with default value @@ -284,7 +284,7 @@ func parseVariables(opts *options.TerragruntOptions, moduleDir string) ([]*confi func parseModuleURL(ctx context.Context, opts *options.TerragruntOptions, vars map[string]interface{}, moduleURL string) (string, error) { parsedModuleURL, err := terraform.ToSourceURL(moduleURL, opts.WorkingDir) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } moduleURL = parsedModuleURL.String() @@ -292,13 +292,13 @@ func parseModuleURL(ctx context.Context, opts *options.TerragruntOptions, vars m // rewrite module url, if required parsedModuleURL, err = rewriteModuleURL(opts, vars, moduleURL) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // add ref to module url, if required parsedModuleURL, err = addRefToModuleURL(ctx, opts, parsedModuleURL, vars) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // regenerate module url with all changes @@ -322,7 +322,7 @@ func rewriteModuleURL(opts *options.TerragruntOptions, vars map[string]interface parsedModuleURL, err := terraform.ToSourceURL(updatedModuleURL, opts.WorkingDir) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return parsedModuleURL, nil @@ -342,7 +342,7 @@ func rewriteModuleURL(opts *options.TerragruntOptions, vars map[string]interface // persist changes in url.URL parsedModuleURL, err := terraform.ToSourceURL(updatedModuleURL, opts.WorkingDir) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return parsedModuleURL, nil @@ -360,7 +360,7 @@ func rewriteTemplateURL(ctx context.Context, opts *options.TerragruntOptions, pa if ref == "" { rootSourceURL, _, err := terraform.SplitSourceURL(updatedTemplateURL, opts.Logger) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } tag, err := shell.GitLastReleaseTag(ctx, opts, rootSourceURL) @@ -393,7 +393,7 @@ func addRefToModuleURL(ctx context.Context, opts *options.TerragruntOptions, par // git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs => git::https://github.com/gruntwork-io/terragrunt.git//test/fixtures/inputs?ref=v0.53.8 rootSourceURL, _, err := terraform.SplitSourceURL(moduleURL, opts.Logger) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } tag, err := shell.GitLastReleaseTag(ctx, opts, rootSourceURL) diff --git a/cli/commands/terraform/action.go b/cli/commands/terraform/action.go index 178106cedf..a8b3e98fec 100644 --- a/cli/commands/terraform/action.go +++ b/cli/commands/terraform/action.go @@ -23,10 +23,10 @@ import ( "github.com/hashicorp/go-multierror" "github.com/mattn/go-zglob" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/codegen" "github.com/gruntwork-io/terragrunt/config" "github.com/gruntwork-io/terragrunt/configstack" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/remote" "github.com/gruntwork-io/terragrunt/shell" @@ -77,7 +77,7 @@ var sourceChangeLocks = sync.Map{} // This will forward all the args and extra_arguments directly to Terraform. func Run(ctx context.Context, opts *options.TerragruntOptions) error { if opts.TerraformCommand == "" { - return errors.WithStackTrace(MissingCommand{}) + return errors.New(MissingCommand{}) } return runTerraform(ctx, opts, new(Target)) @@ -410,21 +410,21 @@ func ShouldCopyLockFile(args []string, terraformConfig *config.TerraformConfig) // Run the given action function surrounded by hooks. That is, run the before hooks first, then, if there were no // errors, run the action, and finally, run the after hooks. Return any errors hit from the hooks or action. func runActionWithHooks(ctx context.Context, description string, terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig, action func(ctx context.Context) error) error { - var allErrors *multierror.Error + var allErrors *errors.MultiError beforeHookErrors := processHooks(ctx, terragruntConfig.Terraform.GetBeforeHooks(), terragruntOptions, terragruntConfig, allErrors) - allErrors = multierror.Append(allErrors, beforeHookErrors) + allErrors = allErrors.Append(beforeHookErrors) var actionErrors error if beforeHookErrors == nil { actionErrors = action(ctx) - allErrors = multierror.Append(allErrors, actionErrors) + allErrors = allErrors.Append(actionErrors) } else { terragruntOptions.Logger.Errorf("Errors encountered running before_hooks. Not running '%s'.", description) } postHookErrors := processHooks(ctx, terragruntConfig.Terraform.GetAfterHooks(), terragruntOptions, terragruntConfig, allErrors) errorHookErrors := processErrorHooks(ctx, terragruntConfig.Terraform.GetErrorHooks(), terragruntOptions, allErrors) - allErrors = multierror.Append(allErrors, postHookErrors, errorHookErrors) + allErrors = allErrors.Append(postHookErrors, errorHookErrors) return allErrors.ErrorOrNil() } @@ -456,15 +456,7 @@ func RunTerraformWithRetry(ctx context.Context, terragruntOptions *options.Terra for i := 0; i < terragruntOptions.RetryMaxAttempts; i++ { if out, err := shell.RunTerraformCommandWithOutput(ctx, terragruntOptions, terragruntOptions.TerraformCliArgs...); err != nil { if out == nil || !IsRetryable(terragruntOptions, out) { - logger := terragruntOptions.Logger - - if execErr := util.Unwrap[util.ProcessExecutionError](err); execErr != nil { - if execErr.Stderr != "" { - logger = logger.WithField("stderr", "\n"+execErr.Stderr) - } - } - - logger.Errorf("%s invocation failed in %s", terragruntOptions.TerraformImplementation, terragruntOptions.WorkingDir) + terragruntOptions.Logger.Errorf("%s invocation failed in %s", terragruntOptions.TerraformImplementation, terragruntOptions.WorkingDir) return err } else { @@ -473,7 +465,7 @@ func RunTerraformWithRetry(ctx context.Context, terragruntOptions *options.Terra case <-time.After(terragruntOptions.RetrySleepInterval): // try again case <-ctx.Done(): - return errors.WithStackTrace(ctx.Err()) + return errors.New(ctx.Err()) } } } else { @@ -481,7 +473,7 @@ func RunTerraformWithRetry(ctx context.Context, terragruntOptions *options.Terra } } - return errors.WithStackTrace(MaxRetriesExceeded{terragruntOptions}) + return errors.New(MaxRetriesExceeded{terragruntOptions}) } // IsRetryable checks whether there was an error and if the output matches any of the configured RetryableErrors @@ -490,7 +482,7 @@ func IsRetryable(opts *options.TerragruntOptions, out *util.CmdOutput) bool { return false } // When -json is enabled, Terraform will send all output, errors included, to stdout. - return util.MatchesAny(opts.RetryableErrors, out.Stderr) || util.MatchesAny(opts.RetryableErrors, out.Stdout) + return util.MatchesAny(opts.RetryableErrors, out.Stderr.String()) || util.MatchesAny(opts.RetryableErrors, out.Stdout.String()) } // Prepare for running 'terraform init' by initializing remote state storage and adding backend configuration arguments @@ -521,20 +513,20 @@ func CheckFolderContainsTerraformCode(terragruntOptions *options.TerragruntOptio hclFiles, err := zglob.Glob(terragruntOptions.WorkingDir + "/**/*.tf") if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } files = append(files, hclFiles...) jsonFiles, err := zglob.Glob(terragruntOptions.WorkingDir + "/**/*.tf.json") if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } files = append(files, jsonFiles...) if len(files) == 0 { - return errors.WithStackTrace(NoTerraformFilesFound(terragruntOptions.WorkingDir)) + return errors.New(NoTerraformFilesFound(terragruntOptions.WorkingDir)) } return nil @@ -544,7 +536,7 @@ func CheckFolderContainsTerraformCode(terragruntOptions *options.TerragruntOptio func checkTerraformCodeDefinesBackend(terragruntOptions *options.TerragruntOptions, backendType string) error { terraformBackendRegexp, err := regexp.Compile(fmt.Sprintf(`backend[[:blank:]]+"%s"`, backendType)) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } definesBackend, err := util.Grep(terraformBackendRegexp, terragruntOptions.WorkingDir+"/**/*.tf") @@ -558,7 +550,7 @@ func checkTerraformCodeDefinesBackend(terragruntOptions *options.TerragruntOptio terraformJSONBackendRegexp, err := regexp.Compile(fmt.Sprintf(`(?m)"backend":[[:space:]]*{[[:space:]]*"%s"`, backendType)) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } definesJSONBackend, err := util.Grep(terraformJSONBackendRegexp, terragruntOptions.WorkingDir+"/**/*.tf.json") @@ -570,7 +562,7 @@ func checkTerraformCodeDefinesBackend(terragruntOptions *options.TerragruntOptio return nil } - return errors.WithStackTrace(BackendNotDefined{Opts: terragruntOptions, BackendType: backendType}) + return errors.New(BackendNotDefined{Opts: terragruntOptions, BackendType: backendType}) } // Prepare for running any command other than 'terraform init' by running 'terraform init' if necessary @@ -733,7 +725,7 @@ func checkProtectedModule(terragruntOptions *options.TerragruntOptions, terragru } if terragruntConfig.PreventDestroy != nil && *terragruntConfig.PreventDestroy { - return errors.WithStackTrace(ModuleIsProtected{Opts: terragruntOptions}) + return errors.New(ModuleIsProtected{Opts: terragruntOptions}) } return nil @@ -849,14 +841,14 @@ func setTerragruntNullValues(terragruntOptions *options.TerragruntOptions, terra jsonContents, err := json.MarshalIndent(jsonEmptyVars, "", " ") if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } varFile := filepath.Join(terragruntOptions.WorkingDir, NullTFVarsFile) const ownerReadWritePermissions = 0600 if err := os.WriteFile(varFile, jsonContents, os.FileMode(ownerReadWritePermissions)); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return varFile, nil diff --git a/cli/commands/terraform/action_test.go b/cli/commands/terraform/action_test.go index dec4847f88..e348f7d5a9 100644 --- a/cli/commands/terraform/action_test.go +++ b/cli/commands/terraform/action_test.go @@ -1,14 +1,15 @@ package terraform_test import ( + "bytes" "context" "os" "path/filepath" "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/util" @@ -149,10 +150,8 @@ func TestErrorRetryableOnStdoutError(t *testing.T) { tgOptions.RetryableErrors = retryableErrors tgOptions.AutoRetry = true - out := &util.CmdOutput{ - Stdout: "", - Stderr: "error is here", - } + out := new(util.CmdOutput) + out.Stderr = *bytes.NewBufferString("error is here") retryable := terraform.IsRetryable(tgOptions, out) require.True(t, retryable, "The error should have retried") @@ -168,10 +167,8 @@ func TestErrorMultipleRetryableOnStderrError(t *testing.T) { tgOptions.RetryableErrors = retryableErrors tgOptions.AutoRetry = true - out := &util.CmdOutput{ - Stdout: "", - Stderr: "error is here", - } + out := new(util.CmdOutput) + out.Stderr = *bytes.NewBufferString("error is here") retryable := terraform.IsRetryable(tgOptions, out) require.True(t, retryable, "The error should have retried") @@ -187,10 +184,8 @@ func TestEmptyRetryablesOnStderrError(t *testing.T) { tgOptions.RetryableErrors = retryableErrors tgOptions.AutoRetry = true - out := &util.CmdOutput{ - Stdout: "", - Stderr: "error is here", - } + out := new(util.CmdOutput) + out.Stderr = *bytes.NewBufferString("error is here") retryable := terraform.IsRetryable(tgOptions, out) require.False(t, retryable, "The error should not have retried, the list of retryable errors was empty") @@ -206,10 +201,8 @@ func TestErrorRetryableOnStderrError(t *testing.T) { tgOptions.RetryableErrors = retryableErrors tgOptions.AutoRetry = true - out := &util.CmdOutput{ - Stdout: "", - Stderr: "error is here", - } + out := new(util.CmdOutput) + out.Stderr = *bytes.NewBufferString("error is here") retryable := terraform.IsRetryable(tgOptions, out) require.True(t, retryable, "The error should have retried") @@ -225,10 +218,8 @@ func TestErrorNotRetryableOnStdoutError(t *testing.T) { tgOptions.RetryableErrors = retryableErrors tgOptions.AutoRetry = true - out := &util.CmdOutput{ - Stdout: "error is here", - Stderr: "", - } + out := new(util.CmdOutput) + out.Stdout = *bytes.NewBufferString("error is here") retryable := terraform.IsRetryable(tgOptions, out) require.False(t, retryable, "The error should not retry") @@ -244,10 +235,8 @@ func TestErrorNotRetryableOnStderrError(t *testing.T) { tgOptions.RetryableErrors = retryableErrors tgOptions.AutoRetry = true - out := &util.CmdOutput{ - Stdout: "", - Stderr: "error is here", - } + out := new(util.CmdOutput) + out.Stderr = *bytes.NewBufferString("error is here") retryable := terraform.IsRetryable(tgOptions, out) require.False(t, retryable, "The error should not retry") @@ -473,7 +462,7 @@ func mockOptions(t *testing.T, terragruntConfigPath string, workingDir string, t opts, err := options.NewTerragruntOptionsForTest(terragruntConfigPath) if err != nil { - t.Fatalf("error: %v\n", errors.WithStackTrace(err)) + t.Fatalf("error: %v\n", errors.New(err)) } opts.WorkingDir = workingDir diff --git a/cli/commands/terraform/command.go b/cli/commands/terraform/command.go index 2066126e71..4cddaa7155 100644 --- a/cli/commands/terraform/command.go +++ b/cli/commands/terraform/command.go @@ -4,8 +4,8 @@ package terraform import ( "strings" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/gruntwork-cli/collections" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/terraform" @@ -37,10 +37,10 @@ func Action(opts *options.TerragruntOptions) cli.ActionFunc { if !opts.DisableCommandValidation && !collections.ListContainsElement(nativeTerraformCommands, opts.TerraformCommand) { if strings.HasSuffix(opts.TerraformPath, "terraform") { - return errors.WithStackTrace(WrongTerraformCommand(opts.TerraformCommand)) + return errors.New(WrongTerraformCommand(opts.TerraformCommand)) } else { // We default to tofu if the terraform path does not end in Terraform - return errors.WithStackTrace(WrongTofuCommand(opts.TerraformCommand)) + return errors.New(WrongTofuCommand(opts.TerraformCommand)) } } diff --git a/cli/commands/terraform/creds/providers/externalcmd/provider.go b/cli/commands/terraform/creds/providers/externalcmd/provider.go index 0ec6202e79..90f44192a9 100644 --- a/cli/commands/terraform/creds/providers/externalcmd/provider.go +++ b/cli/commands/terraform/creds/providers/externalcmd/provider.go @@ -7,8 +7,8 @@ import ( "fmt" "strings" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform/creds/providers" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/shell" "golang.org/x/exp/maps" @@ -50,13 +50,13 @@ func (provider *Provider) GetCredentials(ctx context.Context) (*providers.Creden return nil, err } - if output.Stdout == "" { + if output.Stdout.String() == "" { return nil, errors.Errorf("command %s completed successfully, but the response does not contain JSON string", command) } resp := &Response{Envs: make(map[string]string)} - if err := json.Unmarshal([]byte(output.Stdout), &resp); err != nil { + if err := json.Unmarshal(output.Stdout.Bytes(), &resp); err != nil { return nil, errors.Errorf("command %s returned a response with invalid JSON format", command) } diff --git a/cli/commands/terraform/debug.go b/cli/commands/terraform/debug.go index d9818728a5..d0e67bf465 100644 --- a/cli/commands/terraform/debug.go +++ b/cli/commands/terraform/debug.go @@ -7,8 +7,8 @@ import ( "path/filepath" "strings" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/terraform" "github.com/gruntwork-io/terragrunt/util" @@ -46,7 +46,7 @@ func WriteTerragruntDebugFile(terragruntOptions *options.TerragruntOptions, terr fileName := filepath.Join(configFolder, TerragruntTFVarsFile) if err := os.WriteFile(fileName, fileContents, os.FileMode(defaultPermissions)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Variables passed to terraform are located in \"%s\"", fileName) @@ -102,7 +102,7 @@ func terragruntDebugFileContents( jsonContent, err := json.MarshalIndent(jsonValuesByKey, "", " ") if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return jsonContent, nil diff --git a/cli/commands/terraform/download_source.go b/cli/commands/terraform/download_source.go index 174fe66b0a..9b64350068 100644 --- a/cli/commands/terraform/download_source.go +++ b/cli/commands/terraform/download_source.go @@ -9,9 +9,9 @@ import ( "github.com/hashicorp/go-getter" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/terraform" "github.com/gruntwork-io/terragrunt/util" @@ -73,7 +73,7 @@ func DownloadTerraformSourceIfNecessary(ctx context.Context, terraformSource *te terragruntOptions.Logger.Debugf("The --%s flag is set, so deleting the temporary folder %s before downloading source.", commands.TerragruntSourceUpdateFlagName, terraformSource.DownloadDir) if err := os.RemoveAll(terraformSource.DownloadDir); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -158,7 +158,7 @@ func AlreadyHaveLatestCode(terraformSource *terraform.Source, terragruntOptions tfFiles, err := filepath.Glob(terraformSource.WorkingDir + "/*.tf") if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } if len(tfFiles) == 0 { @@ -243,7 +243,7 @@ func downloadSource(terraformSource *terraform.Source, terragruntOptions *option terraformSource.DownloadDir) if err := getter.GetAny(terraformSource.DownloadDir, terraformSource.CanonicalSourceURL.String(), updateGetters(terragruntOptions, terragruntConfig)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/cli/commands/terraform/file_copy_getter.go b/cli/commands/terraform/file_copy_getter.go index 997a64e179..44a4b4edcc 100644 --- a/cli/commands/terraform/file_copy_getter.go +++ b/cli/commands/terraform/file_copy_getter.go @@ -4,7 +4,7 @@ import ( "net/url" "os" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/go-getter" @@ -50,7 +50,7 @@ func (g *FileCopyGetter) Get(dst string, u *url.URL) error { func (g *FileCopyGetter) GetFile(dst string, u *url.URL) error { underlying := &getter.FileGetter{Copy: true} if err := underlying.GetFile(dst, u); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/cli/commands/terraform/hook.go b/cli/commands/terraform/hook.go index 194f199c7d..efb12fcdaf 100644 --- a/cli/commands/terraform/hook.go +++ b/cli/commands/terraform/hook.go @@ -2,15 +2,14 @@ package terraform import ( "context" - "errors" "fmt" "sync" - "github.com/gruntwork-io/terragrunt/telemetry" - "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/shell" + "github.com/gruntwork-io/terragrunt/telemetry" "github.com/gruntwork-io/terragrunt/tflint" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/go-multierror" @@ -22,7 +21,7 @@ const ( HookCtxHookNameEnvName = "TG_CTX_HOOK_NAME" ) -func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragruntOptions *options.TerragruntOptions, previousExecErrors *multierror.Error) error { +func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragruntOptions *options.TerragruntOptions, previousExecErrors *errors.MultiError) error { if len(hooks) == 0 || previousExecErrors.ErrorOrNil() == nil { return nil } @@ -32,7 +31,7 @@ func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragrunt terragruntOptions.Logger.Debugf("Detected %d error Hooks", len(hooks)) customMultierror := multierror.Error{ - Errors: previousExecErrors.Errors, + Errors: previousExecErrors.WrappedErrors(), ErrorFormat: func(err []error) string { result := "" for _, e := range err { @@ -40,10 +39,11 @@ func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragrunt // Check if is process execution error and try to extract output // https://github.com/gruntwork-io/terragrunt/issues/2045 originalError := errors.Unwrap(e) + if originalError != nil { var processExecutionError util.ProcessExecutionError if ok := errors.As(originalError, &processExecutionError); ok { - errorMessage = fmt.Sprintf("%s\n%s", processExecutionError.Stdout, processExecutionError.Stderr) + errorMessage = processExecutionError.Error() } } result = fmt.Sprintf("%s\n%s", result, errorMessage) @@ -89,7 +89,13 @@ func processErrorHooks(ctx context.Context, hooks []config.ErrorHook, terragrunt return errorsOccured.ErrorOrNil() } -func processHooks(ctx context.Context, hooks []config.Hook, terragruntOptions *options.TerragruntOptions, terragruntConfig *config.TerragruntConfig, previousExecErrors *multierror.Error) error { +func processHooks( + ctx context.Context, + hooks []config.Hook, + terragruntOptions *options.TerragruntOptions, + terragruntConfig *config.TerragruntConfig, + previousExecErrors *errors.MultiError, +) error { if len(hooks) == 0 { return nil } @@ -99,7 +105,7 @@ func processHooks(ctx context.Context, hooks []config.Hook, terragruntOptions *o terragruntOptions.Logger.Debugf("Detected %d Hooks", len(hooks)) for _, curHook := range hooks { - allPreviousErrors := multierror.Append(previousExecErrors, errorsOccured) + allPreviousErrors := previousExecErrors.Append(errorsOccured) if shouldRunHook(curHook, terragruntOptions, allPreviousErrors) { err := telemetry.Telemetry(ctx, terragruntOptions, "hook_"+curHook.Name, map[string]interface{}{ "hook": curHook.Name, @@ -116,7 +122,7 @@ func processHooks(ctx context.Context, hooks []config.Hook, terragruntOptions *o return errorsOccured.ErrorOrNil() } -func shouldRunHook(hook config.Hook, terragruntOptions *options.TerragruntOptions, previousExecErrors *multierror.Error) bool { +func shouldRunHook(hook config.Hook, terragruntOptions *options.TerragruntOptions, previousExecErrors *errors.MultiError) bool { // if there's no previous error, execute command // OR if a previous error DID happen AND we want to run anyways // then execute. diff --git a/cli/commands/terraform/version_check.go b/cli/commands/terraform/version_check.go index f54ed9e623..b4911d6c68 100644 --- a/cli/commands/terraform/version_check.go +++ b/cli/commands/terraform/version_check.go @@ -7,8 +7,8 @@ import ( "regexp" "strings" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/shell" "github.com/hashicorp/go-version" @@ -99,12 +99,12 @@ func PopulateTerraformVersion(ctx context.Context, terragruntOptions *options.Te return err } - terraformVersion, err := ParseTerraformVersion(output.Stdout) + terraformVersion, err := ParseTerraformVersion(output.Stdout.String()) if err != nil { return err } - tfImplementation, err := parseTerraformImplementationType(output.Stdout) + tfImplementation, err := parseTerraformImplementationType(output.Stdout.String()) if err != nil { return err } @@ -153,7 +153,7 @@ func CheckTerragruntVersionMeetsConstraint(currentVersion *version.Version, cons } if !versionConstraint.Check(checkedVersion) { - return errors.WithStackTrace(InvalidTerragruntVersion{CurrentVersion: currentVersion, VersionConstraints: versionConstraint}) + return errors.New(InvalidTerragruntVersion{CurrentVersion: currentVersion, VersionConstraints: versionConstraint}) } return nil @@ -167,7 +167,7 @@ func CheckTerraformVersionMeetsConstraint(currentVersion *version.Version, const } if !versionConstraint.Check(currentVersion) { - return errors.WithStackTrace(InvalidTerraformVersion{CurrentVersion: currentVersion, VersionConstraints: versionConstraint}) + return errors.New(InvalidTerraformVersion{CurrentVersion: currentVersion, VersionConstraints: versionConstraint}) } return nil @@ -178,7 +178,7 @@ func ParseTerraformVersion(versionCommandOutput string) (*version.Version, error matches := TerraformVersionRegex.FindStringSubmatch(versionCommandOutput) if len(matches) != versionParts { - return nil, errors.WithStackTrace(InvalidTerraformVersionSyntax(versionCommandOutput)) + return nil, errors.New(InvalidTerraformVersionSyntax(versionCommandOutput)) } return version.NewVersion(matches[2]) @@ -189,7 +189,7 @@ func parseTerraformImplementationType(versionCommandOutput string) (options.Terr matches := TerraformVersionRegex.FindStringSubmatch(versionCommandOutput) if len(matches) != versionParts { - return options.UnknownImpl, errors.WithStackTrace(InvalidTerraformVersionSyntax(versionCommandOutput)) + return options.UnknownImpl, errors.New(InvalidTerraformVersionSyntax(versionCommandOutput)) } rawType := strings.ToLower(matches[1]) diff --git a/cli/commands/terraform/version_check_test.go b/cli/commands/terraform/version_check_test.go index 424814123f..57d7200c92 100644 --- a/cli/commands/terraform/version_check_test.go +++ b/cli/commands/terraform/version_check_test.go @@ -4,8 +4,8 @@ package terraform_test import ( "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/cli/commands/terragrunt-info/action.go b/cli/commands/terragrunt-info/action.go index 223497e80f..855206486e 100644 --- a/cli/commands/terragrunt-info/action.go +++ b/cli/commands/terragrunt-info/action.go @@ -5,7 +5,7 @@ import ( "encoding/json" "fmt" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" @@ -41,11 +41,12 @@ func printTerragruntInfo(opts *options.TerragruntOptions) error { b, err := json.MarshalIndent(group, "", " ") if err != nil { opts.Logger.Errorf("JSON error marshalling terragrunt-info") - return errors.WithStackTrace(err) + + return errors.New(err) } if _, err := fmt.Fprintf(opts.Writer, "%s\n", b); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/cli/provider_cache.go b/cli/provider_cache.go index 94eef69889..ac0662ecb7 100644 --- a/cli/provider_cache.go +++ b/cli/provider_cache.go @@ -12,7 +12,7 @@ import ( "strings" "github.com/google/uuid" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/shell" @@ -74,7 +74,7 @@ func InitProviderCacheServer(opts *options.TerragruntOptions) (*ProviderCache, e } if opts.ProviderCacheDir, err = filepath.Abs(opts.ProviderCacheDir); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if opts.ProviderCacheToken == "" { @@ -314,7 +314,7 @@ func (cache *ProviderCache) createLocalCLIConfig(ctx context.Context, opts *opti if cfgDir := filepath.Dir(filename); !util.FileExists(cfgDir) { if err := os.MkdirAll(cfgDir, os.ModePerm); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -340,6 +340,7 @@ func runTerraformCommand(ctx context.Context, opts *options.TerragruntOptions, a cloneOpts.WorkingDir = opts.WorkingDir cloneOpts.TerraformCliArgs = args cloneOpts.Env = envs + cloneOpts.ForwardTFStdout = true // If the Terraform error matches `httpStatusCacheProviderReg` we ignore it and hide the log from users, otherwise we process the error as is. if output, err := shell.RunTerraformCommandWithOutput(ctx, cloneOpts, cloneOpts.TerraformCliArgs...); err != nil && len(errWriter.Msgs()) == 0 { diff --git a/codegen/generate.go b/codegen/generate.go index 804074ac97..c142846d5a 100644 --- a/codegen/generate.go +++ b/codegen/generate.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/hcl/v2/hclwrite" ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -100,7 +100,7 @@ func WriteToFile(terragruntOptions *options.TerragruntOptions, basePath string, return err } else if shouldRemove { if err := os.Remove(targetPath); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } } @@ -125,7 +125,7 @@ func WriteToFile(terragruntOptions *options.TerragruntOptions, basePath string, const ownerWriteGlobalReadPerms = 0644 if err := os.WriteFile(targetPath, []byte(contentsToWrite), ownerWriteGlobalReadPerms); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Generated file %s.", targetPath) @@ -139,15 +139,17 @@ func shouldContinueWithFileExists(terragruntOptions *options.TerragruntOptions, // TODO: Make exhaustive switch ifExists { //nolint:exhaustive case ExistsError: - return false, errors.WithStackTrace(GenerateFileExistsError{path: path}) + return false, errors.New(GenerateFileExistsError{path: path}) case ExistsSkip: // Do nothing since file exists and skip was configured terragruntOptions.Logger.Debugf("The file path %s already exists and if_exists for code generation set to \"skip\". Will not regenerate file.", path) + return false, nil case ExistsOverwrite: // We will continue to proceed to generate file, but log a message to indicate that we detected the file // exists. terragruntOptions.Logger.Debugf("The file path %s already exists and if_exists for code generation set to \"overwrite\". Regenerating file.", path) + return true, nil case ExistsOverwriteTerragrunt: // If file was not generated, error out because overwrite_terragrunt if_exists setting only handles if the @@ -159,7 +161,8 @@ func shouldContinueWithFileExists(terragruntOptions *options.TerragruntOptions, if !wasGenerated { terragruntOptions.Logger.Errorf("ERROR: The file path %s already exists and was not generated by terragrunt.", path) - return false, errors.WithStackTrace(GenerateFileExistsError{path: path}) + + return false, errors.New(GenerateFileExistsError{path: path}) } // Since file was generated by terragrunt, continue. @@ -168,7 +171,7 @@ func shouldContinueWithFileExists(terragruntOptions *options.TerragruntOptions, return true, nil default: // This shouldn't happen, but we add this case anyway for defensive coding. - return false, errors.WithStackTrace(UnknownGenerateIfExistsVal{""}) + return false, errors.New(UnknownGenerateIfExistsVal{""}) } } @@ -193,7 +196,8 @@ func shouldRemoveWithFileExists(terragruntOptions *options.TerragruntOptions, pa if !wasGenerated { terragruntOptions.Logger.Errorf("ERROR: The file path %s already exists and was not generated by terragrunt.", path) - return false, errors.WithStackTrace(GenerateFileRemoveError{path: path}) + + return false, errors.New(GenerateFileRemoveError{path: path}) } // Since file was generated by terragrunt, removing. @@ -202,7 +206,7 @@ func shouldRemoveWithFileExists(terragruntOptions *options.TerragruntOptions, pa return true, nil default: // This shouldn't happen, but we add this case anyway for defensive coding. - return false, errors.WithStackTrace(UnknownGenerateIfDisabledVal{""}) + return false, errors.New(UnknownGenerateIfDisabledVal{""}) } } @@ -212,7 +216,7 @@ func shouldRemoveWithFileExists(terragruntOptions *options.TerragruntOptions, pa func fileWasGeneratedByTerragrunt(path string) (bool, error) { file, err := os.Open(path) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } defer file.Close() @@ -220,7 +224,7 @@ func fileWasGeneratedByTerragrunt(path string) (bool, error) { firstLine, err := reader.ReadString('\n') if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } return strings.HasSuffix(strings.TrimSpace(firstLine), TerragruntGeneratedSignature), nil @@ -259,12 +263,12 @@ func RemoteStateConfigToTerraformCode(backend string, config map[string]interfac // basic decode of hcl to a map err := hclsimple.Decode("s3_assume_role.hcl", []byte(hclValue), nil, &assumeRoleMap) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } // write assume role map as HCL object ctyVal, err := convertValue(assumeRoleMap) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } backendBlockBody.SetAttributeValue(key, ctyVal.Value) @@ -274,7 +278,7 @@ func RemoteStateConfigToTerraformCode(backend string, config map[string]interfac ctyVal, err := convertValue(config[key]) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } backendBlockBody.SetAttributeValue(key, ctyVal.Value) @@ -286,12 +290,12 @@ func RemoteStateConfigToTerraformCode(backend string, config map[string]interfac func convertValue(v interface{}) (ctyjson.SimpleJSONValue, error) { jsonBytes, err := json.Marshal(v) if err != nil { - return ctyjson.SimpleJSONValue{}, errors.WithStackTrace(err) + return ctyjson.SimpleJSONValue{}, errors.New(err) } var ctyVal ctyjson.SimpleJSONValue if err := ctyVal.UnmarshalJSON(jsonBytes); err != nil { - return ctyjson.SimpleJSONValue{}, errors.WithStackTrace(err) + return ctyjson.SimpleJSONValue{}, errors.New(err) } return ctyVal, nil @@ -311,7 +315,7 @@ func GenerateConfigExistsFromString(val string) (GenerateConfigExists, error) { return ExistsOverwriteTerragrunt, nil } - return ExistsUnknown, errors.WithStackTrace(UnknownGenerateIfExistsVal{val: val}) + return ExistsUnknown, errors.New(UnknownGenerateIfExistsVal{val: val}) } // GenerateConfigDisabledFromString converts a string representation of if_disabled into the enum, returning an error if it is not set to one of the known values. @@ -325,5 +329,5 @@ func GenerateConfigDisabledFromString(val string) (GenerateConfigDisabled, error return DisabledRemoveTerragrunt, nil } - return DisabledUnknown, errors.WithStackTrace(UnknownGenerateIfDisabledVal{val: val}) + return DisabledUnknown, errors.New(UnknownGenerateIfDisabledVal{val: val}) } diff --git a/config/config.go b/config/config.go index 5b6efce25d..a440658423 100644 --- a/config/config.go +++ b/config/config.go @@ -3,7 +3,6 @@ package config import ( "context" - goErrors "errors" "fmt" "net/url" "os" @@ -26,10 +25,10 @@ import ( "github.com/hashicorp/hcl/v2/hclwrite" "github.com/zclconf/go-cty/cty" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/go-commons/files" "github.com/gruntwork-io/terragrunt/codegen" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/remote" "github.com/gruntwork-io/terragrunt/util" @@ -363,7 +362,7 @@ func (include *IncludeConfig) GetMergeStrategy() (MergeStrategyType, error) { case string(DeepMergeMapOnly): return DeepMergeMapOnly, nil default: - return NoMerge, errors.WithStackTrace(InvalidMergeStrategyTypeError(strategy)) + return NoMerge, errors.New(InvalidMergeStrategyTypeError(strategy)) } } @@ -574,7 +573,7 @@ func adjustSourceWithMap(sourceMap map[string]string, source string, modulePath // if both URL and subdir are missing, something went terribly wrong if moduleURL == "" && moduleSubdir == "" { - return "", errors.WithStackTrace(InvalidSourceURLWithMapError{ModulePath: modulePath, ModuleSourceURL: source}) + return "", errors.New(InvalidSourceURLWithMapError{ModulePath: modulePath, ModuleSourceURL: source}) } // If module URL is missing, return the source as is as it will not match anything in the map. @@ -869,7 +868,7 @@ func ParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChild *Inc } if terragruntConfigFile == nil { - return nil, errors.WithStackTrace(CouldNotResolveTerragruntConfigInFileError(file.ConfigPath)) + return nil, errors.New(CouldNotResolveTerragruntConfigInFileError(file.ConfigPath)) } config, err := convertToTerragruntConfig(ctx, file.ConfigPath, terragruntConfigFile) @@ -939,7 +938,7 @@ func decodeAsTerragruntConfigFile(ctx *ParsingContext, file *hclparse.File, eval if err := file.Decode(&terragruntConfig, evalContext); err != nil { var diagErr hcl.Diagnostics // diagErr, ok := errors.Unwrap(err).(hcl.Diagnostics) - ok := goErrors.As(err, &diagErr) + ok := errors.As(err, &diagErr) // in case of render-json command and inputs reference error, we update the inputs with default value if !ok || !isRenderJSONCommand(ctx) || !isAttributeAccessError(diagErr) { @@ -1291,7 +1290,7 @@ func validateGenerateBlocks(blocks *[]terragruntGenerateBlock) error { func configFileHasDependencyBlock(configPath string) (bool, error) { configBytes, err := os.ReadFile(configPath) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } // We use hclwrite to parse the config instead of the normal parser because the normal parser doesn't give us an AST @@ -1299,7 +1298,7 @@ func configFileHasDependencyBlock(configPath string) (bool, error) { // avoid weird parsing errors due to missing dependency data, we do a structural scan here. hclFile, diags := hclwrite.ParseConfig(configBytes, configPath, hcl.InitialPos) if diags.HasErrors() { - return false, errors.WithStackTrace(diags) + return false, errors.New(diags) } for _, block := range hclFile.Body().Blocks() { diff --git a/config/config_as_cty.go b/config/config_as_cty.go index 417c4507e3..5ff278a133 100644 --- a/config/config_as_cty.go +++ b/config/config_as_cty.go @@ -7,7 +7,7 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/remote" ) @@ -594,12 +594,12 @@ func dependencyBlocksAsCty(dependencyBlocks Dependencies) (cty.Value, error) { func convertToCtyWithJSON(val interface{}) (cty.Value, error) { jsonBytes, err := json.Marshal(val) if err != nil { - return cty.NilVal, errors.WithStackTrace(err) + return cty.NilVal, errors.New(err) } var ctyJSONVal ctyjson.SimpleJSONValue if err := ctyJSONVal.UnmarshalJSON(jsonBytes); err != nil { - return cty.NilVal, errors.WithStackTrace(err) + return cty.NilVal, errors.New(err) } return ctyJSONVal.Value, nil @@ -610,12 +610,12 @@ func convertToCtyWithJSON(val interface{}) (cty.Value, error) { func goTypeToCty(val interface{}) (cty.Value, error) { ctyType, err := gocty.ImpliedType(val) if err != nil { - return cty.NilVal, errors.WithStackTrace(err) + return cty.NilVal, errors.New(err) } ctyOut, err := gocty.ToCtyValue(val, ctyType) if err != nil { - return cty.NilVal, errors.WithStackTrace(err) + return cty.NilVal, errors.New(err) } return ctyOut, nil diff --git a/config/config_helpers.go b/config/config_helpers.go index e9fb6c102f..f8e4a9fc75 100644 --- a/config/config_helpers.go +++ b/config/config_helpers.go @@ -2,7 +2,6 @@ package config import ( "encoding/json" - goErrors "errors" "fmt" "os" "path/filepath" @@ -12,8 +11,6 @@ import ( "strings" "unicode/utf8" - "github.com/hashicorp/go-multierror" - "github.com/getsops/sops/v3/cmd/sops/formats" "github.com/getsops/sops/v3/decrypt" "github.com/hashicorp/go-getter" @@ -23,10 +20,10 @@ import ( "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/gocty" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/awshelper" "github.com/gruntwork-io/terragrunt/config/hclparse" "github.com/gruntwork-io/terragrunt/internal/cache" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/internal/locks" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/shell" @@ -233,12 +230,12 @@ func getRepoRoot(ctx *ParsingContext) (string, error) { func getPathFromRepoRoot(ctx *ParsingContext) (string, error) { repoAbsPath, err := shell.GitTopLevelDir(ctx, ctx.TerragruntOptions, ctx.TerragruntOptions.WorkingDir) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } repoRelPath, err := filepath.Rel(repoAbsPath, ctx.TerragruntOptions.WorkingDir) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.ToSlash(repoRelPath), nil @@ -248,12 +245,12 @@ func getPathFromRepoRoot(ctx *ParsingContext) (string, error) { func getPathToRepoRoot(ctx *ParsingContext) (string, error) { repoAbsPath, err := shell.GitTopLevelDir(ctx, ctx.TerragruntOptions, ctx.TerragruntOptions.WorkingDir) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } repoRootPathAbs, err := filepath.Rel(ctx.TerragruntOptions.WorkingDir, repoAbsPath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.ToSlash(strings.TrimSpace(repoRootPathAbs)), nil @@ -263,7 +260,7 @@ func getPathToRepoRoot(ctx *ParsingContext) (string, error) { func GetTerragruntDir(ctx *ParsingContext) (string, error) { terragruntConfigFileAbsPath, err := filepath.Abs(ctx.TerragruntOptions.TerragruntConfigPath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.ToSlash(filepath.Dir(terragruntConfigFileAbsPath)), nil @@ -276,7 +273,7 @@ func GetTerragruntDir(ctx *ParsingContext) (string, error) { func getOriginalTerragruntDir(ctx *ParsingContext) (string, error) { terragruntConfigFileAbsPath, err := filepath.Abs(ctx.TerragruntOptions.OriginalTerragruntConfigPath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.ToSlash(filepath.Dir(terragruntConfigFileAbsPath)), nil @@ -286,14 +283,14 @@ func getOriginalTerragruntDir(ctx *ParsingContext) (string, error) { func GetParentTerragruntDir(ctx *ParsingContext, params []string) (string, error) { parentPath, err := PathRelativeFromInclude(ctx, params) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } currentPath := filepath.Dir(ctx.TerragruntOptions.TerragruntConfigPath) parentPath, err = filepath.Abs(filepath.Join(currentPath, parentPath)) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.ToSlash(parentPath), nil @@ -310,11 +307,11 @@ func parseGetEnvParameters(parameters []string) (EnvVar, error) { envVariable.Name = parameters[0] envVariable.DefaultValue = parameters[1] default: - return envVariable, errors.WithStackTrace(InvalidGetEnvParamsError{ActualNumParams: len(parameters), Example: `getEnv("", "[DEFAULT]")`}) + return envVariable, errors.New(InvalidGetEnvParamsError{ActualNumParams: len(parameters), Example: `getEnv("", "[DEFAULT]")`}) } if envVariable.Name == "" { - return envVariable, errors.WithStackTrace(InvalidEnvParamNameError{EnvName: parameters[0]}) + return envVariable, errors.New(InvalidEnvParamNameError{EnvName: parameters[0]}) } return envVariable, nil @@ -329,7 +326,7 @@ func RunCommand(ctx *ParsingContext, args []string) (string, error) { runCommandCache := cache.ContextCache[string](ctx, RunCmdCacheContextKey) if len(args) == 0 { - return "", errors.WithStackTrace(EmptyStringNotAllowedError("parameter to the run_cmd function")) + return "", errors.New(EmptyStringNotAllowedError("parameter to the run_cmd function")) } suppressOutput := false @@ -369,10 +366,10 @@ func RunCommand(ctx *ParsingContext, args []string) (string, error) { cmdOutput, err := shell.RunShellCommandWithOutput(ctx, ctx.TerragruntOptions, currentPath, suppressOutput, false, args[0], args[1:]...) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } - value := strings.TrimSuffix(cmdOutput.Stdout, "\n") + value := strings.TrimSuffix(cmdOutput.Stdout.String(), "\n") if suppressOutput { ctx.TerragruntOptions.Logger.Debugf("run_cmd output: [REDACTED]") @@ -391,14 +388,14 @@ func getEnvironmentVariable(ctx *ParsingContext, parameters []string) (string, e parameterMap, err := parseGetEnvParameters(parameters) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } envValue, exists := ctx.TerragruntOptions.Env[parameterMap.Name] if !exists { if parameterMap.IsRequired { - return "", errors.WithStackTrace(EnvVarNotFoundError{EnvVar: parameterMap.Name}) + return "", errors.New(EnvVarNotFoundError{EnvVar: parameterMap.Name}) } envValue = parameterMap.DefaultValue @@ -429,12 +426,12 @@ func FindInParentFolders( } if numParams > matchedPats { - return "", errors.WithStackTrace(WrongNumberOfParamsError{Func: "find_in_parent_folders", Expected: "0, 1, or 2", Actual: numParams}) + return "", errors.New(WrongNumberOfParamsError{Func: "find_in_parent_folders", Expected: "0, 1, or 2", Actual: numParams}) } previousDir, err := filepath.Abs(filepath.Dir(ctx.TerragruntOptions.TerragruntConfigPath)) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } previousDir = filepath.ToSlash(previousDir) @@ -453,7 +450,11 @@ func FindInParentFolders( return fallbackParam, nil } - return "", errors.WithStackTrace(ParentFileNotFoundError{Path: ctx.TerragruntOptions.TerragruntConfigPath, File: fileToFindStr, Cause: "Traversed all the way to the root"}) + return "", errors.New(ParentFileNotFoundError{ + Path: ctx.TerragruntOptions.TerragruntConfigPath, + File: fileToFindStr, + Cause: "Traversed all the way to the root", + }) } fileToFind := GetDefaultConfigPath(currentDir) @@ -468,7 +469,11 @@ func FindInParentFolders( previousDir = currentDir } - return "", errors.WithStackTrace(ParentFileNotFoundError{Path: ctx.TerragruntOptions.TerragruntConfigPath, File: fileToFindStr, Cause: fmt.Sprintf("Exceeded maximum folders to check (%d)", ctx.TerragruntOptions.MaxFoldersToCheck)}) + return "", errors.New(ParentFileNotFoundError{ + Path: ctx.TerragruntOptions.TerragruntConfigPath, + File: fileToFindStr, + Cause: fmt.Sprintf("Exceeded maximum folders to check (%d)", ctx.TerragruntOptions.MaxFoldersToCheck), + }) } // PathRelativeToInclude returns the relative path between the included Terragrunt configuration file @@ -619,7 +624,7 @@ func ParseTerragruntConfig(ctx *ParsingContext, configPath string, defaultVal *c targetConfigFileExists := util.FileExists(targetConfig) if !targetConfigFileExists && defaultVal == nil { - return cty.NilVal, errors.WithStackTrace(TerragruntConfigNotFoundError{Path: targetConfig}) + return cty.NilVal, errors.New(TerragruntConfigNotFoundError{Path: targetConfig}) } else if !targetConfigFileExists { return *defaultVal, nil } @@ -645,7 +650,7 @@ func ParseTerragruntConfig(ctx *ParsingContext, configPath string, defaultVal *c for i := 0; i < len(config.TerragruntDependencies); i++ { err := config.TerragruntDependencies[i].setRenderedOutputs(ctx) if err != nil { - return cty.NilVal, errors.WithStackTrace(err) + return cty.NilVal, errors.New(err) } } @@ -665,7 +670,7 @@ func readTerragruntConfigAsFuncImpl(ctx *ParsingContext) function.Function { numParams := len(args) if numParams == 0 || numParams > 2 { - return cty.NilVal, errors.WithStackTrace(WrongNumberOfParamsError{Func: "read_terragrunt_config", Expected: "1 or 2", Actual: numParams}) + return cty.NilVal, errors.New(WrongNumberOfParamsError{Func: "read_terragrunt_config", Expected: "1 or 2", Actual: numParams}) } strArgs, err := ctySliceToStringSlice(args[:1]) @@ -728,7 +733,11 @@ func GetTerragruntSourceForModule(sourcePath string, modulePath string, moduleTe // if both URL and subdir are missing, something went terribly wrong if moduleURL == "" && moduleSubdir == "" { - return "", errors.WithStackTrace(InvalidSourceURLError{ModulePath: modulePath, ModuleSourceURL: *moduleTerragruntConfig.Terraform.Source, TerragruntSource: sourcePath}) + return "", errors.New(InvalidSourceURLError{ + ModulePath: modulePath, + ModuleSourceURL: *moduleTerragruntConfig.Terraform.Source, + TerragruntSource: sourcePath, + }) } // if only subdir is missing, check if we can obtain a valid module name from the URL portion @@ -761,7 +770,7 @@ func getModulePathFromSourceURL(sourceURL string) (string, error) { // if regexp returns less/more than the full match + 1 capture group, then something went wrong with regex (invalid source string) if len(matches) != matchedPats { - return "", errors.WithStackTrace(ParsingModulePathError{ModuleSourceURL: sourceURL}) + return "", errors.New(ParsingModulePathError{ModuleSourceURL: sourceURL}) } return matches[1], nil @@ -786,17 +795,17 @@ func sopsDecryptFile(ctx *ParsingContext, params []string) (string, error) { } if numParams != 1 { - return "", errors.WithStackTrace(WrongNumberOfParamsError{Func: "sops_decrypt_file", Expected: "1", Actual: numParams}) + return "", errors.New(WrongNumberOfParamsError{Func: "sops_decrypt_file", Expected: "1", Actual: numParams}) } format, err := getSopsFileFormat(sourceFile) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } canonicalSourceFile, err := util.CanonicalPath(sourceFile, filepath.Dir(ctx.TerragruntOptions.TerragruntConfigPath)) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // Set environment variables from the TerragruntOptions.Env map. @@ -826,7 +835,7 @@ func sopsDecryptFile(ctx *ParsingContext, params []string) (string, error) { rawData, err := decrypt.File(canonicalSourceFile, format) if err != nil { - return "", errors.WithStackTrace(extractSopsErrors(err)) + return "", errors.New(extractSopsErrors(err)) } if utf8.Valid(rawData) { @@ -836,7 +845,7 @@ func sopsDecryptFile(ctx *ParsingContext, params []string) (string, error) { return value, nil } - return "", errors.WithStackTrace(InvalidSopsFormatError{SourceFilePath: sourceFile}) + return "", errors.New(InvalidSopsFormatError{SourceFilePath: sourceFile}) } // Mapping of SOPS format to string @@ -890,14 +899,14 @@ func getSelectedIncludeBlock(trackInclude TrackInclude, params []string) (*Inclu numParams := len(params) if numParams != 1 { - return nil, errors.WithStackTrace(WrongNumberOfParamsError{Func: "path_relative_from_include", Expected: "1", Actual: numParams}) + return nil, errors.New(WrongNumberOfParamsError{Func: "path_relative_from_include", Expected: "1", Actual: numParams}) } importName := params[0] imported, hasKey := importMap[importName] if !hasKey { - return nil, errors.WithStackTrace(InvalidIncludeKeyError{name: importName}) + return nil, errors.New(InvalidIncludeKeyError{name: importName}) } return &imported, nil @@ -906,7 +915,7 @@ func getSelectedIncludeBlock(trackInclude TrackInclude, params []string) (*Inclu // StartsWith Implementation of Terraform's StartsWith function func StartsWith(ctx *ParsingContext, args []string) (bool, error) { if len(args) == 0 { - return false, errors.WithStackTrace(EmptyStringNotAllowedError("parameter to the startswith function")) + return false, errors.New(EmptyStringNotAllowedError("parameter to the startswith function")) } str := args[0] @@ -922,7 +931,7 @@ func StartsWith(ctx *ParsingContext, args []string) (bool, error) { // EndsWith Implementation of Terraform's EndsWith function func EndsWith(ctx *ParsingContext, args []string) (bool, error) { if len(args) == 0 { - return false, errors.WithStackTrace(EmptyStringNotAllowedError("parameter to the endswith function")) + return false, errors.New(EmptyStringNotAllowedError("parameter to the endswith function")) } str := args[0] @@ -938,17 +947,17 @@ func EndsWith(ctx *ParsingContext, args []string) (bool, error) { // TimeCmp implements Terraform's `timecmp` function that compares two timestamps. func TimeCmp(ctx *ParsingContext, args []string) (int64, error) { if len(args) != matchedPats { - return 0, errors.WithStackTrace(goErrors.New("function can take only two parameters: timestamp_a and timestamp_b")) + return 0, errors.New(errors.New("function can take only two parameters: timestamp_a and timestamp_b")) } tsA, err := util.ParseTimestamp(args[0]) if err != nil { - return 0, errors.WithStackTrace(fmt.Errorf("could not parse first parameter %q: %w", args[0], err)) + return 0, errors.New(fmt.Errorf("could not parse first parameter %q: %w", args[0], err)) } tsB, err := util.ParseTimestamp(args[1]) if err != nil { - return 0, errors.WithStackTrace(fmt.Errorf("could not parse second parameter %q: %w", args[1], err)) + return 0, errors.New(fmt.Errorf("could not parse second parameter %q: %w", args[1], err)) } switch { @@ -965,7 +974,7 @@ func TimeCmp(ctx *ParsingContext, args []string) (int64, error) { // StrContains Implementation of Terraform's StrContains function func StrContains(ctx *ParsingContext, args []string) (bool, error) { if len(args) == 0 { - return false, errors.WithStackTrace(EmptyStringNotAllowedError("parameter to the strcontains function")) + return false, errors.New(EmptyStringNotAllowedError("parameter to the strcontains function")) } str := args[0] @@ -981,30 +990,30 @@ func StrContains(ctx *ParsingContext, args []string) (bool, error) { // readTFVarsFile reads a *.tfvars or *.tfvars.json file and returns the contents as a JSON encoded string func readTFVarsFile(ctx *ParsingContext, args []string) (string, error) { if len(args) != 1 { - return "", errors.WithStackTrace(WrongNumberOfParamsError{Func: "read_tfvars_file", Expected: "1", Actual: len(args)}) + return "", errors.New(WrongNumberOfParamsError{Func: "read_tfvars_file", Expected: "1", Actual: len(args)}) } varFile := args[0] varFile, err := util.CanonicalPath(varFile, ctx.TerragruntOptions.WorkingDir) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } if !util.FileExists(varFile) { - return "", errors.WithStackTrace(TFVarFileNotFoundError{File: varFile}) + return "", errors.New(TFVarFileNotFoundError{File: varFile}) } fileContents, err := os.ReadFile(varFile) if err != nil { - return "", errors.WithStackTrace(fmt.Errorf("could not read file %q: %w", varFile, err)) + return "", errors.New(fmt.Errorf("could not read file %q: %w", varFile, err)) } if strings.HasSuffix(varFile, "json") { var variables map[string]interface{} // just want to be sure that the file is valid json if err := json.Unmarshal(fileContents, &variables); err != nil { - return "", errors.WithStackTrace(fmt.Errorf("could not unmarshal json body of tfvar file: %w", err)) + return "", errors.New(fmt.Errorf("could not unmarshal json body of tfvar file: %w", err)) } return string(fileContents), nil @@ -1017,7 +1026,7 @@ func readTFVarsFile(ctx *ParsingContext, args []string) (string, error) { data, err := json.Marshal(variables) if err != nil { - return "", errors.WithStackTrace(fmt.Errorf("could not marshal json body of tfvar file: %w", err)) + return "", errors.New(fmt.Errorf("could not marshal json body of tfvar file: %w", err)) } return string(data), nil @@ -1074,9 +1083,9 @@ func ParseAndDecodeVarFile(opts *options.TerragruntOptions, varFile string, file return gocty.FromCtyValue(ctyVal, out) } -// extractSopsErrors extracts the original errors from the sops library and returns them as a multierror.Error -func extractSopsErrors(err error) *multierror.Error { - var errs = &multierror.Error{} +// extractSopsErrors extracts the original errors from the sops library and returns them as a errors.MultiError. +func extractSopsErrors(err error) *errors.MultiError { + var errs = &errors.MultiError{} // workaround to extract original errors from sops library // using reflection extract GroupResults from getDataKeyError @@ -1092,7 +1101,9 @@ func extractSopsErrors(err error) *multierror.Error { for i := 0; i < groupResultsField.Len(); i++ { groupErr := groupResultsField.Index(i) if groupErr.CanInterface() { - errs = multierror.Append(errs, groupErr.Interface().(error)) + if err, ok := groupErr.Interface().(error); ok { + errs = errs.Append(err) + } } } } @@ -1100,7 +1111,7 @@ func extractSopsErrors(err error) *multierror.Error { // append the original error if no group results were found if errs.Len() == 0 { - errs = multierror.Append(errs, err) + errs = errs.Append(err) } return errs diff --git a/config/config_helpers_test.go b/config/config_helpers_test.go index 96dd730f99..c1f6a702ea 100644 --- a/config/config_helpers_test.go +++ b/config/config_helpers_test.go @@ -7,8 +7,8 @@ import ( "path/filepath" "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/test/helpers" "github.com/stretchr/testify/assert" diff --git a/config/config_partial.go b/config/config_partial.go index b7a6397820..c3d477a35e 100644 --- a/config/config_partial.go +++ b/config/config_partial.go @@ -1,7 +1,6 @@ package config import ( - goErrors "errors" "fmt" "os" "path/filepath" @@ -13,8 +12,8 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" ) @@ -348,7 +347,7 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi if err := file.Decode(&decoded, evalParsingContext); err != nil { var diagErr hcl.Diagnostics - ok := goErrors.As(err, &diagErr) + ok := errors.As(err, &diagErr) // in case of render-json command and inputs reference error, we update the inputs with default value if !ok || !isRenderJSONCommand(ctx) || !isAttributeAccessError(diagErr) { @@ -426,7 +425,7 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi func partialParseIncludedConfig(ctx *ParsingContext, includedConfig *IncludeConfig) (*TerragruntConfig, error) { if includedConfig.Path == "" { - return nil, errors.WithStackTrace(IncludedConfigMissingPathError(ctx.TerragruntOptions.TerragruntConfigPath)) + return nil, errors.New(IncludedConfigMissingPathError(ctx.TerragruntOptions.TerragruntConfigPath)) } includePath := includedConfig.Path diff --git a/config/config_test.go b/config/config_test.go index 3123f35a4f..0e01718ed8 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -7,8 +7,8 @@ import ( "path/filepath" "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" @@ -480,7 +480,7 @@ include { ctx := config.NewParsingContext(context.Background(), opts) terragruntConfig, err := config.ParseConfigString(ctx, opts.TerragruntConfigPath, cfg, nil) - if assert.NoError(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) { + if assert.NoError(t, err, "Unexpected error: %v", errors.New(err)) { assert.Nil(t, terragruntConfig.Terraform) if assert.NotNil(t, terragruntConfig.RemoteState) { @@ -508,7 +508,7 @@ include { ctx := config.NewParsingContext(context.Background(), opts) terragruntConfig, err := config.ParseConfigString(ctx, opts.TerragruntConfigPath, cfg, nil) - if assert.NoError(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) { + if assert.NoError(t, err, "Unexpected error: %v", errors.New(err)) { assert.Nil(t, terragruntConfig.Terraform) if assert.NotNil(t, terragruntConfig.RemoteState) { @@ -548,7 +548,7 @@ remote_state { ctx := config.NewParsingContext(context.Background(), opts) terragruntConfig, err := config.ParseConfigString(ctx, opts.TerragruntConfigPath, cfg, nil) - if assert.NoError(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) { + if assert.NoError(t, err, "Unexpected error: %v", errors.New(err)) { assert.Nil(t, terragruntConfig.Terraform) if assert.NotNil(t, terragruntConfig.RemoteState) { @@ -596,7 +596,7 @@ dependencies { ctx := config.NewParsingContext(context.Background(), opts) terragruntConfig, err := config.ParseConfigString(ctx, opts.TerragruntConfigPath, cfg, nil) - require.NoError(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) + require.NoError(t, err, "Unexpected error: %v", errors.New(err)) assert.NotNil(t, terragruntConfig.Terraform) assert.NotNil(t, terragruntConfig.Terraform.Source) @@ -645,7 +645,7 @@ func TestParseTerragruntJsonConfigIncludeOverrideAll(t *testing.T) { ctx := config.NewParsingContext(context.Background(), opts) terragruntConfig, err := config.ParseConfigString(ctx, opts.TerragruntConfigPath, cfg, nil) - require.NoError(t, err, "Unexpected error: %v", errors.PrintErrorWithStackTrace(err)) + require.NoError(t, err, "Unexpected error: %v", errors.New(err)) assert.NotNil(t, terragruntConfig.Terraform) assert.NotNil(t, terragruntConfig.Terraform.Source) diff --git a/config/cty_helpers.go b/config/cty_helpers.go index 34bad669a5..923a0bb940 100644 --- a/config/cty_helpers.go +++ b/config/cty_helpers.go @@ -10,7 +10,7 @@ import ( "github.com/zclconf/go-cty/cty/gocty" ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" ) // Create a cty Function that takes as input parameters a slice of strings (var args, so this slice could be of any @@ -151,7 +151,7 @@ func ctySliceToStringSlice(args []cty.Value) ([]string, error) { for _, arg := range args { if arg.Type() != cty.String { - return nil, errors.WithStackTrace(InvalidParameterTypeError{Expected: "string", Actual: arg.Type().FriendlyName()}) + return nil, errors.New(InvalidParameterTypeError{Expected: "string", Actual: arg.Type().FriendlyName()}) } out = append(out, arg.AsString()) @@ -239,12 +239,12 @@ func ParseCtyValueToMap(value cty.Value) (map[string]interface{}, error) { jsonBytes, err := ctyjson.Marshal(value, cty.DynamicPseudoType) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } var ctyJSONOutput CtyJSONOutput if err := json.Unmarshal(jsonBytes, &ctyJSONOutput); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return ctyJSONOutput.Value, nil @@ -270,7 +270,7 @@ func convertValuesMapToCtyVal(valMap map[string]cty.Value) (cty.Value, error) { valMapAsCty, err = gocty.ToCtyValue(valMap, generateTypeFromValuesMap(valMap)) if err != nil { - return valMapAsCty, errors.WithStackTrace(err) + return valMapAsCty, errors.New(err) } } @@ -389,7 +389,7 @@ func UpdateUnknownCtyValValues(value cty.Value) (cty.Value, error) { value, err := gocty.ToCtyValue(updatedValue, value.Type()) if err != nil { - return cty.NilVal, errors.WithStackTrace(err) + return cty.NilVal, errors.New(err) } return value, nil diff --git a/config/dependency.go b/config/dependency.go index 444d54963e..9b6eb7f6a4 100644 --- a/config/dependency.go +++ b/config/dependency.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "encoding/json" - goErrors "errors" "fmt" "io" "os" @@ -24,12 +23,12 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" "golang.org/x/sync/errgroup" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform/creds" "github.com/gruntwork-io/terragrunt/cli/commands/terraform/creds/providers/amazonsts" "github.com/gruntwork-io/terragrunt/cli/commands/terraform/creds/providers/externalcmd" "github.com/gruntwork-io/terragrunt/codegen" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/remote" "github.com/gruntwork-io/terragrunt/shell" @@ -197,6 +196,7 @@ func decodeAndRetrieveOutputs(ctx *ParsingContext, file *hclparse.File) (*cty.Va if err := file.Decode(&decodedDependency, evalParsingContext); err != nil { return nil, err } + // In normal operation, if a dependency block does not have a `config_path` attribute, decoding returns an error since this attribute is required, but the `hclvalidate` command suppresses decoding errors and this causes a cycle between modules, so we need to filter out dependencies without a defined `config_path`. decodedDependency.Dependencies = decodedDependency.Dependencies.FilteredWithoutConfigPath() @@ -339,7 +339,7 @@ func checkForDependencyBlockCyclesUsingDFS( } if util.ListContainsElement(*currentTraversalPaths, dependencyPath) { - return errors.WithStackTrace(DependencyCycleError(append(*currentTraversalPaths, dependencyPath))) + return errors.New(DependencyCycleError(append(*currentTraversalPaths, dependencyPath))) } *currentTraversalPaths = append(*currentTraversalPaths, dependencyPath) @@ -456,7 +456,7 @@ func dependencyBlocksToCtyValue(ctx *ParsingContext, dependencyConfigs []Depende err = TerragruntOutputListEncodingError{Paths: paths, Err: err} } - return &convertedOutput, errors.WithStackTrace(err) + return &convertedOutput, errors.New(err) } // This will attempt to get the outputs from the target terragrunt config if it is applied. If it is not applied, the @@ -488,7 +488,7 @@ func getTerragruntOutputIfAppliedElseConfiguredDefault(ctx *ParsingContext, depe case DeepMergeMapOnly: return deepMergeCtyMapsMapOnly(*dependencyConfig.MockOutputs, *outputVal) default: - return nil, errors.WithStackTrace(InvalidMergeStrategyTypeError(mockMergeStrategy)) + return nil, errors.New(InvalidMergeStrategyTypeError(mockMergeStrategy)) } } else if !isEmpty { return outputVal, err @@ -544,7 +544,7 @@ func getTerragruntOutput(ctx *ParsingContext, dependencyConfig Dependency) (*cty // target config check: make sure the target config exists targetConfigPath := getCleanedTargetConfigPath(dependencyConfig.ConfigPath.AsString(), ctx.TerragruntOptions.TerragruntConfigPath) if !util.FileExists(targetConfigPath) { - return nil, true, errors.WithStackTrace(DependencyConfigNotFound{Path: targetConfigPath}) + return nil, true, errors.New(DependencyConfigNotFound{Path: targetConfigPath}) } jsonBytes, err := getOutputJSONWithCaching(ctx, targetConfigPath) @@ -574,12 +574,12 @@ func getTerragruntOutput(ctx *ParsingContext, dependencyConfig Dependency) (*cty err = TerragruntOutputEncodingError{Path: targetConfigPath, Err: err} } - return &convertedOutput, isEmpty, errors.WithStackTrace(err) + return &convertedOutput, isEmpty, errors.New(err) } func isAwsS3NoSuchKey(err error) bool { var awsErr awserr.Error - if goErrors.As(errors.Unwrap(err), &awsErr) { + if errors.As(err, &awsErr) { return awsErr.Code() == "NoSuchKey" } @@ -675,14 +675,14 @@ func cloneTerragruntOptionsForDependencyOutput(ctx *ParsingContext, targetConfig // DownloadDir needs to be updated to be in the ctx of the new config, if using default _, originalDefaultDownloadDir, err := options.DefaultWorkingAndDownloadDirs(ctx.TerragruntOptions.TerragruntConfigPath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } // Using default, so compute new download dir and update if ctx.TerragruntOptions.DownloadDir == originalDefaultDownloadDir { _, downloadDir, err := options.DefaultWorkingAndDownloadDirs(targetConfig) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } targetOptions.DownloadDir = downloadDir @@ -844,7 +844,7 @@ func getTerragruntOutputJSONFromInitFolder(ctx *ParsingContext, terraformWorking return nil, err } - jsonString := strings.TrimSpace(out.Stdout) + jsonString := strings.TrimSpace(out.Stdout.String()) jsonBytes := []byte(jsonString) ctx.TerragruntOptions.Logger.Debugf("Retrieved output from %s as json: %s", targetConfigPath, jsonString) @@ -951,7 +951,7 @@ func getTerragruntOutputJSONFromRemoteState( return nil, err } - jsonString := strings.TrimSpace(out.Stdout) + jsonString := strings.TrimSpace(out.Stdout.String()) jsonBytes := []byte(jsonString) ctx.TerragruntOptions.Logger.Debugf("Retrieved output from %s as json: %s", targetConfigPath, jsonString) @@ -1057,12 +1057,12 @@ func runTerragruntOutputJSON(ctx *ParsingContext, targetConfig string) ([]byte, err := ctx.TerragruntOptions.RunTerragrunt(ctx, ctx.TerragruntOptions) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } err = stdoutBufferWriter.Flush() if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } jsonString := strings.TrimSpace(stdoutBuffer.String()) @@ -1089,7 +1089,7 @@ func TerraformOutputJSONToCtyValueMap(targetConfigPath string, jsonBytes []byte) err := json.Unmarshal(jsonBytes, &outputs) if err != nil { - return nil, errors.WithStackTrace(TerragruntOutputParsingError{Path: targetConfigPath, Err: err}) + return nil, errors.New(TerragruntOutputParsingError{Path: targetConfigPath, Err: err}) } flattenedOutput := map[string]cty.Value{} @@ -1097,12 +1097,12 @@ func TerraformOutputJSONToCtyValueMap(targetConfigPath string, jsonBytes []byte) for k, v := range outputs { outputType, err := ctyjson.UnmarshalType(v.Type) if err != nil { - return nil, errors.WithStackTrace(TerragruntOutputParsingError{Path: targetConfigPath, Err: err}) + return nil, errors.New(TerragruntOutputParsingError{Path: targetConfigPath, Err: err}) } outputVal, err := ctyjson.Unmarshal(v.Value, outputType) if err != nil { - return nil, errors.WithStackTrace(TerragruntOutputParsingError{Path: targetConfigPath, Err: err}) + return nil, errors.New(TerragruntOutputParsingError{Path: targetConfigPath, Err: err}) } flattenedOutput[k] = outputVal diff --git a/config/hclparse/attributes.go b/config/hclparse/attributes.go index aee290296c..355153a3f3 100644 --- a/config/hclparse/attributes.go +++ b/config/hclparse/attributes.go @@ -1,7 +1,7 @@ package hclparse import ( - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" @@ -66,7 +66,7 @@ func (attr *Attribute) ValidateIdentifier() error { }} if err := attr.HandleDiagnostics(diags); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -77,7 +77,7 @@ func (attr *Attribute) Value(evalCtx *hcl.EvalContext) (cty.Value, error) { evaluatedVal, diags := attr.Expr.Value(evalCtx) if err := attr.HandleDiagnostics(diags); err != nil { - return evaluatedVal, errors.WithStackTrace(err) + return evaluatedVal, errors.New(err) } return evaluatedVal, nil diff --git a/config/hclparse/block.go b/config/hclparse/block.go index 84a2279f37..e10bb311e5 100644 --- a/config/hclparse/block.go +++ b/config/hclparse/block.go @@ -1,7 +1,7 @@ package hclparse import ( - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/hcl/v2" ) @@ -24,7 +24,7 @@ func (block *Block) JustAttributes() (Attributes, error) { hclAttrs, diags := block.Body.JustAttributes() if err := block.HandleDiagnostics(diags); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } attrs := NewAttributes(block.File, hclAttrs) diff --git a/config/hclparse/file.go b/config/hclparse/file.go index 260b724c98..3506f24cb9 100644 --- a/config/hclparse/file.go +++ b/config/hclparse/file.go @@ -3,7 +3,7 @@ package hclparse import ( "fmt" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclparse" @@ -67,7 +67,7 @@ func (file *File) Decode(out interface{}, evalContext *hcl.EvalContext) (err err diags := gohcl.DecodeBody(file.Body, evalContext, out) if err := file.HandleDiagnostics(diags); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -83,7 +83,7 @@ func (file *File) Blocks(name string, isMultipleAllowed bool) ([]*Block, error) // We use PartialContent here, because we are only interested in parsing out the catalog block. parsed, _, diags := file.Body.PartialContent(catalogSchema) if err := file.HandleDiagnostics(diags); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } extractedBlocks := []*Block{} @@ -98,7 +98,7 @@ func (file *File) Blocks(name string, isMultipleAllowed bool) ([]*Block, error) } if len(extractedBlocks) > 1 && !isMultipleAllowed { - return nil, errors.WithStackTrace( + return nil, errors.New( &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Multiple %s block", name), @@ -114,7 +114,7 @@ func (file *File) JustAttributes() (Attributes, error) { hclAttrs, diags := file.Body.JustAttributes() if err := file.HandleDiagnostics(diags); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } attrs := NewAttributes(file, hclAttrs) diff --git a/config/hclparse/options.go b/config/hclparse/options.go index c60de3be00..31cb990d0a 100644 --- a/config/hclparse/options.go +++ b/config/hclparse/options.go @@ -3,7 +3,7 @@ package hclparse import ( "io" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/hashicorp/hcl/v2" ) @@ -28,7 +28,7 @@ func WithDiagnosticsWriter(writer io.Writer, disableColor bool) Option { } if err := diagsWriter.WriteDiagnostics(diags); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/config/hclparse/parser.go b/config/hclparse/parser.go index 0da17b4c2f..599d16f4fa 100644 --- a/config/hclparse/parser.go +++ b/config/hclparse/parser.go @@ -9,7 +9,7 @@ import ( "os" "path/filepath" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclparse" @@ -43,7 +43,8 @@ func (parser *Parser) ParseFromFile(configPath string) (*File, error) { content, err := os.ReadFile(configPath) if err != nil { parser.logger.Warnf("Error reading file %s: %v", configPath, err) - return nil, errors.WithStackTrace(err) + + return nil, errors.New(err) } return parser.ParseFromBytes(content, configPath) @@ -59,7 +60,7 @@ func (parser *Parser) ParseFromBytes(content []byte, configPath string) (file *F // those panics here and convert them to normal errors defer func() { if recovered := recover(); recovered != nil { - err = errors.WithStackTrace(PanicWhileParsingConfigError{RecoveredValue: recovered, ConfigFile: configPath}) + err = errors.New(PanicWhileParsingConfigError{RecoveredValue: recovered, ConfigFile: configPath}) } }() @@ -83,7 +84,8 @@ func (parser *Parser) ParseFromBytes(content []byte, configPath string) (file *F if err := parser.handleDiagnostics(file, diags); err != nil { parser.logger.Warnf("Failed to parse HCL in file %s: %v", configPath, diags) - return nil, errors.WithStackTrace(diags) + + return nil, errors.New(diags) } return file, nil diff --git a/config/include.go b/config/include.go index 7e2841d1ca..f020ed4dcf 100644 --- a/config/include.go +++ b/config/include.go @@ -2,7 +2,6 @@ package config import ( "encoding/json" - goErrors "errors" "fmt" "path/filepath" "strings" @@ -14,7 +13,7 @@ import ( "github.com/hashicorp/hcl/v2/hclwrite" "github.com/imdario/mergo" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -26,7 +25,7 @@ var fieldsCopyLocks = util.NewKeyLocks() // Parse the config of the given include, if one is specified func parseIncludedConfig(ctx *ParsingContext, includedConfig *IncludeConfig) (*TerragruntConfig, error) { if includedConfig.Path == "" { - return nil, errors.WithStackTrace(IncludedConfigMissingPathError(ctx.TerragruntOptions.TerragruntConfigPath)) + return nil, errors.New(IncludedConfigMissingPathError(ctx.TerragruntOptions.TerragruntConfigPath)) } includePath := includedConfig.Path @@ -100,7 +99,7 @@ func parseIncludedConfig(ctx *ParsingContext, includedConfig *IncludeConfig) (*T // user. func handleInclude(ctx *ParsingContext, config *TerragruntConfig, isPartial bool) (*TerragruntConfig, error) { if ctx.TrackInclude == nil { - return nil, goErrors.New("you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message. Code: HANDLE_INCLUDE_NIL_INCLUDE_CONFIG") + return nil, errors.New("you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message.Code: HANDLE_INCLUDE_NIL_INCLUDE_CONFIG") } // We merge in the include blocks in reverse order here. The expectation is that the bottom most elements override @@ -166,7 +165,7 @@ func handleInclude(ctx *ParsingContext, config *TerragruntConfig, isPartial bool // child. func handleIncludeForDependency(ctx *ParsingContext, childDecodedDependency TerragruntDependency) (*TerragruntDependency, error) { if ctx.TrackInclude == nil { - return nil, goErrors.New("you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message. Code: HANDLE_INCLUDE_DEPENDENCY_NIL_INCLUDE_CONFIG") + return nil, errors.New("you reached an impossible condition. This is most likely a bug in terragrunt. Please open an issue at github.com/gruntwork-io/terragrunt with this error message. Code: HANDLE_INCLUDE_DEPENDENCY_NIL_INCLUDE_CONFIG") } // We merge in the include blocks in reverse order here. The expectation is that the bottom most elements override // those in earlier includes, so we need to merge bottom up instead of top down to ensure this. @@ -636,7 +635,7 @@ func deepMergeInputs(childInputs map[string]interface{}, parentInputs map[string err := mergo.Merge(&out, childInputs, mergo.WithAppendSlice, mergo.WithOverride) - return out, errors.WithStackTrace(err) + return out, errors.New(err) } // Merge the hooks (before_hook and after_hook). @@ -707,7 +706,7 @@ func getTrackInclude(ctx *ParsingContext, terragruntIncludeList IncludeConfigs, case hasInclude && includeFromChild != nil: // tgInc appears in a parent that is already included, which means a nested include block. This is not // something we currently support. - err := errors.WithStackTrace(TooManyLevelsOfInheritanceError{ + err := errors.New(TooManyLevelsOfInheritanceError{ ConfigPath: ctx.TerragruntOptions.TerragruntConfigPath, FirstLevelIncludePath: includeFromChild.Path, SecondLevelIncludePath: strings.Join(includedPaths, ","), @@ -754,13 +753,13 @@ func updateBareIncludeBlock(file *hclparse.File) error { default: hclFile, diags := hclwrite.ParseConfig(file.Bytes, file.ConfigPath, hcl.InitialPos) if diags.HasErrors() { - return errors.WithStackTrace(diags) + return errors.New(diags) } for _, block := range hclFile.Body().Blocks() { if block.Type() == MetadataInclude && len(block.Labels()) == 0 { if codeWasUpdated { - return errors.WithStackTrace(MultipleBareIncludeBlocksErr{}) + return errors.New(MultipleBareIncludeBlocksErr{}) } block.SetLabels([]string{bareIncludeKey}) @@ -826,7 +825,7 @@ func updateBareIncludeBlock(file *hclparse.File) error { func updateBareIncludeBlockJSON(fileBytes []byte) ([]byte, bool, error) { var parsed map[string]interface{} if err := json.Unmarshal(fileBytes, &parsed); err != nil { - return nil, false, errors.WithStackTrace(err) + return nil, false, errors.New(err) } includeBlock, hasKey := parsed[MetadataInclude] @@ -844,7 +843,7 @@ func updateBareIncludeBlockJSON(fileBytes []byte) ([]byte, bool, error) { // Could be multiple bare includes, or Case 3. We simplify the handling of this case by erroring out, // ignoring the possibility of Case 3, which, while valid HCL encoding, is too complex to detect and handle // here. Instead we will recommend users use the object encoding. - return nil, false, errors.WithStackTrace(MultipleBareIncludeBlocksErr{}) + return nil, false, errors.New(MultipleBareIncludeBlocksErr{}) } // Make sure this is Case 2, and not Case 3 with a single labeled block. If Case 2, update to inject the labeled @@ -870,7 +869,7 @@ func updateBareIncludeBlockJSON(fileBytes []byte) ([]byte, bool, error) { return nil, false, nil } - return nil, false, errors.WithStackTrace(IncludeIsNotABlockErr{parsed: includeBlock}) + return nil, false, errors.New(IncludeIsNotABlockErr{parsed: includeBlock}) } // updateSingleBareIncludeInParsedJSON replaces the include attribute into a block with the label "" in the json. Note that we @@ -880,7 +879,7 @@ func updateSingleBareIncludeInParsedJSON(parsed map[string]interface{}, newVal i parsed[MetadataInclude] = map[string]interface{}{bareIncludeKey: newVal} updatedBytes, err := json.Marshal(parsed) - return updatedBytes, true, errors.WithStackTrace(err) + return updatedBytes, true, errors.New(err) } // jsonIsIncludeBlock checks if the arbitrary json data is the include block. The data is determined to be an include diff --git a/config/locals.go b/config/locals.go index 4f96e1d6d6..e6fa3e7aae 100644 --- a/config/locals.go +++ b/config/locals.go @@ -4,12 +4,11 @@ import ( "fmt" "strings" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" ) // MaxIter is the maximum number of depth we support in recursively evaluating locals. @@ -52,7 +51,7 @@ func EvaluateLocalsBlock(ctx *ParsingContext, file *hclparse.File) (map[string]c if iterations > MaxIter { // Reached maximum supported iterations, which is most likely an infinite loop bug so cut the iteration // short an return an error. - return nil, errors.WithStackTrace(MaxIterError{}) + return nil, errors.New(MaxIterError{}) } var err error @@ -73,17 +72,17 @@ func EvaluateLocalsBlock(ctx *ParsingContext, file *hclparse.File) (map[string]c // This is an error because we couldn't evaluate all locals ctx.TerragruntOptions.Logger.Debugf("Not all locals could be evaluated:") - var errs *multierror.Error + var errs *errors.MultiError for _, attr := range attrs { diags := canEvaluateLocals(attr.Expr, evaluatedLocals) if err := file.HandleDiagnostics(diags); err != nil { - errs = multierror.Append(errs, err) + errs = errs.Append(err) } } if err := errs.ErrorOrNil(); err != nil { - return nil, errors.WithStackTrace(CouldNotEvaluateAllLocalsError{Err: err}) + return nil, errors.New(CouldNotEvaluateAllLocalsError{Err: err}) } } diff --git a/config/locals_test.go b/config/locals_test.go index 28aab788f3..dcc7c432d5 100644 --- a/config/locals_test.go +++ b/config/locals_test.go @@ -9,9 +9,9 @@ import ( "github.com/stretchr/testify/require" "github.com/zclconf/go-cty/cty/gocty" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" ) func TestEvaluateLocalsBlock(t *testing.T) { diff --git a/config/variable.go b/config/variable.go index 4ea84c1c70..90e7499ebf 100644 --- a/config/variable.go +++ b/config/variable.go @@ -4,8 +4,8 @@ import ( "encoding/json" "fmt" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config/hclparse" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/hcl/v2" @@ -28,7 +28,7 @@ func ParseVariables(opts *options.TerragruntOptions, directoryPath string) ([]*P // list all tf files tfFiles, err := util.ListTfFiles(directoryPath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } parser := hclparse.NewParser(DefaultParserOptions(opts)...) @@ -92,17 +92,17 @@ func ParseVariables(opts *options.TerragruntOptions, directoryPath string) ([]*P if defaultValue != nil { jsonBytes, err := ctyjson.Marshal(*defaultValue, cty.DynamicPseudoType) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } var ctyJSONOutput ctyJSONValue if err := json.Unmarshal(jsonBytes, &ctyJSONOutput); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } jsonBytes, err = json.Marshal(ctyJSONOutput.Value) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } defaultValueText = string(jsonBytes) diff --git a/configstack/module.go b/configstack/module.go index e0ffe2b8e6..6b998fa46b 100644 --- a/configstack/module.go +++ b/configstack/module.go @@ -13,9 +13,9 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/terraform" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/go-commons/files" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/util" @@ -66,7 +66,7 @@ func (module *TerraformModule) checkForCyclesUsingDepthFirstSearch(visitedPaths } if util.ListContainsElement(*currentTraversalPaths, module.Path) { - return errors.WithStackTrace(DependencyCycleError(append(*currentTraversalPaths, module.Path))) + return errors.New(DependencyCycleError(append(*currentTraversalPaths, module.Path))) } *currentTraversalPaths = append(*currentTraversalPaths, module.Path) @@ -190,7 +190,7 @@ func (module *TerraformModule) getDependenciesForModule(modulesMap TerraformModu TerragruntConfigPaths: terragruntConfigPaths, } - return dependencies, errors.WithStackTrace(err) + return dependencies, errors.New(err) } dependencies = append(dependencies, dependencyModule) @@ -280,7 +280,7 @@ func FindWhereWorkingDirIsIncluded(ctx context.Context, terragruntOptions *optio // adding some styling to modules that are excluded from the execution in *-all commands func (modules TerraformModules) WriteDot(w io.Writer, terragruntOptions *options.TerragruntOptions) error { if _, err := w.Write([]byte("digraph {\n")); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defer func(w io.Writer, p []byte) { _, err := w.Write(p) @@ -304,7 +304,7 @@ func (modules TerraformModules) WriteDot(w io.Writer, terragruntOptions *options _, err := w.Write([]byte(nodeLine)) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } for _, target := range source.Dependencies { @@ -315,7 +315,7 @@ func (modules TerraformModules) WriteDot(w io.Writer, terragruntOptions *options _, err := w.Write([]byte(line)) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } } diff --git a/configstack/running_module.go b/configstack/running_module.go index 730a7bf8a5..aa89b01e41 100644 --- a/configstack/running_module.go +++ b/configstack/running_module.go @@ -8,11 +8,10 @@ import ( "sort" "sync" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/telemetry" "github.com/gruntwork-io/terragrunt/terraform" - "github.com/hashicorp/go-multierror" ) const ( @@ -165,7 +164,7 @@ func (module *RunningModule) moduleFinished(moduleErr error) { if moduleErr == nil { module.Module.TerragruntOptions.Logger.Debugf("Module %s has finished successfully!", module.Module.Path) } else { - module.Module.TerragruntOptions.Logger.Errorf("Module %s has finished with an error: %v", module.Module.Path, moduleErr) + module.Module.TerragruntOptions.Logger.Errorf("Module %s has finished with an error", module.Module.Path) } module.Status = Finished @@ -249,7 +248,7 @@ func (modules RunningModules) crossLinkDependencies(dependencyOrder DependencyOr for _, dependency := range module.Module.Dependencies { runningDependency, hasDependency := modules[dependency.Path] if !hasDependency { - return modules, errors.WithStackTrace(DependencyNotFoundWhileCrossLinkingError{module, dependency}) + return modules, errors.New(DependencyNotFoundWhileCrossLinkingError{module, dependency}) } // TODO: Remove lint suppression @@ -324,13 +323,13 @@ func (modules RunningModules) runModules(ctx context.Context, opts *options.Terr // Collect the errors from the given modules and return a single error object to represent them, or nil if no errors // occurred func (modules RunningModules) collectErrors() error { - var result *multierror.Error + var errs *errors.MultiError for _, module := range modules { if module.Err != nil { - result = multierror.Append(result, module.Err) + errs = errs.Append(module.Err) } } - return result.ErrorOrNil() + return errs.ErrorOrNil() } diff --git a/configstack/stack.go b/configstack/stack.go index 6236cd6a88..0e1817b7ef 100644 --- a/configstack/stack.go +++ b/configstack/stack.go @@ -22,8 +22,8 @@ import ( "github.com/gruntwork-io/terragrunt/telemetry" "github.com/gruntwork-io/terragrunt/terraform" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -126,7 +126,7 @@ func (stack *Stack) LogModuleDeployOrder(logger log.Logger, terraformCommand str func (stack *Stack) JSONModuleDeployOrder(terraformCommand string) (string, error) { runGraph, err := stack.GetModuleRunGraph(terraformCommand) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // Convert the module paths to a string array for JSON marshalling @@ -144,7 +144,7 @@ func (stack *Stack) JSONModuleDeployOrder(terraformCommand string) (string, erro j, err := json.MarshalIndent(jsonGraph, "", " ") if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return string(j), nil @@ -201,7 +201,11 @@ func (stack *Stack) Run(ctx context.Context, terragruntOptions *options.Terragru errorStreams := make([]bytes.Buffer, len(stack.Modules)) for n, module := range stack.Modules { - module.TerragruntOptions.ErrWriter = io.MultiWriter(&errorStreams[n], module.TerragruntOptions.ErrWriter) + if !terragruntOptions.NonInteractive { // redirect output to ErrWriter in case of not NonInteractive mode + module.TerragruntOptions.ErrWriter = io.MultiWriter(&errorStreams[n], module.TerragruntOptions.ErrWriter) + } else { + module.TerragruntOptions.ErrWriter = &errorStreams[n] + } } defer stack.summarizePlanAllErrors(terragruntOptions, errorStreams) } @@ -296,12 +300,12 @@ func (stack *Stack) createStackForTerragruntConfigPaths(ctx context.Context, ter "working_dir": stack.terragruntOptions.WorkingDir, }, func(childCtx context.Context) error { if len(terragruntConfigPaths) == 0 { - return errors.WithStackTrace(ErrNoTerraformModulesFound) + return errors.New(ErrNoTerraformModulesFound) } modules, err := stack.ResolveTerraformModules(ctx, terragruntConfigPaths) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } stack.Modules = modules @@ -309,21 +313,21 @@ func (stack *Stack) createStackForTerragruntConfigPaths(ctx context.Context, ter return nil }) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } err = telemetry.Telemetry(ctx, stack.terragruntOptions, "check_for_cycles", map[string]interface{}{ "working_dir": stack.terragruntOptions.WorkingDir, }, func(childCtx context.Context) error { if err := stack.Modules.CheckForCycles(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil }) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -574,7 +578,11 @@ func (stack *Stack) resolveTerraformModule(ctx context.Context, terragruntConfig includeConfig, ) if err != nil { - return nil, errors.WithStackTrace(ProcessingModuleError{UnderlyingError: err, HowThisModuleWasFound: howThisModuleWasFound, ModulePath: terragruntConfigPath}) + return nil, errors.New(ProcessingModuleError{ + UnderlyingError: err, + HowThisModuleWasFound: howThisModuleWasFound, + ModulePath: terragruntConfigPath, + }) } terragruntSource, err := config.GetTerragruntSourceForModule(stack.terragruntOptions.Source, modulePath, terragruntConfig) @@ -671,7 +679,7 @@ func (stack *Stack) resolveExternalDependenciesForModules(ctx context.Context, m // Simple protection from circular dependencies causing a Stack Overflow due to infinite recursion if recursionLevel > maxLevelsOfRecursion { - return allExternalDependencies, errors.WithStackTrace(InfiniteRecursionError{RecursionLevel: maxLevelsOfRecursion, Modules: modulesToSkip}) + return allExternalDependencies, errors.New(InfiniteRecursionError{RecursionLevel: maxLevelsOfRecursion, Modules: modulesToSkip}) } sortedKeys := modulesMap.getSortedKeys() diff --git a/configstack/stack_test.go b/configstack/stack_test.go index 9a64d063c5..4736daef5d 100644 --- a/configstack/stack_test.go +++ b/configstack/stack_test.go @@ -2,21 +2,22 @@ package configstack_test import ( "context" - goErrors "errors" "os" "path/filepath" "reflect" "strings" "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/codegen" "github.com/gruntwork-io/terragrunt/config" "github.com/gruntwork-io/terragrunt/configstack" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/terraform" "github.com/gruntwork-io/terragrunt/util" "github.com/stretchr/testify/require" + + goerrors "github.com/go-errors/errors" ) func TestFindStackInSubfolders(t *testing.T) { @@ -1098,10 +1099,16 @@ func TestResolveTerraformModulesInvalidPaths(t *testing.T) { require.Error(t, actualErr) var processingModuleError configstack.ProcessingModuleError - ok := goErrors.As(actualErr, &processingModuleError) + ok := errors.As(actualErr, &processingModuleError) require.True(t, ok) - unwrapped := errors.Unwrap(processingModuleError.UnderlyingError) + goError := new(goerrors.Error) + + unwrapped := processingModuleError.UnderlyingError + if errors.As(unwrapped, &goError) { + unwrapped = goError.Err + } + require.True(t, os.IsNotExist(unwrapped), "Expected a file not exists error but got %v", processingModuleError.UnderlyingError) } diff --git a/configstack/test_helpers_test.go b/configstack/test_helpers_test.go index a8773ad2a8..0f7a00bf9f 100644 --- a/configstack/test_helpers_test.go +++ b/configstack/test_helpers_test.go @@ -2,17 +2,16 @@ package configstack_test import ( "context" - goErrors "errors" "sort" "testing" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" "github.com/gruntwork-io/terragrunt/configstack" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" - "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type TerraformModuleByPath configstack.TerraformModules @@ -141,9 +140,9 @@ func assertErrorsEqual(t *testing.T, expected error, actual error, messageAndArg actual = errors.Unwrap(actual) var unrecognizedDependencyError configstack.UnrecognizedDependencyError - if ok := goErrors.As(expected, &unrecognizedDependencyError); ok { + if ok := errors.As(expected, &unrecognizedDependencyError); ok { var actualUnrecognized configstack.UnrecognizedDependencyError - ok = goErrors.As(actual, &actualUnrecognized) + ok = errors.As(actual, &actualUnrecognized) if assert.True(t, ok, messageAndArgs...) { assert.Equal(t, unrecognizedDependencyError, actualUnrecognized, messageAndArgs...) } @@ -206,20 +205,20 @@ func optionsWithMockTerragruntCommand(t *testing.T, terragruntConfigPath string, func assertMultiErrorContains(t *testing.T, actualError error, expectedErrors ...error) { t.Helper() - actualError = errors.Unwrap(actualError) - var multiError *multierror.Error - isMultiError := goErrors.As(actualError, &multiError) - if assert.True(t, isMultiError, "Expected a MutliError, but got: %v", actualError) { - assert.Equal(t, len(expectedErrors), len(multiError.Errors)) - for _, expectedErr := range expectedErrors { - found := false - for _, actualErr := range multiError.Errors { - if goErrors.Is(expectedErr, actualErr) { - found = true - break - } + multiError := new(errors.MultiError) + errors.As(actualError, &multiError) + require.NotNil(t, multiError, "Expected a MutliError, but got: %v", actualError) + + assert.Len(t, multiError.WrappedErrors(), len(expectedErrors)) + for _, expectedErr := range expectedErrors { + found := false + for _, actualErr := range multiError.WrappedErrors() { + if errors.Is(expectedErr, actualErr) { + found = true + + break } - assert.True(t, found, "Couldn't find expected error %v", expectedErr) } + assert.True(t, found, "Couldn't find expected error %v", expectedErr) } } diff --git a/docs/_docs/03_community/contributing.md b/docs/_docs/03_community/contributing.md index c5adf241c0..2b2914ad57 100644 --- a/docs/_docs/03_community/contributing.md +++ b/docs/_docs/03_community/contributing.md @@ -159,13 +159,13 @@ In this project, we try to ensure that: 2. Every error generated by our own code (as opposed to errors from Go built-in functions or errors from 3rd party libraries) has a custom type. This makes error handling more precise, as we can decide to handle different types of errors differently. -To accomplish these two goals, we have created an `errors` package that has several helper methods, such as `errors.WithStackTrace(err error)`, which wraps the given `error` in an Error object that contains a stacktrace. Under the hood, the `errors` package is using the [go-errors](https://github.com/go-errors/errors) library, but this may change in the future, so the rest of the code should not depend on `go-errors` directly. +To accomplish these two goals, we have created an `errors` package that has several helper methods, such as `errors.New(err error)`, which wraps the given `error` in an Error object that contains a stacktrace. Under the hood, the `errors` package is using the [go-errors](https://github.com/go-errors/errors) library, but this may change in the future, so the rest of the code should not depend on `go-errors` directly. Here is how the `errors` package should be used: -1. Any time you want to create your own error, create a custom type for it, and when instantiating that type, wrap it with a call to `errors.WithStackTrace`. That way, any time you call a method defined in the Terragrunt code, you know the error it returns already has a stacktrace and you don’t have to wrap it yourself. +1. Any time you want to create your own error, create a custom type for it, and when instantiating that type, wrap it with a call to `errors.New`. That way, any time you call a method defined in the Terragrunt code, you know the error it returns already has a stacktrace and you don’t have to wrap it yourself. -2. Any time you get back an error object from a function built into Go or a 3rd party library, immediately wrap it with `errors.WithStackTrace`. This gives us a stacktrace as close to the source as possible. +2. Any time you get back an error object from a function built into Go or a 3rd party library, immediately wrap it with `errors.New`. This gives us a stacktrace as close to the source as possible. 3. If you need to get back the underlying error, you can use the `errors.IsError` and `errors.Unwrap` functions. diff --git a/dynamodb/dynamo_lock_table.go b/dynamodb/dynamo_lock_table.go index 9bd648c418..d69437a5de 100644 --- a/dynamodb/dynamo_lock_table.go +++ b/dynamodb/dynamo_lock_table.go @@ -2,7 +2,6 @@ package dynamodb import ( - goErrors "errors" "fmt" "net/http" "time" @@ -12,8 +11,8 @@ import ( "github.com/aws/aws-sdk-go/aws/client" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/awshelper" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -75,10 +74,10 @@ func LockTableExistsAndIsActive(tableName string, client *dynamodb.DynamoDB) (bo output, err := client.DescribeTable(&dynamodb.DescribeTableInput{TableName: aws.String(tableName)}) if err != nil { var awsErr awserr.Error - if ok := goErrors.As(err, &awsErr); ok && awsErr.Code() == "ResourceNotFoundException" { + if ok := errors.As(err, &awsErr); ok && awsErr.Code() == "ResourceNotFoundException" { return false, nil } else { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } } @@ -89,7 +88,7 @@ func LockTableExistsAndIsActive(tableName string, client *dynamodb.DynamoDB) (bo func LockTableCheckSSEncryptionIsOn(tableName string, client *dynamodb.DynamoDB) (bool, error) { output, err := client.DescribeTable(&dynamodb.DescribeTableInput{TableName: aws.String(tableName)}) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } return output.Table.SSEDescription != nil && aws.StringValue(output.Table.SSEDescription.Status) == dynamodb.SSEStatusEnabled, nil @@ -122,7 +121,7 @@ func CreateLockTable(tableName string, tags map[string]string, client *dynamodb. if isTableAlreadyBeingCreatedOrUpdatedError(err) { terragruntOptions.Logger.Debugf("Looks like someone created table %s at the same time. Will wait for it to be in active state.", tableName) } else { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -137,7 +136,7 @@ func CreateLockTable(tableName string, tags map[string]string, client *dynamodb. err = tagTableIfTagsGiven(tags, createTableOutput.TableDescription.TableArn, client, terragruntOptions) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -204,7 +203,7 @@ func (retryer DeleteTableRetryer) ShouldRetry(req *request.Request) bool { // updated by someone else func isTableAlreadyBeingCreatedOrUpdatedError(err error) bool { var awsErr awserr.Error - ok := goErrors.As(err, &awsErr) + ok := errors.As(err, &awsErr) return ok && awsErr.Code() == "ResourceInUseException" } @@ -236,14 +235,14 @@ func WaitForTableToBeActiveWithRandomSleep(tableName string, client *dynamodb.Dy time.Sleep(sleepBetweenRetries) } - return errors.WithStackTrace(TableActiveRetriesExceeded{TableName: tableName, Retries: maxRetries}) + return errors.New(TableActiveRetriesExceeded{TableName: tableName, Retries: maxRetries}) } // UpdateLockTableSetSSEncryptionOnIfNecessary encrypts the TFState Lock table - If Necessary func UpdateLockTableSetSSEncryptionOnIfNecessary(tableName string, client *dynamodb.DynamoDB, terragruntOptions *options.TerragruntOptions) error { tableSSEncrypted, err := LockTableCheckSSEncryptionIsOn(tableName, client) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if tableSSEncrypted { @@ -268,12 +267,12 @@ func UpdateLockTableSetSSEncryptionOnIfNecessary(tableName string, client *dynam if isTableAlreadyBeingCreatedOrUpdatedError(err) { terragruntOptions.Logger.Debugf("Looks like someone is already updating table %s at the same time. Will wait for that update to complete.", tableName) } else { - return errors.WithStackTrace(err) + return errors.New(err) } } if err := waitForEncryptionToBeEnabled(tableName, client, terragruntOptions); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return waitForTableToBeActive(tableName, client, MaxRetriesWaitingForTableToBeActive, SleepBetweenTableStatusChecks, terragruntOptions) @@ -286,7 +285,7 @@ func waitForEncryptionToBeEnabled(tableName string, client *dynamodb.DynamoDB, t for i := 0; i < maxRetries; i++ { tableSSEncrypted, err := LockTableCheckSSEncryptionIsOn(tableName, client) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if tableSSEncrypted { @@ -298,7 +297,7 @@ func waitForEncryptionToBeEnabled(tableName string, client *dynamodb.DynamoDB, t time.Sleep(sleepBetweenRetries) } - return errors.WithStackTrace(TableEncryptedRetriesExceeded{TableName: tableName, Retries: maxRetries}) + return errors.New(TableEncryptedRetriesExceeded{TableName: tableName, Retries: maxRetries}) } // Custom error types diff --git a/dynamodb/dynamo_lock_table_test.go b/dynamodb/dynamo_lock_table_test.go index 4bad437e48..39d6f3fe2a 100644 --- a/dynamodb/dynamo_lock_table_test.go +++ b/dynamodb/dynamo_lock_table_test.go @@ -11,8 +11,8 @@ import ( "github.com/aws/aws-sdk-go/aws" awsDynamodb "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/dynamodb" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/engine/engine.go b/engine/engine.go index 77c493483d..8e8330466e 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -5,7 +5,6 @@ import ( "bytes" "context" "encoding/json" - goErrors "errors" "fmt" "io" "net/http" @@ -25,9 +24,9 @@ import ( "google.golang.org/grpc/credentials/insecure" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt-engine-go/engine" "github.com/gruntwork-io/terragrunt-engine-go/proto" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/go-plugin" @@ -80,7 +79,7 @@ func Run( ) (*util.CmdOutput, error) { engineClients, err := engineClientsFromContext(ctx) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } workingDir := runOptions.TerragruntOptions.WorkingDir @@ -89,12 +88,12 @@ func Run( if !found { // download engine if not available if err = DownloadEngine(ctx, runOptions.TerragruntOptions); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } terragruntEngine, client, err := createEngine(runOptions.TerragruntOptions) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } engineClients.Store(workingDir, &engineInstance{ @@ -106,23 +105,23 @@ func Run( instance, _ = engineClients.Load(workingDir) if err := initialize(ctx, runOptions, terragruntEngine); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } } engInst, ok := instance.(*engineInstance) if !ok { - return nil, errors.WithStackTrace(fmt.Errorf("failed to fetch engine instance %s", workingDir)) + return nil, errors.Errorf("failed to fetch engine instance %s", workingDir) } terragruntEngine := engInst.terragruntEngine - cmdOutput, err := invoke(ctx, runOptions, terragruntEngine) + output, err := invoke(ctx, runOptions, terragruntEngine) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } - return cmdOutput, nil + return output, nil } // WithEngineValues add to context default values for engine. @@ -152,7 +151,7 @@ func DownloadEngine(ctx context.Context, opts *options.TerragruntOptions) error if !strings.Contains(e.Source, "://") { tag, err := lastReleaseVersion(ctx, opts) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } e.Version = tag @@ -161,11 +160,11 @@ func DownloadEngine(ctx context.Context, opts *options.TerragruntOptions) error path, err := engineDir(opts) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := util.EnsureDirectory(path); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } localEngineFile := filepath.Join(path, engineFileName(e)) @@ -173,7 +172,7 @@ func DownloadEngine(ctx context.Context, opts *options.TerragruntOptions) error // lock downloading process for only one instance locks, err := downloadLocksFromContext(ctx) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // locking by file where engine is downloaded // however, it will not help in case of multiple parallel Terragrunt runs @@ -215,7 +214,7 @@ func DownloadEngine(ctx context.Context, opts *options.TerragruntOptions) error } if err := client.Get(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -223,14 +222,14 @@ func DownloadEngine(ctx context.Context, opts *options.TerragruntOptions) error opts.Logger.Infof("Verifying checksum for %s", downloadFile) if err := verifyFile(downloadFile, checksumFile, checksumSigFile); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } else { opts.Logger.Warnf("Skipping verification for %s", downloadFile) } if err := extractArchive(opts, downloadFile, localEngineFile); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger.Infof("Engine available as %s", path) @@ -244,7 +243,7 @@ func lastReleaseVersion(ctx context.Context, opts *options.TerragruntOptions) (s versionCache, err := engineVersionsCacheFromContext(ctx) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } if val, found := versionCache.Get(ctx, url); found { @@ -258,26 +257,26 @@ func lastReleaseVersion(ctx context.Context, opts *options.TerragruntOptions) (s req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } client := &http.Client{} resp, err := client.Do(req) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } defer resp.Body.Close() //nolint:errcheck body, err := io.ReadAll(resp.Body) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } var r release if err := json.Unmarshal(body, &r); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } versionCache.Put(ctx, url, r.Tag) @@ -290,7 +289,7 @@ func extractArchive(opts *options.TerragruntOptions, downloadFile string, engine opts.Logger.Info("Downloaded file is not an archive, no extraction needed") // move file directly if it is not an archive if err := os.Rename(downloadFile, engineFile); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -300,7 +299,7 @@ func extractArchive(opts *options.TerragruntOptions, downloadFile string, engine tempDir, err := os.MkdirTemp(path, "temp-") if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defer func() { @@ -310,12 +309,12 @@ func extractArchive(opts *options.TerragruntOptions, downloadFile string, engine }() // extract archive if err := archiver.Unarchive(downloadFile, tempDir); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // process files files, err := os.ReadDir(tempDir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } opts.Logger.Infof("Engine extracted to %s", path) @@ -324,7 +323,7 @@ func extractArchive(opts *options.TerragruntOptions, downloadFile string, engine // handle case where archive contains a single file, most of the cases singleFile := filepath.Join(tempDir, files[0].Name()) if err := os.Rename(singleFile, engineFile); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -336,7 +335,7 @@ func extractArchive(opts *options.TerragruntOptions, downloadFile string, engine dstPath := filepath.Join(path, file.Name()) if err := os.Rename(srcPath, dstPath); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -354,7 +353,7 @@ func engineDir(terragruntOptions *options.TerragruntOptions) (string, error) { if len(cacheDir) == 0 { homeDir, err := os.UserHomeDir() if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } cacheDir = filepath.Join(homeDir, defaultCacheDir) @@ -417,12 +416,12 @@ func isArchiveByHeader(filePath string) bool { func engineClientsFromContext(ctx context.Context) (*sync.Map, error) { val := ctx.Value(terraformCommandContextKey) if val == nil { - return nil, errors.WithStackTrace(goErrors.New("failed to fetch engine clients from context")) + return nil, errors.New(errors.New("failed to fetch engine clients from context")) } result, ok := val.(*sync.Map) if !ok { - return nil, errors.WithStackTrace(goErrors.New("failed to cast engine clients from context")) + return nil, errors.New(errors.New("failed to cast engine clients from context")) } return result, nil @@ -432,12 +431,12 @@ func engineClientsFromContext(ctx context.Context) (*sync.Map, error) { func downloadLocksFromContext(ctx context.Context) (*util.KeyLocks, error) { val := ctx.Value(locksContextKey) if val == nil { - return nil, errors.WithStackTrace(goErrors.New("failed to fetch engine clients from context")) + return nil, errors.New(errors.New("failed to fetch engine clients from context")) } result, ok := val.(*util.KeyLocks) if !ok { - return nil, errors.WithStackTrace(goErrors.New("failed to cast engine clients from context")) + return nil, errors.New(errors.New("failed to cast engine clients from context")) } return result, nil @@ -446,12 +445,12 @@ func downloadLocksFromContext(ctx context.Context) (*util.KeyLocks, error) { func engineVersionsCacheFromContext(ctx context.Context) (*cache.Cache[string], error) { val := ctx.Value(latestVersionsContextKey) if val == nil { - return nil, errors.WithStackTrace(goErrors.New("failed to fetch engine versions cache from context")) + return nil, errors.New(errors.New("failed to fetch engine versions cache from context")) } result, ok := val.(*cache.Cache[string]) if !ok { - return nil, errors.WithStackTrace(goErrors.New("failed to cast engine versions cache from context")) + return nil, errors.New(errors.New("failed to cast engine versions cache from context")) } return result, nil @@ -466,7 +465,7 @@ func Shutdown(ctx context.Context, opts *options.TerragruntOptions) error { // iterate over all engine instances and shutdown engineClients, err := engineClientsFromContext(ctx) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } engineClients.Range(func(key, value interface{}) bool { @@ -489,7 +488,7 @@ func Shutdown(ctx context.Context, opts *options.TerragruntOptions) error { func createEngine(terragruntOptions *options.TerragruntOptions) (*proto.EngineClient, *plugin.Client, error) { path, err := engineDir(terragruntOptions) if err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } localEnginePath := filepath.Join(path, engineFileName(terragruntOptions.Engine)) @@ -501,7 +500,7 @@ func createEngine(terragruntOptions *options.TerragruntOptions) (*proto.EngineCl if !skipCheck && util.FileExists(localEnginePath) && util.FileExists(localChecksumFile) && util.FileExists(localChecksumSigFile) { if err := verifyFile(localEnginePath, localChecksumFile, localChecksumSigFile); err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } } else { terragruntOptions.Logger.Warnf("Skipping verification for %s", localEnginePath) @@ -543,12 +542,12 @@ func createEngine(terragruntOptions *options.TerragruntOptions) (*proto.EngineCl rpcClient, err := client.Client() if err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } rawClient, err := rpcClient.Dispense("plugin") if err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } terragruntEngine := rawClient.(proto.EngineClient) @@ -562,7 +561,7 @@ func invoke(ctx context.Context, runOptions *ExecutionOptions, client *proto.Eng meta, err := ConvertMetaToProtobuf(runOptions.TerragruntOptions.Engine.Meta) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } response, err := (*client).Run(ctx, &proto.RunRequest{ @@ -574,12 +573,15 @@ func invoke(ctx context.Context, runOptions *ExecutionOptions, client *proto.Eng EnvVars: runOptions.TerragruntOptions.Env, }) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } - var stdoutBuf, stderrBuf bytes.Buffer - stdout := io.MultiWriter(runOptions.CmdStdout, &stdoutBuf) - stderr := io.MultiWriter(runOptions.CmdStderr, &stderrBuf) + var ( + output = util.CmdOutput{} + + stdout = io.MultiWriter(runOptions.CmdStdout, &output.Stdout) + stderr = io.MultiWriter(runOptions.CmdStderr, &output.Stderr) + ) var ( stdoutLineBuf, stderrLineBuf bytes.Buffer @@ -593,43 +595,39 @@ func invoke(ctx context.Context, runOptions *ExecutionOptions, client *proto.Eng } if err := processStream(runResp.GetStdout(), &stdoutLineBuf, stdout); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if err := processStream(runResp.GetStderr(), &stderrLineBuf, stderr); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } resultCode = int(runResp.GetResultCode()) } if err := flushBuffer(&stdoutLineBuf, stdout); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if err := flushBuffer(&stderrLineBuf, stderr); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } terragruntOptions.Logger.Debugf("Engine execution done in %v", terragruntOptions.WorkingDir) if resultCode != 0 { err = util.ProcessExecutionError{ - Err: fmt.Errorf("command failed with exit code %d", resultCode), - Stdout: stdoutBuf.String(), - Stderr: stderrBuf.String(), + Err: errors.Errorf("command failed with exit code %d", resultCode), + Output: output, WorkingDir: terragruntOptions.WorkingDir, + Command: runOptions.Command, + Args: runOptions.Args, } - return nil, errors.WithStackTrace(err) - } - - cmdOutput := util.CmdOutput{ - Stdout: stdoutBuf.String(), - Stderr: stderrBuf.String(), + return nil, errors.New(err) } - return &cmdOutput, nil + return &output, nil } // processStream handles the character buffering and line printing for a given stream @@ -639,7 +637,7 @@ func processStream(data string, lineBuf *bytes.Buffer, output io.Writer) error { if ch == '\n' { if _, err := fmt.Fprint(output, lineBuf.String()); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } lineBuf.Reset() @@ -653,7 +651,7 @@ func processStream(data string, lineBuf *bytes.Buffer, output io.Writer) error { func flushBuffer(lineBuf *bytes.Buffer, output io.Writer) error { if lineBuf.Len() > 0 { if _, err := fmt.Fprint(output, lineBuf.String()); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -666,7 +664,7 @@ func initialize(ctx context.Context, runOptions *ExecutionOptions, client *proto meta, err := ConvertMetaToProtobuf(runOptions.TerragruntOptions.Engine.Meta) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Running init for engine in %s", runOptions.WorkingDir) @@ -677,7 +675,7 @@ func initialize(ctx context.Context, runOptions *ExecutionOptions, client *proto Meta: meta, }) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Reading init output for engine in %s", runOptions.WorkingDir) @@ -705,7 +703,7 @@ func shutdown(ctx context.Context, runOptions *ExecutionOptions, terragruntEngin meta, err := ConvertMetaToProtobuf(runOptions.TerragruntOptions.Engine.Meta) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } request, err := (*terragruntEngine).Shutdown(ctx, &proto.ShutdownRequest{ @@ -715,7 +713,7 @@ func shutdown(ctx context.Context, runOptions *ExecutionOptions, terragruntEngin }) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Reading shutdown output for engine in %s", runOptions.WorkingDir) @@ -759,13 +757,13 @@ func ReadEngineOutput(runOptions *ExecutionOptions, output outputFn) error { if response.Stdout != "" { if _, err := cmdStdout.Write([]byte(response.Stdout)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } if response.Stderr != "" { if _, err := cmdStderr.Write([]byte(response.Stderr)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } } diff --git a/engine/verification.go b/engine/verification.go index 824ccba806..7645d664fc 100644 --- a/engine/verification.go +++ b/engine/verification.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/ProtonMail/go-crypto/openpgp" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" ) @@ -17,30 +17,30 @@ import ( func verifyFile(checkedFile, checksumsFile, signatureFile string) error { checksums, err := os.ReadFile(checksumsFile) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } checksumsSignature, err := os.ReadFile(signatureFile) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // validate first checksum file signature keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(PublicKey)) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } _, err = openpgp.CheckDetachedSignature(keyring, bytes.NewReader(checksums), bytes.NewReader(checksumsSignature), nil) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // verify checksums // calculate checksum of package file packageChecksum, err := util.FileSHA256(checkedFile) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // match expected checksum @@ -51,7 +51,7 @@ func verifyFile(checkedFile, checksumsFile, signatureFile string) error { var expectedSHA256Sum [sha256.Size]byte if _, err := hex.Decode(expectedSHA256Sum[:], expectedChecksum); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if !bytes.Equal(expectedSHA256Sum[:], packageChecksum) { diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 0000000000..481e5eb42c --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,50 @@ +// Package errors contains helper functions for wrapping errors with stack traces, stack output, and panic recovery. +package errors + +import ( + "fmt" + + goerrors "github.com/go-errors/errors" +) + +const ( + newSkip = 2 + errorfSkip = 2 +) + +// New creates a new instance of Error. +// If the given value does not contain an stack trace, it will be created. +func New(val any) error { + if val == nil { + return nil + } + + return newWithSkip(newSkip, val) +} + +// Errorf creates a new error with the given format and values. +// It can be used as a drop-in replacement for fmt.Errorf() to provide descriptive errors in return values. +// If none of the given values contains an stack trace, it will be created. +func Errorf(format string, vals ...any) error { + return errorfWithSkip(errorfSkip, format, vals...) +} + +func newWithSkip(skip int, val any) error { + if err, ok := val.(error); ok && ContainsStackTrace(err) { + return fmt.Errorf("%w", err) + } + + return goerrors.Wrap(val, skip) +} + +func errorfWithSkip(skip int, format string, vals ...any) error { + err := fmt.Errorf(format, vals...) //nolint:err113 + + for _, val := range vals { + if val, ok := val.(error); ok && val != nil && ContainsStackTrace(val) { + return err + } + } + + return goerrors.Wrap(err, skip) +} diff --git a/internal/errors/export.go b/internal/errors/export.go new file mode 100644 index 0000000000..4fe15d58be --- /dev/null +++ b/internal/errors/export.go @@ -0,0 +1,31 @@ +package errors + +import "errors" + +// As finds the first error in err's tree that matches target, and if one is found, sets +// target to that error value and returns true. Otherwise, it returns false. +func As(err error, target any) bool { + return errors.As(err, target) +} + +// Is reports whether any error in err's tree matches target. +func Is(err, target error) bool { + return errors.Is(err, target) +} + +// // New returns an error that formats as the given text. +// func New(text string) error { +// return errors.New(text) +// } + +// Join returns an error that wraps the given errors. +func Join(errs ...error) error { + return errors.Join(errs...) +} + +// Unwrap returns the result of calling the Unwrap method on err, if err's +// type contains an Unwrap method returning error. +// Otherwise, Unwrap returns nil. +func Unwrap(err error) error { + return errors.Unwrap(err) +} diff --git a/internal/errors/multierror.go b/internal/errors/multierror.go new file mode 100644 index 0000000000..b4adcedb90 --- /dev/null +++ b/internal/errors/multierror.go @@ -0,0 +1,104 @@ +package errors + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-multierror" +) + +// MultiError is an error type to track multiple errors. +type MultiError struct { + inner *multierror.Error +} + +// WrappedErrors returns the error slice that this Error is wrapping. +func (errs *MultiError) WrappedErrors() []error { + if errs.inner == nil { + return nil + } + + return errs.inner.WrappedErrors() +} + +func (errs *MultiError) Unwrap() []error { + return errs.WrappedErrors() +} + +// ErrorOrNil returns an error interface if this Error represents +// a list of errors, or returns nil if the list of errors is empty. +func (errs *MultiError) ErrorOrNil() error { + if errs == nil || errs.inner == nil { + return nil + } + + if err := errs.inner.ErrorOrNil(); err != nil { + return errs + } + + return nil +} + +// Append is a helper function that will append more errors +// onto an Multierror in order to create a larger errs-error. +func (errs *MultiError) Append(appendErrs ...error) *MultiError { + if errs == nil { + errs = &MultiError{inner: new(multierror.Error)} + } + + return &MultiError{inner: multierror.Append(errs.inner, appendErrs...)} +} + +// Len implements sort.Interface function for length. +func (errs *MultiError) Len() int { + return len(errs.inner.Errors) +} + +// Swap implements sort.Interface function for swapping elements. +func (errs *MultiError) Swap(i, j int) { + errs.inner.Errors[i], errs.inner.Errors[j] = errs.inner.Errors[j], errs.inner.Errors[i] +} + +// Less implements sort.Interface function for determining order. +func (errs *MultiError) Less(i, j int) bool { + return errs.inner.Errors[i].Error() < errs.inner.Errors[j].Error() +} + +// Error implements the error interface. +func (errs *MultiError) Error() string { + unwrappedErrs := UnwrapMultiErrors(errs) + + strs := make([]string, len(unwrappedErrs)) + + for i := range unwrappedErrs { + strs[i] = addIndent(unwrappedErrs[i].Error()) + } + + errStr := strings.Join(strs, "\n\n") + + if len(strs) == 1 { + return fmt.Sprintf("error occurred:\n\n%s\n", errStr) + } + + return fmt.Sprintf("%d errors occurred:\n\n%s\n", len(strs), errStr) +} + +func addIndent(str string) string { + // for output on Windows OS + str = strings.ReplaceAll(str, "\r\n", "\n") + rawLines := strings.Split(str, "\n") + + var lines []string //nolint:prealloc + + for i, line := range rawLines { + format := " %s" + if i == 0 { + format = "* %s" + } + + line = fmt.Sprintf(format, line) + lines = append(lines, line) + } + + return strings.Join(lines, "\n") +} diff --git a/internal/errors/util.go b/internal/errors/util.go new file mode 100644 index 0000000000..4d32bcb243 --- /dev/null +++ b/internal/errors/util.go @@ -0,0 +1,116 @@ +package errors + +import ( + "context" + "errors" + "fmt" + "strings" + + goerrors "github.com/go-errors/errors" +) + +// ErrorStack returns an stack trace if available. +func ErrorStack(err error) string { + var errStacks []string + + for _, err := range UnwrapMultiErrors(err) { + for { + if err, ok := err.(interface{ ErrorStack() string }); ok { + errStacks = append(errStacks, err.ErrorStack()) + } + + if err = errors.Unwrap(err); err == nil { + break + } + } + } + + return strings.Join(errStacks, "\n") +} + +// ContainsStackTrace returns true if the given error contain the stack trace. +// Useful to avoid creating a nested stack trace. +func ContainsStackTrace(err error) bool { + for _, err := range UnwrapMultiErrors(err) { + for { + if err, ok := err.(interface{ ErrorStack() string }); ok && err != nil { + return true + } + + if err = errors.Unwrap(err); err == nil { + break + } + } + } + + return false +} + +// IsContextCanceled returns `true` if error has occurred by event `context.Canceled` which is not really an error. +func IsContextCanceled(err error) bool { + return errors.Is(err, context.Canceled) +} + +// IsError returns true if actual is the same type of error as expected. This method unwraps the given error objects (if they +// are wrapped in objects with a stacktrace) and then does a simple equality check on them. +func IsError(actual error, expected error) bool { + return goerrors.Is(actual, expected) +} + +// Recover tries to recover from panics, and if it succeeds, calls the given onPanic function with an error that +// explains the cause of the panic. This function should only be called from a defer statement. +func Recover(onPanic func(cause error)) { + if rec := recover(); rec != nil { + err, isError := rec.(error) + if !isError { + err = fmt.Errorf("%v", rec) //nolint:err113 + } + + onPanic(New(err)) + } +} + +// UnwrapMultiErrors unwraps all nested multierrors into error slice. +func UnwrapMultiErrors(err error) []error { + errs := []error{err} + + for index := 0; index < len(errs); index++ { + err := errs[index] + + for { + if err, ok := err.(interface{ Unwrap() []error }); ok { + errs = append(errs[:index], errs[index+1:]...) + index-- + + for _, err := range err.Unwrap() { + errs = append(errs, New(err)) + } + + break + } + + if err = errors.Unwrap(err); err == nil { + break + } + } + } + + return errs +} + +// UnwrapErrors unwraps all nested multierrors, and errors that were wrapped with `fmt.Errorf("%w", err)`. +func UnwrapErrors(err error) []error { + var errs []error + + for _, err := range UnwrapMultiErrors(err) { + for { + errs = append(errs, err) + + if err = errors.Unwrap(err); err == nil { + break + } + } + } + + return errs +} diff --git a/internal/os/exec/cmd.go b/internal/os/exec/cmd.go new file mode 100644 index 0000000000..1523e873c4 --- /dev/null +++ b/internal/os/exec/cmd.go @@ -0,0 +1,133 @@ +// Package exec runs external commands. It wraps exec.Cmd package with support for allocating a pseudo-terminal. +package exec + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "time" + + "github.com/gruntwork-io/terragrunt/internal/os/signal" + "github.com/gruntwork-io/terragrunt/pkg/log" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/gruntwork-io/terragrunt/internal/errors" +) + +// Cmd is a command type. +type Cmd struct { + *exec.Cmd + + filename string + + logger log.Logger + usePTY bool + + forwardSignalDelay time.Duration + interruptSignal os.Signal +} + +// Command returns the `Cmd` struct to execute the named program with +// the given arguments. +func Command(name string, args ...string) *Cmd { + cmd := &Cmd{ + Cmd: exec.Command(name, args...), + logger: log.Default(), + filename: filepath.Base(name), + interruptSignal: signal.InterruptSignal, + } + + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd +} + +// Configure sets options to the `Cmd`. +func (cmd *Cmd) Configure(opts ...Option) { + for _, opt := range opts { + opt(cmd) + } +} + +// Start starts the specified command but does not wait for it to complete. +func (cmd *Cmd) Start() error { + // If we need to allocate a ptty for the command, route through the ptty routine. + // Otherwise, directly call the command. + if cmd.usePTY { + if err := runCommandWithPTY(cmd.logger, cmd.Cmd); err != nil { + return err + } + } else if err := cmd.Cmd.Start(); err != nil { + return errors.New(err) + } + + return nil +} + +// RegisterGracefullyShutdown registers a graceful shutdown for the command in two ways: +// 1. If the context cancel contains a cause with a signal, this means that Terragrunt received the signal from the OS, +// since our executed command may also receive the same signal, we need to give the command time to gracefully shutting down, +// to avoid the command receiving this signal twice. +// Thus we will send the signal to the executed command with a delay or immediately if Terragrunt receives this same signal again. +// 2. If the context does not contain any causes, this means that there was some failure and we need to terminate all executed commands, +// in this situation we are sure that commands did not receive any signal, so we send them an interrupt signal immediately. +func (cmd *Cmd) RegisterGracefullyShutdown(ctx context.Context) func() { + ctxShutdown, cancelShutdown := context.WithCancel(context.Background()) + + go func() { + select { + case <-ctxShutdown.Done(): + case <-ctx.Done(): + if cause := new(signal.ContextCanceledError); errors.As(context.Cause(ctx), &cause) && cause.Signal != nil { + cmd.ForwardSignal(ctxShutdown, cause.Signal) + + return + } + + cmd.SendSignal(cmd.interruptSignal) + } + }() + + return cancelShutdown +} + +// ForwardSignal forwards a given `sig` with a delay if cmd.forwardSignalDelay is greater than 0, +// and if the same signal is received again, it is forwarded immediately. +func (cmd *Cmd) ForwardSignal(ctx context.Context, sig os.Signal) { + ctxDelay, cancelDelay := context.WithCancel(ctx) + defer cancelDelay() + + signal.NotifierWithContext(ctx, func(_ os.Signal) { + cancelDelay() + }, sig) + + if cmd.forwardSignalDelay > 0 { + cmd.logger.Debugf("%s signal will be forwarded to %s with delay %s", + cases.Title(language.English).String(sig.String()), + cmd.filename, + cmd.forwardSignalDelay, + ) + } + + select { + case <-ctx.Done(): + return + case <-time.After(cmd.forwardSignalDelay): + case <-ctxDelay.Done(): + } + + cmd.SendSignal(sig) +} + +// SendSignal sends the given `sig` to the executed command. +func (cmd *Cmd) SendSignal(sig os.Signal) { + cmd.logger.Debugf("%s signal is forwarded to %s", cases.Title(language.English).String(sig.String()), cmd.filename) + + if err := cmd.Process.Signal(sig); err != nil { + cmd.logger.Errorf("Failed to forwarding signal %s to %s: %v", sig, cmd.filename, err) + } +} diff --git a/internal/os/exec/cmd_unix_test.go b/internal/os/exec/cmd_unix_test.go new file mode 100644 index 0000000000..8f956068b3 --- /dev/null +++ b/internal/os/exec/cmd_unix_test.go @@ -0,0 +1,114 @@ +//go:build linux || darwin +// +build linux darwin + +package exec_test + +import ( + "errors" + "os" + "strconv" + "testing" + "time" + + "github.com/gruntwork-io/terragrunt/internal/os/exec" + "github.com/gruntwork-io/terragrunt/util" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + errExplicitError = errors.New("this is an explicit error") +) + +func TestExitCodeUnix(t *testing.T) { + t.Parallel() + + for index := 0; index <= 255; index++ { + cmd := exec.Command("testdata/test_exit_code.sh", strconv.Itoa(index)) + err := cmd.Run() + + if index == 0 { + require.NoError(t, err) + } else { + require.Error(t, err) + } + retCode, err := util.GetExitCode(err) + require.NoError(t, err) + assert.Equal(t, index, retCode) + } + + // assert a non exec.ExitError returns an error + retCode, retErr := util.GetExitCode(errExplicitError) + require.Error(t, retErr, "An error was expected") + assert.Equal(t, errExplicitError, retErr) + assert.Equal(t, 0, retCode) +} + +func TestNewSignalsForwarderWaitUnix(t *testing.T) { + t.Parallel() + + expectedWait := 5 + + cmd := exec.Command("testdata/test_sigint_wait.sh", strconv.Itoa(expectedWait)) + + runChannel := make(chan error) + + go func() { + runChannel <- cmd.Run() + }() + + time.Sleep(time.Second) + start := time.Now() + cmd.Process.Signal(os.Interrupt) + + err := <-runChannel + require.Error(t, err) + + retCode, err := util.GetExitCode(err) + require.NoError(t, err) + + assert.Equal(t, expectedWait, retCode) + assert.WithinDuration(t, time.Now(), start.Add(time.Duration(expectedWait)*time.Second), time.Second, + "Expected to wait 5 (+/-1) seconds after SIGINT") +} + +// There isn't a proper way to catch interrupts in Windows batch scripts, so this test exists only for Unix. +func TestNewSignalsForwarderMultipleUnix(t *testing.T) { + t.Parallel() + + expectedInterrupts := 10 + + cmd := exec.Command("testdata/test_sigint_multiple.sh", strconv.Itoa(expectedInterrupts)) + + runChannel := make(chan error) + + go func() { + runChannel <- cmd.Run() + }() + + time.Sleep(time.Second) + + interruptAndWaitForProcess := func() (int, error) { + var interrupts int + var err error + for { + time.Sleep(500 * time.Millisecond) + select { + case err = <-runChannel: + return interrupts, err + default: + cmd.Process.Signal(os.Interrupt) + interrupts++ + } + } + } + + interrupts, err := interruptAndWaitForProcess() + require.Error(t, err) + + retCode, err := util.GetExitCode(err) + require.NoError(t, err) + assert.LessOrEqual(t, retCode, interrupts, "Subprocess received wrong number of signals") + assert.Equal(t, expectedInterrupts, retCode, "Subprocess didn't receive multiple signals") +} diff --git a/internal/os/exec/cmd_windows_test.go b/internal/os/exec/cmd_windows_test.go new file mode 100644 index 0000000000..6f1fbf8e22 --- /dev/null +++ b/internal/os/exec/cmd_windows_test.go @@ -0,0 +1,96 @@ +//go:build windows +// +build windows + +package exec_test + +import ( + "bytes" + "context" + "errors" + "os" + "strconv" + "testing" + "time" + + "github.com/sirupsen/logrus" + + "github.com/gruntwork-io/terragrunt/internal/os/exec" + "github.com/gruntwork-io/terragrunt/options" + "github.com/stretchr/testify/assert" +) + +func TestWindowsConsolePrepare(t *testing.T) { + t.Parallel() + + testOptions := options.NewTerragruntOptions() + + stdout := bytes.Buffer{} + + var testLogger = logrus.New() + testLogger.Out = &stdout + testLogger.Level = logrus.DebugLevel + + testOptions.Logger = testLogger.WithContext(context.Background()) + + PrepareConsole(testOptions) + + assert.Contains(t, stdout.String(), "level=debug msg=\"failed to get console mode: The handle is invalid.") +} + +func TestExitCodeWindows(t *testing.T) { + t.Parallel() + + for i := 0; i <= 255; i++ { + cmd := exec.Command(`testdata\test_exit_code.bat`, strconv.Itoa(i)) + err := cmd.Run() + + if i == 0 { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + retCode, err := GetExitCode(err) + assert.NoError(t, err) + assert.Equal(t, i, retCode) + } + + // assert a non exec.ExitError returns an error + err := errors.New("This is an explicit error") + retCode, retErr := GetExitCode(err) + assert.Error(t, retErr, "An error was expected") + assert.Equal(t, err, retErr) + assert.Equal(t, 0, retCode) +} + +func TestNewSignalsForwarderWaitWindows(t *testing.T) { + t.Parallel() + + expectedWait := 5 + + terragruntOptions, err := options.NewTerragruntOptionsForTest("") + assert.Nil(t, err, "Unexpected error creating NewTerragruntOptionsForTest: %v", err) + + cmd := exec.Command(`testdata\test_sigint_wait.bat`, strconv.Itoa(expectedWait)) + + runChannel := make(chan error) + + go func() { + runChannel <- cmd.Run() + }() + + time.Sleep(time.Second) + // start := time.Now() + // Note: sending interrupt on Windows is not supported by Windows and not implemented in Go + cmd.Process.Signal(os.Kill) + err = <-runChannel + + assert.Error(t, err) + + // Since we can't send an interrupt on Windows, our test script won't handle it gracefully and exit after the expected wait time, + // so this part of the test process cannot be done on Windows + // retCode, err := GetExitCode(err) + // assert.NoError(t, err) + // assert.Equal(t, retCode, expectedWait) + // assert.WithinDuration(t, start.Add(time.Duration(expectedWait)*time.Second), time.Now(), time.Second, + // "Expected to wait 5 (+/-1) seconds after SIGINT") +} diff --git a/internal/os/exec/opts.go b/internal/os/exec/opts.go new file mode 100644 index 0000000000..c0d6f19985 --- /dev/null +++ b/internal/os/exec/opts.go @@ -0,0 +1,41 @@ +package exec + +import ( + "time" + + "github.com/gruntwork-io/go-commons/collections" + "github.com/gruntwork-io/terragrunt/pkg/log" +) + +const envVarsListFormat = "%s=%s" + +// Option is type for passing options to the Cmd. +type Option func(*Cmd) + +// WithLogger sets Logger to the Cmd. +func WithLogger(logger log.Logger) Option { + return func(cmd *Cmd) { + cmd.logger = logger + } +} + +// WithUsePTY enables a pty for the Cmd. +func WithUsePTY(state bool) Option { + return func(cmd *Cmd) { + cmd.usePTY = state + } +} + +// WithEnv sets envs to the Cmd. +func WithEnv(env map[string]string) Option { + return func(cmd *Cmd) { + cmd.Env = collections.KeyValueStringSliceWithFormat(env, envVarsListFormat) + } +} + +// WithForwardSignalDelay sets forwarding signal delay to the Cmd. +func WithForwardSignalDelay(delay time.Duration) Option { + return func(cmd *Cmd) { + cmd.forwardSignalDelay = delay + } +} diff --git a/shell/ptty_unix.go b/internal/os/exec/ptty_unix.go similarity index 69% rename from shell/ptty_unix.go rename to internal/os/exec/ptty_unix.go index c7f1ffee28..b216c2c580 100644 --- a/shell/ptty_unix.go +++ b/internal/os/exec/ptty_unix.go @@ -1,7 +1,7 @@ //go:build !windows // +build !windows -package shell +package exec import ( "io" @@ -13,31 +13,31 @@ import ( "golang.org/x/term" "github.com/creack/pty" - "github.com/gruntwork-io/go-commons/errors" - "github.com/gruntwork-io/terragrunt/options" + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/log" ) -// runCommandWithPTTY will allocate a pseudo-tty to run the subcommand in. This is only necessary when running +// runCommandWithPTY will allocate a pseudo-tty to run the subcommand in. This is only necessary when running // interactive commands, so that terminal features like readline work through the subcommand when stdin, stdout, and // stderr is being shared. // NOTE: This is based on the quickstart example from https://github.com/creack/pty -func runCommandWithPTTY(terragruntOptions *options.TerragruntOptions, cmd *exec.Cmd, cmdStdout io.Writer, _ io.Writer) (err error) { +func runCommandWithPTY(logger log.Logger, cmd *exec.Cmd) (err error) { // NOTE: in order to ensure we can return errors that occur in cleanup, we use a variable binding for the return // value so that it can be updated. pseudoTerminal, startErr := pty.Start(cmd) defer func() { if closeErr := pseudoTerminal.Close(); closeErr != nil { - terragruntOptions.Logger.Errorf("Error closing pty: %s", closeErr) + logger.Errorf("Error closing pty: %s", closeErr) // Only overwrite the previous error if there was no error since this error has lower priority than any // errors in the main routine if err == nil { - err = errors.WithStackTrace(closeErr) + err = errors.New(closeErr) } } }() if startErr != nil { - return errors.WithStackTrace(startErr) + return errors.New(startErr) } // Every time the current terminal size changes, we need to make sure the PTY also updates the size. @@ -48,7 +48,7 @@ func runCommandWithPTTY(terragruntOptions *options.TerragruntOptions, cmd *exec. for range ch { if inheritSizeErr := pty.InheritSize(os.Stdin, pseudoTerminal); inheritSizeErr != nil { // We don't propagate this error upstream because it does not affect normal operation of the command - terragruntOptions.Logger.Errorf("error resizing pty: %s", inheritSizeErr) + logger.Errorf("error resizing pty: %s", inheritSizeErr) } } }() @@ -57,16 +57,16 @@ func runCommandWithPTTY(terragruntOptions *options.TerragruntOptions, cmd *exec. // Set stdin in raw mode so that we preserve readline properties oldState, setRawErr := term.MakeRaw(int(os.Stdin.Fd())) if setRawErr != nil { - return errors.WithStackTrace(setRawErr) + return errors.New(setRawErr) } defer func() { if restoreErr := term.Restore(int(os.Stdin.Fd()), oldState); restoreErr != nil { - terragruntOptions.Logger.Errorf("Error restoring terminal state: %s", restoreErr) + logger.Errorf("Error restoring terminal state: %s", restoreErr) // Only overwrite the previous error if there was no error since this error has lower priority than any // errors in the main routine if err == nil { - err = errors.WithStackTrace(restoreErr) + err = errors.New(restoreErr) } } }() @@ -78,27 +78,27 @@ func runCommandWithPTTY(terragruntOptions *options.TerragruntOptions, cmd *exec. // We don't propagate this error upstream because it does not affect normal operation of the command. A repeat // of the same stdin in this case should resolve the issue. if copyStdinErr != nil { - terragruntOptions.Logger.Errorf("Error forwarding stdin: %s", copyStdinErr) + logger.Errorf("Error forwarding stdin: %s", copyStdinErr) } // signal that stdin copy is done stdinDone <- copyStdinErr }() // ... and the pty to stdout. - _, copyStdoutErr := io.Copy(cmdStdout, pseudoTerminal) + _, copyStdoutErr := io.Copy(cmd.Stdout, pseudoTerminal) if copyStdoutErr != nil { - return errors.WithStackTrace(copyStdoutErr) + return errors.New(copyStdoutErr) } // Wait for stdin copy to complete before returning - if copyStdinErr := <-stdinDone; copyStdinErr != nil && !errors.IsError(copyStdinErr, io.EOF) { - terragruntOptions.Logger.Errorf("Error forwarding stdin: %s", copyStdinErr) + logger.Errorf("Error forwarding stdin: %s", copyStdinErr) } return nil } -func PrepareConsole(terragruntOptions *options.TerragruntOptions) { +// PrepareConsole is run at the start of the application to set up the console. +func PrepareConsole(_ log.Logger) { // No operation function to match windows execution } diff --git a/internal/os/exec/ptty_windows.go b/internal/os/exec/ptty_windows.go new file mode 100644 index 0000000000..44263d62b6 --- /dev/null +++ b/internal/os/exec/ptty_windows.go @@ -0,0 +1,55 @@ +//go:build windows +// +build windows + +package exec + +import ( + "os" + "os/exec" + "strings" + + "golang.org/x/sys/windows" + + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/pkg/log" +) + +const InvalidHandleErrorMessage = "The handle is invalid" + +// PrepareConsole enables support for escape sequences +// https://stackoverflow.com/questions/56460651/golang-fmt-print-033c-and-fmt-print-x1bc-are-not-clearing-screenansi-es +// https://github.com/containerd/console/blob/f652dc3/console_windows.go#L46 +func PrepareConsole(logger log.Logger) { + enableVirtualTerminalProcessing(logger, os.Stdin) + enableVirtualTerminalProcessing(logger, os.Stderr) + enableVirtualTerminalProcessing(logger, os.Stdout) +} + +func enableVirtualTerminalProcessing(logger log.Logger, file *os.File) { + var mode uint32 + handle := windows.Handle(file.Fd()) + if err := windows.GetConsoleMode(handle, &mode); err != nil { + if strings.Contains(err.Error(), InvalidHandleErrorMessage) { + logger.Debugf("failed to get console mode: %v\n", err) + } else { + logger.Errorf("failed to get console mode: %v\n", err) + } + return + } + + if err := windows.SetConsoleMode(handle, mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { + logger.Errorf("failed to set console mode: %v\n", err) + if secondError := windows.SetConsoleMode(handle, mode); secondError != nil { + logger.Errorf("failed to set console mode: %v\n", secondError) + return + } + } +} + +// For windows, there is no concept of a pseudoTTY so we run as if there is no pseudoTTY. +func runCommandWithPTY(logger log.Logger, cmd *exec.Cmd) error { + if err := cmd.Start(); err != nil { + return errors.New(err) + } + return nil +} diff --git a/testdata/infinite_loop.bat b/internal/os/exec/testdata/infinite_loop.bat similarity index 100% rename from testdata/infinite_loop.bat rename to internal/os/exec/testdata/infinite_loop.bat diff --git a/testdata/test_exit_code.bat b/internal/os/exec/testdata/test_exit_code.bat similarity index 100% rename from testdata/test_exit_code.bat rename to internal/os/exec/testdata/test_exit_code.bat diff --git a/testdata/test_exit_code.sh b/internal/os/exec/testdata/test_exit_code.sh similarity index 100% rename from testdata/test_exit_code.sh rename to internal/os/exec/testdata/test_exit_code.sh diff --git a/testdata/test_sigint_multiple.sh b/internal/os/exec/testdata/test_sigint_multiple.sh similarity index 100% rename from testdata/test_sigint_multiple.sh rename to internal/os/exec/testdata/test_sigint_multiple.sh diff --git a/testdata/test_sigint_wait.sh b/internal/os/exec/testdata/test_sigint_wait.sh similarity index 100% rename from testdata/test_sigint_wait.sh rename to internal/os/exec/testdata/test_sigint_wait.sh diff --git a/internal/os/signal/context_canceled.go b/internal/os/signal/context_canceled.go new file mode 100644 index 0000000000..d3786523c6 --- /dev/null +++ b/internal/os/signal/context_canceled.go @@ -0,0 +1,26 @@ +package signal + +import ( + "context" + "os" +) + +// ContextCanceledError contains a signal to pass through when the context is cancelled. +type ContextCanceledError struct { + Signal os.Signal +} + +// NewContextCanceledError returns a new `ContextCanceledError` instance. +func NewContextCanceledError(sig os.Signal) *ContextCanceledError { + return &ContextCanceledError{Signal: sig} +} + +// Error implements the `Error` method. +func (ContextCanceledError) Error() string { + return context.Canceled.Error() +} + +// Unwrap implements the `Unwrap` method. +func (ContextCanceledError) Unwrap() error { + return context.Canceled +} diff --git a/internal/os/signal/signal.go b/internal/os/signal/signal.go new file mode 100644 index 0000000000..d40caf18fe --- /dev/null +++ b/internal/os/signal/signal.go @@ -0,0 +1,43 @@ +// Package signal provides convenience methods for intercepting and handling OS signals. +package signal + +import ( + "context" + "os" + "os/signal" +) + +// NotifyFunc is a callback function for Notifier. +type NotifyFunc func(sig os.Signal) + +// Notifier registers a handler for receiving signals from the OS. +// When signal is receiving, it calls the given callback func `notifyFn`. +func Notifier(notifyFn NotifyFunc, trackSignals ...os.Signal) { + NotifierWithContext(context.Background(), notifyFn, trackSignals...) +} + +// NotifierWithContext does the same as `Notifier`, but if the given `ctx` becomes `Done`, the notification is stopped. +func NotifierWithContext(ctx context.Context, notifyFn NotifyFunc, trackSignals ...os.Signal) { + sigCh := make(chan os.Signal, 1) + + signal.Notify(sigCh, trackSignals...) + + go func() { + for { + select { + case <-ctx.Done(): + signal.Stop(sigCh) + close(sigCh) + + return + + case sig, ok := <-sigCh: + if !ok { + return + } + + notifyFn(sig) + } + } + }() +} diff --git a/internal/os/signal/signal_unix.go b/internal/os/signal/signal_unix.go new file mode 100644 index 0000000000..ce5bd12780 --- /dev/null +++ b/internal/os/signal/signal_unix.go @@ -0,0 +1,15 @@ +//go:build !windows +// +build !windows + +package signal + +import ( + "os" + "syscall" +) + +// InterruptSignal is an interrupt signal. +var InterruptSignal = syscall.SIGINT //nolint:gochecknoglobals + +// InterruptSignals contains a list of signals that are treated as interrupts. +var InterruptSignals = []os.Signal{syscall.SIGTERM, syscall.SIGINT} //nolint:gochecknoglobals diff --git a/internal/os/signal/signal_windows.go b/internal/os/signal/signal_windows.go new file mode 100644 index 0000000000..9b78acaaa9 --- /dev/null +++ b/internal/os/signal/signal_windows.go @@ -0,0 +1,14 @@ +//go:build windows +// +build windows + +package signal + +import ( + "os" +) + +// InterruptSignal is an interrupt signal. +var InterruptSignal os.Signal = nil + +// InterruptSignals contains a list of signals that are treated as interrupts. +var InterruptSignals []os.Signal = []os.Signal{} diff --git a/internal/view/human_render.go b/internal/view/human_render.go index f4b67297dd..1b2ded121d 100644 --- a/internal/view/human_render.go +++ b/internal/view/human_render.go @@ -8,7 +8,7 @@ import ( "sort" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/internal/view/diagnostic" "github.com/hashicorp/hcl/v2" "github.com/mitchellh/colorstring" @@ -112,7 +112,7 @@ func (render *HumanRender) Diagnostic(diag *diagnostic.Diagnostic) (string, erro // this is where we put the text of a native Go error it may not always // be pure text that lends itself well to word-wrapping. if _, err := fmt.Fprintf(&buf, render.colorize.Color("[bold]%s[reset]\n\n"), diag.Summary); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } sourceSnippets, err := render.SourceSnippets(diag) @@ -132,12 +132,12 @@ func (render *HumanRender) Diagnostic(diag *diagnostic.Diagnostic) (string, erro } if _, err := fmt.Fprintf(&buf, "%s\n", line); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } } else { if _, err := fmt.Fprintf(&buf, "%s\n", diag.Detail); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } } @@ -194,7 +194,7 @@ func (render *HumanRender) SourceSnippets(diag *diagnostic.Diagnostic) (string, } if _, err := fmt.Fprintf(buf, " on %s line %d%s:\n", diag.Range.Filename, diag.Range.Start.Line, contextStr); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } // Split the snippet and render the highlighted section with underlines @@ -237,7 +237,7 @@ func (render *HumanRender) SourceSnippets(diag *diagnostic.Diagnostic) (string, snippet.StartLine+i, line, ); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } @@ -257,7 +257,7 @@ func (render *HumanRender) SourceSnippets(diag *diagnostic.Diagnostic) (string, if callInfo := snippet.FunctionCall; callInfo != nil && callInfo.Signature != nil { if _, err := fmt.Fprintf(buf, render.colorize.Color(" [dark_gray]│[reset] while calling [bold]%s[reset]("), callInfo.CalledAs); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } for i, param := range callInfo.Signature.Params { @@ -282,7 +282,7 @@ func (render *HumanRender) SourceSnippets(diag *diagnostic.Diagnostic) (string, for _, value := range values { if _, err := fmt.Fprintf(buf, render.colorize.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), value.Traversal, value.Statement); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } } diff --git a/internal/view/json_render.go b/internal/view/json_render.go index 3cb3ee4b50..cc348917bb 100644 --- a/internal/view/json_render.go +++ b/internal/view/json_render.go @@ -3,7 +3,7 @@ package view import ( "encoding/json" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/internal/view/diagnostic" ) @@ -24,7 +24,7 @@ func (render *JSONRender) ShowConfigPath(filenames []string) (string, error) { func (render *JSONRender) toJSON(val any) (string, error) { jsonBytes, err := json.Marshal(val) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } if len(jsonBytes) == 0 { diff --git a/internal/view/writer.go b/internal/view/writer.go index 1ae64fa7b2..0fab5a8d83 100644 --- a/internal/view/writer.go +++ b/internal/view/writer.go @@ -4,7 +4,7 @@ import ( "fmt" "io" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/internal/view/diagnostic" "github.com/gruntwork-io/terragrunt/util" ) @@ -58,7 +58,7 @@ func (writer *Writer) ShowConfigPath(diags diagnostic.Diagnostics) error { func (writer *Writer) output(output string) error { if _, err := fmt.Fprint(writer, output); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/main.go b/main.go index e7b127199b..d1e5c1d3b9 100644 --- a/main.go +++ b/main.go @@ -1,18 +1,15 @@ package main import ( - goErrors "errors" "os" - "strings" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli" "github.com/gruntwork-io/terragrunt/cli/commands" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/util" - "github.com/hashicorp/go-multierror" ) // The main entrypoint for Terragrunt @@ -34,8 +31,8 @@ func checkForErrorsAndExit(logger log.Logger) func(error) { if err == nil { os.Exit(0) } else { - logger.Debugf(printErrorWithStackTrace(err)) - logger.Errorf(err.Error()) + logger.Error(err.Error()) + logger.Debug(errors.ErrorStack(err)) // exit with the underlying error code exitCode, exitCodeErr := util.GetExitCode(err) @@ -54,21 +51,6 @@ func checkForErrorsAndExit(logger log.Logger) func(error) { } } -func printErrorWithStackTrace(err error) string { - var multierror *multierror.Error - - if goErrors.As(err, &multierror) { - var errsStr []string - for _, err := range multierror.Errors { - errsStr = append(errsStr, errors.PrintErrorWithStackTrace(err)) - } - - return strings.Join(errsStr, "\n") - } - - return errors.PrintErrorWithStackTrace(err) -} - func parseAndSetLogEnvs(opts *options.TerragruntOptions) { if levelStr := os.Getenv(commands.TerragruntLogLevelEnvName); levelStr != "" { level, err := log.ParseLevel(levelStr) diff --git a/options/options.go b/options/options.go index d5e29ea1e4..7fe5146deb 100644 --- a/options/options.go +++ b/options/options.go @@ -3,7 +3,6 @@ package options import ( "context" - goErrors "errors" "fmt" "io" "math" @@ -11,7 +10,7 @@ import ( "path/filepath" "time" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" "github.com/gruntwork-io/terragrunt/util" @@ -449,7 +448,7 @@ func NewTerragruntOptionsWithWriters(stdout, stderr io.Writer) *TerragruntOption TerraformLogsToJSON: false, JSONDisableDependentModules: false, RunTerragrunt: func(ctx context.Context, opts *TerragruntOptions) error { - return errors.WithStackTrace(ErrRunTerragruntCommandNotSet) + return errors.New(ErrRunTerragruntCommandNotSet) }, ProviderCacheRegistryNames: defaultProviderCacheRegistryNames, OutputFolder: "", @@ -463,7 +462,7 @@ func NewTerragruntOptionsWithConfigPath(terragruntConfigPath string) (*Terragrun workingDir, downloadDir, err := DefaultWorkingAndDownloadDirs(terragruntConfigPath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } opts.WorkingDir = workingDir @@ -480,7 +479,7 @@ func DefaultWorkingAndDownloadDirs(terragruntConfigPath string) (string, string, downloadDir, err := filepath.Abs(filepath.Join(workingDir, util.TerragruntCacheDir)) if err != nil { - return "", "", errors.WithStackTrace(err) + return "", "", errors.New(err) } return filepath.ToSlash(workingDir), filepath.ToSlash(downloadDir), nil @@ -495,7 +494,7 @@ func GetDefaultIAMAssumeRoleSessionName() string { func NewTerragruntOptionsForTest(terragruntConfigPath string, options ...TerragruntOptionsFunc) (*TerragruntOptions, error) { opts, err := NewTerragruntOptionsWithConfigPath(terragruntConfigPath) if err != nil { - log.WithOptions(log.WithLevel(log.DebugLevel)).Errorf("%v\n", errors.WithStackTrace(err)) + log.WithOptions(log.WithLevel(log.DebugLevel)).Errorf("%v\n", errors.New(err)) return nil, err } @@ -701,7 +700,7 @@ func (opts *TerragruntOptions) DataDir() string { return util.JoinPath(opts.WorkingDir, tfDataDir) } -// identifyDefaultWrappedExecutable - return default path used for wrapped executable +// identifyDefaultWrappedExecutable returns default path used for wrapped executable. func identifyDefaultWrappedExecutable() string { if util.IsCommandExecutable(TofuDefaultPath, "-version") { return TofuDefaultPath @@ -710,7 +709,7 @@ func identifyDefaultWrappedExecutable() string { return TerraformDefaultPath } -// EngineOptions Options for the Terragrunt engine +// EngineOptions Options for the Terragrunt engine. type EngineOptions struct { Source string Version string @@ -718,6 +717,5 @@ type EngineOptions struct { Meta map[string]interface{} } -// Custom error types - -var ErrRunTerragruntCommandNotSet = goErrors.New("the RunTerragrunt option has not been set on this TerragruntOptions object") +// ErrRunTerragruntCommandNotSet is a custom error type indicating that the command is not set. +var ErrRunTerragruntCommandNotSet = errors.New("the RunTerragrunt option has not been set on this TerragruntOptions object") diff --git a/pkg/cli/app.go b/pkg/cli/app.go index d52efa8035..88174f5bb8 100644 --- a/pkg/cli/app.go +++ b/pkg/cli/app.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) @@ -45,6 +45,11 @@ type App struct { // OsExiter is the function used when the app exits. If not set defaults to os.Exit. OsExiter func(code int) + // ExitErrHandler processes any error encountered while running an App before + // it is returned to the caller. If no function is provided, HandleExitCoder + // is used as the default behavior. + ExitErrHandler ExitErrHandlerFunc + // Autocomplete enables or disables subcommand auto-completion support. // This is enabled by default when NewApp is called. Otherwise, this // must enabled explicitly. @@ -109,7 +114,6 @@ func (app *App) RunContext(ctx context.Context, arguments []string) (err error) app.SkipFlagParsing = true app.Authors = []*cli.Author{{Name: app.Author}} - app.App.Action = func(parentCtx *cli.Context) error { cmd := app.newRootCommand() @@ -118,7 +122,7 @@ func (app *App) RunContext(ctx context.Context, arguments []string) (err error) if app.Autocomplete { if err := app.setupAutocomplete(args); err != nil { - return app.handleExitCoder(err) + return app.handleExitCoder(ctx, err) } if compLine := os.Getenv(envCompleteLine); compLine != "" { @@ -220,6 +224,14 @@ func (app *App) setupAutocomplete(arguments []string) error { return nil } -func (app *App) handleExitCoder(err error) error { - return handleExitCoder(err, app.OsExiter) +func (app *App) handleExitCoder(ctx *Context, err error) error { + if err == nil || err.Error() == "" { + return nil + } + + if app.ExitErrHandler != nil { + return app.ExitErrHandler(ctx, err) + } + + return handleExitCoder(ctx, err, app.OsExiter) } diff --git a/pkg/cli/autocomplete.go b/pkg/cli/autocomplete.go index c71c117c28..6dda5551d9 100644 --- a/pkg/cli/autocomplete.go +++ b/pkg/cli/autocomplete.go @@ -1,14 +1,13 @@ package cli import ( - goErrors "errors" "fmt" "io" "os" "strings" "unicode/utf8" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/posener/complete/cmd/install" ) @@ -41,7 +40,7 @@ type autocompleteInstaller struct{} func (i *autocompleteInstaller) Install(cmd string) error { if err := install.Install(cmd); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -49,7 +48,7 @@ func (i *autocompleteInstaller) Install(cmd string) error { func (i *autocompleteInstaller) Uninstall(cmd string) error { if err := install.Uninstall(cmd); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -95,7 +94,7 @@ func printCommandSuggestions(arg string, commands []*Command, writer io.Writer) } if len(errs) > 0 { - return goErrors.Join(errs...) + return errors.Join(errs...) } return nil @@ -129,7 +128,7 @@ func printFlagSuggestions(arg string, flags []Flag, writer io.Writer) error { } if len(errs) > 0 { - return goErrors.Join(errs...) + return errors.Join(errs...) } return nil diff --git a/pkg/cli/bool_flag.go b/pkg/cli/bool_flag.go index 1b3ffc69b3..b6b8c99088 100644 --- a/pkg/cli/bool_flag.go +++ b/pkg/cli/bool_flag.go @@ -3,7 +3,7 @@ package cli import ( libflag "flag" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) diff --git a/pkg/cli/command.go b/pkg/cli/command.go index 9311c0d3bc..17349be0df 100644 --- a/pkg/cli/command.go +++ b/pkg/cli/command.go @@ -5,7 +5,7 @@ import ( "io" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) @@ -118,19 +118,19 @@ func (cmd *Command) Run(ctx *Context, args Args) (err error) { } if err := cmd.Flags.RunActions(ctx); err != nil { - return ctx.App.handleExitCoder(err) + return ctx.App.handleExitCoder(ctx, err) } defer func() { if cmd.After != nil && err == nil { err = cmd.After(ctx) - err = ctx.App.handleExitCoder(err) + err = ctx.App.handleExitCoder(ctx, err) } }() if cmd.Before != nil { if err := cmd.Before(ctx); err != nil { - return ctx.App.handleExitCoder(err) + return ctx.App.handleExitCoder(ctx, err) } } @@ -145,7 +145,7 @@ func (cmd *Command) Run(ctx *Context, args Args) (err error) { if cmd.Action != nil { if err = cmd.Action(ctx); err != nil { - return ctx.App.handleExitCoder(err) + return ctx.App.handleExitCoder(ctx, err) } } @@ -213,7 +213,7 @@ func (cmd *Command) flagSetParse(flagSet *libflag.FlagSet, args []string) ([]str errStr := err.Error() if cmd.DisallowUndefinedFlags || !strings.HasPrefix(errStr, errFlagUndefined) { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } undefArg = strings.Trim(strings.TrimPrefix(errStr, errFlagUndefined), " -") diff --git a/pkg/cli/errors.go b/pkg/cli/errors.go index aad91324aa..13499cccfe 100644 --- a/pkg/cli/errors.go +++ b/pkg/cli/errors.go @@ -70,7 +70,7 @@ func NewExitError(message interface{}, exitCode int) cli.ExitCoder { // code found, or exit code 1 if no ExitCoder is found. // // This function is the default error-handling behavior for an App. -func handleExitCoder(err error, osExiter func(code int)) error { +func handleExitCoder(_ *Context, err error, osExiter func(code int)) error { if err == nil { return nil } diff --git a/pkg/cli/funcs.go b/pkg/cli/funcs.go index d12191e5cc..c7128569f1 100644 --- a/pkg/cli/funcs.go +++ b/pkg/cli/funcs.go @@ -8,3 +8,7 @@ type ActionFunc func(ctx *Context) error // SplitterFunc is used to parse flags containing multiple values. type SplitterFunc func(s, sep string) []string + +// ExitErrHandlerFunc is executed if provided in order to handle exitError values +// returned by Actions and Before/After functions. +type ExitErrHandlerFunc func(ctx *Context, err error) error diff --git a/pkg/cli/generic_flag.go b/pkg/cli/generic_flag.go index 6628c9c826..e98b3e1f17 100644 --- a/pkg/cli/generic_flag.go +++ b/pkg/cli/generic_flag.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) @@ -210,7 +210,7 @@ func (val *genericType[T]) Set(str string) error { case *bool: v, err := strconv.ParseBool(str) if err != nil { - return errors.WithStackTrace(InvalidValueError{underlyingError: err, msg: `must be one of: "0", "1", "f", "t", "false", "true"`}) + return errors.New(InvalidValueError{underlyingError: err, msg: `must be one of: "0", "1", "f", "t", "false", "true"`}) } *dest = v @@ -218,7 +218,7 @@ func (val *genericType[T]) Set(str string) error { case *int: v, err := strconv.ParseInt(str, 0, strconv.IntSize) if err != nil { - return errors.WithStackTrace(InvalidValueError{underlyingError: err, msg: "must be 32-bit integer"}) + return errors.New(InvalidValueError{underlyingError: err, msg: "must be 32-bit integer"}) } *dest = int(v) @@ -226,7 +226,7 @@ func (val *genericType[T]) Set(str string) error { case *uint: v, err := strconv.ParseUint(str, 10, 64) if err != nil { - return errors.WithStackTrace(InvalidValueError{underlyingError: err, msg: "must be 32-bit unsigned integer"}) + return errors.New(InvalidValueError{underlyingError: err, msg: "must be 32-bit unsigned integer"}) } *dest = uint(v) @@ -234,7 +234,7 @@ func (val *genericType[T]) Set(str string) error { case *int64: v, err := strconv.ParseInt(str, 0, 64) if err != nil { - return errors.WithStackTrace(InvalidValueError{underlyingError: err, msg: "must be 64-bit integer"}) + return errors.New(InvalidValueError{underlyingError: err, msg: "must be 64-bit integer"}) } *dest = v diff --git a/pkg/cli/help.go b/pkg/cli/help.go index a6eaecc9f5..33851ee386 100644 --- a/pkg/cli/help.go +++ b/pkg/cli/help.go @@ -1,7 +1,7 @@ package cli import ( - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) diff --git a/pkg/cli/map_flag.go b/pkg/cli/map_flag.go index dd28504921..8c6d96f6e0 100644 --- a/pkg/cli/map_flag.go +++ b/pkg/cli/map_flag.go @@ -5,7 +5,7 @@ import ( "strings" "github.com/gruntwork-io/go-commons/collections" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) @@ -207,7 +207,7 @@ func (flag *mapValue[K, V]) Set(str string) error { parts := flag.splitter(str, flag.valSep) if len(parts) != flatPatsCount { - return errors.WithStackTrace(NewInvalidKeyValueError(flag.valSep, str)) + return errors.New(NewInvalidKeyValueError(flag.valSep, str)) } key := flag.keyType.Clone(new(K)) diff --git a/pkg/cli/slice_flag.go b/pkg/cli/slice_flag.go index 5f25ea1ec6..ac00d2f975 100644 --- a/pkg/cli/slice_flag.go +++ b/pkg/cli/slice_flag.go @@ -4,7 +4,7 @@ import ( libflag "flag" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/urfave/cli/v2" ) diff --git a/pkg/log/format/formatter.go b/pkg/log/format/formatter.go index f80331f7ac..60765eb0a5 100644 --- a/pkg/log/format/formatter.go +++ b/pkg/log/format/formatter.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/sirupsen/logrus" "golang.org/x/exp/maps" @@ -90,7 +90,7 @@ func (formatter *Formatter) Format(entry *logrus.Entry) ([]byte, error) { } if err := buf.WriteByte('\n'); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return buf.Bytes(), nil @@ -181,7 +181,7 @@ func (formatter *Formatter) printFormatted(buf *bytes.Buffer, entry *logrus.Entr } if _, err := fmt.Fprintf(buf, "%s%s%s%s%s", timestamp, level, prefix, tfBinary, entry.Message); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } keys := formatter.keys(entry.Data, PrefixKeyName, TFBinaryKeyName) @@ -202,7 +202,7 @@ func (formatter *Formatter) appendKeyValue(buf *bytes.Buffer, key string, value } if _, err := fmt.Fprintf(buf, keyFmt, key); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := formatter.appendValue(buf, value); err != nil { @@ -222,7 +222,7 @@ func (formatter *Formatter) appendValue(buf *bytes.Buffer, value interface{}) er str = value.Error() default: if _, err := fmt.Fprint(buf, value); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -234,7 +234,7 @@ func (formatter *Formatter) appendValue(buf *bytes.Buffer, value interface{}) er } if _, err := fmt.Fprintf(buf, valueFmt, value); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/pkg/log/hooks/relative_path.go b/pkg/log/hooks/relative_path.go index 23366b4e83..00985a2a78 100644 --- a/pkg/log/hooks/relative_path.go +++ b/pkg/log/hooks/relative_path.go @@ -8,7 +8,7 @@ import ( "regexp" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" "github.com/sirupsen/logrus" @@ -47,7 +47,7 @@ func NewRelativePathHook(baseDir string) (*RelativePathHook, error) { relPath, err := filepath.Rel(baseDir, absPath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } reversIndex-- diff --git a/pkg/log/level.go b/pkg/log/level.go index 99f2ca6868..68d741f73e 100644 --- a/pkg/log/level.go +++ b/pkg/log/level.go @@ -3,7 +3,7 @@ package log import ( "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/sirupsen/logrus" ) diff --git a/remote/remote_state.go b/remote/remote_state.go index 2e034e883c..b501b3463f 100644 --- a/remote/remote_state.go +++ b/remote/remote_state.go @@ -3,15 +3,14 @@ package remote import ( "context" - goErrors "errors" "fmt" "reflect" "sync" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands" "github.com/gruntwork-io/terragrunt/codegen" "github.com/gruntwork-io/terragrunt/internal/cache" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" ) @@ -82,7 +81,7 @@ func (state *RemoteState) FillDefaults() { // Validate that the remote state is configured correctly func (state *RemoteState) Validate() error { if state.Backend == "" { - return errors.WithStackTrace(ErrRemoteBackendMissing) + return errors.New(ErrRemoteBackendMissing) } return nil @@ -226,7 +225,7 @@ func (state RemoteState) ToTerraformInitArgs() []string { // GenerateTerraformCode generates the terraform code for configuring remote state backend. func (state *RemoteState) GenerateTerraformCode(terragruntOptions *options.TerragruntOptions) error { if state.Generate == nil { - return errors.WithStackTrace(ErrGenerateCalledWithNoGenerateAttr) + return errors.New(ErrGenerateCalledWithNoGenerateAttr) } // Make sure to strip out terragrunt specific configurations from the config. @@ -261,8 +260,8 @@ func (state *RemoteState) GenerateTerraformCode(terragruntOptions *options.Terra // Custom errors var ( - ErrRemoteBackendMissing = goErrors.New("the remote_state.backend field cannot be empty") - ErrGenerateCalledWithNoGenerateAttr = goErrors.New("generate code routine called when no generate attribute is configured") + ErrRemoteBackendMissing = errors.New("the remote_state.backend field cannot be empty") + ErrGenerateCalledWithNoGenerateAttr = errors.New("generate code routine called when no generate attribute is configured") ) type BucketCreationNotAllowed string diff --git a/remote/remote_state_gcs.go b/remote/remote_state_gcs.go index 4deda7275b..ebf5fa819b 100644 --- a/remote/remote_state_gcs.go +++ b/remote/remote_state_gcs.go @@ -3,7 +3,6 @@ package remote import ( "context" "encoding/json" - goErrors "errors" "fmt" "os" "reflect" @@ -13,7 +12,7 @@ import ( "google.golang.org/api/impersonate" "cloud.google.com/go/storage" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/shell" @@ -249,7 +248,7 @@ func (initializer GCSInitializer) GetTerraformInitArgs(config map[string]interfa func parseGCSConfig(config map[string]interface{}) (*RemoteStateConfigGCS, error) { var gcsConfig RemoteStateConfigGCS if err := mapstructure.Decode(config, &gcsConfig); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return &gcsConfig, nil @@ -263,11 +262,11 @@ func parseExtendedGCSConfig(config map[string]interface{}) (*ExtendedRemoteState ) if err := mapstructure.Decode(config, &gcsConfig); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if err := mapstructure.Decode(config, &extendedConfig); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } extendedConfig.remoteStateConfigGCS = gcsConfig @@ -280,7 +279,7 @@ func validateGCSConfig(extendedConfig *ExtendedRemoteStateConfigGCS) error { var config = extendedConfig.remoteStateConfigGCS if config.Bucket == "" { - return errors.WithStackTrace(MissingRequiredGCSRemoteStateConfig("bucket")) + return errors.New(MissingRequiredGCSRemoteStateConfig("bucket")) } return nil @@ -295,12 +294,12 @@ func createGCSBucketIfNecessary(ctx context.Context, gcsClient *storage.Client, // A project must be specified in order for terragrunt to automatically create a storage bucket. if config.Project == "" { - return errors.WithStackTrace(MissingRequiredGCSRemoteStateConfig("project")) + return errors.New(MissingRequiredGCSRemoteStateConfig("project")) } // A location must be specified in order for terragrunt to automatically create a storage bucket. if config.Location == "" { - return errors.WithStackTrace(MissingRequiredGCSRemoteStateConfig("location")) + return errors.New(MissingRequiredGCSRemoteStateConfig("location")) } if terragruntOptions.FailIfBucketCreationRequired { @@ -336,7 +335,7 @@ func checkIfGCSVersioningEnabled(gcsClient *storage.Client, config *RemoteStateC attrs, err := bucket.Attrs(ctx) if err != nil { // ErrBucketNotExist - return errors.WithStackTrace(err) + return errors.New(err) } if !attrs.VersioningEnabled { @@ -385,7 +384,7 @@ func AddLabelsToGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteState _, err := bucket.Update(ctx, bucketAttrs) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -424,9 +423,11 @@ func CreateGCSBucket(gcsClient *storage.Client, config *ExtendedRemoteStateConfi bucketAttrs.BucketPolicyOnly = storage.BucketPolicyOnly{Enabled: true} } - err := bucket.Create(ctx, projectID, bucketAttrs) + if err := bucket.Create(ctx, projectID, bucketAttrs); err != nil { + return errors.Errorf("error creating GCS bucket %s: %w", config.remoteStateConfigGCS.Bucket, err) + } - return errors.WithStackTraceAndPrefix(err, "Error creating GCS bucket %s", config.remoteStateConfigGCS.Bucket) + return nil } // WaitUntilGCSBucketExists waits for the GCS bucket specified in the given config to be created. @@ -446,7 +447,7 @@ func WaitUntilGCSBucketExists(gcsClient *storage.Client, config *RemoteStateConf } } - return errors.WithStackTrace(MaxRetriesWaitingForS3BucketExceeded(config.Bucket)) + return errors.New(MaxRetriesWaitingForS3BucketExceeded(config.Bucket)) } // DoesGCSBucketExist returns true if the GCS bucket specified in the given config exists and the current user has the @@ -467,7 +468,7 @@ func DoesGCSBucketExist(gcsClient *storage.Client, config *RemoteStateConfigGCS) } it := bucket.Objects(ctx, nil) - if _, err := it.Next(); goErrors.Is(err, storage.ErrBucketNotExist) { + if _, err := it.Next(); errors.Is(err, storage.ErrBucketNotExist) { return false } diff --git a/remote/remote_state_s3.go b/remote/remote_state_s3.go index 621ccca590..aa2df72315 100644 --- a/remote/remote_state_s3.go +++ b/remote/remote_state_s3.go @@ -2,7 +2,6 @@ package remote import ( "context" - goErrors "errors" "fmt" "reflect" "strconv" @@ -11,9 +10,9 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/s3" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/awshelper" "github.com/gruntwork-io/terragrunt/dynamodb" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/shell" @@ -359,11 +358,11 @@ func (s3Initializer S3Initializer) buildInitializerCacheKey( func (s3Initializer S3Initializer) Initialize(ctx context.Context, remoteState *RemoteState, terragruntOptions *options.TerragruntOptions) error { s3ConfigExtended, err := ParseExtendedS3Config(remoteState.Config) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := ValidateS3Config(s3ConfigExtended); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } var s3Config = s3ConfigExtended.RemoteStateConfigS3 @@ -390,31 +389,31 @@ func (s3Initializer S3Initializer) Initialize(ctx context.Context, remoteState * s3Client, err := CreateS3Client(s3ConfigExtended.GetAwsSessionConfig(), terragruntOptions) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := createS3BucketIfNecessary(ctx, s3Client, s3ConfigExtended, terragruntOptions); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if !terragruntOptions.DisableBucketUpdate && !s3ConfigExtended.DisableBucketUpdate { if err := updateS3BucketIfNecessary(ctx, s3Client, s3ConfigExtended, terragruntOptions); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } if !s3ConfigExtended.SkipBucketVersioning { if _, err := checkIfVersioningEnabled(s3Client, &s3Config, terragruntOptions); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } if err := createLockTableIfNecessary(s3ConfigExtended, s3ConfigExtended.DynamotableTags, terragruntOptions); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := UpdateLockTableSetSSEncryptionOnIfNecessary(&s3Config, s3ConfigExtended, terragruntOptions); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } initializedRemoteStateCache.Put(ctx, cacheKey, true) @@ -469,11 +468,11 @@ func ParseExtendedS3Config(config map[string]interface{}) (*ExtendedRemoteStateC ) if err := mapstructure.Decode(config, &s3Config); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if err := mapstructure.Decode(config, &extendedConfig); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } _, targetPrefixExists := config["accesslogging_target_prefix"] @@ -491,15 +490,15 @@ func ValidateS3Config(extendedConfig *ExtendedRemoteStateConfigS3) error { var config = extendedConfig.RemoteStateConfigS3 if config.Region == "" { - return errors.WithStackTrace(MissingRequiredS3RemoteStateConfig("region")) + return errors.New(MissingRequiredS3RemoteStateConfig("region")) } if config.Bucket == "" { - return errors.WithStackTrace(MissingRequiredS3RemoteStateConfig("bucket")) + return errors.New(MissingRequiredS3RemoteStateConfig("bucket")) } if config.Key == "" { - return errors.WithStackTrace(MissingRequiredS3RemoteStateConfig("key")) + return errors.New(MissingRequiredS3RemoteStateConfig("key")) } return nil @@ -555,7 +554,7 @@ func updateS3BucketIfNecessary(ctx context.Context, s3Client *s3.S3, config *Ext return BucketCreationNotAllowed(config.RemoteStateConfigS3.Bucket) } - return errors.WithStackTrace(fmt.Errorf("remote state S3 bucket %s does not exist or you don't have permissions to access it", config.RemoteStateConfigS3.Bucket)) + return errors.New(fmt.Errorf("remote state S3 bucket %s does not exist or you don't have permissions to access it", config.RemoteStateConfigS3.Bucket)) } needsUpdate, bucketUpdatesRequired, err := checkIfS3BucketNeedsUpdate(s3Client, config, terragruntOptions) @@ -657,32 +656,37 @@ func configureAccessLogBucket(ctx context.Context, terragruntOptions *options.Te if err := CreateLogsS3BucketIfNecessary(ctx, s3Client, aws.String(config.AccessLoggingBucketName), terragruntOptions); err != nil { terragruntOptions.Logger.Errorf("Could not create logs bucket %s for AWS S3 bucket %s\n%s", config.AccessLoggingBucketName, config.RemoteStateConfigS3.Bucket, err.Error()) - return errors.WithStackTrace(err) + + return errors.New(err) } if !config.SkipAccessLoggingBucketPublicAccessBlocking { if err := EnablePublicAccessBlockingForS3Bucket(s3Client, config.AccessLoggingBucketName, terragruntOptions); err != nil { terragruntOptions.Logger.Errorf("Could not enable public access blocking on %s\n%s", config.AccessLoggingBucketName, err.Error()) - return errors.WithStackTrace(err) + + return errors.New(err) } } if err := EnableAccessLoggingForS3BucketWide(s3Client, config, terragruntOptions); err != nil { terragruntOptions.Logger.Errorf("Could not enable access logging on %s\n%s", config.RemoteStateConfigS3.Bucket, err.Error()) - return errors.WithStackTrace(err) + + return errors.New(err) } if !config.SkipAccessLoggingBucketSSEncryption { if err := EnableSSEForS3BucketWide(s3Client, config.AccessLoggingBucketName, s3.ServerSideEncryptionAes256, config, terragruntOptions); err != nil { terragruntOptions.Logger.Errorf("Could not enable encryption on %s\n%s", config.AccessLoggingBucketName, err.Error()) - return errors.WithStackTrace(err) + + return errors.New(err) } } if !config.SkipAccessLoggingBucketEnforcedTLS { if err := EnableEnforcedTLSAccesstoS3Bucket(s3Client, config.AccessLoggingBucketName, config, terragruntOptions); err != nil { terragruntOptions.Logger.Errorf("Could not enable TLS access on %s\n%s", config.AccessLoggingBucketName, err.Error()) - return errors.WithStackTrace(err) + + return errors.New(err) } } @@ -802,7 +806,7 @@ func checkIfVersioningEnabled(s3Client *s3.S3, config *RemoteStateConfigS3, terr out, err := s3Client.GetBucketVersioning(&s3.GetBucketVersioningInput{Bucket: aws.String(config.Bucket)}) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } // NOTE: There must be a bug in the AWS SDK since out == nil when versioning is not enabled. In the future, @@ -933,7 +937,7 @@ func TagS3BucketAccessLogging(s3Client *s3.S3, config *ExtendedRemoteStateConfig _, err := s3Client.PutBucketTagging(&putBucketTaggingInput) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Tagged S3 bucket with %s", config.AccessLoggingBucketTags) @@ -961,7 +965,7 @@ func TagS3Bucket(s3Client *s3.S3, config *ExtendedRemoteStateConfigS3, terragrun _, err := s3Client.PutBucketTagging(&putBucketTaggingInput) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Tagged S3 bucket with %s", config.S3BucketTags) @@ -1000,7 +1004,7 @@ func WaitUntilS3BucketExists(s3Client *s3.S3, config *RemoteStateConfigS3, terra } } - return errors.WithStackTrace(MaxRetriesWaitingForS3BucketExceeded(config.Bucket)) + return errors.New(MaxRetriesWaitingForS3BucketExceeded(config.Bucket)) } // CreateS3Bucket creates the S3 bucket specified in the given config. @@ -1009,7 +1013,7 @@ func CreateS3Bucket(s3Client *s3.S3, bucket *string, terragruntOptions *options. // https://github.com/aws/aws-sdk-go/blob/v1.44.245/service/s3/api.go#L41760 _, err := s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: bucket, ObjectOwnership: aws.String("ObjectWriter")}) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } terragruntOptions.Logger.Debugf("Created S3 bucket %s", aws.StringValue(bucket)) @@ -1021,7 +1025,7 @@ func CreateS3Bucket(s3Client *s3.S3, bucket *string, terragruntOptions *options. // or is in progress. This usually happens when running many tests in parallel or xxx-all commands. func isBucketAlreadyOwnedByYouError(err error) bool { var awsErr awserr.Error - ok := goErrors.As(err, &awsErr) + ok := errors.As(err, &awsErr) return ok && (awsErr.Code() == "BucketAlreadyOwnedByYou" || awsErr.Code() == "OperationAborted") } @@ -1030,7 +1034,7 @@ func isBucketAlreadyOwnedByYouError(err error) bool { func isBucketCreationErrorRetriable(err error) bool { var awsErr awserr.Error - ok := goErrors.As(err, &awsErr) + ok := errors.As(err, &awsErr) if !ok { return true } @@ -1045,12 +1049,12 @@ func EnableRootAccesstoS3Bucket(s3Client *s3.S3, config *ExtendedRemoteStateConf accountID, err := awshelper.GetAWSAccountID(config.GetAwsSessionConfig(), terragruntOptions) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error getting AWS account ID %s for bucket %s", accountID, bucket) + return errors.Errorf("error getting AWS account ID %s for bucket %s: %w", accountID, bucket, err) } partition, err := awshelper.GetAWSPartition(config.GetAwsSessionConfig(), terragruntOptions) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error getting AWS partition %s for bucket %s", partition, bucket) + return errors.Errorf("error getting AWS partition %s for bucket %s: %w", partition, bucket, err) } var policyInBucket awshelper.Policy @@ -1069,7 +1073,7 @@ func EnableRootAccesstoS3Bucket(s3Client *s3.S3, config *ExtendedRemoteStateConf policyInBucket, err = awshelper.UnmarshalPolicy(*policyOutput.Policy) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error unmarshalling policy for bucket %s", bucket) + return errors.Errorf("error unmarshalling policy for bucket %s: %w", bucket, err) } } @@ -1105,7 +1109,7 @@ func EnableRootAccesstoS3Bucket(s3Client *s3.S3, config *ExtendedRemoteStateConf policy, err := awshelper.MarshalPolicy(rootS3Policy) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error marshalling policy for bucket %s", bucket) + return errors.Errorf("error marshalling policy for bucket %s: %w", bucket, err) } _, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{ @@ -1113,7 +1117,7 @@ func EnableRootAccesstoS3Bucket(s3Client *s3.S3, config *ExtendedRemoteStateConf Policy: aws.String(string(policy)), }) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error putting policy for bucket %s", bucket) + return errors.Errorf("error putting policy for bucket %s: %w", bucket, err) } terragruntOptions.Logger.Debugf("Enabled root access to bucket %s", bucket) @@ -1131,7 +1135,7 @@ func checkIfBucketRootAccess(s3Client *s3.S3, config *RemoteStateConfigS3, terra if err != nil { // NoSuchBucketPolicy error is considered as no policy. var awsErr awserr.Error - if ok := goErrors.As(err, &awsErr); ok { + if ok := errors.As(err, &awsErr); ok { if awsErr.Code() == "NoSuchBucketPolicy" { return false, nil } @@ -1139,7 +1143,7 @@ func checkIfBucketRootAccess(s3Client *s3.S3, config *RemoteStateConfigS3, terra terragruntOptions.Logger.Debugf("Could not get policy for bucket %s", config.Bucket) - return false, errors.WithStackTraceAndPrefix(err, "Error checking if bucket %s is have root access", config.Bucket) + return false, errors.Errorf("error checking if bucket %s is have root access: %w", config.Bucket, err) } // If the bucket has no policy, it is not enforced @@ -1149,7 +1153,7 @@ func checkIfBucketRootAccess(s3Client *s3.S3, config *RemoteStateConfigS3, terra policyInBucket, err := awshelper.UnmarshalPolicy(*policyOutput.Policy) if err != nil { - return false, errors.WithStackTraceAndPrefix(err, "Error unmarshalling policy for bucket %s", config.Bucket) + return false, errors.Errorf("error unmarshalling policy for bucket %s: %w", config.Bucket, err) } for _, statement := range policyInBucket.Statement { @@ -1170,7 +1174,7 @@ func EnableEnforcedTLSAccesstoS3Bucket(s3Client *s3.S3, bucket string, config *E partition, err := awshelper.GetAWSPartition(config.GetAwsSessionConfig(), terragruntOptions) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } var policyInBucket awshelper.Policy @@ -1188,7 +1192,7 @@ func EnableEnforcedTLSAccesstoS3Bucket(s3Client *s3.S3, bucket string, config *E policyInBucket, err = awshelper.UnmarshalPolicy(*policyOutput.Policy) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error unmarshalling policy for bucket %s", bucket) + return errors.Errorf("error unmarshalling policy for bucket %s: %w", bucket, err) } } @@ -1225,7 +1229,7 @@ func EnableEnforcedTLSAccesstoS3Bucket(s3Client *s3.S3, bucket string, config *E policy, err := awshelper.MarshalPolicy(tlsS3Policy) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error marshalling policy for bucket %s", bucket) + return errors.Errorf("error marshalling policy for bucket %s: %w", bucket, err) } _, err = s3Client.PutBucketPolicy(&s3.PutBucketPolicyInput{ @@ -1233,7 +1237,7 @@ func EnableEnforcedTLSAccesstoS3Bucket(s3Client *s3.S3, bucket string, config *E Policy: aws.String(string(policy)), }) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error putting policy for bucket %s", bucket) + return errors.Errorf("error putting policy for bucket %s: %w", bucket, err) } terragruntOptions.Logger.Debugf("Enabled enforced TLS access for bucket %s", bucket) @@ -1252,7 +1256,7 @@ func checkIfBucketEnforcedTLS(s3Client *s3.S3, config *RemoteStateConfigS3, terr // S3 API error codes: // http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html var awsErr awserr.Error - if ok := goErrors.As(err, &awsErr); ok { + if ok := errors.As(err, &awsErr); ok { // Enforced TLS policy if is not found bucket policy if awsErr.Code() == "NoSuchBucketPolicy" { terragruntOptions.Logger.Debugf("Could not get policy for bucket %s", config.Bucket) @@ -1260,7 +1264,7 @@ func checkIfBucketEnforcedTLS(s3Client *s3.S3, config *RemoteStateConfigS3, terr } } - return false, errors.WithStackTraceAndPrefix(err, "Error checking if bucket %s is enforced with TLS", config.Bucket) + return false, errors.Errorf("error checking if bucket %s is enforced with TLS: %w", config.Bucket, err) } if policyOutput.Policy == nil { @@ -1269,7 +1273,7 @@ func checkIfBucketEnforcedTLS(s3Client *s3.S3, config *RemoteStateConfigS3, terr policyInBucket, err := awshelper.UnmarshalPolicy(*policyOutput.Policy) if err != nil { - return false, errors.WithStackTraceAndPrefix(err, "Error unmarshalling policy for bucket %s", config.Bucket) + return false, errors.Errorf("error unmarshalling policy for bucket %s: %w", config.Bucket, err) } for _, statement := range policyInBucket.Statement { @@ -1294,7 +1298,7 @@ func EnableVersioningForS3Bucket(s3Client *s3.S3, config *RemoteStateConfigS3, t _, err := s3Client.PutBucketVersioning(&input) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error enabling versioning on S3 bucket %s", config.Bucket) + return errors.Errorf("error enabling versioning on S3 bucket %s: %w", config.Bucket, err) } terragruntOptions.Logger.Debugf("Enabled versioning on S3 bucket %s", config.Bucket) @@ -1308,12 +1312,12 @@ func EnableSSEForS3BucketWide(s3Client *s3.S3, bucketName string, algorithm stri accountID, err := awshelper.GetAWSAccountID(config.GetAwsSessionConfig(), terragruntOptions) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } partition, err := awshelper.GetAWSPartition(config.GetAwsSessionConfig(), terragruntOptions) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defEnc := &s3.ServerSideEncryptionByDefault{ @@ -1333,7 +1337,7 @@ func EnableSSEForS3BucketWide(s3Client *s3.S3, bucketName string, algorithm stri _, err = s3Client.PutBucketEncryption(input) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error enabling bucket-wide SSE on AWS S3 bucket %s", bucketName) + return errors.Errorf("error enabling bucket-wide SSE on AWS S3 bucket %s: %w", bucketName, err) } terragruntOptions.Logger.Debugf("Enabled bucket-wide SSE on AWS S3 bucket %s", bucketName) @@ -1363,7 +1367,8 @@ func checkIfSSEForS3MatchesConfig( output, err := s3Client.GetBucketEncryption(input) if err != nil { terragruntOptions.Logger.Debugf("Error checking if SSE is enabled for AWS S3 bucket %s: %s", config.RemoteStateConfigS3.Bucket, err.Error()) - return false, errors.WithStackTraceAndPrefix(err, "Error checking if SSE is enabled for AWS S3 bucket %s", config.RemoteStateConfigS3.Bucket) + + return false, errors.Errorf("error checking if SSE is enabled for AWS S3 bucket %s: %w", config.RemoteStateConfigS3.Bucket, err) } if output.ServerSideEncryptionConfiguration == nil { @@ -1391,7 +1396,7 @@ func EnableAccessLoggingForS3BucketWide(s3Client *s3.S3, config *ExtendedRemoteS if !config.SkipAccessLoggingBucketACL { if err := configureBucketAccessLoggingACL(s3Client, aws.String(logsBucket), terragruntOptions); err != nil { - return errors.WithStackTraceAndPrefix(err, "Error configuring bucket access logging ACL on S3 bucket %s", config.RemoteStateConfigS3.Bucket) + return errors.Errorf("error configuring bucket access logging ACL on S3 bucket %s: %w", config.RemoteStateConfigS3.Bucket, err) } } @@ -1399,7 +1404,7 @@ func EnableAccessLoggingForS3BucketWide(s3Client *s3.S3, config *ExtendedRemoteS terragruntOptions.Logger.Debugf("Putting bucket logging on S3 bucket %s with TargetBucket %s and TargetPrefix %s\n%s", bucket, logsBucket, logsBucketPrefix, loggingInput) if _, err := s3Client.PutBucketLogging(&loggingInput); err != nil { - return errors.WithStackTraceAndPrefix(err, "Error enabling bucket-wide Access Logging on AWS S3 bucket %s", config.RemoteStateConfigS3.Bucket) + return errors.Errorf("error enabling bucket-wide Access Logging on AWS S3 bucket %s: %w", config.RemoteStateConfigS3.Bucket, err) } terragruntOptions.Logger.Debugf("Enabled bucket-wide Access Logging on AWS S3 bucket %s", bucket) @@ -1415,7 +1420,7 @@ func checkS3AccessLoggingConfiguration(s3Client *s3.S3, config *ExtendedRemoteSt output, err := s3Client.GetBucketLogging(input) if err != nil { terragruntOptions.Logger.Debugf("Error checking if Access Logging is enabled for AWS S3 bucket %s: %s", config.RemoteStateConfigS3.Bucket, err.Error()) - return false, errors.WithStackTraceAndPrefix(err, "Error checking if Access Logging is enabled for AWS S3 bucket %s", config.RemoteStateConfigS3.Bucket) + return false, errors.Errorf("error checking if Access Logging is enabled for AWS S3 bucket %s: %w", config.RemoteStateConfigS3.Bucket, err) } if output.LoggingEnabled == nil { @@ -1450,7 +1455,7 @@ func EnablePublicAccessBlockingForS3Bucket(s3Client *s3.S3, bucketName string, t ) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error blocking all public access to S3 bucket %s", bucketName) + return errors.Errorf("error blocking all public access to S3 bucket %s: %w", bucketName, err) } terragruntOptions.Logger.Debugf("Blocked all public access to S3 bucket %s", bucketName) @@ -1466,7 +1471,7 @@ func checkIfS3PublicAccessBlockingEnabled(s3Client *s3.S3, config *RemoteStateCo }) if err != nil { var awsErr awserr.Error - if ok := goErrors.As(err, &awsErr); ok { + if ok := errors.As(err, &awsErr); ok { // Enforced block public access if is not found bucket policy if awsErr.Code() == "NoSuchPublicAccessBlockConfiguration" { terragruntOptions.Logger.Debugf("Could not get public access block for bucket %s", config.Bucket) @@ -1474,7 +1479,7 @@ func checkIfS3PublicAccessBlockingEnabled(s3Client *s3.S3, config *RemoteStateCo } } - return false, errors.WithStackTraceAndPrefix(err, "Error checking if S3 bucket %s is configured to block public access", config.Bucket) + return false, errors.Errorf("error checking if S3 bucket %s is configured to block public access: %w", config.Bucket, err) } return ValidatePublicAccessBlock(output) @@ -1521,7 +1526,7 @@ func configureBucketAccessLoggingACL(s3Client *s3.S3, bucket *string, terragrunt } if _, err := s3Client.PutBucketAcl(&aclInput); err != nil { - return errors.WithStackTraceAndPrefix(err, "Error granting WRITE and READ_ACP permissions to S3 Log Delivery (%s) for bucket %s", s3LogDeliveryGranteeURI, *bucket) + return errors.Errorf("error granting WRITE and READ_ACP permissions to S3 Log Delivery (%s) for bucket %s: %w", s3LogDeliveryGranteeURI, *bucket, err) } return waitUntilBucketHasAccessLoggingACL(s3Client, bucket, terragruntOptions) @@ -1535,7 +1540,7 @@ func waitUntilBucketHasAccessLoggingACL(s3Client *s3.S3, bucket *string, terragr for i := 0; i < maxRetries; i++ { out, err := s3Client.GetBucketAcl(&s3.GetBucketAclInput{Bucket: bucket}) if err != nil { - return errors.WithStackTraceAndPrefix(err, "Error getting ACL for bucket %s", *bucket) + return errors.Errorf("error getting ACL for bucket %s: %w", *bucket, err) } hasReadAcp := false @@ -1562,7 +1567,7 @@ func waitUntilBucketHasAccessLoggingACL(s3Client *s3.S3, bucket *string, terragr time.Sleep(s3TimeBetweenRetries) } - return errors.WithStackTrace(MaxRetriesWaitingForS3ACLExceeded(aws.StringValue(bucket))) + return errors.New(MaxRetriesWaitingForS3ACLExceeded(aws.StringValue(bucket))) } // DoesS3BucketExist checks if the S3 bucket specified in the given config exists. @@ -1583,7 +1588,7 @@ func checkBucketAccess(s3Client *s3.S3, bucket *string, key *string) error { var awsErr awserr.Error - ok := goErrors.As(err, &awsErr) + ok := errors.As(err, &awsErr) if !ok { return err } @@ -1593,7 +1598,7 @@ func checkBucketAccess(s3Client *s3.S3, bucket *string, key *string) error { return nil } - return errors.WithStackTraceAndPrefix(err, "Error checking access to S3 bucket %s", *bucket) + return errors.Errorf("error checking access to S3 bucket %s: %w", *bucket, err) } // Create a table for locks in DynamoDB if the user has configured a lock table and the table doesn't already exist diff --git a/remote/terraform_state_file.go b/remote/terraform_state_file.go index 49ece2d12d..09b4f0e370 100644 --- a/remote/terraform_state_file.go +++ b/remote/terraform_state_file.go @@ -5,7 +5,7 @@ import ( "fmt" "os" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" ) @@ -74,13 +74,13 @@ func ParseTerraformStateFile(path string) (*TerraformState, error) { bytes, err := os.ReadFile(path) if err != nil { - return nil, errors.WithStackTrace(CantParseTerraformStateFileError{Path: path, UnderlyingErr: err}) + return nil, errors.New(CantParseTerraformStateFileError{Path: path, UnderlyingErr: err}) } state, err := ParseTerraformState(bytes) if err != nil { - return nil, errors.WithStackTrace(CantParseTerraformStateFileError{Path: path, UnderlyingErr: err}) + return nil, errors.New(CantParseTerraformStateFileError{Path: path, UnderlyingErr: err}) } return state, nil @@ -95,7 +95,7 @@ func ParseTerraformState(terraformStateData []byte) (*TerraformState, error) { } if err := json.Unmarshal(terraformStateData, terraformState); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return terraformState, nil diff --git a/shell/error_explainer.go b/shell/error_explainer.go index 58451942f5..21c698ec73 100644 --- a/shell/error_explainer.go +++ b/shell/error_explainer.go @@ -1,7 +1,6 @@ package shell import ( - goErrors "errors" "fmt" "regexp" "strings" @@ -10,8 +9,7 @@ import ( "github.com/gruntwork-io/gruntwork-cli/collections" - "github.com/gruntwork-io/go-commons/errors" - "github.com/hashicorp/go-multierror" + "github.com/gruntwork-io/terragrunt/internal/errors" ) // terraformErrorsMatcher List of errors that we know how to explain to the user. The key is a regex that matches the error message, and the value is the explanation. @@ -33,30 +31,17 @@ var terraformErrorsMatcher = map[string]string{ // ExplainError will try to explain the error to the user, if we know how to do so. func ExplainError(err error) string { - errorsToProcess := []error{err} - - var multiErrors *multierror.Error - - // multiErrors, ok := err.(*multierror.Error) - if ok := goErrors.As(err, &multiErrors); ok { - errorsToProcess = multiErrors.Errors - } - explanations := map[string]string{} // iterate over each error, unwrap it, and check for error output - for _, errorItem := range errorsToProcess { - originalError := errors.Unwrap(errorItem) - if originalError == nil { - continue - } + for _, err := range errors.UnwrapErrors(err) { + message := err.Error() - message := originalError.Error() // extract process output, if it is the case var processError util.ProcessExecutionError - if ok := goErrors.As(originalError, &processError); ok { - errorOutput := processError.Stderr - stdOut := processError.Stdout + if ok := errors.As(err, &processError); ok { + errorOutput := processError.Output.Stderr.String() + stdOut := processError.Output.Stdout.String() message = fmt.Sprintf("%s\n%s", stdOut, errorOutput) } diff --git a/shell/error_explainer_test.go b/shell/error_explainer_test.go index 04ddc64973..7375f18f85 100644 --- a/shell/error_explainer_test.go +++ b/shell/error_explainer_test.go @@ -1,13 +1,12 @@ package shell_test import ( - "errors" + "bytes" "testing" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/shell" "github.com/gruntwork-io/terragrunt/util" - - "github.com/hashicorp/go-multierror" "github.com/stretchr/testify/assert" ) @@ -46,15 +45,16 @@ func TestExplainError(t *testing.T) { t.Run(tt.errorOutput, func(t *testing.T) { t.Parallel() - err := multierror.Append(&multierror.Error{}, util.ProcessExecutionError{ + output := util.CmdOutput{} + output.Stderr = *bytes.NewBufferString(tt.errorOutput) + + errs := new(errors.MultiError) + errs = errs.Append(util.ProcessExecutionError{ Err: errors.New(""), - Stdout: "", - Stderr: tt.errorOutput, + Output: output, }) - explanation := shell.ExplainError(err) + explanation := shell.ExplainError(errs) assert.Contains(t, explanation, tt.explanation) - }) } - } diff --git a/shell/git.go b/shell/git.go new file mode 100644 index 0000000000..575778fee9 --- /dev/null +++ b/shell/git.go @@ -0,0 +1,136 @@ +package shell + +import ( + "bytes" + "context" + "net/url" + "strings" + + "github.com/gruntwork-io/terragrunt/internal/cache" + "github.com/gruntwork-io/terragrunt/internal/errors" + "github.com/gruntwork-io/terragrunt/options" + "github.com/hashicorp/go-version" +) + +const ( + gitPrefix = "git::" + refsTags = "refs/tags/" + + tagSplitPart = 2 +) + +// GitTopLevelDir fetches git repository path from passed directory. +func GitTopLevelDir(ctx context.Context, terragruntOptions *options.TerragruntOptions, path string) (string, error) { + runCache := cache.ContextCache[string](ctx, RunCmdCacheContextKey) + cacheKey := "top-level-dir-" + path + + if gitTopLevelDir, found := runCache.Get(ctx, cacheKey); found { + return gitTopLevelDir, nil + } + + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + + opts, err := options.NewTerragruntOptionsWithConfigPath(path) + if err != nil { + return "", err + } + + opts.Env = terragruntOptions.Env + opts.Writer = &stdout + opts.ErrWriter = &stderr + + cmd, err := RunShellCommandWithOutput(ctx, opts, path, true, false, "git", "rev-parse", "--show-toplevel") + if err != nil { + return "", err + } + + cmdOutput := strings.TrimSpace(cmd.Stdout.String()) + terragruntOptions.Logger.Debugf("git show-toplevel result: \n%v\n%v\n%v\n", stdout.String(), stderr.String(), cmdOutput) + runCache.Put(ctx, cacheKey, cmdOutput) + + return cmdOutput, nil +} + +// GitRepoTags fetches git repository tags from passed url. +func GitRepoTags(ctx context.Context, opts *options.TerragruntOptions, gitRepo *url.URL) ([]string, error) { + repoPath := gitRepo.String() + // remove git:: part if present + repoPath = strings.TrimPrefix(repoPath, gitPrefix) + + stdout := bytes.Buffer{} + stderr := bytes.Buffer{} + + gitOpts, err := options.NewTerragruntOptionsWithConfigPath(opts.WorkingDir) + if err != nil { + return nil, err + } + + gitOpts.Env = opts.Env + gitOpts.Writer = &stdout + gitOpts.ErrWriter = &stderr + + output, err := RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, true, false, "git", "ls-remote", "--tags", repoPath) + if err != nil { + return nil, errors.New(err) + } + + var tags []string + + tagLines := strings.Split(output.Stdout.String(), "\n") + + for _, line := range tagLines { + fields := strings.Fields(line) + if len(fields) >= tagSplitPart { + tags = append(tags, fields[1]) + } + } + + return tags, nil +} + +// GitLastReleaseTag fetches git repository last release tag. +func GitLastReleaseTag(ctx context.Context, opts *options.TerragruntOptions, gitRepo *url.URL) (string, error) { + tags, err := GitRepoTags(ctx, opts, gitRepo) + if err != nil { + return "", err + } + + if len(tags) == 0 { + return "", nil + } + + return LastReleaseTag(tags), nil +} + +// LastReleaseTag returns last release tag from passed tags slice. +func LastReleaseTag(tags []string) string { + semverTags := extractSemVerTags(tags) + if len(semverTags) == 0 { + return "" + } + // find last semver tag + lastVersion := semverTags[0] + for _, ver := range semverTags { + if ver.GreaterThanOrEqual(lastVersion) { + lastVersion = ver + } + } + + return lastVersion.Original() +} + +// extractSemVerTags - extract semver tags from passed tags slice. +func extractSemVerTags(tags []string) []*version.Version { + var semverTags []*version.Version + + for _, tag := range tags { + t := strings.TrimPrefix(tag, refsTags) + if v, err := version.NewVersion(t); err == nil { + // consider only semver tags + semverTags = append(semverTags, v) + } + } + + return semverTags +} diff --git a/shell/prompt.go b/shell/prompt.go index 389d175f84..14d0c190f4 100644 --- a/shell/prompt.go +++ b/shell/prompt.go @@ -6,7 +6,7 @@ import ( "os" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" ) @@ -25,12 +25,14 @@ func PromptUserForInput(ctx context.Context, prompt string, terragruntOptions *o n, err := terragruntOptions.ErrWriter.Write([]byte(prompt)) if err != nil { terragruntOptions.Logger.Error(err) - return "", errors.WithStackTrace(err) + + return "", errors.New(err) } if n != len(prompt) { terragruntOptions.Logger.Errorln("Failed to write data") - return "", errors.WithStackTrace(err) + + return "", errors.New(err) } reader := bufio.NewReader(os.Stdin) @@ -41,7 +43,7 @@ func PromptUserForInput(ctx context.Context, prompt string, terragruntOptions *o go func() { input, err := reader.ReadString('\n') if err != nil { - errCh <- errors.WithStackTrace(err) + errCh <- errors.New(err) return } inputCh <- strings.TrimSpace(input) @@ -61,7 +63,7 @@ func PromptUserForInput(ctx context.Context, prompt string, terragruntOptions *o func PromptUserForYesNo(ctx context.Context, prompt string, terragruntOptions *options.TerragruntOptions) (bool, error) { resp, err := PromptUserForInput(ctx, prompt+" (y/n) ", terragruntOptions) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } switch strings.ToLower(resp) { diff --git a/shell/ptty_windows.go b/shell/ptty_windows.go deleted file mode 100644 index 8e4e0b6371..0000000000 --- a/shell/ptty_windows.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build windows -// +build windows - -package shell - -import ( - "io" - "os" - "os/exec" - "strings" - - "golang.org/x/sys/windows" - - "github.com/gruntwork-io/go-commons/errors" - "github.com/gruntwork-io/terragrunt/options" -) - -const InvalidHandleErrorMessage = "The handle is invalid" - -// PrepareConsole enables support for escape sequences -// https://stackoverflow.com/questions/56460651/golang-fmt-print-033c-and-fmt-print-x1bc-are-not-clearing-screenansi-es -// https://github.com/containerd/console/blob/f652dc3/console_windows.go#L46 -func PrepareConsole(terragruntOptions *options.TerragruntOptions) { - enableVirtualTerminalProcessing(terragruntOptions, os.Stdin) - enableVirtualTerminalProcessing(terragruntOptions, os.Stderr) - enableVirtualTerminalProcessing(terragruntOptions, os.Stdout) -} - -func enableVirtualTerminalProcessing(options *options.TerragruntOptions, file *os.File) { - var mode uint32 - handle := windows.Handle(file.Fd()) - if err := windows.GetConsoleMode(handle, &mode); err != nil { - if strings.Contains(err.Error(), InvalidHandleErrorMessage) { - options.Logger.Debugf("failed to get console mode: %v\n", err) - } else { - options.Logger.Errorf("failed to get console mode: %v\n", err) - } - return - } - - if err := windows.SetConsoleMode(handle, mode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING); err != nil { - options.Logger.Errorf("failed to set console mode: %v\n", err) - if secondError := windows.SetConsoleMode(handle, mode); secondError != nil { - options.Logger.Errorf("failed to set console mode: %v\n", secondError) - return - } - } -} - -// For windows, there is no concept of a pseudoTTY so we run as if there is no pseudoTTY. -func runCommandWithPTTY(terragruntOptions *options.TerragruntOptions, cmd *exec.Cmd, cmdStdout io.Writer, cmdStderr io.Writer) error { - cmd.Stdin = os.Stdin - cmd.Stdout = cmdStdout - cmd.Stderr = cmdStderr - if err := cmd.Start(); err != nil { - // bad path, binary not executable, &c - return errors.WithStackTrace(err) - } - return nil -} diff --git a/shell/run_shell_cmd.go b/shell/run_shell_cmd.go index a3128bdb50..06e6b68c55 100644 --- a/shell/run_shell_cmd.go +++ b/shell/run_shell_cmd.go @@ -2,23 +2,18 @@ package shell import ( - "bytes" "context" "fmt" "io" - "net/url" "os" - "os/exec" - "os/signal" "path/filepath" "strings" "time" "github.com/mattn/go-isatty" - "github.com/gruntwork-io/terragrunt/internal/cache" - "github.com/gruntwork-io/terragrunt/engine" + "github.com/gruntwork-io/terragrunt/internal/os/exec" "github.com/gruntwork-io/terragrunt/pkg/cli" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/format" @@ -27,10 +22,8 @@ import ( "github.com/gruntwork-io/terragrunt/telemetry" - "github.com/hashicorp/go-version" - "github.com/gruntwork-io/go-commons/collections" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" ) @@ -42,16 +35,7 @@ import ( // kill -INT - # sends SIGINT to the process group // Since we cannot know how the signal is sent, we should give `tofu`/`terraform` time to gracefully exit // if it receives the signal directly from the shell, to avoid sending the second interrupt signal to `tofu`/`terraform`. -const SignalForwardingDelay = time.Second * 30 - -const ( - gitPrefix = "git::" - refsTags = "refs/tags/" - - tagSplitPart = 2 - - logMsgSeparator = "\n" -) +const SignalForwardingDelay = time.Second * 15 const ( // tfLogMsgPrefix is a message prefix that is prepended to each TF_LOG output lines when the output is integrated in TG log, for example: @@ -59,42 +43,40 @@ const ( // TF_LOG: using github.com/zclconf/go-cty v1.14.3 // TF_LOG: Go runtime version: go1.22.1 tfLogMsgPrefix = "TF_LOG: " + + logMsgSeparator = "\n" ) // Commands that implement a REPL need a pseudo TTY when run as a subprocess in order for the readline properties to be // preserved. This is a list of terraform commands that have this property, which is used to determine if terragrunt // should allocate a ptty when running that terraform command. var terraformCommandsThatNeedPty = []string{ - "console", + terraform.CommandNameConsole, } // RunTerraformCommand runs the given Terraform command. -func RunTerraformCommand(ctx context.Context, terragruntOptions *options.TerragruntOptions, args ...string) error { - needPTY, err := isTerraformCommandThatNeedsPty(args) - if err != nil { - return err - } - - _, err = RunShellCommandWithOutput(ctx, terragruntOptions, "", false, needPTY, terragruntOptions.TerraformPath, args...) - - return err -} +func RunTerraformCommand(ctx context.Context, opts *options.TerragruntOptions, args ...string) error { + _, err := RunTerraformCommandWithOutput(ctx, opts, args...) -// RunShellCommand runs the given shell command. -func RunShellCommand(ctx context.Context, terragruntOptions *options.TerragruntOptions, command string, args ...string) error { - _, err := RunShellCommandWithOutput(ctx, terragruntOptions, "", false, false, command, args...) return err } // RunTerraformCommandWithOutput runs the given Terraform command, writing its stdout/stderr to the terminal AND returning stdout/stderr to this // method's caller -func RunTerraformCommandWithOutput(ctx context.Context, terragruntOptions *options.TerragruntOptions, args ...string) (*util.CmdOutput, error) { - needPTY, err := isTerraformCommandThatNeedsPty(args) +func RunTerraformCommandWithOutput(ctx context.Context, opts *options.TerragruntOptions, args ...string) (*util.CmdOutput, error) { + needsPTY, err := isTerraformCommandThatNeedsPty(args) if err != nil { return nil, err } - return RunShellCommandWithOutput(ctx, terragruntOptions, "", false, needPTY, terragruntOptions.TerraformPath, args...) + return RunShellCommandWithOutput(ctx, opts, "", false, needsPTY, opts.TerraformPath, args...) +} + +// RunShellCommand runs the given shell command. +func RunShellCommand(ctx context.Context, opts *options.TerragruntOptions, command string, args ...string) error { + _, err := RunShellCommandWithOutput(ctx, opts, "", false, false, command, args...) + + return err } // RunShellCommandWithOutput runs the specified shell command with the specified arguments. @@ -107,7 +89,7 @@ func RunShellCommandWithOutput( opts *options.TerragruntOptions, workingDir string, suppressStdout bool, - allocatePseudoTty bool, + needsPTY bool, command string, args ...string, ) (*util.CmdOutput, error) { @@ -118,8 +100,8 @@ func RunShellCommandWithOutput( } var ( - output *util.CmdOutput = nil - commandDir = workingDir + output = util.CmdOutput{} + commandDir = workingDir ) if workingDir == "" { @@ -133,150 +115,111 @@ func RunShellCommandWithOutput( }, func(childCtx context.Context) error { opts.Logger.Debugf("Running command: %s %s", command, strings.Join(args, " ")) - cmd := exec.Command(command, args...) - - // TODO: consider adding prefix from opts logger to stdout and stderr - cmd.Env = toEnvVarsList(opts.Env) - cmd.Dir = commandDir - var ( outWriter = opts.Writer errWriter = opts.ErrWriter ) - // redirect output through logger with json wrapping if opts.JSONLogFormat && opts.TerraformLogsToJSON { logger := opts.Logger.WithField("workingDir", opts.WorkingDir).WithField("executedCommandArgs", args) outWriter = logger.WithOptions(log.WithOutput(errWriter)).Writer() errWriter = logger.WithOptions(log.WithOutput(errWriter)).WriterLevel(log.ErrorLevel) - } else if command == opts.TerraformPath { - if opts.ForwardTFStdout || shouldForceForwardTFStdout(args) { - // We only display the output receipt notification when we show it to the user, and do nothing when we hide it, for example when `outWriter` is io.Discard. - if _, ok := outWriter.(*os.File); ok { - outWriter = util.WriterNotifier(outWriter, func(p []byte) { - opts.Logger.Infof("Retrieved output from %s", opts.TerraformPath) - }) - } - } else { - logger := opts.Logger.WithField(format.TFBinaryKeyName, filepath.Base(opts.TerraformPath)) - - outWriter = writer.New( - writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))), - writer.WithDefaultLevel(log.StdoutLevel), - writer.WithMsgSeparator(logMsgSeparator), - ) - - errWriter = writer.New( - writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))), - writer.WithDefaultLevel(log.StderrLevel), - writer.WithMsgSeparator(logMsgSeparator), - writer.WithParseFunc(terraform.ParseLogFunc(tfLogMsgPrefix, false)), - ) - } + } else if command == opts.TerraformPath && !opts.TerraformLogsToJSON && !opts.ForwardTFStdout && !shouldForceForwardTFStdout(args) { + logger := opts.Logger.WithField(format.TFBinaryKeyName, filepath.Base(opts.TerraformPath)) + + outWriter = writer.New( + writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))), + writer.WithDefaultLevel(log.StdoutLevel), + writer.WithMsgSeparator(logMsgSeparator), + ) + + errWriter = writer.New( + writer.WithLogger(logger.WithOptions(log.WithOutput(errWriter))), + writer.WithDefaultLevel(log.StderrLevel), + writer.WithMsgSeparator(logMsgSeparator), + writer.WithParseFunc(terraform.ParseLogFunc(tfLogMsgPrefix, false)), + ) } var ( - stdoutBuf bytes.Buffer - stderrBuf bytes.Buffer - - cmdStderr = io.MultiWriter(errWriter, &stderrBuf) - cmdStdout = io.MultiWriter(outWriter, &stdoutBuf) + cmdStderr = io.MultiWriter(errWriter, &output.Stderr) + cmdStdout = io.MultiWriter(outWriter, &output.Stdout) ) if suppressStdout { opts.Logger.Debugf("Command output will be suppressed.") - cmdStdout = io.MultiWriter(&stdoutBuf) - } - - if command == opts.TerraformPath && opts.Engine != nil && !opts.EngineEnabled { - opts.Logger.Debugf("Engine is not enabled, running command directly in %s", commandDir) + cmdStdout = io.MultiWriter(&output.Stdout) } - useEngine := opts.Engine != nil && opts.EngineEnabled - - // If the engine is enabled and the command is IaC executable, use the engine to run the command. - if useEngine && command == opts.TerraformPath { - opts.Logger.Debugf("Using engine to run command: %s %s", command, strings.Join(args, " ")) - - cmdOutput, err := engine.Run(ctx, &engine.ExecutionOptions{ - TerragruntOptions: opts, - CmdStdout: cmdStdout, - CmdStderr: cmdStderr, - WorkingDir: cmd.Dir, - SuppressStdout: suppressStdout, - AllocatePseudoTty: allocatePseudoTty, - Command: command, - Args: args, - }) - if err != nil { - return errors.WithStackTrace(err) - } - - output = cmdOutput + if command == opts.TerraformPath { + // If the engine is enabled and the command is IaC executable, use the engine to run the command. + if opts.Engine != nil && opts.EngineEnabled { + opts.Logger.Debugf("Using engine to run command: %s %s", command, strings.Join(args, " ")) + + cmdOutput, err := engine.Run(ctx, &engine.ExecutionOptions{ + TerragruntOptions: opts, + CmdStdout: cmdStdout, + CmdStderr: cmdStderr, + WorkingDir: commandDir, + SuppressStdout: suppressStdout, + AllocatePseudoTty: needsPTY, + Command: command, + Args: args, + }) + if err != nil { + return errors.New(err) + } - return err - } + output = *cmdOutput - // If we need to allocate a ptty for the command, route through the ptty routine. Otherwise, directly call the - // command. - if allocatePseudoTty { - if err := runCommandWithPTTY(opts, cmd, cmdStdout, cmdStderr); err != nil { return err } - } else { - cmd.Stdin = os.Stdin - cmd.Stdout = cmdStdout - cmd.Stderr = cmdStderr - - if err := cmd.Start(); err != nil { - // bad path, binary not executable, &c - return errors.WithStackTrace(err) - } + + opts.Logger.Debugf("Engine is not enabled, running command directly in %s", commandDir) } - // Make sure to forward signals to the subcommand. - cmdChannel := make(chan error) // used for closing the signals forwarder goroutine - signalChannel := NewSignalsForwarder(InterruptSignals, cmd, opts.Logger, cmdChannel) + cmd := exec.Command(command, args...) + cmd.Dir = commandDir + cmd.Stdout = cmdStdout + cmd.Stderr = cmdStderr + cmd.Configure( + exec.WithLogger(opts.Logger), + exec.WithUsePTY(needsPTY), + exec.WithEnv(opts.Env), + exec.WithForwardSignalDelay(SignalForwardingDelay), + ) - defer func(signalChannel *SignalsForwarder) { - err := signalChannel.Close() - if err != nil { - opts.Logger.Warnf("Error closing signal channel: %v", err) + if err := cmd.Start(); err != nil { + err = util.ProcessExecutionError{ + Err: err, + Args: args, + Command: command, + WorkingDir: cmd.Dir, } - }(&signalChannel) - - err := cmd.Wait() - cmdChannel <- err - output = &util.CmdOutput{ - Stdout: stdoutBuf.String(), - Stderr: stderrBuf.String(), + return errors.New(err) } - if err != nil { - opts.Logger.Warnf("Failed to execute %s in %s\n%s\n%s\n%v", command+" "+strings.Join(args, " "), cmd.Dir, stdoutBuf.String(), stderrBuf.String(), err) + cancelShutdown := cmd.RegisterGracefullyShutdown(ctx) + defer cancelShutdown() + + if err := cmd.Wait(); err != nil { err = util.ProcessExecutionError{ Err: err, - Stdout: stdoutBuf.String(), - Stderr: stderrBuf.String(), + Args: args, + Command: command, + Output: output, WorkingDir: cmd.Dir, } + + return errors.New(err) } - return errors.WithStackTrace(err) + return nil }) - return output, err -} - -func toEnvVarsList(envVarsAsMap map[string]string) []string { - envVarsAsList := []string{} - for key, value := range envVarsAsMap { - envVarsAsList = append(envVarsAsList, fmt.Sprintf("%s=%s", key, value)) - } - - return envVarsAsList + return &output, err } // isTerraformCommandThatNeedsPty returns true if the sub command of terraform we are running requires a pty. @@ -287,7 +230,7 @@ func isTerraformCommandThatNeedsPty(args []string) (bool, error) { fi, err := os.Stdin.Stat() if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } // if there is data in the stdin, then the terraform console is used in non-interactive mode, for example `echo "1 + 5" | terragrunt console`. @@ -303,161 +246,6 @@ func isTerraformCommandThatNeedsPty(args []string) (bool, error) { return true, nil } -type SignalsForwarder chan os.Signal - -// NewSignalsForwarder Forwards signals to a command, waiting for the command to finish. -func NewSignalsForwarder(signals []os.Signal, c *exec.Cmd, logger log.Logger, cmdChannel chan error) SignalsForwarder { - signalChannel := make(chan os.Signal, 1) - signal.Notify(signalChannel, signals...) - - go func() { - for { - select { - case s := <-signalChannel: - select { - case <-time.After(SignalForwardingDelay): - logger.Debugf("Forward signal %v to terraform.", s) - - err := c.Process.Signal(s) - if err != nil { - logger.Errorf("Error forwarding signal: %v", err) - } - case <-cmdChannel: - return - } - case <-cmdChannel: - return - } - } - }() - - return signalChannel -} - -func (signalChannel *SignalsForwarder) Close() error { - signal.Stop(*signalChannel) - *signalChannel <- nil - close(*signalChannel) - - return nil -} - -// GitTopLevelDir - fetch git repository path from passed directory -func GitTopLevelDir(ctx context.Context, terragruntOptions *options.TerragruntOptions, path string) (string, error) { - runCache := cache.ContextCache[string](ctx, RunCmdCacheContextKey) - cacheKey := "top-level-dir-" + path - - if gitTopLevelDir, found := runCache.Get(ctx, cacheKey); found { - return gitTopLevelDir, nil - } - - stdout := bytes.Buffer{} - stderr := bytes.Buffer{} - - opts, err := options.NewTerragruntOptionsWithConfigPath(path) - if err != nil { - return "", err - } - - opts.Env = terragruntOptions.Env - opts.Writer = &stdout - opts.ErrWriter = &stderr - - cmd, err := RunShellCommandWithOutput(ctx, opts, path, true, false, "git", "rev-parse", "--show-toplevel") - if err != nil { - return "", err - } - - cmdOutput := strings.TrimSpace(cmd.Stdout) - terragruntOptions.Logger.Debugf("git show-toplevel result: \n%v\n%v\n%v\n", stdout.String(), stderr.String(), cmdOutput) - runCache.Put(ctx, cacheKey, cmdOutput) - - return cmdOutput, nil -} - -// GitRepoTags - fetch git repository tags from passed url -func GitRepoTags(ctx context.Context, opts *options.TerragruntOptions, gitRepo *url.URL) ([]string, error) { - repoPath := gitRepo.String() - // remove git:: part if present - repoPath = strings.TrimPrefix(repoPath, gitPrefix) - - stdout := bytes.Buffer{} - stderr := bytes.Buffer{} - - gitOpts, err := options.NewTerragruntOptionsWithConfigPath(opts.WorkingDir) - if err != nil { - return nil, err - } - - gitOpts.Env = opts.Env - gitOpts.Writer = &stdout - gitOpts.ErrWriter = &stderr - - output, err := RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, true, false, "git", "ls-remote", "--tags", repoPath) - if err != nil { - return nil, errors.WithStackTrace(err) - } - - var tags []string - - tagLines := strings.Split(output.Stdout, "\n") - - for _, line := range tagLines { - fields := strings.Fields(line) - if len(fields) >= tagSplitPart { - tags = append(tags, fields[1]) - } - } - - return tags, nil -} - -// GitLastReleaseTag - fetch git repository last release tag -func GitLastReleaseTag(ctx context.Context, opts *options.TerragruntOptions, gitRepo *url.URL) (string, error) { - tags, err := GitRepoTags(ctx, opts, gitRepo) - if err != nil { - return "", err - } - - if len(tags) == 0 { - return "", nil - } - - return LastReleaseTag(tags), nil -} - -// LastReleaseTag - return last release tag from passed tags slice. -func LastReleaseTag(tags []string) string { - semverTags := extractSemVerTags(tags) - if len(semverTags) == 0 { - return "" - } - // find last semver tag - lastVersion := semverTags[0] - for _, ver := range semverTags { - if ver.GreaterThanOrEqual(lastVersion) { - lastVersion = ver - } - } - - return lastVersion.Original() -} - -// extractSemVerTags - extract semver tags from passed tags slice. -func extractSemVerTags(tags []string) []*version.Version { - var semverTags []*version.Version - - for _, tag := range tags { - t := strings.TrimPrefix(tag, refsTags) - if v, err := version.NewVersion(t); err == nil { - // consider only semver tags - semverTags = append(semverTags, v) - } - } - - return semverTags -} - // shouldForceForwardTFStdout returns true if at least one of the conditions is met, args contains the `-json` flag or the `output` or `state` command. func shouldForceForwardTFStdout(args cli.Args) bool { tfCommands := []string{ diff --git a/shell/run_shell_cmd_output_test.go b/shell/run_shell_cmd_output_test.go index 6dfa8f3542..b06ad2bb1a 100644 --- a/shell/run_shell_cmd_output_test.go +++ b/shell/run_shell_cmd_output_test.go @@ -61,7 +61,7 @@ func testCommandOutputOrder(t *testing.T, withPtty bool, fullOutput []string, st func TestCommandOutputPrefix(t *testing.T) { t.Parallel() prefix := "PREFIX" - terraformPath := "../testdata/test_outputs.sh" + terraformPath := "testdata/test_outputs.sh" prefixedOutput := []string{} for _, line := range FullOutput { prefixedOutput = append(prefixedOutput, fmt.Sprintf("prefix=%s binary=%s msg=%s", prefix, filepath.Base(terraformPath), line)) @@ -97,7 +97,7 @@ func testCommandOutput(t *testing.T, withOptions func(*options.TerragruntOptions withOptions(terragruntOptions) - out, err := shell.RunShellCommandWithOutput(context.Background(), terragruntOptions, "", !allocateStdout, false, "../testdata/test_outputs.sh", "same") + out, err := shell.RunShellCommandWithOutput(context.Background(), terragruntOptions, "", !allocateStdout, false, "testdata/test_outputs.sh", "same") assert.NotNil(t, out, "Should get output") require.NoError(t, err, "Should have no error") @@ -121,10 +121,10 @@ func assertOutputs( assert.Contains(t, allOutputs[i], expectedAllOutputs[i], allOutputs[i]) } - stdOutputs := strings.Split(strings.TrimSpace(out.Stdout), "\n") + stdOutputs := strings.Split(strings.TrimSpace(out.Stdout.String()), "\n") assert.Equal(t, expectedStdOutputs, stdOutputs) - stdErrs := strings.Split(strings.TrimSpace(out.Stderr), "\n") + stdErrs := strings.Split(strings.TrimSpace(out.Stderr.String()), "\n") assert.Equal(t, expectedStdErrs, stdErrs) } } diff --git a/shell/run_shell_cmd_unix_test.go b/shell/run_shell_cmd_unix_test.go index a697067c72..fcdabb4dce 100644 --- a/shell/run_shell_cmd_unix_test.go +++ b/shell/run_shell_cmd_unix_test.go @@ -5,128 +5,20 @@ package shell_test import ( "context" - goerrors "errors" "fmt" - "os" - "os/exec" "strconv" "syscall" "testing" "time" + "github.com/gruntwork-io/terragrunt/internal/os/signal" "github.com/gruntwork-io/terragrunt/shell" - "github.com/gruntwork-io/terragrunt/util" "github.com/gruntwork-io/terragrunt/options" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestExitCodeUnix(t *testing.T) { - t.Parallel() - - for i := 0; i <= 255; i++ { - cmd := exec.Command("../testdata/test_exit_code.sh", strconv.Itoa(i)) - err := cmd.Run() - - if i == 0 { - require.NoError(t, err) - } else { - require.Error(t, err) - } - retCode, err := util.GetExitCode(err) - require.NoError(t, err) - assert.Equal(t, i, retCode) - } - - // assert a non exec.ExitError returns an error - err := goerrors.New("This is an explicit error") - retCode, retErr := util.GetExitCode(err) - require.Error(t, retErr, "An error was expected") - assert.Equal(t, err, retErr) - assert.Equal(t, 0, retCode) -} - -func TestNewSignalsForwarderWaitUnix(t *testing.T) { - t.Parallel() - - expectedWait := 5 - - terragruntOptions, err := options.NewTerragruntOptionsForTest("") - require.NoError(t, err, "Unexpected error creating NewTerragruntOptionsForTest: %v", err) - - cmd := exec.Command("../testdata/test_sigint_wait.sh", strconv.Itoa(expectedWait)) - - cmdChannel := make(chan error) - runChannel := make(chan error) - - signalChannel := shell.NewSignalsForwarder(shell.InterruptSignals, cmd, terragruntOptions.Logger, cmdChannel) - defer signalChannel.Close() - - go func() { - runChannel <- cmd.Run() - }() - - time.Sleep(1000 * time.Millisecond) - start := time.Now() - cmd.Process.Signal(os.Interrupt) - err = <-runChannel - cmdChannel <- err - require.Error(t, err) - retCode, err := util.GetExitCode(err) - require.NoError(t, err) - assert.Equal(t, expectedWait, retCode) - assert.WithinDuration(t, time.Now(), start.Add(time.Duration(expectedWait)*time.Second), time.Second, - "Expected to wait 5 (+/-1) seconds after SIGINT") - -} - -// There isn't a proper way to catch interrupts in Windows batch scripts, so this test exists only for Unix -func TestNewSignalsForwarderMultipleUnix(t *testing.T) { - t.Parallel() - - expectedInterrupts := 10 - terragruntOptions, err := options.NewTerragruntOptionsForTest("") - require.NoError(t, err, "Unexpected error creating NewTerragruntOptionsForTest: %v", err) - - cmd := exec.Command("../testdata/test_sigint_multiple.sh", strconv.Itoa(expectedInterrupts)) - - cmdChannel := make(chan error) - runChannel := make(chan error) - - signalChannel := shell.NewSignalsForwarder(shell.InterruptSignals, cmd, terragruntOptions.Logger, cmdChannel) - defer signalChannel.Close() - - go func() { - runChannel <- cmd.Run() - }() - - time.Sleep(1000 * time.Millisecond) - - interruptAndWaitForProcess := func() (int, error) { - var interrupts int - var err error - for { - time.Sleep(500 * time.Millisecond) - select { - case err = <-runChannel: - return interrupts, err - default: - cmd.Process.Signal(os.Interrupt) - interrupts++ - } - } - } - - interrupts, err := interruptAndWaitForProcess() - cmdChannel <- err - require.Error(t, err) - retCode, err := util.GetExitCode(err) - require.NoError(t, err) - assert.LessOrEqual(t, retCode, interrupts, "Subprocess received wrong number of signals") - assert.Equal(t, expectedInterrupts, retCode, "Subprocess didn't receive multiple signals") -} - func TestRunShellCommandWithOutputInterrupt(t *testing.T) { t.Parallel() @@ -136,15 +28,20 @@ func TestRunShellCommandWithOutputInterrupt(t *testing.T) { errCh := make(chan error) expectedWait := 5 + ctx, cancel := context.WithCancelCause(context.Background()) + + cmdPath := "testdata/test_sigint_wait.sh" + go func() { - _, err := shell.RunShellCommandWithOutput(context.Background(), terragruntOptions, "", false, false, "../testdata/test_sigint_wait.sh", strconv.Itoa(expectedWait)) + _, err := shell.RunShellCommandWithOutput(ctx, terragruntOptions, "", false, false, cmdPath, strconv.Itoa(expectedWait)) errCh <- err }() time.AfterFunc(3*time.Second, func() { - syscall.Kill(os.Getpid(), syscall.SIGINT) + cancel(signal.NewContextCanceledError(syscall.SIGINT)) }) - expectedErr := fmt.Sprintf("[.] exit status %d", expectedWait) - assert.EqualError(t, <-errCh, expectedErr) + actualErr := <-errCh + expectedErr := fmt.Sprintf("Failed to execute \"%s 5\" in .\n\nexit status %d", cmdPath, expectedWait) + assert.EqualError(t, actualErr, expectedErr) } diff --git a/shell/run_shell_cmd_windows_test.go b/shell/run_shell_cmd_windows_test.go index cee6453bc5..478694c12b 100644 --- a/shell/run_shell_cmd_windows_test.go +++ b/shell/run_shell_cmd_windows_test.go @@ -4,102 +4,18 @@ package shell import ( - "bytes" "context" - goerrors "errors" "fmt" "os" - "os/exec" "strconv" "testing" "time" - "github.com/sirupsen/logrus" - + "github.com/gruntwork-io/terragrunt/internal/os/signal" "github.com/gruntwork-io/terragrunt/options" "github.com/stretchr/testify/assert" ) -func TestWindowsConsolePrepare(t *testing.T) { - t.Parallel() - - testOptions := options.NewTerragruntOptions() - - stdout := bytes.Buffer{} - - var testLogger = logrus.New() - testLogger.Out = &stdout - testLogger.Level = logrus.DebugLevel - - testOptions.Logger = testLogger.WithContext(context.Background()) - - PrepareConsole(testOptions) - - assert.Contains(t, stdout.String(), "level=debug msg=\"failed to get console mode: The handle is invalid.") -} - -func TestExitCodeWindows(t *testing.T) { - t.Parallel() - - for i := 0; i <= 255; i++ { - cmd := exec.Command(`..\testdata\test_exit_code.bat`, strconv.Itoa(i)) - err := cmd.Run() - - if i == 0 { - assert.NoError(t, err) - } else { - assert.Error(t, err) - } - retCode, err := GetExitCode(err) - assert.NoError(t, err) - assert.Equal(t, i, retCode) - } - - // assert a non exec.ExitError returns an error - err := goerrors.New("This is an explicit error") - retCode, retErr := GetExitCode(err) - assert.Error(t, retErr, "An error was expected") - assert.Equal(t, err, retErr) - assert.Equal(t, 0, retCode) -} - -func TestNewSignalsForwarderWaitWindows(t *testing.T) { - t.Parallel() - - expectedWait := 5 - - terragruntOptions, err := options.NewTerragruntOptionsForTest("") - assert.Nil(t, err, "Unexpected error creating NewTerragruntOptionsForTest: %v", err) - - cmd := exec.Command(`..\testdata\test_sigint_wait.bat`, strconv.Itoa(expectedWait)) - - cmdChannel := make(chan error) - runChannel := make(chan error) - - signalChannel := NewSignalsForwarder(forwardSignals, cmd, terragruntOptions.Logger, cmdChannel) - defer signalChannel.Close() - - go func() { - runChannel <- cmd.Run() - }() - - time.Sleep(1000 * time.Millisecond) - // start := time.Now() - // Note: sending interrupt on Windows is not supported by Windows and not implemented in Go - cmd.Process.Signal(os.Kill) - err = <-runChannel - cmdChannel <- err - assert.Error(t, err) - - // Since we can't send an interrupt on Windows, our test script won't handle it gracefully and exit after the expected wait time, - // so this part of the test process cannot be done on Windows - // retCode, err := GetExitCode(err) - // assert.NoError(t, err) - // assert.Equal(t, retCode, expectedWait) - // assert.WithinDuration(t, start.Add(time.Duration(expectedWait)*time.Second), time.Now(), time.Second, - // "Expected to wait 5 (+/-1) seconds after SIGINT") -} - func TestRunShellCommandWithOutputInterrupt(t *testing.T) { t.Parallel() @@ -109,18 +25,20 @@ func TestRunShellCommandWithOutputInterrupt(t *testing.T) { errCh := make(chan error) expectedWait := 5 + ctx, cancel := context.WithCancelCause(context.Background()) + + cmdPath := "testdata/test_sigint_wait.bat" + go func() { - _, err := RunShellCommandWithOutput(context.Background(), terragruntOptions, "", false, false, "../testdata/test_sigint_wait.bat", strconv.Itoa(expectedWait)) + _, err := RunShellCommandWithOutput(ctx, terragruntOptions, "", false, false, cmdPath, strconv.Itoa(expectedWait)) errCh <- err }() time.AfterFunc(3*time.Second, func() { - process, err := os.FindProcess(os.Getpid()) - assert.NoError(t, err) - - process.Signal(os.Kill) + cancel(signal.NewContextCanceledCause(os.Kill)) }) - expectedErr := fmt.Sprintf("[.] exit status %d", expectedWait) - assert.EqualError(t, <-errCh, expectedErr) + actualErr := <-errCh + expectedErr := fmt.Sprintf("Failed to execute %s 5 in .\n\nexit status %d", cmdPath, expectedWait) + assert.EqualError(t, actualErr, expectedErr) } diff --git a/shell/signal.go b/shell/signal.go deleted file mode 100644 index 8e4f72a707..0000000000 --- a/shell/signal.go +++ /dev/null @@ -1,21 +0,0 @@ -package shell - -import ( - "fmt" - "os" - "os/signal" -) - -// RegisterSignalHandler registers a handler of interrupt signal from the OS. -// When signal is receiving, it calls the given callback func `notifyFn`. -func RegisterSignalHandler(notifyFn func(os.Signal), sigs ...os.Signal) { - go func() { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, sigs...) - - sig := <-sigCh - // Carriage return helps prevent "^C" from being printed - fmt.Print("\r") - notifyFn(sig) - }() -} diff --git a/shell/signal_unix.go b/shell/signal_unix.go deleted file mode 100644 index 3c1f6b3eb3..0000000000 --- a/shell/signal_unix.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !windows -// +build !windows - -package shell - -import ( - "os" - "syscall" -) - -var InterruptSignals []os.Signal = []os.Signal{syscall.SIGTERM, syscall.SIGINT} diff --git a/shell/signal_windows.go b/shell/signal_windows.go deleted file mode 100644 index 1ca934ef05..0000000000 --- a/shell/signal_windows.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build windows -// +build windows - -package shell - -import ( - "os" -) - -var InterruptSignals []os.Signal = []os.Signal{} diff --git a/testdata/test_outputs.sh b/shell/testdata/test_outputs.sh similarity index 100% rename from testdata/test_outputs.sh rename to shell/testdata/test_outputs.sh diff --git a/testdata/test_sigint_wait.bat b/shell/testdata/test_sigint_wait.bat similarity index 100% rename from testdata/test_sigint_wait.bat rename to shell/testdata/test_sigint_wait.bat diff --git a/shell/testdata/test_sigint_wait.sh b/shell/testdata/test_sigint_wait.sh new file mode 100755 index 0000000000..8fc76ff7cc --- /dev/null +++ b/shell/testdata/test_sigint_wait.sh @@ -0,0 +1,12 @@ +#!/bin/bash -e + +WAIT_TIME=$1 + +trap int_handler INT + +function int_handler() { + sleep $WAIT_TIME + exit $WAIT_TIME +} + +while true; do sleep 0.1; done \ No newline at end of file diff --git a/terraform/cache/handlers/provider_filesystem_mirror.go b/terraform/cache/handlers/provider_filesystem_mirror.go index 05b4def0e7..7bb1daad95 100644 --- a/terraform/cache/handlers/provider_filesystem_mirror.go +++ b/terraform/cache/handlers/provider_filesystem_mirror.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/terraform/cache/models" "github.com/gruntwork-io/terragrunt/terraform/cache/router" "github.com/gruntwork-io/terragrunt/terraform/cache/services" @@ -111,11 +111,11 @@ func (handler *ProviderFilesystemMirrorHandler) readMirrorData(filename string, data, err := os.ReadFile(filename) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := json.Unmarshal(data, value); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/terraform/cache/handlers/provider_network_mirror.go b/terraform/cache/handlers/provider_network_mirror.go index 56c51e10ec..646dfda3c5 100644 --- a/terraform/cache/handlers/provider_network_mirror.go +++ b/terraform/cache/handlers/provider_network_mirror.go @@ -8,7 +8,7 @@ import ( "path/filepath" "strings" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/terraform/cache/helpers" "github.com/gruntwork-io/terragrunt/terraform/cache/models" "github.com/gruntwork-io/terragrunt/terraform/cache/router" @@ -30,7 +30,7 @@ type ProviderNetworkMirrorHandler struct { func NewProviderNetworkMirrorHandler(providerService *services.ProviderService, cacheProviderHTTPStatusCode int, networkMirror *cliconfig.ProviderInstallationNetworkMirror, credsSource *cliconfig.CredentialsSource) (ProviderHandler, error) { networkMirrorURL, err := url.Parse(networkMirror.URL) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return &ProviderNetworkMirrorHandler{ @@ -120,7 +120,7 @@ func (handler *ProviderNetworkMirrorHandler) do(ctx echo.Context, method, reqPat req, err := http.NewRequestWithContext(ctx.Request().Context(), method, reqURL, nil) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if handler.credsSource != nil { @@ -132,7 +132,7 @@ func (handler *ProviderNetworkMirrorHandler) do(ctx echo.Context, method, reqPat resp, err := handler.Client.Do(req) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defer resp.Body.Close() //nolint:errcheck diff --git a/terraform/cache/handlers/registry_urls.go b/terraform/cache/handlers/registry_urls.go index 91651bc90e..9448b2b3c8 100644 --- a/terraform/cache/handlers/registry_urls.go +++ b/terraform/cache/handlers/registry_urls.go @@ -7,7 +7,7 @@ import ( "io" "net/http" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" ) const ( @@ -38,18 +38,18 @@ func DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, erro req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } resp, err := (&http.Client{}).Do(req) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } defer resp.Body.Close() //nolint:errcheck switch resp.StatusCode { case http.StatusNotFound, http.StatusInternalServerError: - return nil, errors.WithStackTrace(NotFoundWellKnownURL{wellKnownURL}) + return nil, errors.New(NotFoundWellKnownURL{wellKnownURL}) case http.StatusOK: default: return nil, fmt.Errorf("%s returned %s", url, resp.Status) @@ -57,12 +57,12 @@ func DiscoveryURL(ctx context.Context, registryName string) (*RegistryURLs, erro content, err := io.ReadAll(resp.Body) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } urls := new(RegistryURLs) if err := json.Unmarshal(content, urls); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return urls, nil diff --git a/terraform/cache/helpers/http.go b/terraform/cache/helpers/http.go index 8c44ba9e4a..270050d58f 100644 --- a/terraform/cache/helpers/http.go +++ b/terraform/cache/helpers/http.go @@ -12,7 +12,7 @@ import ( "os" "strconv" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/go-getter/v2" ) @@ -21,7 +21,7 @@ func Fetch(ctx context.Context, req *http.Request, dst io.Writer) error { resp, err := (&http.Client{}).Do(req) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defer resp.Body.Close() //nolint:errcheck @@ -35,7 +35,7 @@ func Fetch(ctx context.Context, req *http.Request, dst io.Writer) error { } if written, err := getter.Copy(ctx, dst, reader); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } else if resp.ContentLength != -1 && written != resp.ContentLength { return errors.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, written) } @@ -47,7 +47,7 @@ func Fetch(ctx context.Context, req *http.Request, dst io.Writer) error { func FetchToFile(ctx context.Context, req *http.Request, dst string) error { file, err := os.Create(dst) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defer file.Close() //nolint:errcheck @@ -56,7 +56,7 @@ func FetchToFile(ctx context.Context, req *http.Request, dst string) error { } if err := file.Sync(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/terraform/cache/middleware/key_auth.go b/terraform/cache/middleware/key_auth.go index ee5185538e..e45741bb40 100644 --- a/terraform/cache/middleware/key_auth.go +++ b/terraform/cache/middleware/key_auth.go @@ -1,7 +1,7 @@ package middleware import ( - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) diff --git a/terraform/cache/middleware/recover.go b/terraform/cache/middleware/recover.go index d53ed2bdc7..7f8fd89795 100644 --- a/terraform/cache/middleware/recover.go +++ b/terraform/cache/middleware/recover.go @@ -1,7 +1,7 @@ package middleware import ( - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/labstack/echo/v4" ) @@ -10,7 +10,7 @@ func Recover(logger log.Logger) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) (er error) { defer errors.Recover(func(err error) { - logger.Debugf(errors.PrintErrorWithStackTrace(err)) + logger.Debug(errors.ErrorStack(err)) er = err }) diff --git a/terraform/cache/server.go b/terraform/cache/server.go index 048b6bc41a..f3ac68372b 100644 --- a/terraform/cache/server.go +++ b/terraform/cache/server.go @@ -6,7 +6,7 @@ import ( "net" "net/http" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/terraform/cache/controllers" "github.com/gruntwork-io/terragrunt/terraform/cache/handlers" "github.com/gruntwork-io/terragrunt/terraform/cache/middleware" @@ -71,7 +71,7 @@ func (server *Server) DiscoveryURL(ctx context.Context, registryName string) (*h func (server *Server) Listen() (net.Listener, error) { ln, err := net.Listen("tcp", server.Addr()) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } server.Server.Addr = ln.Addr().String() @@ -103,7 +103,7 @@ func (server *Server) Run(ctx context.Context, ln net.Listener) error { defer cancel() if err := server.Shutdown(ctx); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/terraform/cache/services/provider_cache.go b/terraform/cache/services/provider_cache.go index e594606bba..d026ab9dc8 100644 --- a/terraform/cache/services/provider_cache.go +++ b/terraform/cache/services/provider_cache.go @@ -13,7 +13,7 @@ import ( "sync" "time" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/terraform/cache/helpers" "github.com/gruntwork-io/terragrunt/terraform/cache/models" @@ -21,7 +21,6 @@ import ( "github.com/gruntwork-io/terragrunt/terraform/getproviders" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/go-getter/v2" - "github.com/hashicorp/go-multierror" svchost "github.com/hashicorp/terraform-svchost" "golang.org/x/sync/errgroup" ) @@ -201,14 +200,14 @@ func (cache *ProviderCache) warmUp(ctx context.Context) error { } if err := os.MkdirAll(filepath.Dir(cache.packageDir), os.ModePerm); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if util.FileExists(cache.userProviderDir) { cache.logger.Debugf("Create symlink file %s to %s", cache.packageDir, cache.userProviderDir) if err := os.Symlink(cache.userProviderDir, cache.packageDir); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } cache.logger.Infof("Cached %s from user plugins directory", cache.Provider) @@ -239,7 +238,7 @@ func (cache *ProviderCache) warmUp(ctx context.Context) error { cache.logger.Debugf("Unpack provider archive %s", cache.archivePath) if err := unzip.Decompress(cache.packageDir, cache.archivePath, true, unzipFileMode); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } auth, err := cache.AuthenticatePackage(ctx) @@ -255,7 +254,7 @@ func (cache *ProviderCache) warmUp(ctx context.Context) error { func (cache *ProviderCache) newRequest(ctx context.Context, url string) (*http.Request, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if cache.credsSource == nil { @@ -275,7 +274,7 @@ func (cache *ProviderCache) removeArchive() error { cache.logger.Debugf("Remove provider cached archive %s", cache.archivePath) if err := os.Remove(cache.archivePath); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -286,7 +285,7 @@ func (cache *ProviderCache) acquireLockFile(ctx context.Context) (*util.Lockfile lockfile := util.NewLockfile(cache.lockfilePath) if err := os.MkdirAll(filepath.Dir(cache.lockfilePath), os.ModePerm); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if err := util.DoWithRetry(ctx, "Acquiring lock file "+cache.lockfilePath, maxRetriesLockFile, retryDelayLockFile, cache.logger, log.DebugLevel, func(ctx context.Context) error { @@ -341,12 +340,12 @@ func (service *ProviderService) WaitForCacheReady(requestID string) ([]getprovid var ( providers []getproviders.Provider - merr = &multierror.Error{} + errs = &errors.MultiError{} ) for _, provider := range service.providerCaches.FindByRequestID(requestID) { if provider.err != nil { - merr = multierror.Append(merr, fmt.Errorf("unable to cache provider: %s, err: %w", provider, provider.err)) + errs = errs.Append(fmt.Errorf("unable to cache provider: %s, err: %w", provider, provider.err)) } if provider.ready { @@ -354,7 +353,7 @@ func (service *ProviderService) WaitForCacheReady(requestID string) ([]getprovid } } - return providers, merr.ErrorOrNil() + return providers, errs.ErrorOrNil() } // CacheProvider starts caching the given provider using non-blocking approach. @@ -414,7 +413,7 @@ func (service *ProviderService) Run(ctx context.Context) error { service.logger.Debugf("Provider cache dir %q", service.cacheDir) if err := os.MkdirAll(service.cacheDir, os.ModePerm); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } tempDir, err := util.GetTempDir() @@ -424,7 +423,7 @@ func (service *ProviderService) Run(ctx context.Context) error { service.tempDir = filepath.Join(tempDir, "providers") - merr := &multierror.Error{} + errs := &errors.MultiError{} errGroup, ctx := errgroup.WithContext(ctx) for { @@ -432,21 +431,21 @@ func (service *ProviderService) Run(ctx context.Context) error { case cache := <-service.providerCacheWarmUpCh: errGroup.Go(func() error { if err := service.startProviderCaching(ctx, cache); err != nil { - merr = multierror.Append(merr, err) + errs = errs.Append(err) } return nil }) case <-ctx.Done(): if err := errGroup.Wait(); err != nil { - merr = multierror.Append(merr, err) + errs = errs.Append(err) } if err := service.providerCaches.removeArchive(); err != nil { - merr = multierror.Append(merr, errors.WithStackTrace(err)) + errs = errs.Append(err) } - return merr.ErrorOrNil() + return errs.ErrorOrNil() } } } diff --git a/terraform/cliconfig/config.go b/terraform/cliconfig/config.go index 9dae74c134..99c8ac7ff8 100644 --- a/terraform/cliconfig/config.go +++ b/terraform/cliconfig/config.go @@ -4,7 +4,7 @@ package cliconfig import ( "os" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclwrite" svchost "github.com/hashicorp/terraform-svchost" @@ -109,7 +109,7 @@ func (cfg *Config) Save(configPath string) error { const ownerWriteGlobalReadPerms = 0644 if err := os.WriteFile(configPath, file.Bytes(), os.FileMode(ownerWriteGlobalReadPerms)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil diff --git a/terraform/cliconfig/user_config.go b/terraform/cliconfig/user_config.go index 9451bd4a1d..d15f3e7957 100644 --- a/terraform/cliconfig/user_config.go +++ b/terraform/cliconfig/user_config.go @@ -3,7 +3,7 @@ package cliconfig import ( "path/filepath" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/terraform/command/cliconfig" "github.com/hashicorp/terraform/tfdiags" ) @@ -43,7 +43,7 @@ func loadUserConfig( func UserProviderDir() (string, error) { configDir, err := cliconfig.ConfigDir() if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.Join(configDir, "plugins"), nil diff --git a/terraform/getproviders/hash.go b/terraform/getproviders/hash.go index 2191ef7507..93ae828a5d 100644 --- a/terraform/getproviders/hash.go +++ b/terraform/getproviders/hash.go @@ -9,7 +9,7 @@ import ( "os" "path/filepath" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/rogpeppe/go-internal/dirhash" ) @@ -40,18 +40,18 @@ func (scheme HashScheme) New(value string) Hash { func PackageHashLegacyZipSHA(path string) (Hash, error) { archivePath, err := filepath.EvalSymlinks(path) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } file, err := os.Open(archivePath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } defer file.Close() hash := sha256.New() if _, err = io.Copy(hash, file); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } gotHash := hash.Sum(nil) @@ -73,7 +73,7 @@ func PackageHashV1(path string) (Hash, error) { } if fileInfo, err := os.Stat(packageDir); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } else if !fileInfo.IsDir() { return "", errors.Errorf("packageDir is not a directory %q", packageDir) } diff --git a/terraform/getproviders/lock.go b/terraform/getproviders/lock.go index 273d1c1e6b..57a66aec44 100644 --- a/terraform/getproviders/lock.go +++ b/terraform/getproviders/lock.go @@ -12,7 +12,7 @@ import ( "strings" "unicode" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/terraform" "github.com/gruntwork-io/terragrunt/util" "github.com/hashicorp/hcl/v2" @@ -32,14 +32,14 @@ func UpdateLockfile(ctx context.Context, workingDir string, providers []Provider if util.FileExists(filename) { content, err := os.ReadFile(filename) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } var diags hcl.Diagnostics file, diags = hclwrite.ParseConfig(content, filename, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { - return errors.WithStackTrace(diags) + return errors.New(diags) } } @@ -49,7 +49,7 @@ func UpdateLockfile(ctx context.Context, workingDir string, providers []Provider const ownerWriteGlobalReadPerms = 0644 if err := os.WriteFile(filename, file.Bytes(), ownerWriteGlobalReadPerms); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return nil @@ -185,7 +185,7 @@ func getAttributeValueAsSlice(attr *hclwrite.Attribute) ([]string, error) { var val []string if err := json.Unmarshal(valBytes, &val); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return val, nil diff --git a/terraform/getproviders/package_authentication.go b/terraform/getproviders/package_authentication.go index 7a2dec9991..98bdc35eb7 100644 --- a/terraform/getproviders/package_authentication.go +++ b/terraform/getproviders/package_authentication.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/sha256" "encoding/hex" - goErrors "errors" "io" "os" "strings" @@ -12,7 +11,7 @@ import ( "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/util" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/ProtonMail/go-crypto/openpgp" openpgpArmor "github.com/ProtonMail/go-crypto/openpgp/armor" @@ -126,7 +125,7 @@ func NewArchiveChecksumAuthentication(wantSHA256Sum [sha256.Size]byte) PackageAu func (auth archiveHashAuthentication) Authenticate(path string) (*PackageAuthenticationResult, error) { if fileInfo, err := os.Stat(path); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } else if fileInfo.IsDir() { return nil, errors.Errorf("cannot check archive hash for non-archive location %s", path) } @@ -250,7 +249,7 @@ func (auth signatureAuthentication) Authenticate(location string) (*PackageAuthe func (auth signatureAuthentication) checkDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader, config *packet.Config) error { entity, err := openpgp.CheckDetachedSignature(keyring, signed, signature, config) - if goErrors.Is(err, openpgpErrors.ErrKeyExpired) { + if errors.Is(err, openpgpErrors.ErrKeyExpired) { for id := range entity.Identities { log.Warnf("expired openpgp key from %s\n", id) } @@ -275,7 +274,7 @@ func (auth signatureAuthentication) findSigningKey() (string, string, error) { if err := auth.checkDetachedSignature(keyring, bytes.NewReader(auth.Document), bytes.NewReader(auth.Signature), nil); err != nil { // If the signature issuer does not match the the key, keep trying the rest of the provided keys. - if goErrors.Is(err, openpgpErrors.ErrUnknownIssuer) { + if errors.Is(err, openpgpErrors.ErrUnknownIssuer) { continue } diff --git a/terraform/getter.go b/terraform/getter.go index 41a364e84c..04ca75ed3a 100644 --- a/terraform/getter.go +++ b/terraform/getter.go @@ -3,7 +3,6 @@ package terraform import ( "context" "encoding/json" - goErrors "errors" "fmt" "io" "net/http" @@ -19,7 +18,7 @@ import ( "github.com/hashicorp/go-getter" safetemp "github.com/hashicorp/go-safetemp" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" ) @@ -129,11 +128,11 @@ func (tfrGetter *RegistryGetter) Get(dstPath string, srcURL *url.URL) error { versionList, hasVersion := queryValues[versionQueryKey] if !hasVersion { - return errors.WithStackTrace(MalformedRegistryURLErr{reason: "missing version query"}) + return errors.New(MalformedRegistryURLErr{reason: "missing version query"}) } if len(versionList) != 1 { - return errors.WithStackTrace(MalformedRegistryURLErr{reason: "more than one version query"}) + return errors.New(MalformedRegistryURLErr{reason: "more than one version query"}) } version := versionList[0] @@ -178,7 +177,7 @@ func (tfrGetter *RegistryGetter) Get(dstPath string, srcURL *url.URL) error { // GetFile is not implemented for the Terraform module registry Getter since the terraform module registry doesn't serve // a single file. func (tfrGetter *RegistryGetter) GetFile(dst string, src *url.URL) error { - return errors.WithStackTrace(goErrors.New("GetFile is not implemented for the Terraform Registry Getter")) + return errors.New(errors.New("GetFile is not implemented for the Terraform Registry Getter")) } // getSubdir downloads the source into the destination, but with the proper subdir. @@ -201,30 +200,31 @@ func (tfrGetter *RegistryGetter) getSubdir(_ context.Context, dstPath, sourceURL } // Download that into the given directory if err := getter.Get(tempdirPath, sourceURL, opts...); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // Process any globbing sourcePath, err := getter.SubdirGlob(tempdirPath, subDir) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // Make sure the subdir path actually exists if _, err := os.Stat(sourcePath); err != nil { details := fmt.Sprintf("could not stat download path %s (error: %s)", sourcePath, err) - return errors.WithStackTrace(ModuleDownloadErr{sourceURL: sourceURL, details: details}) + + return errors.New(ModuleDownloadErr{sourceURL: sourceURL, details: details}) } // Copy the subdirectory into our actual destination. if err := os.RemoveAll(dstPath); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // Make the final destination const ownerWriteGlobalReadExecutePerms = 0755 if err := os.MkdirAll(dstPath, ownerWriteGlobalReadExecutePerms); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } // We use a temporary manifest file here that is deleted at the end of this routine since we don't intend to come @@ -261,7 +261,8 @@ func GetModuleRegistryURLBasePath(ctx context.Context, logger log.Logger, domain var respJSON RegistryServicePath if err := json.Unmarshal(bodyData, &respJSON); err != nil { reason := fmt.Sprintf("Error parsing response body %s: %s", string(bodyData), err) - return "", errors.WithStackTrace(ServiceDiscoveryErr{reason: reason}) + + return "", errors.New(ServiceDiscoveryErr{reason: reason}) } return respJSON.ModulesPath, nil @@ -273,7 +274,8 @@ func GetTerraformGetHeader(ctx context.Context, logger log.Logger, url url.URL) body, header, err := httpGETAndGetResponse(ctx, logger, url) if err != nil { details := "error receiving HTTP data" - return "", errors.WithStackTrace(ModuleDownloadErr{sourceURL: url.String(), details: details}) + + return "", errors.New(ModuleDownloadErr{sourceURL: url.String(), details: details}) } terraformGet := header.Get("X-Terraform-Get") @@ -285,7 +287,8 @@ func GetTerraformGetHeader(ctx context.Context, logger log.Logger, url url.URL) var responseJSON map[string]string if err := json.Unmarshal(body, &responseJSON); err != nil { reason := fmt.Sprintf("Error parsing response body %s: %s", string(body), err) - return "", errors.WithStackTrace(ModuleDownloadErr{sourceURL: url.String(), details: reason}) + + return "", errors.New(ModuleDownloadErr{sourceURL: url.String(), details: reason}) } // get location value from responseJSON terraformGet = responseJSON["location"] @@ -295,7 +298,8 @@ func GetTerraformGetHeader(ctx context.Context, logger log.Logger, url url.URL) if terraformGet == "" { details := "no source URL was returned in header X-Terraform-Get and in location response from download URL" - return "", errors.WithStackTrace(ModuleDownloadErr{sourceURL: url.String(), details: details}) + + return "", errors.New(ModuleDownloadErr{sourceURL: url.String(), details: details}) } return terraformGet, nil @@ -311,7 +315,7 @@ func GetDownloadURLFromHeader(moduleURL url.URL, terraformGet string) (string, e if strings.HasPrefix(terraformGet, "/") || strings.HasPrefix(terraformGet, "./") || strings.HasPrefix(terraformGet, "../") { relativePathURL, err := url.Parse(terraformGet) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } terraformGetURL := moduleURL.ResolveReference(relativePathURL) @@ -326,7 +330,7 @@ func GetDownloadURLFromHeader(moduleURL url.URL, terraformGet string) (string, e func httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL url.URL) ([]byte, *http.Header, error) { req, err := http.NewRequestWithContext(ctx, "GET", getURL.String(), nil) if err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } // Handle authentication via env var. Authentication is done by providing the registry token as a bearer token in @@ -338,7 +342,7 @@ func httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL url.UR resp, err := httpClient.Do(req) if err != nil { - return nil, nil, errors.WithStackTrace(err) + return nil, nil, errors.New(err) } defer func() { @@ -349,12 +353,12 @@ func httpGETAndGetResponse(ctx context.Context, logger log.Logger, getURL url.UR }() if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, nil, errors.WithStackTrace(RegistryAPIErr{url: getURL.String(), statusCode: resp.StatusCode}) + return nil, nil, errors.New(RegistryAPIErr{url: getURL.String(), statusCode: resp.StatusCode}) } bodyData, err := io.ReadAll(resp.Body) - return bodyData, &resp.Header, errors.WithStackTrace(err) + return bodyData, &resp.Header, errors.New(err) } // BuildRequestURL - create url to download module using moduleRegistryBasePath diff --git a/terraform/log.go b/terraform/log.go index ee0cbe5d84..d37b23c21b 100644 --- a/terraform/log.go +++ b/terraform/log.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/gruntwork-io/terragrunt/pkg/log/writer" ) diff --git a/terraform/source.go b/terraform/source.go index 1813cec82e..be07e2566b 100644 --- a/terraform/source.go +++ b/terraform/source.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/go-getter" urlhelper "github.com/hashicorp/go-getter/helper/url" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" ) @@ -110,13 +110,13 @@ func (src Source) WriteVersionFile() error { // This ensures we attempt to redownload the source next time. version, err = util.GenerateRandomSha256() if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } const ownerReadWriteGroupReadPerms = 0640 - return errors.WithStackTrace(os.WriteFile(src.VersionFile, []byte(version), ownerReadWriteGroupReadPerms)) + return errors.New(os.WriteFile(src.VersionFile, []byte(version), ownerReadWriteGroupReadPerms)) } // NewSource takes the given source path and create a Source struct from it, including the folder where the source should @@ -206,7 +206,7 @@ func ToSourceURL(source string, workingDir string) (*url.URL, error) { // parse the URL. rawSourceURLWithGetter, err := getter.Detect(source, workingDir, getter.Detectors) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } return parseSourceURL(rawSourceURLWithGetter) @@ -230,7 +230,7 @@ func normalizeSourceURL(source string, workingDir string) (string, error) { for _, detector := range detectors { _, ok, err := detector.Detect(newSource, workingDir) if err != nil { - return source, errors.WithStackTrace(err) + return source, errors.New(err) } if ok { @@ -257,7 +257,7 @@ func parseSourceURL(source string) (*url.URL, error) { // Parse the URL without the getter prefix canonicalSourceURL, err := urlhelper.Parse(rawSourceURL) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } // Reattach the "getter" prefix as part of the scheme @@ -283,7 +283,7 @@ func SplitSourceURL(sourceURL *url.URL, logger log.Logger) (*url.URL, string, er if len(pathSplitOnDoubleSlash) > 1 { sourceURLModifiedPath, err := parseSourceURL(sourceURL.String()) if err != nil { - return nil, "", errors.WithStackTrace(err) + return nil, "", errors.New(err) } sourceURLModifiedPath.Path = pathSplitOnDoubleSlash[0] @@ -313,7 +313,7 @@ func SplitSourceURL(sourceURL *url.URL, logger log.Logger) (*url.URL, string, er func encodeSourceName(sourceURL *url.URL) (string, error) { sourceURLNoQuery, err := parseSourceURL(sourceURL.String()) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } sourceURLNoQuery.RawQuery = "" diff --git a/terraform/terraform.go b/terraform/terraform.go index e2a794e44c..757dc77666 100644 --- a/terraform/terraform.go +++ b/terraform/terraform.go @@ -1,7 +1,7 @@ package terraform import ( - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/hashicorp/terraform-config-inspect/tfconfig" ) @@ -53,7 +53,7 @@ const ( func ModuleVariables(modulePath string) ([]string, []string, error) { module, diags := tfconfig.LoadModule(modulePath) if diags.HasErrors() { - return nil, nil, errors.WithStackTrace(diags) + return nil, nil, errors.New(diags) } required := []string{} diff --git a/test/fixtures/error-print/custom-tf-script.sh b/test/fixtures/error-print/custom-tf-script.sh index 68317042f5..7311bd4ed3 100755 --- a/test/fixtures/error-print/custom-tf-script.sh +++ b/test/fixtures/error-print/custom-tf-script.sh @@ -2,4 +2,4 @@ echo "Custom error from script" >&2 -exit 1 \ No newline at end of file +exit 1 diff --git a/test/fixtures/hooks/skip-on-error/terragrunt.hcl b/test/fixtures/hooks/skip-on-error/terragrunt.hcl index b43282829c..415963228e 100644 --- a/test/fixtures/hooks/skip-on-error/terragrunt.hcl +++ b/test/fixtures/hooks/skip-on-error/terragrunt.hcl @@ -45,6 +45,6 @@ terraform { error_hook "e" { commands = ["apply", "plan"] execute = ["echo", "PATTERN_MATCHING_ERROR_HOOK"] - on_errors = [".*executable file not found.*"] + on_errors = ["(?m).*executable file not found.*"] } } diff --git a/test/integration_aws_test.go b/test/integration_aws_test.go index 8536ec5083..ed7d404486 100644 --- a/test/integration_aws_test.go +++ b/test/integration_aws_test.go @@ -954,8 +954,8 @@ func TestAwsMockOutputsFromRemoteState(t *testing.T) { //nolint: paralleltest _, stderr, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt init --terragrunt-fetch-dependency-output-from-state --terragrunt-non-interactive --terragrunt-working-dir %s/app2", environmentPath)) require.NoError(t, err) - assert.True(t, strings.Contains(stderr, "Failed to read outputs")) - assert.True(t, strings.Contains(stderr, "fallback to mock outputs")) + assert.Contains(t, stderr, "Failed to read outputs") + assert.Contains(t, stderr, "fallback to mock outputs") } func TestAwsParallelStateInit(t *testing.T) { diff --git a/test/integration_destroy_test.go b/test/integration_destroy_test.go index 4992c2848e..f9cae42d69 100644 --- a/test/integration_destroy_test.go +++ b/test/integration_destroy_test.go @@ -13,11 +13,10 @@ import ( "testing" "github.com/gruntwork-io/terragrunt/cli/commands" - "github.com/hashicorp/go-multierror" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/util" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -199,11 +198,16 @@ func TestPreventDestroyDependenciesIncludedConfig(t *testing.T) { logBufferContentsLineByLine(t, destroyAllStdout, "destroy-all stdout") logBufferContentsLineByLine(t, destroyAllStderr, "destroy-all stderr") - if assert.Error(t, err) { - underlying := errors.Unwrap(err) - assert.IsType(t, &multierror.Error{}, underlying) + require.Error(t, err) + + var multiErrors *errors.MultiError + + if ok := errors.As(err, &multiErrors); ok { + err = multiErrors } + assert.IsType(t, &errors.MultiError{}, err) + // Check that modules C, D and E were deleted and modules A and B weren't. for moduleName, modulePath := range modulePaths { var ( diff --git a/test/integration_download_test.go b/test/integration_download_test.go index 64f90395e9..f81e25974c 100644 --- a/test/integration_download_test.go +++ b/test/integration_download_test.go @@ -8,11 +8,9 @@ import ( "strings" "testing" - "github.com/hashicorp/go-multierror" - - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/cli/commands/terraform" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" tfsource "github.com/gruntwork-io/terragrunt/terraform" "github.com/gruntwork-io/terragrunt/util" "github.com/stretchr/testify/assert" @@ -536,11 +534,16 @@ func TestPreventDestroyDependencies(t *testing.T) { logBufferContentsLineByLine(t, destroyAllStdout, "destroy-all stdout") logBufferContentsLineByLine(t, destroyAllStderr, "destroy-all stderr") - if assert.Error(t, err) { - underlying := errors.Unwrap(err) - assert.IsType(t, &multierror.Error{}, underlying) + require.Error(t, err) + + var multiErrors *errors.MultiError + + if ok := errors.As(err, &multiErrors); ok { + err = multiErrors } + assert.IsType(t, &errors.MultiError{}, err) + // Check that modules C, D and E were deleted and modules A and B weren't. for moduleName, modulePath := range modulePaths { var ( diff --git a/test/integration_gcp_test.go b/test/integration_gcp_test.go index 7b2bbf1214..3478d55680 100644 --- a/test/integration_gcp_test.go +++ b/test/integration_gcp_test.go @@ -4,7 +4,7 @@ package test_test import ( "context" - goErrors "errors" + "errors" "fmt" "os" "path" @@ -166,20 +166,15 @@ func validateGCSBucketExistsAndIsLabeled(t *testing.T, location string, bucketNa remoteStateConfig := remote.RemoteStateConfigGCS{Bucket: bucketName} gcsClient, err := remote.CreateGCSClient(remoteStateConfig) - if err != nil { - t.Fatalf("Error creating GCS client: %v", err) - } + require.NoError(t, err, "Error creating GCS client") // verify the bucket exists assert.True(t, remote.DoesGCSBucketExist(gcsClient, &remoteStateConfig), "Terragrunt failed to create remote state GCS bucket %s", bucketName) // verify the bucket location - ctx := context.Background() bucket := gcsClient.Bucket(bucketName) - attrs, err := bucket.Attrs(ctx) - if err != nil { - t.Fatal(err) - } + attrs, err := bucket.Attrs(context.Background()) + require.NoError(t, err) assert.Equal(t, strings.ToUpper(location), attrs.Location, "Did not find GCS bucket in expected location.") @@ -278,7 +273,7 @@ func deleteGCSBucket(t *testing.T, bucketName string) { for { objectAttrs, err := it.Next() - if goErrors.Is(err, iterator.Done) { + if errors.Is(err, iterator.Done) { break } diff --git a/test/integration_test.go b/test/integration_test.go index 27a41cb979..b7257010f9 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -4,7 +4,6 @@ package test_test import ( "bytes" "encoding/json" - goErrors "errors" "fmt" "io" "math/rand" @@ -18,7 +17,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/go-commons/version" "github.com/gruntwork-io/terragrunt/awshelper" "github.com/gruntwork-io/terragrunt/cli" @@ -27,6 +25,7 @@ import ( terragruntinfo "github.com/gruntwork-io/terragrunt/cli/commands/terragrunt-info" "github.com/gruntwork-io/terragrunt/codegen" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/internal/view/diagnostic" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/pkg/log" @@ -549,14 +548,15 @@ func TestTerragruntReportsTerraformErrorsWithPlanAll(t *testing.T) { stderr bytes.Buffer ) // Call runTerragruntCommand directly because this command contains failures (which causes runTerragruntRedirectOutput to abort) but we don't care. - if err := runTerragruntCommand(t, cmd, &stdout, &stderr); err == nil { - t.Fatalf("Failed to properly fail command: %v. The terraform should be bad", cmd) - } + err := runTerragruntCommand(t, cmd, &stdout, &stderr) + require.Error(t, err, "Failed to properly fail command: %v. The terraform should be bad", cmd) + output := stdout.String() errOutput := stderr.String() fmt.Printf("STDERR is %s.\n STDOUT is %s", errOutput, output) - assert.True(t, strings.Contains(errOutput, "missingvar1") || strings.Contains(output, "missingvar1")) - assert.True(t, strings.Contains(errOutput, "missingvar2") || strings.Contains(output, "missingvar2")) + + require.ErrorContains(t, err, "missingvar1") + require.ErrorContains(t, err, "missingvar2") } func TestTerragruntGraphDependenciesCommand(t *testing.T) { @@ -637,8 +637,8 @@ func TestInvalidSource(t *testing.T) { require.Error(t, err) var workingDirNotFoundErr terraform.WorkingDirNotFound - // _, ok := errors.Unwrap(err).(terraform.WorkingDirNotFound) - ok := goErrors.As(err, &workingDirNotFoundErr) + + ok := errors.As(err, &workingDirNotFoundErr) assert.True(t, ok) } @@ -826,7 +826,7 @@ func TestTerragruntMissingDependenciesFail(t *testing.T) { err := runTerragruntCommand(t, "terragrunt init --terragrunt-working-dir "+generateTestCase, &stdout, &stderr) require.Error(t, err) var parsedError config.DependencyDirNotFoundError - ok := goErrors.As(err, &parsedError) + ok := errors.As(err, &parsedError) assert.True(t, ok) assert.Len(t, parsedError.Dir, 1) assert.Contains(t, parsedError.Dir[0], "hl3-release") @@ -2174,7 +2174,7 @@ func TestTerragruntGenerateBlockRemoveTerragruntFail(t *testing.T) { require.Error(t, err) var generateFileRemoveError codegen.GenerateFileRemoveError - ok := goErrors.As(err, &generateFileRemoveError) + ok := errors.As(err, &generateFileRemoveError) assert.True(t, ok) assert.FileExists(t, filepath.Join(generateTestCase, "backend.tf")) @@ -2242,7 +2242,7 @@ func TestTerragruntGenerateBlockOverwriteTerragruntFail(t *testing.T) { err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+generateTestCase, &stdout, &stderr) require.Error(t, err) var generateFileExistsError codegen.GenerateFileExistsError - ok := goErrors.As(err, &generateFileExistsError) + ok := errors.As(err, &generateFileExistsError) assert.True(t, ok) } @@ -2312,7 +2312,7 @@ func TestTerragruntGenerateBlockSameNameFail(t *testing.T) { err := runTerragruntCommand(t, "terragrunt init --terragrunt-working-dir "+generateTestCase, &stdout, &stderr) require.Error(t, err) var parsedError config.DuplicatedGenerateBlocksError - ok := goErrors.As(err, &parsedError) + ok := errors.As(err, &parsedError) assert.True(t, ok) assert.Len(t, parsedError.BlockName, 1) assert.Contains(t, parsedError.BlockName, "backend") @@ -2330,7 +2330,7 @@ func TestTerragruntGenerateBlockSameNameIncludeFail(t *testing.T) { err := runTerragruntCommand(t, "terragrunt init --terragrunt-working-dir "+generateTestCase, &stdout, &stderr) require.Error(t, err) var parsedError config.DuplicatedGenerateBlocksError - ok := goErrors.As(err, &parsedError) + ok := errors.As(err, &parsedError) assert.True(t, ok) assert.Len(t, parsedError.BlockName, 1) assert.Contains(t, parsedError.BlockName, "backend") @@ -2348,7 +2348,7 @@ func TestTerragruntGenerateBlockMultipleSameNameFail(t *testing.T) { err := runTerragruntCommand(t, "terragrunt init --terragrunt-working-dir "+generateTestCase, &stdout, &stderr) require.Error(t, err) var parsedError config.DuplicatedGenerateBlocksError - ok := goErrors.As(err, &parsedError) + ok := errors.As(err, &parsedError) assert.True(t, ok) assert.Len(t, parsedError.BlockName, 2) assert.Contains(t, parsedError.BlockName, "backend") @@ -2423,7 +2423,7 @@ func TestTerragruntRemoteStateCodegenErrorsIfExists(t *testing.T) { err := runTerragruntCommand(t, "terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+generateTestCase, &stdout, &stderr) require.Error(t, err) var generateFileExistsError codegen.GenerateFileExistsError - ok := goErrors.As(err, &generateFileExistsError) + ok := errors.As(err, &generateFileExistsError) assert.True(t, ok) } @@ -2631,7 +2631,7 @@ func TestTerragruntVersionConstraintsPartialParse(t *testing.T) { require.Error(t, err) var invalidVersionError terraform.InvalidTerragruntVersion - ok := goErrors.As(err, &invalidVersionError) + ok := errors.As(err, &invalidVersionError) assert.True(t, ok) } @@ -2744,7 +2744,7 @@ func runTerragruntRedirectOutput(t *testing.T, command string, writer io.Writer, stderr = stderrAsBuffer.String() } - t.Fatalf("Failed to run Terragrunt command '%s' due to error: %s\n\nStdout: %s\n\nStderr: %s", command, errors.PrintErrorWithStackTrace(err), stdout, stderr) + t.Fatalf("Failed to run Terragrunt command '%s' due to error: %s\n\nStdout: %s\n\nStderr: %s", command, errors.ErrorStack(err), stdout, stderr) } } @@ -2972,7 +2972,7 @@ func TestShowErrorWhenRunAllInvokedWithoutArguments(t *testing.T) { err := runTerragruntCommand(t, "terragrunt run-all --terragrunt-non-interactive --terragrunt-working-dir "+appPath, &stdout, &stderr) require.Error(t, err) var missingCommandError runall.MissingCommand - ok := goErrors.As(err, &missingCommandError) + ok := errors.As(err, &missingCommandError) assert.True(t, ok) } @@ -3229,14 +3229,11 @@ func TestModulePathInPlanErrorMessage(t *testing.T) { tmpEnvPath := copyEnvironment(t, testFixtureModulePathError) rootPath := util.JoinPath(tmpEnvPath, testFixtureModulePathError, "app") - stdout := bytes.Buffer{} - stderr := bytes.Buffer{} - - err := runTerragruntCommand(t, "terragrunt plan -no-color --terragrunt-non-interactive --terragrunt-working-dir "+rootPath, &stdout, &stderr) + stdout, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt plan -no-color --terragrunt-non-interactive --terragrunt-working-dir "+rootPath) require.Error(t, err) - output := fmt.Sprintf("%s\n%s\n%v\n", stdout.String(), stderr.String(), err.Error()) - assert.Contains(t, output, fmt.Sprintf("[%s]", util.JoinPath(tmpEnvPath, testFixtureModulePathError, "d1"))) - assert.Contains(t, output, "1 error occurred") + output := stdout + "\n" + stderr + "\n" + err.Error() + "\n" + + assert.Contains(t, output, "error occurred") } func TestModulePathInRunAllPlanErrorMessage(t *testing.T) { @@ -3873,8 +3870,8 @@ func TestErrorMessageIncludeInOutput(t *testing.T) { cleanupTerraformFolder(t, tmpEnvPath) testPath := util.JoinPath(tmpEnvPath, testFixtureErrorPrint) - _, stderr, err := runTerragruntCommandWithOutput(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir "+testPath+" --terragrunt-tfpath "+testPath+"/custom-tf-script.sh --terragrunt-log-level debug") + _, _, err := runTerragruntCommandWithOutput(t, "terragrunt apply --terragrunt-non-interactive --terragrunt-working-dir "+testPath+" --terragrunt-tfpath "+testPath+"/custom-tf-script.sh --terragrunt-log-level debug") require.Error(t, err) - assert.Contains(t, stderr, "Custom error from script") + assert.Contains(t, err.Error(), "Custom error from script") } diff --git a/tflint/tflint.go b/tflint/tflint.go index 85014fc9cd..4c347930ac 100644 --- a/tflint/tflint.go +++ b/tflint/tflint.go @@ -12,8 +12,8 @@ import ( "github.com/gruntwork-io/terragrunt/shell" - "github.com/gruntwork-io/go-commons/errors" "github.com/gruntwork-io/terragrunt/config" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/options" "github.com/gruntwork-io/terragrunt/util" "github.com/terraform-linters/tflint/cmd" @@ -59,7 +59,7 @@ func RunTflintWithOpts(ctx context.Context, opts *options.TerragruntOptions, con cli, err := cmd.NewCLI(opts.Writer, opts.ErrWriter) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } tflintArgs, externalTfLint := tflintArguments(hook.Execute[1:]) @@ -72,14 +72,14 @@ func RunTflintWithOpts(ctx context.Context, opts *options.TerragruntOptions, con _, err := shell.RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, false, false, initArgs[0], initArgs[1:]...) if err != nil { - return errors.WithStackTrace(ErrorRunningTflint{args: initArgs}) + return errors.New(ErrorRunningTflint{args: initArgs}) } } else { opts.Logger.Debugf("Running internal tflint init with args %v", initArgs) statusCode := cli.Run(initArgs) if statusCode != 0 { - return errors.WithStackTrace(ErrorRunningTflint{args: initArgs}) + return errors.New(ErrorRunningTflint{args: initArgs}) } } @@ -98,7 +98,7 @@ func RunTflintWithOpts(ctx context.Context, opts *options.TerragruntOptions, con _, err := shell.RunShellCommandWithOutput(ctx, opts, opts.WorkingDir, false, false, args[0], args[1:]...) if err != nil { - return errors.WithStackTrace(ErrorRunningTflint{args: args}) + return errors.New(ErrorRunningTflint{args: args}) } opts.Logger.Info("Tflint has run successfully. No issues found.") @@ -108,13 +108,13 @@ func RunTflintWithOpts(ctx context.Context, opts *options.TerragruntOptions, con switch statusCode { case cmd.ExitCodeError: - return errors.WithStackTrace(ErrorRunningTflint{args: initArgs}) + return errors.New(ErrorRunningTflint{args: initArgs}) case cmd.ExitCodeIssuesFound: - return errors.WithStackTrace(IssuesFound{}) + return errors.New(IssuesFound{}) case cmd.ExitCodeOK: opts.Logger.Info("Tflint has run successfully. No issues found.") default: - return errors.WithStackTrace(UnknownError{statusCode: statusCode}) + return errors.New(UnknownError{statusCode: statusCode}) } } @@ -247,7 +247,7 @@ func findTflintConfigInProject(terragruntOptions *options.TerragruntOptions) (st terragruntOptions.Logger.Debugf("Finding .tflint.hcl file from %s and going to %s", previousDir, currentDir) if currentDir == previousDir { - return "", errors.WithStackTrace(ConfigNotFound{cause: "Traversed all the day to the root"}) + return "", errors.New(ConfigNotFound{cause: "Traversed all the day to the root"}) } fileToFind := util.JoinPath(previousDir, ".tflint.hcl") @@ -259,7 +259,7 @@ func findTflintConfigInProject(terragruntOptions *options.TerragruntOptions) (st previousDir = currentDir } - return "", errors.WithStackTrace(ConfigNotFound{ + return "", errors.New(ConfigNotFound{ cause: fmt.Sprintf("Exceeded maximum folders to check (%d)", terragruntOptions.MaxFoldersToCheck), }) } diff --git a/util/file.go b/util/file.go index 678b091f4c..09fb6c22d2 100644 --- a/util/file.go +++ b/util/file.go @@ -4,7 +4,6 @@ import ( "bytes" "crypto/sha256" "encoding/gob" - goErrors "errors" "io" "os" "path/filepath" @@ -15,7 +14,7 @@ import ( "fmt" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" "github.com/mattn/go-zglob" homedir "github.com/mitchellh/go-homedir" @@ -36,18 +35,18 @@ func FileOrData(maybePath string) (string, error) { // character is ~, and if it is, there is a high chance of it being a path instead of data contents. expandedMaybePath, err := homedir.Expand(maybePath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } if IsFile(expandedMaybePath) { contents, err := os.ReadFile(expandedMaybePath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return string(contents), nil } else if IsDir(expandedMaybePath) { - return "", errors.WithStackTrace(PathIsNotFile{path: expandedMaybePath}) + return "", errors.New(PathIsNotFile{path: expandedMaybePath}) } return expandedMaybePath, nil @@ -68,10 +67,11 @@ func FileNotExists(path string) bool { // EnsureDirectory creates a directory at this path if it does not exist, or error if the path exists and is a file. func EnsureDirectory(path string) error { if FileExists(path) && IsFile(path) { - return errors.WithStackTrace(PathIsNotDirectory{path}) + return errors.New(PathIsNotDirectory{path}) } else if !FileExists(path) { const ownerReadWriteExecutePerms = 0700 - return errors.WithStackTrace(os.MkdirAll(path, ownerReadWriteExecutePerms)) + + return errors.New(os.MkdirAll(path, ownerReadWriteExecutePerms)) } return nil @@ -87,7 +87,7 @@ func CanonicalPath(path string, basePath string) (string, error) { absPath, err := filepath.Abs(path) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return CleanPath(absPath), nil @@ -157,7 +157,7 @@ func Grep(regex *regexp.Regexp, glob string) (bool, error) { // So we use a third-party library. matches, err := zglob.Glob(glob) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } for _, match := range matches { @@ -167,7 +167,7 @@ func Grep(regex *regexp.Regexp, glob string) (bool, error) { bytes, err := os.ReadFile(match) if err != nil { - return false, errors.WithStackTrace(err) + return false, errors.New(err) } if regex.Match(bytes) { @@ -202,17 +202,17 @@ func GetPathRelativeTo(path string, basePath string) (string, error) { inputFolderAbs, err := filepath.Abs(basePath) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } fileAbs, err := filepath.Abs(path) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } relPath, err := filepath.Rel(inputFolderAbs, fileAbs) if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } return filepath.ToSlash(relPath), nil @@ -222,7 +222,7 @@ func GetPathRelativeTo(path string, basePath string) (string, error) { func ReadFileAsString(path string) (string, error) { bytes, err := os.ReadFile(path) if err != nil { - return "", errors.WithStackTraceAndPrefix(err, "Error reading file at path %s", path) + return "", errors.Errorf("error reading file at path %s: %w", path, err) } return string(bytes), nil @@ -243,9 +243,9 @@ func expandGlobPath(source, absoluteGlobPath string) ([]string, error) { includeExpandedGlobs := []string{} absoluteExpandGlob, err := zglob.Glob(absoluteGlobPath) - if err != nil && !goErrors.Is(err, os.ErrNotExist) { + if err != nil && !errors.Is(err, os.ErrNotExist) { // we ignore not exist error as we only care about the globs that exist in the src dir - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } for _, absoluteExpandGlobPath := range absoluteExpandGlob { @@ -263,7 +263,7 @@ func expandGlobPath(source, absoluteGlobPath string) ([]string, error) { if IsDir(absoluteExpandGlobPath) { dirExpandGlob, err := expandGlobPath(source, absoluteExpandGlobPath+"/*") if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } includeExpandedGlobs = append(includeExpandedGlobs, dirExpandGlob...) @@ -285,7 +285,7 @@ func CopyFolderContents(logger log.Logger, source, destination, manifestFile str expandGlob, err := expandGlobPath(source, globPath) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } includeExpandedGlobs = append(includeExpandedGlobs, expandGlob...) @@ -305,16 +305,16 @@ func CopyFolderContents(logger log.Logger, source, destination, manifestFile str func CopyFolderContentsWithFilter(logger log.Logger, source, destination, manifestFile string, filter func(absolutePath string) bool) error { const ownerReadWriteExecutePerms = 0700 if err := os.MkdirAll(destination, ownerReadWriteExecutePerms); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } manifest := NewFileManifest(logger, destination, manifestFile) if err := manifest.Clean(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := manifest.Create(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } defer func(manifest *fileManifest) { @@ -329,7 +329,7 @@ func CopyFolderContentsWithFilter(logger log.Logger, source, destination, manife // was to use filepath.Walk, but that doesn't work because it ignores symlinks. So, now we turn to filepath.Glob. files, err := filepath.Glob(source + "/*") if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } for _, file := range files { @@ -347,11 +347,11 @@ func CopyFolderContentsWithFilter(logger log.Logger, source, destination, manife if IsDir(file) { info, err := os.Lstat(file) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := os.MkdirAll(dest, info.Mode()); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := CopyFolderContentsWithFilter(logger, file, dest, manifestFile, filter); err != nil { @@ -366,7 +366,7 @@ func CopyFolderContentsWithFilter(logger log.Logger, source, destination, manife const ownerReadWriteExecutePerms = 0700 if err := os.MkdirAll(parentDir, ownerReadWriteExecutePerms); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if err := CopyFile(file, dest); err != nil { @@ -409,7 +409,7 @@ func TerragruntExcludes(path string) bool { func CopyFile(source string, destination string) error { contents, err := os.ReadFile(source) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return WriteFileWithSamePermissions(source, destination, contents) @@ -420,7 +420,7 @@ func CopyFile(source string, destination string) error { func WriteFileWithSamePermissions(source string, destination string, contents []byte) error { fileInfo, err := os.Stat(source) if err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } return os.WriteFile(destination, contents, fileInfo.Mode()) @@ -559,7 +559,7 @@ func (manifest *fileManifest) clean(manifestPath string) error { err = decoder.Decode(&manifestEntry) if err != nil { - if goErrors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) { break } else { return err @@ -569,11 +569,11 @@ func (manifest *fileManifest) clean(manifestPath string) error { if manifestEntry.IsDir { // join the directory entry path with the manifest file name and call clean() if err := manifest.clean(filepath.Join(manifestEntry.Path, manifest.ManifestFile)); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } else { if err := os.Remove(manifestEntry.Path); err != nil && !os.IsNotExist(err) { - return errors.WithStackTrace(err) + return errors.New(err) } } } @@ -677,14 +677,14 @@ func IsDirectoryEmpty(dirPath string) (bool, error) { func GetCacheDir() (string, error) { cacheDir, err := os.UserCacheDir() if err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } cacheDir = filepath.Join(cacheDir, "terragrunt") if !FileExists(cacheDir) { if err := os.MkdirAll(cacheDir, os.ModePerm); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } @@ -697,7 +697,7 @@ func GetTempDir() (string, error) { if !FileExists(tempDir) { if err := os.MkdirAll(tempDir, os.ModePerm); err != nil { - return "", errors.WithStackTrace(err) + return "", errors.New(err) } } @@ -762,7 +762,7 @@ func MatchSha256Checksum(file, filename []byte) []byte { func FileSHA256(filePath string) ([]byte, error) { file, err := os.Open(filePath) if err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } defer file.Close() //nolint:errcheck @@ -772,7 +772,7 @@ func FileSHA256(filePath string) ([]byte, error) { for { n, err := file.Read(buffer) if err != nil && err != io.EOF { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } if n == 0 { @@ -780,7 +780,7 @@ func FileSHA256(filePath string) ([]byte, error) { } if _, err := hash.Write(buffer[:n]); err != nil { - return nil, errors.WithStackTrace(err) + return nil, errors.New(err) } } diff --git a/util/lockfile.go b/util/lockfile.go index 3fce6a26eb..7f0f0c08cb 100644 --- a/util/lockfile.go +++ b/util/lockfile.go @@ -4,7 +4,7 @@ import ( "os" "github.com/gofrs/flock" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" ) type Lockfile struct { @@ -22,12 +22,12 @@ func (lockfile *Lockfile) Unlock() error { return nil } if err := lockfile.Flock.Unlock(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } if FileExists(lockfile.Path()) { if err := os.Remove(lockfile.Path()); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } } @@ -36,7 +36,7 @@ func (lockfile *Lockfile) Unlock() error { func (lockfile *Lockfile) TryLock() error { if locked, err := lockfile.Flock.TryLock(); err != nil { - return errors.WithStackTrace(err) + return errors.New(err) } else if !locked { return errors.Errorf("unable to lock file %s", lockfile.Path()) } diff --git a/util/retry.go b/util/retry.go index 43ce873cdb..d659e25302 100644 --- a/util/retry.go +++ b/util/retry.go @@ -2,11 +2,10 @@ package util import ( "context" - goErrors "errors" "fmt" "time" - "github.com/gruntwork-io/go-commons/errors" + "github.com/gruntwork-io/terragrunt/internal/errors" "github.com/gruntwork-io/terragrunt/pkg/log" ) @@ -23,13 +22,14 @@ func DoWithRetry(ctx context.Context, actionDescription string, maxRetries int, } var fatalErr FatalError - if ok := goErrors.As(err, &fatalErr); ok { + if ok := errors.As(err, &fatalErr); ok { return err } if ctx.Err() != nil { logger.Debugf("%s returned an error: %s.", actionDescription, err.Error()) - return errors.WithStackTrace(ctx.Err()) + + return errors.New(ctx.Err()) } logger.Errorf("%s returned an error: %s. Retry %d of %d. Sleeping for %s and will try again.", actionDescription, err.Error(), i, maxRetries, sleepBetweenRetries) @@ -37,7 +37,7 @@ func DoWithRetry(ctx context.Context, actionDescription string, maxRetries int, select { case <-time.After(sleepBetweenRetries): // Try again case <-ctx.Done(): - return errors.WithStackTrace(ctx.Err()) + return errors.New(ctx.Err()) } } diff --git a/util/shell.go b/util/shell.go index 17b8a1d160..62b1dd6759 100644 --- a/util/shell.go +++ b/util/shell.go @@ -1,13 +1,14 @@ package util import ( - goErrors "errors" + "bytes" "fmt" - "os/exec" + "strings" "syscall" - "github.com/gruntwork-io/go-commons/errors" - "github.com/hashicorp/go-multierror" + "os/exec" + + "github.com/gruntwork-io/terragrunt/internal/errors" ) // IsCommandExecutable - returns true if a command can be executed without errors. @@ -19,7 +20,7 @@ func IsCommandExecutable(command string, args ...string) bool { if err := cmd.Run(); err != nil { var exitErr *exec.ExitError - if ok := goErrors.As(err, &exitErr); ok { + if ok := errors.As(err, &exitErr); ok { return exitErr.ExitCode() == 0 } @@ -30,13 +31,13 @@ func IsCommandExecutable(command string, args ...string) bool { } type CmdOutput struct { - Stdout string - Stderr string + Stdout bytes.Buffer + Stderr bytes.Buffer } // GetExitCode returns the exit code of a command. If the error does not // implement iErrorCode or is not an exec.ExitError -// or *multierror.Error type, the error is returned. +// or *errors.MultiError type, the error is returned. func GetExitCode(err error) (int, error) { // Interface to determine if we can retrieve an exit status from an error type iErrorCode interface { @@ -48,14 +49,14 @@ func GetExitCode(err error) (int, error) { } var exiterr *exec.ExitError - if ok := goErrors.As(err, &exiterr); ok { + if ok := errors.As(err, &exiterr); ok { status := exiterr.Sys().(syscall.WaitStatus) return status.ExitStatus(), nil } - var multiErr *multierror.Error - if ok := goErrors.As(err, &multiErr); ok { - for _, err := range multiErr.Errors { + var multiErr *errors.MultiError + if ok := errors.As(err, &multiErr); ok { + for _, err := range multiErr.WrappedErrors() { exitCode, exitCodeErr := GetExitCode(err) if exitCodeErr == nil { return exitCode, nil @@ -69,32 +70,25 @@ func GetExitCode(err error) (int, error) { // ProcessExecutionError - error returned when a command fails, contains StdOut and StdErr type ProcessExecutionError struct { Err error - Stdout string - Stderr string + Output CmdOutput WorkingDir string + Command string + Args []string } func (err ProcessExecutionError) Error() string { - // Include in error message the working directory where the command was run, so it's easier for the user to - return fmt.Sprintf("[%s] %s", err.WorkingDir, err.Err.Error()) + return fmt.Sprintf("Failed to execute \"%s %s\" in %s\n%s\n%v", + err.Command, + strings.Join(err.Args, " "), + err.WorkingDir, + err.Output.Stderr.String(), + err.Err) } func (err ProcessExecutionError) ExitStatus() (int, error) { return GetExitCode(err.Err) } -func Unwrap[V error](err error) *V { - var target = new(V) - - for { - if ok := goErrors.As(err, target); ok { - return target - } - - if err = goErrors.Unwrap(err); err == nil { - break - } - } - - return target +func (err ProcessExecutionError) Unwrap() error { + return err.Err }