Skip to content

Commit 8d5dbc9

Browse files
nikpivkinsimar7
andauthoredOct 19, 2024··
fix(misconf): properly expand dynamic blocks (#7612)
Signed-off-by: nikpivkin <[email protected]> Co-authored-by: Simar <[email protected]>
1 parent c225883 commit 8d5dbc9

File tree

5 files changed

+409
-214
lines changed

5 files changed

+409
-214
lines changed
 

‎pkg/iac/scanners/terraform/module_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -567,7 +567,7 @@ resource "something" "else" {
567567
for_each = toset(["true"])
568568
569569
content {
570-
ok = each.value
570+
ok = blah.value
571571
}
572572
}
573573
}

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

+7-42
Original file line numberDiff line numberDiff line change
@@ -260,48 +260,28 @@ func (e *evaluator) evaluateSteps() {
260260
}
261261

262262
func (e *evaluator) expandBlocks(blocks terraform.Blocks) terraform.Blocks {
263-
return e.expandDynamicBlocks(e.expandBlockForEaches(e.expandBlockCounts(blocks), false)...)
263+
return e.expandDynamicBlocks(e.expandBlockForEaches(e.expandBlockCounts(blocks))...)
264264
}
265265

266266
func (e *evaluator) expandDynamicBlocks(blocks ...*terraform.Block) terraform.Blocks {
267267
for _, b := range blocks {
268-
e.expandDynamicBlock(b)
269-
}
270-
return blocks
271-
}
272-
273-
func (e *evaluator) expandDynamicBlock(b *terraform.Block) {
274-
for _, sub := range b.AllBlocks() {
275-
e.expandDynamicBlock(sub)
276-
}
277-
for _, sub := range b.AllBlocks().OfType("dynamic") {
278-
if sub.IsExpanded() {
279-
continue
280-
}
281-
blockName := sub.TypeLabel()
282-
expanded := e.expandBlockForEaches(terraform.Blocks{sub}, true)
283-
for _, ex := range expanded {
284-
if content := ex.GetBlock("content"); content.IsNotNil() {
285-
_ = e.expandDynamicBlocks(content)
286-
b.InjectBlock(content, blockName)
287-
}
288-
}
289-
if len(expanded) > 0 {
290-
sub.MarkExpanded()
268+
if err := b.ExpandBlock(); err != nil {
269+
e.logger.Error(`Failed to expand dynamic block.`,
270+
log.String("block", b.FullName()), log.Err(err))
291271
}
292272
}
273+
return blocks
293274
}
294275

295276
func isBlockSupportsForEachMetaArgument(block *terraform.Block) bool {
296277
return slices.Contains([]string{
297278
"module",
298279
"resource",
299280
"data",
300-
"dynamic",
301281
}, block.Type())
302282
}
303283

304-
func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks, isDynamic bool) terraform.Blocks {
284+
func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Blocks {
305285

306286
var forEachFiltered terraform.Blocks
307287

@@ -348,7 +328,7 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks, isDynamic bool
348328
// is the value of the collection. The exception is the use of for-each inside a dynamic block,
349329
// because in this case the collection element may not be a primitive value.
350330
if (forEachVal.Type().IsCollectionType() || forEachVal.Type().IsTupleType()) &&
351-
!forEachVal.Type().IsMapType() && !isDynamic {
331+
!forEachVal.Type().IsMapType() {
352332
stringVal, err := convert.Convert(val, cty.String)
353333
if err != nil {
354334
e.logger.Error(
@@ -374,22 +354,7 @@ func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks, isDynamic bool
374354

375355
ctx.Set(eachObj, "each")
376356
ctx.Set(eachObj, block.TypeLabel())
377-
378-
if isDynamic {
379-
if iterAttr := block.GetAttribute("iterator"); iterAttr.IsNotNil() {
380-
refs := iterAttr.AllReferences()
381-
if len(refs) == 1 {
382-
ctx.Set(idx, refs[0].TypeLabel(), "key")
383-
ctx.Set(val, refs[0].TypeLabel(), "value")
384-
} else {
385-
e.logger.Debug("Ignoring iterator attribute in dynamic block, expected one reference",
386-
log.Int("refs", len(refs)))
387-
}
388-
}
389-
}
390-
391357
forEachFiltered = append(forEachFiltered, clone)
392-
393358
clones[idx.AsString()] = clone.Values()
394359
})
395360

‎pkg/iac/scanners/terraform/parser/parser_test.go

+247-153
Original file line numberDiff line numberDiff line change
@@ -1367,139 +1367,284 @@ func TestCountMetaArgumentInModule(t *testing.T) {
13671367
}
13681368

13691369
func TestDynamicBlocks(t *testing.T) {
1370-
t.Run("arg is list of int", func(t *testing.T) {
1371-
modules := parse(t, map[string]string{
1372-
"main.tf": `
1373-
resource "aws_security_group" "sg-webserver" {
1374-
vpc_id = "1111"
1375-
dynamic "ingress" {
1370+
tests := []struct {
1371+
name string
1372+
src string
1373+
expected []any
1374+
}{
1375+
{
1376+
name: "for-each use tuple of int",
1377+
src: `resource "test_resource" "test" {
1378+
dynamic "foo" {
13761379
for_each = [80, 443]
13771380
content {
1378-
from_port = ingress.value
1379-
to_port = ingress.value
1380-
protocol = "tcp"
1381-
cidr_blocks = ["0.0.0.0/0"]
1381+
bar = foo.value
13821382
}
13831383
}
1384-
}
1385-
`,
1386-
})
1387-
require.Len(t, modules, 1)
1388-
1389-
secGroups := modules.GetResourcesByType("aws_security_group")
1390-
assert.Len(t, secGroups, 1)
1391-
ingressBlocks := secGroups[0].GetBlocks("ingress")
1392-
assert.Len(t, ingressBlocks, 2)
1393-
1394-
var inboundPorts []int
1395-
for _, ingress := range ingressBlocks {
1396-
fromPort := ingress.GetAttribute("from_port").AsIntValueOrDefault(-1, ingress).Value()
1397-
inboundPorts = append(inboundPorts, fromPort)
1398-
}
1399-
1400-
assert.True(t, compareSets([]int{80, 443}, inboundPorts))
1401-
})
1402-
1403-
t.Run("empty for-each", func(t *testing.T) {
1404-
modules := parse(t, map[string]string{
1405-
"main.tf": `
1406-
resource "aws_lambda_function" "analyzer" {
1407-
dynamic "vpc_config" {
1384+
}`,
1385+
expected: []any{float64(80), float64(443)},
1386+
},
1387+
{
1388+
name: "for-each use list of int",
1389+
src: `resource "test_resource" "test" {
1390+
dynamic "foo" {
1391+
for_each = tolist([80, 443])
1392+
content {
1393+
bar = foo.value
1394+
}
1395+
}
1396+
}`,
1397+
expected: []any{float64(80), float64(443)},
1398+
},
1399+
{
1400+
name: "for-each use set of int",
1401+
src: `resource "test_resource" "test" {
1402+
dynamic "foo" {
1403+
for_each = toset([80, 443])
1404+
content {
1405+
bar = foo.value
1406+
}
1407+
}
1408+
}`,
1409+
expected: []any{float64(80), float64(443)},
1410+
},
1411+
{
1412+
name: "for-each use list of bool",
1413+
src: `resource "test_resource" "test" {
1414+
dynamic "foo" {
1415+
for_each = tolist([true])
1416+
content {
1417+
bar = foo.value
1418+
}
1419+
}
1420+
}`,
1421+
expected: []any{true},
1422+
},
1423+
{
1424+
name: "empty for-each",
1425+
src: `resource "test_resource" "test" {
1426+
dynamic "foo" {
14081427
for_each = []
14091428
content {}
14101429
}
1430+
}`,
1431+
expected: []any{},
1432+
},
1433+
{
1434+
name: "for-each use tuple of objects",
1435+
src: `variable "test_var" {
1436+
type = list(object({ enabled = bool }))
1437+
default = [{ enabled = true }]
14111438
}
1412-
`,
1413-
})
1414-
require.Len(t, modules, 1)
14151439
1416-
functions := modules.GetResourcesByType("aws_lambda_function")
1417-
assert.Len(t, functions, 1)
1418-
vpcConfigs := functions[0].GetBlocks("vpc_config")
1419-
assert.Empty(t, vpcConfigs)
1420-
})
1440+
resource "test_resource" "test" {
1441+
dynamic "foo" {
1442+
for_each = var.test_var
14211443
1422-
t.Run("arg is list of bool", func(t *testing.T) {
1423-
modules := parse(t, map[string]string{
1424-
"main.tf": `
1425-
resource "aws_lambda_function" "analyzer" {
1426-
dynamic "vpc_config" {
1427-
for_each = [true]
1428-
content {}
1444+
content {
1445+
bar = foo.value.enabled
1446+
}
1447+
}
1448+
}`,
1449+
expected: []any{true},
1450+
},
1451+
{
1452+
name: "attribute ref to object key",
1453+
src: `variable "some_var" {
1454+
type = map(
1455+
object({
1456+
tag = string
1457+
})
1458+
)
1459+
default = {
1460+
ssh = { "tag" = "login" }
1461+
http = { "tag" = "proxy" }
1462+
https = { "tag" = "proxy" }
14291463
}
14301464
}
1431-
`,
1432-
})
1433-
require.Len(t, modules, 1)
1434-
1435-
functions := modules.GetResourcesByType("aws_lambda_function")
1436-
assert.Len(t, functions, 1)
1437-
vpcConfigs := functions[0].GetBlocks("vpc_config")
1438-
assert.Len(t, vpcConfigs, 1)
1439-
})
14401465
1441-
t.Run("arg is list of objects", func(t *testing.T) {
1442-
modules := parse(t, map[string]string{
1443-
"main.tf": `locals {
1444-
cluster_network_policy = [{
1445-
enabled = true
1446-
}]
1466+
resource "test_resource" "test" {
1467+
dynamic "foo" {
1468+
for_each = { for name, values in var.some_var : name => values }
1469+
content {
1470+
bar = foo.key
1471+
}
1472+
}
1473+
}`,
1474+
expected: []any{"ssh", "http", "https"},
1475+
},
1476+
{
1477+
name: "attribute ref to object value",
1478+
src: `variable "some_var" {
1479+
type = map(
1480+
object({
1481+
tag = string
1482+
})
1483+
)
1484+
default = {
1485+
ssh = { "tag" = "login" }
1486+
http = { "tag" = "proxy" }
1487+
https = { "tag" = "proxy" }
1488+
}
14471489
}
14481490
1449-
resource "google_container_cluster" "primary" {
1450-
name = "test"
1491+
resource "test_resource" "test" {
1492+
dynamic "foo" {
1493+
for_each = { for name, values in var.some_var : name => values }
1494+
content {
1495+
bar = foo.value.tag
1496+
}
1497+
}
1498+
}`,
1499+
expected: []any{"login", "proxy", "proxy"},
1500+
},
1501+
{
1502+
name: "attribute ref to map key",
1503+
src: `variable "some_var" {
1504+
type = map
1505+
default = {
1506+
ssh = { "tag" = "login" }
1507+
http = { "tag" = "proxy" }
1508+
https = { "tag" = "proxy" }
1509+
}
1510+
}
14511511
1452-
dynamic "network_policy" {
1453-
for_each = local.cluster_network_policy
1512+
resource "test_resource" "test" {
1513+
dynamic "foo" {
1514+
for_each = var.some_var
1515+
content {
1516+
bar = foo.key
1517+
}
1518+
}
1519+
}`,
1520+
expected: []any{"ssh", "http", "https"},
1521+
},
1522+
{
1523+
name: "attribute ref to map value",
1524+
src: `variable "some_var" {
1525+
type = map
1526+
default = {
1527+
ssh = { "tag" = "login" }
1528+
http = { "tag" = "proxy" }
1529+
https = { "tag" = "proxy" }
1530+
}
1531+
}
14541532
1533+
resource "test_resource" "test" {
1534+
dynamic "foo" {
1535+
for_each = var.some_var
14551536
content {
1456-
enabled = network_policy.value.enabled
1537+
bar = foo.value.tag
14571538
}
14581539
}
14591540
}`,
1460-
})
1461-
require.Len(t, modules, 1)
1541+
expected: []any{"login", "proxy", "proxy"},
1542+
},
1543+
{
1544+
name: "dynamic block with iterator",
1545+
src: `resource "test_resource" "test" {
1546+
dynamic "foo" {
1547+
for_each = ["foo", "bar"]
1548+
iterator = some_iterator
1549+
content {
1550+
bar = some_iterator.value
1551+
}
1552+
}
1553+
}`,
1554+
expected: []any{"foo", "bar"},
1555+
},
1556+
{
1557+
name: "iterator and parent block with same name",
1558+
src: `resource "test_resource" "test" {
1559+
dynamic "foo" {
1560+
for_each = ["foo", "bar"]
1561+
iterator = foo
1562+
content {
1563+
bar = foo.value
1564+
}
1565+
}
1566+
}`,
1567+
expected: []any{"foo", "bar"},
1568+
},
1569+
{
1570+
name: "for-each use null value",
1571+
src: `resource "test_resource" "test" {
1572+
dynamic "foo" {
1573+
for_each = null
1574+
content {
1575+
bar = foo.value
1576+
}
1577+
}
1578+
}`,
1579+
expected: []any{},
1580+
},
1581+
{
1582+
name: "no for-each attribute",
1583+
src: `resource "test_resource" "test" {
1584+
dynamic "foo" {
1585+
content {
1586+
bar = foo.value
1587+
}
1588+
}
1589+
}`,
1590+
expected: []any{},
1591+
},
1592+
}
14621593

1463-
clusters := modules.GetResourcesByType("google_container_cluster")
1464-
assert.Len(t, clusters, 1)
1594+
for _, tt := range tests {
1595+
t.Run(tt.name, func(t *testing.T) {
1596+
modules := parse(t, map[string]string{
1597+
"main.tf": tt.src,
1598+
})
1599+
require.Len(t, modules, 1)
14651600

1466-
networkPolicies := clusters[0].GetBlocks("network_policy")
1467-
assert.Len(t, networkPolicies, 1)
1601+
resource := modules.GetResourcesByType("test_resource")
1602+
require.Len(t, resource, 1)
1603+
blocks := resource[0].GetBlocks("foo")
14681604

1469-
enabled := networkPolicies[0].GetAttribute("enabled")
1470-
assert.True(t, enabled.Value().True())
1471-
})
1605+
var vals []any
1606+
for _, attr := range blocks {
1607+
vals = append(vals, attr.GetAttribute("bar").GetRawValue())
1608+
}
14721609

1473-
t.Run("nested dynamic", func(t *testing.T) {
1474-
modules := parse(t, map[string]string{
1475-
"main.tf": `
1476-
resource "test_block" "this" {
1477-
name = "name"
1478-
location = "loc"
1479-
dynamic "env" {
1480-
for_each = ["1", "2"]
1481-
content {
1482-
dynamic "value_source" {
1483-
for_each = [true, true]
1484-
content {}
1485-
}
1610+
assert.ElementsMatch(t, tt.expected, vals)
1611+
})
14861612
}
1613+
}
1614+
1615+
func TestNestedDynamicBlock(t *testing.T) {
1616+
modules := parse(t, map[string]string{
1617+
"main.tf": `resource "test_resource" "test" {
1618+
dynamic "foo" {
1619+
for_each = ["1", "1"]
1620+
content {
1621+
dynamic "bar" {
1622+
for_each = [true, true]
1623+
content {
1624+
baz = foo.value
1625+
qux = bar.value
1626+
}
1627+
}
1628+
}
14871629
}
14881630
}`,
1489-
})
1490-
require.Len(t, modules, 1)
1631+
})
1632+
require.Len(t, modules, 1)
14911633

1492-
testResources := modules.GetResourcesByType("test_block")
1493-
assert.Len(t, testResources, 1)
1494-
envs := testResources[0].GetBlocks("env")
1495-
assert.Len(t, envs, 2)
1634+
testResources := modules.GetResourcesByType("test_resource")
1635+
assert.Len(t, testResources, 1)
1636+
blocks := testResources[0].GetBlocks("foo")
1637+
assert.Len(t, blocks, 2)
14961638

1497-
var sources []*terraform.Block
1498-
for _, env := range envs {
1499-
sources = append(sources, env.GetBlocks("value_source")...)
1639+
var nested []*terraform.Block
1640+
for _, block := range blocks {
1641+
nested = append(nested, block.GetBlocks("bar")...)
1642+
for _, b := range nested {
1643+
assert.Equal(t, "1", b.GetAttribute("baz").GetRawValue())
1644+
assert.Equal(t, true, b.GetAttribute("qux").GetRawValue())
15001645
}
1501-
assert.Len(t, sources, 4)
1502-
})
1646+
}
1647+
assert.Len(t, nested, 4)
15031648
}
15041649

15051650
func parse(t *testing.T, files map[string]string) terraform.Modules {
@@ -1513,21 +1658,6 @@ func parse(t *testing.T, files map[string]string) terraform.Modules {
15131658
return modules
15141659
}
15151660

1516-
func compareSets(a, b []int) bool {
1517-
m := make(map[int]bool)
1518-
for _, el := range a {
1519-
m[el] = true
1520-
}
1521-
1522-
for _, el := range b {
1523-
if !m[el] {
1524-
return false
1525-
}
1526-
}
1527-
1528-
return true
1529-
}
1530-
15311661
func TestModuleRefersToOutputOfAnotherModule(t *testing.T) {
15321662
files := map[string]string{
15331663
"main.tf": `
@@ -1775,42 +1905,6 @@ variable "foo" {}
17751905
assert.Equal(t, "bar", blocks[0].GetAttribute("foo").Value().AsString())
17761906
}
17771907

1778-
func TestDynamicWithIterator(t *testing.T) {
1779-
fsys := fstest.MapFS{
1780-
"main.tf": &fstest.MapFile{
1781-
Data: []byte(`resource "aws_s3_bucket" "this" {
1782-
dynamic versioning {
1783-
for_each = [true]
1784-
iterator = ver
1785-
1786-
content {
1787-
enabled = ver.value
1788-
}
1789-
}
1790-
}`),
1791-
},
1792-
}
1793-
1794-
parser := New(
1795-
fsys, "",
1796-
OptionStopOnHCLError(true),
1797-
OptionWithDownloads(false),
1798-
)
1799-
require.NoError(t, parser.ParseFS(context.TODO(), "."))
1800-
1801-
modules, _, err := parser.EvaluateAll(context.TODO())
1802-
require.NoError(t, err)
1803-
1804-
assert.Len(t, modules, 1)
1805-
1806-
buckets := modules.GetResourcesByType("aws_s3_bucket")
1807-
assert.Len(t, buckets, 1)
1808-
1809-
attr, _ := buckets[0].GetNestedAttribute("versioning.enabled")
1810-
1811-
assert.True(t, attr.Value().True())
1812-
}
1813-
18141908
func Test_AWSRegionNameDefined(t *testing.T) {
18151909

18161910
fs := testutil.CreateFS(t, map[string]string{

‎pkg/iac/terraform/attribute.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -729,15 +729,16 @@ func (a *Attribute) IsTrue() bool {
729729
if a == nil {
730730
return false
731731
}
732-
switch a.Value().Type() {
732+
val := a.Value()
733+
switch val.Type() {
733734
case cty.Bool:
734-
return a.Value().True()
735+
return val.True()
735736
case cty.String:
736-
val := a.Value().AsString()
737+
val := val.AsString()
737738
val = strings.Trim(val, "\"")
738739
return strings.EqualFold(val, "true")
739740
case cty.Number:
740-
val := a.Value().AsBigFloat()
741+
val := val.AsBigFloat()
741742
f, _ := val.Float64()
742743
return f > 0
743744
}

‎pkg/iac/terraform/block.go

+149-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package terraform
22

33
import (
4+
"errors"
45
"fmt"
56
"io/fs"
67
"strconv"
@@ -137,34 +138,42 @@ func (b *Block) GetRawValue() any {
137138
return nil
138139
}
139140

140-
func (b *Block) InjectBlock(block *Block, name string) {
141-
block.hclBlock.Labels = []string{}
142-
block.hclBlock.Type = name
141+
func (b *Block) injectBlock(block *Block) {
143142
for attrName, attr := range block.Attributes() {
144-
b.context.Root().SetByDot(attr.Value(), fmt.Sprintf("%s.%s.%s", b.reference.String(), name, attrName))
143+
path := fmt.Sprintf("%s.%s.%s", b.reference.String(), block.hclBlock.Type, attrName)
144+
b.context.Root().SetByDot(attr.Value(), path)
145145
}
146146
b.childBlocks = append(b.childBlocks, block)
147147
}
148148

149-
func (b *Block) MarkExpanded() {
149+
func (b *Block) markExpanded() {
150150
b.expanded = true
151151
}
152152

153153
func (b *Block) IsExpanded() bool {
154154
return b.expanded
155155
}
156156

157-
func (b *Block) Clone(index cty.Value) *Block {
158-
var childCtx *context.Context
159-
if b.context != nil {
160-
childCtx = b.context.NewChild()
161-
} else {
162-
childCtx = context.NewContext(&hcl.EvalContext{}, nil)
157+
func (b *Block) inherit(ctx *context.Context, index ...cty.Value) *Block {
158+
return NewBlock(b.copyBlock(), ctx, b.moduleBlock, b.parentBlock, b.moduleSource, b.moduleFS, index...)
159+
}
160+
161+
func (b *Block) copyBlock() *hcl.Block {
162+
hclBlock := *b.hclBlock
163+
return &hclBlock
164+
}
165+
166+
func (b *Block) childContext() *context.Context {
167+
if b.context == nil {
168+
return context.NewContext(&hcl.EvalContext{}, nil)
163169
}
170+
return b.context.NewChild()
171+
}
164172

165-
cloneHCL := *b.hclBlock
173+
func (b *Block) Clone(index cty.Value) *Block {
174+
childCtx := b.childContext()
175+
clone := b.inherit(childCtx, index)
166176

167-
clone := NewBlock(&cloneHCL, childCtx, b.moduleBlock, b.parentBlock, b.moduleSource, b.moduleFS, index)
168177
if len(clone.hclBlock.Labels) > 0 {
169178
position := len(clone.hclBlock.Labels) - 1
170179
labels := make([]string, len(clone.hclBlock.Labels))
@@ -188,7 +197,7 @@ func (b *Block) Clone(index cty.Value) *Block {
188197
}
189198
indexVal, _ := gocty.ToCtyValue(index, cty.Number)
190199
clone.context.SetByDot(indexVal, "count.index")
191-
clone.MarkExpanded()
200+
clone.markExpanded()
192201
b.cloneIndex++
193202
return clone
194203
}
@@ -446,6 +455,17 @@ func (b *Block) LocalName() string {
446455
return b.reference.String()
447456
}
448457

458+
func (b *Block) FullLocalName() string {
459+
if b.parentBlock != nil {
460+
return fmt.Sprintf(
461+
"%s.%s",
462+
b.parentBlock.FullLocalName(),
463+
b.LocalName(),
464+
)
465+
}
466+
return b.LocalName()
467+
}
468+
449469
func (b *Block) FullName() string {
450470

451471
if b.moduleBlock != nil {
@@ -576,3 +596,118 @@ func (b *Block) IsNil() bool {
576596
func (b *Block) IsNotNil() bool {
577597
return !b.IsNil()
578598
}
599+
600+
func (b *Block) ExpandBlock() error {
601+
var (
602+
expanded []*Block
603+
errs []error
604+
)
605+
606+
for _, child := range b.childBlocks {
607+
if child.Type() == "dynamic" {
608+
blocks, err := child.expandDynamic()
609+
if err != nil {
610+
errs = append(errs, err)
611+
continue
612+
}
613+
expanded = append(expanded, blocks...)
614+
}
615+
}
616+
617+
for _, block := range expanded {
618+
b.injectBlock(block)
619+
}
620+
621+
return errors.Join(errs...)
622+
}
623+
624+
func (b *Block) expandDynamic() ([]*Block, error) {
625+
if b.IsExpanded() || b.Type() != "dynamic" {
626+
return nil, nil
627+
}
628+
629+
realBlockType := b.TypeLabel()
630+
if realBlockType == "" {
631+
return nil, errors.New("dynamic block must have 1 label")
632+
}
633+
634+
forEachVal, err := b.validateForEach()
635+
if err != nil {
636+
return nil, fmt.Errorf("invalid for-each in %s block: %w", b.FullLocalName(), err)
637+
}
638+
639+
var (
640+
expanded []*Block
641+
errs []error
642+
)
643+
644+
forEachVal.ForEachElement(func(key, val cty.Value) (stop bool) {
645+
if val.IsNull() {
646+
return
647+
}
648+
649+
iteratorName, err := b.iteratorName(realBlockType)
650+
if err != nil {
651+
errs = append(errs, err)
652+
return
653+
}
654+
655+
forEachCtx := b.childContext()
656+
obj := cty.ObjectVal(map[string]cty.Value{
657+
"key": key,
658+
"value": val,
659+
})
660+
forEachCtx.Set(obj, iteratorName)
661+
662+
if content := b.GetBlock("content"); content != nil {
663+
inherited := content.inherit(forEachCtx)
664+
inherited.hclBlock.Labels = []string{}
665+
inherited.hclBlock.Type = realBlockType
666+
if err := inherited.ExpandBlock(); err != nil {
667+
errs = append(errs, err)
668+
return
669+
}
670+
expanded = append(expanded, inherited)
671+
}
672+
return
673+
})
674+
675+
if len(expanded) > 0 {
676+
b.markExpanded()
677+
}
678+
679+
return expanded, errors.Join(errs...)
680+
}
681+
682+
func (b *Block) validateForEach() (cty.Value, error) {
683+
forEachAttr := b.GetAttribute("for_each")
684+
if forEachAttr == nil {
685+
return cty.NilVal, errors.New("for_each attribute required")
686+
}
687+
688+
forEachVal := forEachAttr.Value()
689+
690+
if !forEachVal.CanIterateElements() {
691+
return cty.NilVal, fmt.Errorf("cannot use a %s value in for_each. An iterable collection is required", forEachVal.GoString())
692+
}
693+
694+
return forEachVal, nil
695+
}
696+
697+
func (b *Block) iteratorName(blockType string) (string, error) {
698+
iteratorAttr := b.GetAttribute("iterator")
699+
if iteratorAttr == nil {
700+
return blockType, nil
701+
}
702+
703+
traversal, diags := hcl.AbsTraversalForExpr(iteratorAttr.hclAttribute.Expr)
704+
if diags.HasErrors() {
705+
return "", diags
706+
}
707+
708+
if len(traversal) != 1 {
709+
return "", fmt.Errorf("dynamic iterator must be a single variable name")
710+
}
711+
712+
return traversal.RootName(), nil
713+
}

0 commit comments

Comments
 (0)
Please sign in to comment.