Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Improve *-all error message output #722

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
6 changes: 5 additions & 1 deletion cli/cli_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,11 @@ func checkDeprecated(command string, terragruntOptions *options.TerragruntOption
// terragrunt command
func runCommand(command string, terragruntOptions *options.TerragruntOptions) (finalEff error) {
if isMultiModuleCommand(command) {
return runMultiModuleCommand(command, terragruntOptions)
err := runMultiModuleCommand(command, terragruntOptions)

// wait until all detailed errors have been printed
<-configstack.DonePrintingDetailedError
return err
}
return runTerragrunt(terragruntOptions)
}
Expand Down
20 changes: 20 additions & 0 deletions configstack/running_module.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package configstack

import (
"bytes"
"fmt"
"io"
"strings"
"sync"

Expand All @@ -27,6 +29,8 @@ type runningModule struct {
Dependencies map[string]*runningModule
NotifyWhenDone []*runningModule
FlagExcluded bool
OutStream bytes.Buffer
Writer io.Writer
}

// This controls in what order dependencies should be enforced between modules
Expand All @@ -48,6 +52,7 @@ func newRunningModule(module *TerraformModule) *runningModule {
Dependencies: map[string]*runningModule{},
NotifyWhenDone: []*runningModule{},
FlagExcluded: module.FlagExcluded,
Writer: module.TerragruntOptions.Writer,
}
}

Expand Down Expand Up @@ -130,6 +135,7 @@ func removeFlagExcluded(modules map[string]*runningModule) map[string]*runningMo
Err: module.Err,
NotifyWhenDone: module.NotifyWhenDone,
Status: module.Status,
Writer: module.Module.TerragruntOptions.Writer,
}

