Skip to content

Commit c413f5a

Browse files
committed
fix(terraform): 'count' meta argument sourced from submodule output
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. (cherry picked from commit 494b512)
1 parent b538f64 commit c413f5a

3 files changed

Lines changed: 183 additions & 5 deletions

File tree

pkg/iac/scanners/terraform/parser/evaluator.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,19 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str
162162
e.blocks = e.expandBlockForEaches(e.blocks)
163163

164164
// rootModule is initialized here, but not fully evaluated until all submodules are evaluated.
165-
// Initializing it up front to keep the module hierarchy of parents correct.
166-
rootModule := terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores)
165+
// A pointer for this module is needed up front to correctly set the module parent hierarchy.
166+
// The actual instance is created at the end, when all terraform blocks
167+
// are evaluated.
168+
rootModule := new(terraform.Module)
167169
submodules := e.evaluateSubmodules(ctx, rootModule, fsMap)
168170

169171
e.logger.Debug("Starting post-submodules evaluation...")
170172
e.evaluateSteps()
171173

172174
e.logger.Debug("Module evaluation complete.")
175+
// terraform.NewModule must be called at the end, as `e.blocks` can be
176+
// changed up until the last moment.
177+
*rootModule = *terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores)
173178
return append(terraform.Modules{rootModule}, submodules...), fsMap
174179
}
175180

