Skip to content

Commit

Permalink
Errors block (#3584)
Browse files Browse the repository at this point in the history
* Error configs

* Retry config update

* add ignore config

* Spacing cleanup

* errors config parse

* Errors clonning

* Add cty values parsing

* Errors config clone

* retry config fix

* Errors block extraction

* Retry update

* cty test update

* metadata errors

* cty errors parsing

* errors config setup

* add errors fields

* Update cty test

* Errors block proecessing

* Default errors block handling

* errors block parsing

* Errors handling test

* Add function to process errors

* Retry errors execution

* Errors handling

* Ignore example execution

* add check for not handling example2 errors

* Run all ignore test

* add test for retries

* Add test with multiple retry errors

* Add test for failing retries

* Add example for signal generation

* add searching of parent

* Example run all test

* Fixed linter errors

* errors feature flag

* Markdown docs update

* Feature flags and errors block

* Markdown update

* Markdown update

* Tests update

* Log message update

* Description update

* add intreruption handling

* options update

* Docs update

* Markdown update

* Usage of compilled regexpes

* Lint fixes
  • Loading branch information
denis256 authored Dec 3, 2024
1 parent 0e8342f commit ba1e4b5
Show file tree
Hide file tree
Showing 37 changed files with 1,194 additions and 16 deletions.
19 changes: 16 additions & 3 deletions cli/commands/terraform/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,23 @@ func runTerraform(ctx context.Context, terragruntOptions *options.TerragruntOpti

terragruntOptions.Engine = engine

errConfig, err := terragruntConfig.ErrorsConfig()
if err != nil {
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

terragruntOptions.Errors = errConfig

terragruntOptionsClone, err := terragruntOptions.Clone(terragruntOptions.TerragruntConfigPath)
if err != nil {
return err
}

terragruntOptionsClone.TerraformCommand = CommandNameTerragruntReadConfig

if err := processHooks(ctx, terragruntConfig.Terraform.GetAfterHooks(), terragruntOptionsClone, terragruntConfig, nil); err != nil {
if err = terragruntOptionsClone.RunWithErrorHandling(ctx, func() error {
return processHooks(ctx, terragruntConfig.Terraform.GetAfterHooks(), terragruntOptionsClone, terragruntConfig, nil)
}); err != nil {
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

Expand All @@ -142,7 +151,9 @@ func runTerraform(ctx context.Context, terragruntOptions *options.TerragruntOpti
terragruntOptions.OriginalIAMRoleOptions,
)

if err := credsGetter.ObtainAndUpdateEnvIfNecessary(ctx, terragruntOptions, amazonsts.NewProvider(terragruntOptions)); err != nil {
if err := terragruntOptions.RunWithErrorHandling(ctx, func() error {
return credsGetter.ObtainAndUpdateEnvIfNecessary(ctx, terragruntOptions, amazonsts.NewProvider(terragruntOptions))
}); err != nil {
return err
}

Expand Down Expand Up @@ -234,7 +245,9 @@ func runTerraform(ctx context.Context, terragruntOptions *options.TerragruntOpti
}
}

if err := runTerragruntWithConfig(ctx, terragruntOptions, updatedTerragruntOptions, terragruntConfig, target); err != nil {
if err := terragruntOptions.RunWithErrorHandling(ctx, func() error {
return runTerragruntWithConfig(ctx, terragruntOptions, updatedTerragruntOptions, terragruntConfig, target)
}); err != nil {
return target.runErrorCallback(terragruntOptions, terragruntConfig, err)
}

Expand Down
5 changes: 5 additions & 0 deletions config/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,11 @@ func convertToTerragruntCatalogConfig(ctx *ParsingContext, configPath string, te
terragruntConfig.SetFieldMetadata(MetadataExclude, defaultMetadata)
}

if terragruntConfigFromFile.Errors != nil {
terragruntConfig.Errors = terragruntConfigFromFile.Errors
terragruntConfig.SetFieldMetadata(MetadataErrors, defaultMetadata)
}

if ctx.Locals != nil && *ctx.Locals != cty.NilVal {
// we should ignore any errors from `parseCtyValueToMap` as some `locals` values might have been incorrectly evaluated, that results to `json.Unmarshal` error.
// for example if the locals block looks like `{"var1":, "var2":"value2"}`, `parseCtyValueToMap` returns the map with "var2" value and an syntax error,
Expand Down
89 changes: 89 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"

Expand Down Expand Up @@ -69,6 +70,9 @@ const (
MetadataInclude = "include"
MetadataFeatureFlag = "feature"
MetadataExclude = "exclude"
MetadataErrors = "errors"
MetadataRetry = "retry"
MetadataIgnore = "ignore"
)

var (
Expand Down Expand Up @@ -125,6 +129,7 @@ type TerragruntConfig struct {
Engine *EngineConfig
FeatureFlags FeatureFlags
Exclude *ExcludeConfig
Errors *ErrorsConfig

// Fields used for internal tracking
// Indicates whether this is the result of a partial evaluation
Expand Down Expand Up @@ -197,6 +202,7 @@ type terragruntConfigFile struct {
TerragruntDependencies []Dependency `hcl:"dependency,block"`
FeatureFlags []*FeatureFlag `hcl:"feature,block"`
Exclude *ExcludeConfig `hcl:"exclude,block"`
Errors *ErrorsConfig `hcl:"errors,block"`

// We allow users to configure code generation via blocks:
//
Expand Down Expand Up @@ -1169,6 +1175,11 @@ func convertToTerragruntConfig(ctx *ParsingContext, configPath string, terragrun
terragruntConfig.SetFieldMetadata(MetadataExclude, defaultMetadata)
}

if terragruntConfigFromFile.Errors != nil {
terragruntConfig.Errors = terragruntConfigFromFile.Errors
terragruntConfig.SetFieldMetadata(MetadataErrors, defaultMetadata)
}

generateBlocks := []terragruntGenerateBlock{}
generateBlocks = append(generateBlocks, terragruntConfigFromFile.GenerateBlocks...)

Expand Down Expand Up @@ -1434,3 +1445,81 @@ func (cfg *TerragruntConfig) EngineOptions() (*options.EngineOptions, error) {
Meta: meta,
}, nil
}

// ErrorsConfig fetch errors configuration for options package
func (cfg *TerragruntConfig) ErrorsConfig() (*options.ErrorsConfig, error) {
if cfg.Errors == nil {
return nil, nil
}

result := &options.ErrorsConfig{
Retry: make(map[string]*options.RetryConfig),
Ignore: make(map[string]*options.IgnoreConfig),
}

for _, retryBlock := range cfg.Errors.Retry {
if retryBlock == nil {
continue
}

compiledPatterns := make([]*regexp.Regexp, 0, len(retryBlock.RetryableErrors))

for _, pattern := range retryBlock.RetryableErrors {
compiled, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("invalid retry pattern %q in block %q: %w",
pattern, retryBlock.Label, err)
}

compiledPatterns = append(compiledPatterns, compiled)
}

result.Retry[retryBlock.Label] = &options.RetryConfig{
Name: retryBlock.Label,
RetryableErrors: compiledPatterns,
MaxAttempts: retryBlock.MaxAttempts,
SleepIntervalSec: retryBlock.SleepIntervalSec,
}
}

for _, ignoreBlock := range cfg.Errors.Ignore {
if ignoreBlock == nil {
continue
}

var signals map[string]interface{}

if ignoreBlock.Signals != nil {
value, err := convertValuesMapToCtyVal(ignoreBlock.Signals)
if err != nil {
return nil, err
}

signals, err = ParseCtyValueToMap(value)
if err != nil {
return nil, err
}
}

compiledPatterns := make([]*regexp.Regexp, 0, len(ignoreBlock.IgnorableErrors))

for _, pattern := range ignoreBlock.IgnorableErrors {
compiled, err := regexp.Compile(pattern)
if err != nil {
return nil, fmt.Errorf("invalid retry pattern %q in block %q: %w",
pattern, ignoreBlock.Label, err)
}

compiledPatterns = append(compiledPatterns, compiled)
}

result.Ignore[ignoreBlock.Label] = &options.IgnoreConfig{
Name: ignoreBlock.Label,
IgnorableErrors: compiledPatterns,
Message: ignoreBlock.Message,
Signals: signals,
}
}

return result, nil
}
38 changes: 38 additions & 0 deletions config/config_as_cty.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ func TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
output[MetadataExclude] = excludeConfigCty
}

errorsConfigCty, err := errorsConfigAsCty(config.Errors)
if err != nil {
return cty.NilVal, err
}

if errorsConfigCty != cty.NilVal {
output[MetadataErrors] = errorsConfigCty
}

terraformConfigCty, err := terraformConfigAsCty(config.Terraform)
if err != nil {
return cty.NilVal, err
Expand Down Expand Up @@ -649,6 +658,35 @@ func featureFlagsBlocksAsCty(featureFlagBlocks FeatureFlags) (cty.Value, error)
return convertValuesMapToCtyVal(out)
}

// Serialize errors configuration as cty.Value.
func errorsConfigAsCty(config *ErrorsConfig) (cty.Value, error) {
if config == nil {
return cty.NilVal, nil
}

output := map[string]cty.Value{}

retryCty, err := goTypeToCty(config.Retry)
if err != nil {
return cty.NilVal, err
}

if retryCty != cty.NilVal {
output[MetadataRetry] = retryCty
}

ignoreCty, err := goTypeToCty(config.Ignore)
if err != nil {
return cty.NilVal, err
}

if ignoreCty != cty.NilVal {
output[MetadataIgnore] = ignoreCty
}

return convertValuesMapToCtyVal(output)
}

// Converts arbitrary go types that are json serializable to a cty Value by using json as an intermediary
// representation. This avoids the strict type nature of cty, where you need to know the output type beforehand to
// serialize to cty.
Expand Down
20 changes: 20 additions & 0 deletions config/config_as_cty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ func TestTerragruntConfigAsCtyDrift(t *testing.T) {
Default: &cty.Zero,
},
},
Errors: &config.ErrorsConfig{
Retry: []*config.RetryBlock{
{
Label: "test",
RetryableErrors: []string{"test"},
MaxAttempts: 0,
SleepIntervalSec: 0,
},
},
Ignore: []*config.IgnoreBlock{
{
Label: "test",
IgnorableErrors: nil,
Message: "",
Signals: nil,
},
},
},
GenerateConfigs: map[string]codegen.GenerateConfig{
"provider": {
Path: "foo",
Expand Down Expand Up @@ -260,6 +278,8 @@ func terragruntConfigStructFieldToMapKey(t *testing.T, fieldName string) (string
return "feature", true
case "Exclude":
return "exclude", true
case "Errors":
return "errors", true
default:
t.Fatalf("Unknown struct property: %s", fieldName)
// This should not execute
Expand Down
29 changes: 21 additions & 8 deletions config/config_partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
RemoteStateBlock
FeatureFlagsBlock
ExcludeBlock
ErrorsBlock
)

// terragruntIncludeMultiple is a struct that can be used to only decode the include block with labels.
Expand All @@ -54,10 +55,10 @@ type terragruntFeatureFlags struct {
Remain hcl.Body `hcl:",remain"`
}

// terragruntExclude is a struct that can be used to only decode the exclude block.
type terragruntExclude struct {
Exclude *ExcludeConfig `hcl:"exclude,block"`
Remain hcl.Body `hcl:",remain"`
// terragruntErrors struct to decode errors block
type terragruntErrors struct {
Errors *ErrorsConfig `hcl:"errors,block"`
Remain hcl.Body `hcl:",remain"`
}

// terragruntTerraform is a struct that can be used to only decode the terraform block.
Expand Down Expand Up @@ -535,9 +536,7 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
}

case ExcludeBlock:
decoded := terragruntExclude{}
err := file.Decode(&decoded, evalParsingContext)

decoded, err := processExcludes(ctx, output, file)
if err != nil {
return nil, err
}
Expand All @@ -548,12 +547,26 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
output.Exclude = decoded.Exclude
}

case ErrorsBlock:
decoded := terragruntErrors{}
err := file.Decode(&decoded, evalParsingContext)

if err != nil {
return nil, err
}

if output.Errors != nil {
output.Errors.Merge(decoded.Errors)
} else {
output.Errors = decoded.Errors
}

default:
return nil, InvalidPartialBlockName{decode}
}
}

// If this file includes another, parse and merge the partial blocks. Otherwise just return this config.
// If this file includes another, parse and merge the partial blocks. Otherwise, just return this config.
if len(ctx.TrackInclude.CurrentList) > 0 {
config, err := handleInclude(ctx, output, true)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion config/dependency.go
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ func getOutputJSONWithCaching(ctx *ParsingContext, targetConfig string) ([]byte,
// - The config path to the dependency module's config
// - The original config path to the dependency module's config
//
// That way, everything in that dependnecy happens within its own ctx.
// That way, everything in that dependency happens within its own ctx.
func cloneTerragruntOptionsForDependency(ctx *ParsingContext, targetConfigPath string) (*options.TerragruntOptions, error) {
targetOptions, err := ctx.TerragruntOptions.Clone(targetConfigPath)
if err != nil {
Expand Down
Loading

0 comments on commit ba1e4b5

Please sign in to comment.