Skip to content

Commit

Permalink
Exclude block (#3551)
Browse files Browse the repository at this point in the history
* Add exclude block parsing

* add exclude block parsing

* Exclude parsing

* Handling excludes from includes

* Add exclude config

* Working exclude config parsing

* Flag value conversion

* Modules excluding

* Updated log messages

* Add exclusions handling

* version check cleanup

* Add handling of "all" actions

* Linter fixes

* Added handling of exclude output actions

* linter update

* Add exclude flag default

* Add test for default exclusions

* Test exclude disabled

* Add test to exclude by action

* Set exclude flag test

* Dependencies exclusion changes

* Added example documentation

* Add test for outputs fetching

* markdown lint fixes

* Strict linter fixes

* Integration test update

* Exclude documentation update

* imports fixes
  • Loading branch information
denis256 authored Nov 19, 2024
1 parent d8adfda commit f23a6b6
Show file tree
Hide file tree
Showing 39 changed files with 757 additions and 84 deletions.
5 changes: 5 additions & 0 deletions config/catalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,11 @@ func convertToTerragruntCatalogConfig(ctx *ParsingContext, configPath string, te
terragruntConfig.SetFieldMetadata(MetadataEngine, defaultMetadata)
}

if terragruntConfigFromFile.Exclude != nil {
terragruntConfig.Exclude = terragruntConfigFromFile.Exclude
terragruntConfig.SetFieldMetadata(MetadataExclude, 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
13 changes: 11 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const (
MetadataDependentModules = "dependent_modules"
MetadataInclude = "include"
MetadataFeatureFlag = "feature"
MetadataExclude = "exclude"
)

var (
Expand Down Expand Up @@ -123,6 +124,7 @@ type TerragruntConfig struct {
RetrySleepIntervalSec *int
Engine *EngineConfig
FeatureFlags FeatureFlags
Exclude *ExcludeConfig

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

// We allow users to configure code generation via blocks:
//
Expand Down Expand Up @@ -887,7 +890,7 @@ func ParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChild *Inc
return nil, err
}

// If this file includes another, parse and merge it. Otherwise just return this config.
// If this file includes another, parse and merge it. Otherwise, just return this config.
if ctx.TrackInclude != nil {
mergedConfig, err := handleInclude(ctx, config, false)
if err != nil {
Expand All @@ -901,6 +904,7 @@ func ParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChild *Inc
// original locals for the current config being handled, as that is the locals list that is in scope for this
// config.
mergedConfig.Locals = config.Locals
mergedConfig.Exclude = config.Exclude

return mergedConfig, nil
}
Expand Down Expand Up @@ -1153,13 +1157,18 @@ func convertToTerragruntConfig(ctx *ParsingContext, configPath string, terragrun
terragruntConfig.SetFieldMetadata(MetadataEngine, defaultMetadata)
}

if terragruntConfig.FeatureFlags != nil {
if terragruntConfigFromFile.FeatureFlags != nil {
terragruntConfig.FeatureFlags = terragruntConfigFromFile.FeatureFlags
for _, flag := range terragruntConfig.FeatureFlags {
terragruntConfig.SetFieldMetadataWithType(MetadataFeatureFlag, flag.Name, defaultMetadata)
}
}

if terragruntConfigFromFile.Exclude != nil {
terragruntConfig.Exclude = terragruntConfigFromFile.Exclude
terragruntConfig.SetFieldMetadata(MetadataExclude, defaultMetadata)
}

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

Expand Down
40 changes: 38 additions & 2 deletions config/config_as_cty.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ func TerragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
output[MetadataEngine] = engineConfigCty
}

excludeConfigCty, err := excludeConfigAsCty(config.Exclude)
if err != nil {
return cty.NilVal, err
}

if excludeConfigCty != cty.NilVal {
output[MetadataExclude] = excludeConfigCty
}

terraformConfigCty, err := terraformConfigAsCty(config.Terraform)
if err != nil {
return cty.NilVal, err
Expand Down Expand Up @@ -461,6 +470,13 @@ type ctyEngineConfig struct {
Meta cty.Value `cty:"meta"`
}

// ctyExclude exclude representation for cty.
type ctyExclude struct {
If bool `cty:"if"`
Actions []string `cty:"actions"`
ExcludeDependencies bool `cty:"exclude_dependencies"`
}

// Serialize CatalogConfig to a cty Value, but with maps instead of lists for the blocks.
func catalogConfigAsCty(config *CatalogConfig) (cty.Value, error) {
if config == nil {
Expand Down Expand Up @@ -504,6 +520,26 @@ func engineConfigAsCty(config *EngineConfig) (cty.Value, error) {
return goTypeToCty(configCty)
}

// excludeConfigAsCty serialize exclude configuration to a cty Value.
func excludeConfigAsCty(config *ExcludeConfig) (cty.Value, error) {
if config == nil {
return cty.NilVal, nil
}

excludeDependencies := false
if config.ExcludeDependencies != nil {
excludeDependencies = *config.ExcludeDependencies
}

configCty := ctyExclude{
If: config.If,
Actions: config.Actions,
ExcludeDependencies: excludeDependencies,
}

return goTypeToCty(configCty)
}

// CtyTerraformConfig is an alternate representation of TerraformConfig that converts internal blocks into a map that
// maps the name to the underlying struct, as opposed to a list representation.
type CtyTerraformConfig struct {
Expand Down Expand Up @@ -598,10 +634,10 @@ func dependencyBlocksAsCty(dependencyBlocks Dependencies) (cty.Value, error) {
}

// Serialize the list of feature flags to a cty Value as a map that maps the feature names to the cty representation.
func featureFlagsBlocksAsCty(dependencyBlocks FeatureFlags) (cty.Value, error) {
func featureFlagsBlocksAsCty(featureFlagBlocks FeatureFlags) (cty.Value, error) {
out := map[string]cty.Value{}

for _, feature := range dependencyBlocks {
for _, feature := range featureFlagBlocks {
featureCty, err := goTypeToCty(feature)
if err != nil {
return cty.NilVal, err
Expand Down
3 changes: 3 additions & 0 deletions config/config_as_cty_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ func TestTerragruntConfigAsCtyDrift(t *testing.T) {
}`,
},
},
Exclude: &config.ExcludeConfig{},
}
ctyVal, err := config.TerragruntConfigAsCty(&testConfig)
require.NoError(t, err)
Expand Down Expand Up @@ -257,6 +258,8 @@ func terragruntConfigStructFieldToMapKey(t *testing.T, fieldName string) (string
return "engine", true
case "FeatureFlags":
return "feature", true
case "Exclude":
return "exclude", true
default:
t.Fatalf("Unknown struct property: %s", fieldName)
// This should not execute
Expand Down
149 changes: 109 additions & 40 deletions config/config_partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const (
TerragruntVersionConstraints
RemoteStateBlock
FeatureFlagsBlock
ExcludeBlock
)

// terragruntIncludeMultiple is a struct that can be used to only decode the include block with labels.
Expand All @@ -53,6 +54,12 @@ 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"`
}

// terragruntTerraform is a struct that can be used to only decode the terraform block.
type terragruntTerraform struct {
Terraform *TerraformConfig `hcl:"terraform,block"`
Expand Down Expand Up @@ -141,25 +148,69 @@ func DecodeBaseBlocks(ctx *ParsingContext, file *hclparse.File, includeFromChild
return nil, err
}

flagsAsCtyVal, err := flagsAsCty(ctx, tgFlags.FeatureFlags)
if err != nil {
return nil, err
}

// Evaluate all the expressions in the locals block separately and generate the variables list to use in the
// evaluation ctx.
locals, err := EvaluateLocalsBlock(ctx.WithTrackInclude(trackInclude).WithFeatures(&flagsAsCtyVal), file)
if err != nil {
return nil, err
}

localsAsCtyVal, err := convertValuesMapToCtyVal(locals)
if err != nil {
return nil, err
}

return &DecodedBaseBlocks{
TrackInclude: trackInclude,
Locals: &localsAsCtyVal,
FeatureFlags: &flagsAsCtyVal,
}, nil
}

func flagsAsCty(ctx *ParsingContext, tgFlags FeatureFlags) (cty.Value, error) {
evaluatedFlags := map[string]cty.Value{}
// copy default feature flags to evaluated flags
// extract all flags in map by name
flagByName := map[string]*FeatureFlag{}
for _, flag := range tgFlags {
flagByName[flag.Name] = flag
}

for name, value := range ctx.TerragruntOptions.FeatureFlags {
evaluatedFlag, err := flagToCtyValue(name, value)
// convert flag value to respective type
var evaluatedFlag cty.Value

if err != nil {
return nil, err
existingFlag, exists := flagByName[name]

if exists {
flag, err := flagToTypedCtyValue(name, existingFlag.Default.Type(), value)
if err != nil {
return cty.NilVal, err
}

evaluatedFlag = flag
} else {
flag, err := flagToCtyValue(name, value)
if err != nil {
return cty.NilVal, err
}

evaluatedFlag = flag
}

evaluatedFlags[name] = evaluatedFlag
}

for _, flag := range tgFlags.FeatureFlags {
for _, flag := range tgFlags {
if _, exists := evaluatedFlags[flag.Name]; !exists {
contextFlag, err := flagToCtyValue(flag.Name, *flag.Default)

if err != nil {
return nil, err
return cty.NilVal, err
}

evaluatedFlags[flag.Name] = contextFlag
Expand All @@ -169,26 +220,10 @@ func DecodeBaseBlocks(ctx *ParsingContext, file *hclparse.File, includeFromChild
flagsAsCtyVal, err := convertValuesMapToCtyVal(evaluatedFlags)

if err != nil {
return nil, err
}

// Evaluate all the expressions in the locals block separately and generate the variables list to use in the
// evaluation ctx.
locals, err := EvaluateLocalsBlock(ctx.WithTrackInclude(trackInclude).WithFeatures(&flagsAsCtyVal), file)
if err != nil {
return nil, err
}

localsAsCtyVal, err := convertValuesMapToCtyVal(locals)
if err != nil {
return nil, err
return cty.NilVal, err
}

return &DecodedBaseBlocks{
TrackInclude: trackInclude,
Locals: &localsAsCtyVal,
FeatureFlags: &flagsAsCtyVal,
}, nil
return flagsAsCtyVal, nil
}

func PartialParseConfigFile(ctx *ParsingContext, configPath string, include *IncludeConfig) (*TerragruntConfig, error) {
Expand Down Expand Up @@ -258,6 +293,8 @@ func TerragruntConfigFromPartialConfig(ctx *ParsingContext, file *hclparse.File,
// - TerragruntVersionConstraints: Parses the attributes related to constraining terragrunt and terraform versions in
// the config.
// - RemoteStateBlock: Parses the `remote_state` block in the config
// - FeatureFlagsBlock: Parses the `feature` block in the config
// - ExcludeBlock : Parses the `exclude` block in the config
//
// Note that the following blocks are always decoded:
// - locals
Expand Down Expand Up @@ -473,10 +510,31 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
return nil, err
}

if decoded.FeatureFlags != nil {
if output.FeatureFlags != nil {
flags, err := deepMergeFeatureBlocks(output.FeatureFlags, decoded.FeatureFlags)
if err != nil {
return nil, err
}

output.FeatureFlags = flags
} else {
output.FeatureFlags = decoded.FeatureFlags
}

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

if err != nil {
return nil, err
}

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

default:
return nil, InvalidPartialBlockName{decode}
}
Expand All @@ -491,10 +549,35 @@ func PartialParseConfig(ctx *ParsingContext, file *hclparse.File, includeFromChi
// Saving processed includes into configuration, direct assignment since nested includes aren't supported
config.ProcessedIncludes = ctx.TrackInclude.CurrentMap

output = config
}

return processExcludes(ctx, output, file)
}

// processExcludes evaluate exclude blocks and merge them into the config.
func processExcludes(ctx *ParsingContext, config *TerragruntConfig, file *hclparse.File) (*TerragruntConfig, error) {
flagsAsCtyVal, err := flagsAsCty(ctx, config.FeatureFlags)
if err != nil {
return nil, err
}

excludeConfig, err := evaluateExcludeBlocks(ctx.WithFeatures(&flagsAsCtyVal), file)
if err != nil {
return nil, err
}

if excludeConfig == nil {
return config, nil
}

return output, nil
if config.Exclude != nil {
config.Exclude.Merge(excludeConfig)
} else {
config.Exclude = excludeConfig
}

return config, nil
}

func partialParseIncludedConfig(ctx *ParsingContext, includedConfig *IncludeConfig) (*TerragruntConfig, error) {
Expand Down Expand Up @@ -528,20 +611,6 @@ func decodeAsTerragruntInclude(file *hclparse.File, evalParsingContext *hcl.Eval
return tgInc.Include, nil
}

func flagToCtyValue(name string, value interface{}) (cty.Value, error) {
ctyValue, err := goTypeToCty(value)
if err != nil {
return cty.NilVal, err
}

ctyFlag := ctyFeatureFlag{
Name: name,
Value: ctyValue,
}

return goTypeToCty(ctyFlag)
}

// Custom error types

type InvalidPartialBlockName struct {
Expand Down
Loading

0 comments on commit f23a6b6

Please sign in to comment.