// Only add dependencies that should not be excluded
Expand All @@ -154,6 +160,8 @@ func runModules(modules map[string]*runningModule) error {
waitGroup.Add(1)
go func(module *runningModule) {
defer waitGroup.Done()
module.Module.TerragruntOptions.Writer = &module.OutStream
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit confused by this... You're overwriting TerragruntOptions.ErrWriter to write to both module.OutStream and module.ErrStream... But what was the Terragrunt.LOptions.ErrWriter value set to before that? Are module.OutStream and module.ErrStream initialized to anything? Will this buffer those errors until the very end or stream to stdout / stderr?

Same questions go for TerragruntOptions.Writer, with the additional one of what happens when you point a second item to module.OutStream?

module.Module.TerragruntOptions.ErrWriter = &module.OutStream
module.runModuleWhenReady()
}(module)
}
Expand All @@ -170,9 +178,17 @@ func collectErrors(modules map[string]*runningModule) error {
for _, module := range modules {
if module.Err != nil {
errs = append(errs, module.Err)

// send all non dependency errors to the DetailedErrorChan
if _, isDepErr := module.Err.(DependencyFinishedWithError); !isDepErr {
DetailedErrorChan <- map[string]string{module.Module.Path: module.OutStream.String()}
}
}
}

// close the DetailedErrorChan after all errors have been sent
close(DetailedErrorChan)

if len(errs) == 0 {
return nil
} else {
Expand Down Expand Up @@ -223,6 +239,7 @@ func (module *runningModule) runNow() error {
module.Module.TerragruntOptions.Logger.Printf("Running module %s now", module.Module.Path)
return module.Module.TerragruntOptions.RunTerragrunt(module.Module.TerragruntOptions)
}

}

// Record that a module has finished executing and notify all of this module's dependencies
Expand All @@ -233,6 +250,9 @@ func (module *runningModule) moduleFinished(moduleErr error) {
module.Module.TerragruntOptions.Logger.Printf("Module %s has finished with an error: %v", module.Module.Path, moduleErr)
}

// print the separated module output
fmt.Fprintf(module.Writer, "%s\n%v\n\n%v\n", OutputMessageSeparator, module.Module.Path, module.OutStream.String())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why fmt.Fprintf here?


module.Status = Finished
module.Err = moduleErr

Expand Down
58 changes: 57 additions & 1 deletion configstack/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import (
"fmt"
"strings"

"sort"

"github.com/gruntwork-io/terragrunt/config"
"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/util"
"sort"
)

// Represents a stack of Terraform modules (i.e. folders with Terraform templates) that you can "spin up" or
Expand All @@ -19,6 +20,17 @@ type Stack struct {
Modules []*TerraformModule
}

var (
// DetailedErrorMap is a map which contains the module name and matching detailed error messages
DetailedErrorMap map[string][]string
// DetailedErrorChan is the channel which processes all detailed error messages
DetailedErrorChan chan map[string]string
// DonePrintingDetailedError is a channel which blocks the go routine until all detailed errors have been displayed
DonePrintingDetailedError chan bool
// OutputMessageSeparator is the string used for separating the different module outputs
OutputMessageSeparator = strings.Repeat("-", 132)
)

// Render this stack as a human-readable string
func (stack *Stack) String() string {
modules := []string{}
Expand Down Expand Up @@ -125,6 +137,9 @@ func createStackForTerragruntConfigPaths(path string, terragruntConfigPaths []st
return nil, errors.WithStackTrace(NoTerraformModulesFound)
}

// configure the channels for collecting detailed error messages
setupDetailedErrorChannel(terragruntOptions)

modules, err := ResolveTerraformModules(terragruntConfigPaths, terragruntOptions, howThesePathsWereFound)
if err != nil {
return nil, err
Expand All @@ -138,6 +153,47 @@ func createStackForTerragruntConfigPaths(path string, terragruntConfigPaths []st
return stack, nil
}

// setupDetailedErrorChannel configures the channels responsible for storing detailed error messages
func setupDetailedErrorChannel(terragruntOptions *options.TerragruntOptions) {
DetailedErrorMap = make(map[string][]string)
DetailedErrorChan = make(chan map[string]string)
DonePrintingDetailedError = make(chan bool)

// process all detailed error messages
go collectModuleErrors(terragruntOptions)
}

// collectModuleErrors listens on the DetailedErrorChan and maps errors to their matching modules
func collectModuleErrors(terragruntOptions *options.TerragruntOptions) {
for {
errorMap, more := <-DetailedErrorChan
if more {
for module, err := range errorMap {
DetailedErrorMap[module] = append(DetailedErrorMap[module], err)
}
} else {
printDetailedErrorSummary(terragruntOptions)
DonePrintingDetailedError <- true
return
}
}
}

// printDetailedErrorSummary logs the detailed error messages
func printDetailedErrorSummary(terragruntOptions *options.TerragruntOptions) {
summaryMessage := []string{}
summaryMessage = append(summaryMessage, "Encountered the following root-causes:")
for module, errSlice := range DetailedErrorMap {
summaryMessage = append(summaryMessage, OutputMessageSeparator)
summaryMessage = append(summaryMessage, fmt.Sprintf("Module %s:", module))
for _, err := range errSlice {
summaryMessage = append(summaryMessage, err)
}
}

terragruntOptions.Logger.Printf("%s \n", strings.Join(summaryMessage, "\n"))
}

// Custom error types

var NoTerraformModulesFound = fmt.Errorf("Could not find any subfolders with Terragrunt configuration files")
Expand Down
6 changes: 6 additions & 0 deletions shell/run_shell_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ func RunShellCommand(terragruntOptions *options.TerragruntOptions, command strin
return err
}

func RunTerraformCommandAndRedirectOutputToLogger(terragruntOptions *options.TerragruntOptions, args ...string) error {
output, err := RunShellCommandWithOutput(terragruntOptions, "", terragruntOptions.TerraformPath, args...)
terragruntOptions.Logger.Println(output)
return err
}

// Run the given Terraform command, writing its stdout/stderr to the terminal AND returning stdout/stderr to this
// method's caller
func RunTerraformCommandWithOutput(terragruntOptions *options.TerragruntOptions, args ...string) (*CmdOutput, error) {
Expand Down