@@ -282,6 +287,10 @@ func (e *evaluator) evaluateSteps() {
282287
e.logger.Debug("Starting iteration", log.Int("iteration", i))
283288
e.evaluateStep()
284289

290+
// Always attempt to expand any blocks that might now be expandable
291+
// due to new context being set.
292+
e.blocks = e.expandBlocks(e.blocks)
293+
285294
// if ctx matches the last evaluation, we can bail, nothing left to resolve
286295
if i > 0 && reflect.DeepEqual(lastContext.Variables, e.ctx.Inner().Variables) {
287296
e.logger.Debug("Context unchanged", log.Int("iteration", i))
@@ -330,8 +339,14 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Bloc
330339
}
331340

332341
forEachVal := forEachAttr.Value()
342+
if !forEachVal.IsKnown() {
343+
// Defer the expansion of the block if it is unknown. It might be known at a later
344+
// execution step.
345+
forEachFiltered = append(forEachFiltered, block)
346+
continue
347+
}
333348

334-
if forEachVal.IsNull() || !forEachVal.IsKnown() || !forEachAttr.IsIterable() {
349+
if forEachVal.IsNull() || !forEachAttr.IsIterable() {
335350
e.logger.Debug(`Failed to expand block. Invalid "for-each" argument. Must be known and iterable.`,
336351
log.String("block", block.FullName()),
337352
log.String("value", forEachVal.GoString()),
@@ -426,8 +441,15 @@ func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks
426441
countFiltered = append(countFiltered, block)
427442
continue
428443
}
429-
count := 1
444+
430445
countAttrVal := countAttr.Value()
446+
if !countAttrVal.IsKnown() {
447+
// Defer to the next pass when the count might be known
448+
countFiltered = append(countFiltered, block)
449+
continue
450+
}
451+
452+
count := 1
431453
if !countAttrVal.IsNull() && countAttrVal.IsKnown() && countAttrVal.Type() == cty.Number {
432454
count = int(countAttr.AsNumber())
433455
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
package parser
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestBlockExpandWithSubmoduleOutput(t *testing.T) {
11+
// `count` meta attributes are incorrectly handled when referencing
12+
// a module output.
13+
files := map[string]string{
14+
"main.tf": `
15+
module "foo" {
16+
source = "./modules/foo"
17+
}
18+
data "this_resource" "this" {
19+
count = module.foo.staticZero
20+
}
21+
data "that_resource" "this" {
22+
count = module.foo.staticFive
23+
}
24+
25+
data "for_each_resource_empty" "this" {
26+
for_each = module.foo.empty_list
27+
}
28+
data "for_each_resource_abc" "this" {
29+
for_each = module.foo.list_abc
30+
}
31+
32+
data "dynamic_block" "that" {
33+
dynamic "element" {
34+
for_each = module.foo.list_abc
35+
content {
36+
foo = element.value
37+
}
38+
}
39+
}
40+
`,
41+
"modules/foo/main.tf": `
42+
output "staticZero" {
43+
value = 0
44+
}
45+
output "staticFive" {
46+
value = 5
47+
}
48+
49+
output "empty_list" {
50+
value = []
51+
}
52+
output "list_abc" {
53+
value = ["a", "b", "c"]
54+
}
55+
`,
56+
}
57+
58+
modules := parse(t, files)
59+
require.Len(t, modules, 2)
60+
61+
datas := modules.GetDatasByType("this_resource")
62+
require.Empty(t, datas)
63+
64+
datas = modules.GetDatasByType("that_resource")
65+
require.Len(t, datas, 5)
66+
67+
datas = modules.GetDatasByType("for_each_resource_empty")
68+
require.Empty(t, datas)
69+
70+
datas = modules.GetDatasByType("for_each_resource_abc")
71+
require.Len(t, datas, 3)
72+
73+
dyn := modules.GetDatasByType("dynamic_block")
74+
require.Len(t, dyn, 1)
75+
require.Len(t, dyn[0].GetBlocks("element"), 3, "dynamic expand")
76+
}
77+
78+
func TestBlockExpandWithSubmoduleOutputNested(t *testing.T) {
79+
files := map[string]string{
80+
"main.tf": `
81+
module "alpha" {
82+
source = "./nestedcount"
83+
set_count = 2
84+
}
85+
module "beta" {
86+
source = "./nestedcount"
87+
set_count = module.alpha.set_count
88+
}
89+
module "charlie" {
90+
count = module.beta.set_count - 1
91+
source = "./nestedcount"
92+
set_count = module.beta.set_count
93+
}
94+
data "repeatable" "foo" {
95+
count = module.charlie[0].set_count
96+
value = "foo"
97+
}
98+
`,
99+
"setcount/main.tf": `
100+
variable "set_count" {
101+
type = number
102+
}
103+
output "set_count" {
104+
value = var.set_count
105+
}
106+
`,
107+
"nestedcount/main.tf": `
108+
variable "set_count" {
109+
type = number
110+
}
111+
module "nested_mod" {
112+
source = "../setcount"
113+
set_count = var.set_count
114+
}
115+
output "set_count" {
116+
value = module.nested_mod.set_count
117+
}
118+
`,
119+
}
120+
121+
modules := parse(t, files)
122+
require.Len(t, modules, 7)
123+
124+
datas := modules.GetDatasByType("repeatable")
125+
assert.Len(t, datas, 2)
126+
}
127+
128+
func TestBlockCountModules(t *testing.T) {
129+
t.Skip(
130+
"This test is currently failing. " +
131+
"The count passed to `module bar` is not being set correctly. " +
132+
"The count value is sourced from the output of `module foo`. " +
133+
"Submodules cannot be dependent on the output of other submodules right now. ",
134+
)
135+
// `count` meta attributes are incorrectly handled when referencing
136+
// a module output.
137+
files := map[string]string{
138+
"main.tf": `
139+
module "foo" {
140+
source = "./modules/foo"
141+
}
142+
module "bar" {
143+
source = "./modules/foo"
144+
count = module.foo.staticZero
145+
}
146+
`,
147+
"modules/foo/main.tf": `
148+
output "staticZero" {
149+
value = 0
150+
}
151+
`,
152+
}
153+
154+
modules := parse(t, files)
155+
require.Len(t, modules, 2)
156+
}

pkg/iac/terraform/block.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,7 +620,7 @@ func (b *Block) expandDynamic() ([]*Block, error) {
620620
return nil, fmt.Errorf("invalid for-each in %s block: %w", b.FullLocalName(), err)
621621
}
622622

623-
if !forEachVal.IsKnown() {
623+
if !forEachVal.IsWhollyKnown() {
624624
return nil, errors.New("for-each must be known")
625625
}
626626

0 commit comments

Comments
 (0)