From 4f7ff4c9f5c95b0074d44e1f536ebfc01a4dfba2 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Mar 2025 12:42:17 -0500 Subject: [PATCH 01/12] fix(terraform): expand `count` blocks can depend on submodule returns Do not expand unknown `count` blocks. Run `expandBlocks` in eval to allow submodule returns to affect the `count` when using module outputs. --- .../scanners/terraform/parser/evaluator.go | 22 ++++++++++++--- .../scanners/terraform/parser/parser_test.go | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 59aa69fc87d5..3fa9656ee018 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -140,14 +140,20 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str e.blocks = e.expandBlocks(e.blocks) // rootModule is initialized here, but not fully evaluated until all submodules are evaluated. - // Initializing it up front to keep the module hierarchy of parents correct. - rootModule := terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores) + // A pointer for this module is needed up front to correctly set the module parent hierarchy. + // The actual instance is created at the end, when all terraform blocks + // are evaluated. + rootModule := new(terraform.Module) + submodules := e.evaluateSubmodules(ctx, rootModule, fsMap) e.logger.Debug("Starting post-submodules evaluation...") e.evaluateSteps() e.logger.Debug("Module evaluation complete.") + // terraform.NewModule must be called at the end, as `e.blocks` can be + // changed up until the last moment. + *rootModule = *terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores) return append(terraform.Modules{rootModule}, submodules...), fsMap } @@ -254,6 +260,9 @@ func (e *evaluator) evaluateSteps() { e.logger.Debug("Starting iteration", log.Int("iteration", i)) e.evaluateStep() + // Always attempt to expand any blocks that might now be expandable + // due to new context being set. + e.blocks = e.expandBlocks(e.blocks) // if ctx matches the last evaluation, we can bail, nothing left to resolve if i > 0 && reflect.DeepEqual(lastContext.Variables, e.ctx.Inner().Variables) { @@ -401,8 +410,15 @@ func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks countFiltered = append(countFiltered, block) continue } - count := 1 + countAttrVal := countAttr.Value() + if countAttrVal.IsNull() { + // Defer to the next pass when the count might be known + countFiltered = append(countFiltered, block) + continue + } + + count := 1 if !countAttrVal.IsNull() && countAttrVal.IsKnown() && countAttrVal.Type() == cty.Number { count = int(countAttr.AsNumber()) } diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index ff9624734a0f..0c74f6357e11 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1704,6 +1704,33 @@ resource "test_resource" "this" { assert.Equal(t, "test_value", attr.GetRawValue()) } +func TestBlockCount(t *testing.T) { + // `count` meta attributes are incorrectly handled when referencing + // a module output. + files := map[string]string{ + "main.tf": ` +module "foo" { + source = "./modules/foo" +} +data "test_resource" "this" { + count = module.foo.staticZero +} +`, + "modules/foo/main.tf": ` +output "staticZero" { + value = 0 +} +`, + } + + modules := parse(t, files) + require.Len(t, modules, 2) + + // no data resources should exist as their counts are 0 + datas := modules.GetDatasByType("test_resource") + require.Empty(t, datas) +} + // TestNestedModulesOptions ensures parser options are carried to the nested // submodule evaluators. // The test will include an invalid module that will fail to download From cd4d4b368e50e5e4f566e1a48e814636e2897aa5 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Mar 2025 12:50:53 -0500 Subject: [PATCH 02/12] update unit test --- pkg/iac/scanners/terraform/parser/parser_test.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index 0c74f6357e11..ed0b4e86d7bd 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1712,23 +1712,32 @@ func TestBlockCount(t *testing.T) { module "foo" { source = "./modules/foo" } -data "test_resource" "this" { +data "this_resource" "this" { count = module.foo.staticZero } + +data "that_resource" "this" { + count = module.foo.staticFive +} `, "modules/foo/main.tf": ` output "staticZero" { value = 0 } +output "staticFive" { + value = 5 +} `, } modules := parse(t, files) require.Len(t, modules, 2) - // no data resources should exist as their counts are 0 - datas := modules.GetDatasByType("test_resource") + datas := modules.GetDatasByType("this_resource") require.Empty(t, datas) + + datas = modules.GetDatasByType("that_resource") + require.Len(t, datas, 5) } // TestNestedModulesOptions ensures parser options are carried to the nested From fc4b5f54e729cad74d302e85deb80494bd39eebc Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Mar 2025 12:56:50 -0500 Subject: [PATCH 03/12] test(terraform): add counter example test The depth of submodule evalutation is still limited --- .../scanners/terraform/parser/parser_test.go | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index ed0b4e86d7bd..ddb8dc8e1344 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1740,6 +1740,59 @@ output "staticFive" { require.Len(t, datas, 5) } +func TestBlockCountNested(t *testing.T) { + // `count` meta attributes are incorrectly handled when referencing + // a module output. + files := map[string]string{ + "main.tf": ` +module "alpha" { + source = "./nestedcount" + set_count = 2 +} + +module "charlie" { + source = "./nestedcount" + set_count = module.beta.set_count +} + + +data "repeatable" "foo" { + count = module.charlie.set_count + value = "foo" +} +`, + "setcount/main.tf": ` +variable "set_count" { + type = number +} + +output "set_count" { + value = var.set_count +} +`, + "nestedcount/main.tf": ` +variable "set_count" { + type = number +} + +module "nested_mod" { + source = "../setcount" + set_count = var.set_count +} + +output "set_count" { + value = module.nested_mod.set_count +} +`, + } + + modules := parse(t, files) + require.Len(t, modules, 5) + + datas := modules.GetDatasByType("repeatable") + require.Len(t, datas, 2) +} + // TestNestedModulesOptions ensures parser options are carried to the nested // submodule evaluators. // The test will include an invalid module that will fail to download From 9b757cc2cb3bc44cd4b047e7dd96c66851702828 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Mar 2025 13:02:45 -0500 Subject: [PATCH 04/12] fixup test --- pkg/iac/scanners/terraform/parser/parser_test.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index ddb8dc8e1344..5f18f1e310bf 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1750,14 +1750,21 @@ module "alpha" { set_count = 2 } +module "beta" { + source = "./nestedcount" + set_count = module.alpha.set_count +} + + module "charlie" { + count = module.beta.set_count - 1 source = "./nestedcount" set_count = module.beta.set_count } data "repeatable" "foo" { - count = module.charlie.set_count + count = module.charlie[0].set_count value = "foo" } `, @@ -1787,10 +1794,10 @@ output "set_count" { } modules := parse(t, files) - require.Len(t, modules, 5) + require.Len(t, modules, 7) datas := modules.GetDatasByType("repeatable") - require.Len(t, datas, 2) + assert.Len(t, datas, 2) } // TestNestedModulesOptions ensures parser options are carried to the nested From c6831f9a1d1c7beff545003fa89dbaff71451437 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 19 Mar 2025 14:04:56 -0500 Subject: [PATCH 05/12] test(terraform): add failing unit test with ambition to resolve Module counts are incorrectly being handled --- .../scanners/terraform/parser/parser_test.go | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index 5f18f1e310bf..5bde247c668d 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1800,6 +1800,32 @@ output "set_count" { assert.Len(t, datas, 2) } +func TestBlockCountModules(t *testing.T) { + t.Skip("This test is currently failing, the 'count = 0' module 'bar' is still loaded") + // `count` meta attributes are incorrectly handled when referencing + // a module output. + files := map[string]string{ + "main.tf": ` +module "foo" { + source = "./modules/foo" +} + +module "bar" { + source = "./modules/foo" + count = module.foo.staticZero +} +`, + "modules/foo/main.tf": ` +output "staticZero" { + value = 0 +} +`, + } + + modules := parse(t, files) + require.Len(t, modules, 2) +} + // TestNestedModulesOptions ensures parser options are carried to the nested // submodule evaluators. // The test will include an invalid module that will fail to download From 6eb60632508a85ebcddaadc1504bc9361a1aa30a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 9 Apr 2025 10:06:17 -0500 Subject: [PATCH 06/12] chore: check for unknown and null to match upstream change --- pkg/iac/scanners/terraform/parser/evaluator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 3fa9656ee018..1646fbf0605e 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -412,7 +412,7 @@ func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks } countAttrVal := countAttr.Value() - if countAttrVal.IsNull() { + if countAttrVal.IsNull() || !countAttrVal.IsKnown() { // Defer to the next pass when the count might be known countFiltered = append(countFiltered, block) continue From 073fdefab487d45392ae6f5723ed1d2da7d1b331 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 2 May 2025 10:30:54 -0500 Subject: [PATCH 07/12] chore: fix expand for_each using module output --- .../scanners/terraform/parser/evaluator.go | 7 +++- .../scanners/terraform/parser/parser_test.go | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index efd79dda9065..57621f65968f 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -324,8 +324,13 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Bloc } forEachVal := forEachAttr.Value() + if !forEachVal.IsKnown() { + // If the value is unknown, it might be known at a later execution step. + forEachFiltered = append(forEachFiltered, block) + continue + } - if forEachVal.IsNull() || !forEachVal.IsKnown() || !forEachAttr.IsIterable() { + if forEachVal.IsNull() || !forEachAttr.IsIterable() { e.logger.Debug(`Failed to expand block. Invalid "for-each" argument. Must be known and iterable.`, log.String("block", block.FullName()), log.String("value", forEachVal.GoString()), diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index b352b128f923..829492e90ec3 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1863,6 +1863,22 @@ data "this_resource" "this" { data "that_resource" "this" { count = module.foo.staticFive } + +data "for_each_resource_empty" "this" { + for_each = module.foo.empty_list +} +data "for_each_resource_abc" "this" { + for_each = module.foo.list_abc +} + +data "dynamic_block" "that" { + dynamic "element" { + for_each = module.foo.list_abc + content { + foo = element.value + } + } +} `, "modules/foo/main.tf": ` output "staticZero" { @@ -1871,6 +1887,13 @@ output "staticZero" { output "staticFive" { value = 5 } + +output "empty_list" { + value = [] +} +output "list_abc" { + value = ["a", "b", "c"] +} `, } @@ -1882,6 +1905,16 @@ output "staticFive" { datas = modules.GetDatasByType("that_resource") require.Len(t, datas, 5) + + datas = modules.GetDatasByType("for_each_resource_empty") + require.Len(t, datas, 0) + + datas = modules.GetDatasByType("for_each_resource_abc") + require.Len(t, datas, 3) + + dyn := modules.GetDatasByType("dynamic_block") + require.Len(t, dyn, 1) + require.Len(t, dyn[0].GetBlocks("element"), 3, "dynamic expand") } func TestBlockCountNested(t *testing.T) { From f0b293a2c6d5b2b7da163e3d5c66c078f051d7ab Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 2 May 2025 10:37:01 -0500 Subject: [PATCH 08/12] test rename --- pkg/iac/scanners/terraform/parser/parser_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index 829492e90ec3..9edd70bf37a5 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1849,7 +1849,7 @@ output "value" { } } -func TestBlockCount(t *testing.T) { +func TestBlockExpandWithSubmoduleOutput(t *testing.T) { // `count` meta attributes are incorrectly handled when referencing // a module output. files := map[string]string{ @@ -1917,9 +1917,7 @@ output "list_abc" { require.Len(t, dyn[0].GetBlocks("element"), 3, "dynamic expand") } -func TestBlockCountNested(t *testing.T) { - // `count` meta attributes are incorrectly handled when referencing - // a module output. +func TestBlockExpandWithSubmoduleOutputNested(t *testing.T) { files := map[string]string{ "main.tf": ` module "alpha" { From 86b3b93887e18c67020c1e7777b3e07fff8d2184 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 2 May 2025 10:41:21 -0500 Subject: [PATCH 09/12] update skip comment --- pkg/iac/scanners/terraform/parser/parser_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index 9edd70bf37a5..e9959af515e3 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1968,7 +1968,12 @@ output "set_count" { } func TestBlockCountModules(t *testing.T) { - t.Skip("This test is currently failing, the 'count = 0' module 'bar' is still loaded") + t.Skip( + "This test is currently failing. " + + "The count passed to `module bar` is not being set correctly. " + + "The count value is sourced from the output of `module foo`. " + + "Submodules cannot be dependent on the output of other submodules right now. ", + ) // `count` meta attributes are incorrectly handled when referencing // a module output. files := map[string]string{ From 36938e36807d43f7276e769ce0cfe42c296326e7 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 6 May 2025 16:35:43 -0500 Subject: [PATCH 10/12] linting --- pkg/iac/scanners/terraform/parser/parser_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index e9959af515e3..262d0b8fb4b2 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -1907,7 +1907,7 @@ output "list_abc" { require.Len(t, datas, 5) datas = modules.GetDatasByType("for_each_resource_empty") - require.Len(t, datas, 0) + require.Empty(t, datas) datas = modules.GetDatasByType("for_each_resource_abc") require.Len(t, datas, 3) From a979854198eb0f0cd3ab326850c969719c8d1d5b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Wed, 21 May 2025 10:15:55 -0500 Subject: [PATCH 11/12] chore: leave `null` counts as 1, not sure what to do with them --- pkg/iac/scanners/terraform/parser/evaluator.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 57621f65968f..295e5926075f 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -325,7 +325,8 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Bloc forEachVal := forEachAttr.Value() if !forEachVal.IsKnown() { - // If the value is unknown, it might be known at a later execution step. + // Defer the expansion of the block if it is unknown. It might be known at a later + // execution step. forEachFiltered = append(forEachFiltered, block) continue } @@ -427,7 +428,7 @@ func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks } countAttrVal := countAttr.Value() - if countAttrVal.IsNull() || !countAttrVal.IsKnown() { + if !countAttrVal.IsKnown() { // Defer to the next pass when the count might be known countFiltered = append(countFiltered, block) continue From 97409b80078e28ec04da85f8a03b3af817501920 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 27 Jan 2025 13:38:16 -0600 Subject: [PATCH 12/12] chore(terraform): hook into evaluateStep behavior with custom hooks evaluateStep handles many terraform semantics such as default values for 'variables'. A hook into these steps allows defining additional semantics, likely to mirror those of the actual provider implementation. --- .../scanners/terraform/parser/evaluator.go | 11 +++ pkg/iac/scanners/terraform/parser/option.go | 6 ++ pkg/iac/scanners/terraform/parser/parser.go | 3 + .../scanners/terraform/parser/parser_test.go | 75 +++++++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go index 31152cef3637..7c60dfe51c67 100644 --- a/pkg/iac/scanners/terraform/parser/evaluator.go +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -39,6 +39,9 @@ type evaluator struct { parentParser *Parser allowDownloads bool skipCachedModules bool + // stepHooks are functions that are called after each evaluation step. + // They can be used to provide additional semantics to other terraform blocks. + stepHooks []EvaluateStepHook } func newEvaluator( @@ -56,6 +59,7 @@ func newEvaluator( logger *log.Logger, allowDownloads bool, skipCachedModules bool, + stepHooks []EvaluateStepHook, ) *evaluator { // create a context to store variables and make functions available @@ -88,9 +92,12 @@ func newEvaluator( logger: logger, allowDownloads: allowDownloads, skipCachedModules: skipCachedModules, + stepHooks: stepHooks, } } +type EvaluateStepHook func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) + func (e *evaluator) evaluateStep() { e.ctx.Set(e.getValuesByBlockType("variable"), "var") @@ -104,6 +111,10 @@ func (e *evaluator) evaluateStep() { e.ctx.Set(e.getValuesByBlockType("data"), "data") e.ctx.Set(e.getValuesByBlockType("output"), "output") e.ctx.Set(e.getValuesByBlockType("module"), "module") + + for _, hook := range e.stepHooks { + hook(e.ctx, e.blocks, e.inputVars) + } } // exportOutputs is used to export module outputs to the parent module diff --git a/pkg/iac/scanners/terraform/parser/option.go b/pkg/iac/scanners/terraform/parser/option.go index 86cf69cc3778..80dbeec14c37 100644 --- a/pkg/iac/scanners/terraform/parser/option.go +++ b/pkg/iac/scanners/terraform/parser/option.go @@ -10,6 +10,12 @@ import ( type Option func(p *Parser) +func OptionWithEvalHook(hooks EvaluateStepHook) Option { + return func(p *Parser) { + p.stepHooks = append(p.stepHooks, hooks) + } +} + func OptionWithTFVarsPaths(paths ...string) Option { return func(p *Parser) { p.tfvarsPaths = paths diff --git a/pkg/iac/scanners/terraform/parser/parser.go b/pkg/iac/scanners/terraform/parser/parser.go index f303253bb424..b399b81ab1bc 100644 --- a/pkg/iac/scanners/terraform/parser/parser.go +++ b/pkg/iac/scanners/terraform/parser/parser.go @@ -55,6 +55,7 @@ type Parser struct { // cwd is optional, if left to empty string, 'os.Getwd' // will be used for populating 'path.cwd' in terraform. cwd string + stepHooks []EvaluateStepHook } // New creates a new Parser @@ -70,6 +71,7 @@ func New(moduleFS fs.FS, moduleSource string, opts ...Option) *Parser { configsFS: moduleFS, logger: slog.Default(), tfvars: make(map[string]cty.Value), + stepHooks: make([]EvaluateStepHook, 0), } for _, option := range opts { @@ -322,6 +324,7 @@ func (p *Parser) Load(_ context.Context) (*evaluator, error) { p.logger.With(log.Prefix("terraform evaluator")), p.allowDownloads, p.skipCachedModules, + p.stepHooks, ), nil } diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go index 772f815930f4..96e7aac5ef48 100644 --- a/pkg/iac/scanners/terraform/parser/parser_test.go +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -17,6 +17,7 @@ import ( "github.com/aquasecurity/trivy/internal/testutil" "github.com/aquasecurity/trivy/pkg/iac/terraform" + tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context" "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/set" ) @@ -2280,6 +2281,80 @@ func TestTFVarsFileDoesNotExist(t *testing.T) { assert.ErrorContains(t, err, "file does not exist") } +func Test_OptionsWithEvalHook(t *testing.T) { + fs := testutil.CreateFS(t, map[string]string{ + "main.tf": ` +data "your_custom_data" "this" { + default = ["foo", "foh", "fum"] + unaffected = "bar" +} + +// Testing the hook affects some value, which is used in another evaluateStep +// action (expanding blocks) +data "random_thing" "that" { + dynamic "repeated" { + for_each = data.your_custom_data.this.value + content { + value = repeated.value + } + } +} + +locals { + referenced = data.your_custom_data.this.value + static_ref = data.your_custom_data.this.unaffected +} +`}) + + parser := New(fs, "", OptionWithEvalHook( + // A basic example of how to have a 'default' value for a data block. + // To see a more practical example, see how 'evaluateVariable' handles + // the 'default' value of a variable. + func(ctx *tfcontext.Context, blocks terraform.Blocks, inputVars map[string]cty.Value) { + dataBlocks := blocks.OfType("data") + for _, block := range dataBlocks { + if len(block.Labels()) >= 1 && block.Labels()[0] == "your_custom_data" { + def := block.GetAttribute("default") + ctx.Set(cty.ObjectVal(map[string]cty.Value{ + "value": def.Value(), + }), "data", "your_custom_data", "this") + } + } + + }, + )) + + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, _, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + // Check the default value of the data block + blocks := rootModule.GetDatasByType("your_custom_data") + assert.Len(t, blocks, 1) + expList := cty.TupleVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foh"), cty.StringVal("fum")}) + assert.True(t, expList.Equals(blocks[0].GetAttribute("default").Value()).True(), "default value matched list") + assert.Equal(t, "bar", blocks[0].GetAttribute("unaffected").Value().AsString()) + + // Check the referenced 'data.your_custom_data.this.value' exists in the eval + // context, and it is the default value of the data block. + locals := rootModule.GetBlocks().OfType("locals") + assert.Len(t, locals, 1) + assert.True(t, expList.Equals(locals[0].GetAttribute("referenced").Value()).True(), "referenced value matched list") + assert.Equal(t, "bar", locals[0].GetAttribute("static_ref").Value().AsString()) + + // Check the dynamic block is expanded correctly + dynamicBlocks := rootModule.GetDatasByType("random_thing") + assert.Len(t, dynamicBlocks, 1) + assert.Len(t, dynamicBlocks[0].GetBlocks("repeated"), 3) + for i, repeat := range dynamicBlocks[0].GetBlocks("repeated") { + assert.Equal(t, expList.Index(cty.NumberIntVal(int64(i))), repeat.GetAttribute("value").Value()) + } +} + func Test_OptionsWithTfVars(t *testing.T) { fs := testutil.CreateFS(t, map[string]string{ "main.tf": `resource "test" "this" {