Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions pkg/iac/scanners/terraform/parser/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,14 +143,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
}

Expand Down Expand Up @@ -264,6 +270,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) {
Expand Down Expand Up @@ -315,8 +324,14 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Bloc
}

forEachVal := forEachAttr.Value()
if !forEachVal.IsKnown() {
// Defer the expansion of the block if it 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()),
Expand Down Expand Up @@ -411,8 +426,15 @@ func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks
countFiltered = append(countFiltered, block)
continue
}
count := 1

countAttrVal := countAttr.Value()
if !countAttrVal.IsKnown() {
// 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())
}
Expand Down
148 changes: 148 additions & 0 deletions pkg/iac/scanners/terraform/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,154 @@
}
}

func TestBlockExpandWithSubmoduleOutput(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 "this_resource" "this" {
count = module.foo.staticZero
}
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" {
value = 0
}
output "staticFive" {
value = 5
}

output "empty_list" {
value = []
}
output "list_abc" {
value = ["a", "b", "c"]
}
`,
}

modules := parse(t, files)
require.Len(t, modules, 2)

datas := modules.GetDatasByType("this_resource")
require.Empty(t, datas)

datas = modules.GetDatasByType("that_resource")
require.Len(t, datas, 5)

datas = modules.GetDatasByType("for_each_resource_empty")
require.Empty(t, datas)

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 TestBlockExpandWithSubmoduleOutputNested(t *testing.T) {
files := map[string]string{
"main.tf": `
module "alpha" {
source = "./nestedcount"
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[0].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, 7)

datas := modules.GetDatasByType("repeatable")
assert.Len(t, datas, 2)
}

func TestBlockCountModules(t *testing.T) {
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{
"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
Expand Down Expand Up @@ -2326,7 +2474,7 @@
})

parser := New(fs, "", OptionStopOnHCLError(true))
require.NoError(t, parser.ParseFS(t.Context(), "code"))

Check failure on line 2477 in pkg/iac/scanners/terraform/parser/parser_test.go

View workflow job for this annotation

GitHub Actions / Test (macos-latest)

assignment mismatch: 3 variables but parser.EvaluateAll returns 2 values

Check failure on line 2477 in pkg/iac/scanners/terraform/parser/parser_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

assignment mismatch: 3 variables but parser.EvaluateAll returns 2 values (typecheck)
modules, err := parser.EvaluateAll(t.Context())
require.NoError(t, err)
require.Len(t, modules, 1)
Expand Down
Loading