Skip to content

Commit e25de25

Browse files
Emyrknikpivkin
andauthored
fix(terraform): evaluateStep to correctly set EvalContext for multiple instances of blocks (#8555)
Signed-off-by: nikpivkin <[email protected]> Co-authored-by: nikpivkin <[email protected]>
1 parent 4b84dab commit e25de25

File tree

4 files changed

+292
-19
lines changed

4 files changed

+292
-19
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package parser
2+
3+
import "github.com/zclconf/go-cty/cty"
4+
5+
// insertTupleElement inserts a value into a tuple at the specified index.
6+
// If the idx is outside the bounds of the list, it grows the tuple to
7+
// the new size, and fills in `cty.NilVal` for the missing elements.
8+
//
9+
// This function will not panic. If the list value is not a list, it will
10+
// be replaced with an empty list.
11+
func insertTupleElement(list cty.Value, idx int, val cty.Value) cty.Value {
12+
if list.IsNull() || !list.Type().IsTupleType() {
13+
// better than a panic
14+
list = cty.EmptyTupleVal
15+
}
16+
17+
if idx < 0 {
18+
// Nothing to do?
19+
return list
20+
}
21+
22+
// Create a new list of the correct length, copying in the old list
23+
// values for matching indices.
24+
newList := make([]cty.Value, max(idx+1, list.LengthInt()))
25+
for it := list.ElementIterator(); it.Next(); {
26+
key, elem := it.Element()
27+
elemIdx, _ := key.AsBigFloat().Int64()
28+
newList[elemIdx] = elem
29+
}
30+
// Insert the new value.
31+
newList[idx] = val
32+
33+
return cty.TupleVal(newList)
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package parser
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
"github.com/zclconf/go-cty/cty"
8+
)
9+
10+
func Test_insertTupleElement(t *testing.T) {
11+
t.Parallel()
12+
13+
tests := []struct {
14+
name string
15+
start cty.Value
16+
index int
17+
value cty.Value
18+
want cty.Value
19+
}{
20+
{
21+
name: "empty",
22+
start: cty.Value{},
23+
index: 0,
24+
value: cty.NilVal,
25+
want: cty.TupleVal([]cty.Value{cty.NilVal}),
26+
},
27+
{
28+
name: "empty to length",
29+
start: cty.Value{},
30+
index: 2,
31+
value: cty.NilVal,
32+
want: cty.TupleVal([]cty.Value{cty.NilVal, cty.NilVal, cty.NilVal}),
33+
},
34+
{
35+
name: "insert to empty",
36+
start: cty.EmptyTupleVal,
37+
index: 1,
38+
value: cty.NumberIntVal(5),
39+
want: cty.TupleVal([]cty.Value{cty.NilVal, cty.NumberIntVal(5)}),
40+
},
41+
{
42+
name: "insert to existing",
43+
start: cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3)}),
44+
index: 1,
45+
value: cty.NumberIntVal(5),
46+
want: cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(5), cty.NumberIntVal(3)}),
47+
},
48+
{
49+
name: "insert to existing, extends",
50+
start: cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3)}),
51+
index: 4,
52+
value: cty.NumberIntVal(5),
53+
want: cty.TupleVal([]cty.Value{
54+
cty.NumberIntVal(1), cty.NumberIntVal(2),
55+
cty.NumberIntVal(3), cty.NilVal,
56+
cty.NumberIntVal(5),
57+
}),
58+
},
59+
{
60+
name: "mixed list",
61+
start: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}),
62+
index: 1,
63+
value: cty.BoolVal(true),
64+
want: cty.TupleVal([]cty.Value{
65+
cty.StringVal("a"), cty.BoolVal(true), cty.NumberIntVal(3),
66+
}),
67+
},
68+
{
69+
name: "replace end",
70+
start: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}),
71+
index: 2,
72+
value: cty.StringVal("end"),
73+
want: cty.TupleVal([]cty.Value{
74+
cty.StringVal("a"), cty.NumberIntVal(2), cty.StringVal("end"),
75+
}),
76+
},
77+
78+
// Some bad arguments
79+
{
80+
name: "negative index",
81+
start: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}),
82+
index: -1,
83+
value: cty.BoolVal(true),
84+
want: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}),
85+
},
86+
{
87+
name: "non-list",
88+
start: cty.BoolVal(true),
89+
index: 1,
90+
value: cty.BoolVal(true),
91+
want: cty.TupleVal([]cty.Value{cty.NilVal, cty.BoolVal(true)}),
92+
},
93+
}
94+
95+
for _, tt := range tests {
96+
tt := tt
97+
t.Run(tt.name, func(t *testing.T) {
98+
t.Parallel()
99+
100+
require.Equal(t, tt.want, insertTupleElement(tt.start, tt.index, tt.value))
101+
})
102+
}
103+
}

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

+59-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"io/fs"
7+
"maps"
78
"reflect"
89
"slices"
910

@@ -521,7 +522,6 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
521522
values := make(map[string]cty.Value)
522523

523524
for _, b := range blocksOfType {
524-
525525
switch b.Type() {
526526
case "variable": // variables are special in that their value comes from the "default" attribute
527527
val, err := e.evaluateVariable(b)
@@ -536,9 +536,7 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
536536
}
537537
values[b.Label()] = val
538538
case "locals", "moved", "import":
539-
for key, val := range b.Values().AsValueMap() {
540-
values[key] = val
541-
}
539+
maps.Copy(values, b.Values().AsValueMap())
542540
case "provider", "module", "check":
543541
if b.Label() == "" {
544542
continue
@@ -549,19 +547,27 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
549547
continue
550548
}
551549

552-
blockMap, ok := values[b.Labels()[0]]
550+
// Data blocks should all be loaded into the top level 'values'
551+
// object. The hierarchy of the map is:
552+
// values = map[<type>]map[<name>] =
553+
// Block -> Block's attributes as a cty.Object
554+
// Tuple(Block) -> Instances of the block
555+
// Object(Block) -> Field values are instances of the block
556+
ref := b.Reference()
557+
typeValues, ok := values[ref.TypeLabel()]
553558
if !ok {
554-
values[b.Labels()[0]] = cty.ObjectVal(make(map[string]cty.Value))
555-
blockMap = values[b.Labels()[0]]
559+
typeValues = cty.ObjectVal(make(map[string]cty.Value))
560+
values[ref.TypeLabel()] = typeValues
556561
}
557562

558-
valueMap := blockMap.AsValueMap()
563+
valueMap := typeValues.AsValueMap()
559564
if valueMap == nil {
560565
valueMap = make(map[string]cty.Value)
561566
}
567+
valueMap[ref.NameLabel()] = blockInstanceValues(b, valueMap)
562568

563-
valueMap[b.Labels()[1]] = b.Values()
564-
values[b.Labels()[0]] = cty.ObjectVal(valueMap)
569+
// Update the map of all blocks with the same type.
570+
values[ref.TypeLabel()] = cty.ObjectVal(valueMap)
565571
}
566572
}
567573

@@ -572,23 +578,57 @@ func (e *evaluator) getResources() map[string]cty.Value {
572578
values := make(map[string]map[string]cty.Value)
573579

574580
for _, b := range e.blocks {
575-
if b.Type() != "resource" {
576-
continue
577-
}
578-
579-
if len(b.Labels()) < 2 {
581+
if b.Type() != "resource" || len(b.Labels()) < 2 {
580582
continue
581583
}
582584

583-
val, exists := values[b.Labels()[0]]
585+
ref := b.Reference()
586+
typeValues, exists := values[ref.TypeLabel()]
584587
if !exists {
585-
val = make(map[string]cty.Value)
586-
values[b.Labels()[0]] = val
588+
typeValues = make(map[string]cty.Value)
589+
values[ref.TypeLabel()] = typeValues
587590
}
588-
val[b.Labels()[1]] = b.Values()
591+
typeValues[ref.NameLabel()] = blockInstanceValues(b, typeValues)
589592
}
590593

591594
return lo.MapValues(values, func(v map[string]cty.Value, _ string) cty.Value {
592595
return cty.ObjectVal(v)
593596
})
594597
}
598+
599+
// blockInstanceValues returns a cty.Value containing the values of the block instances.
600+
// If the count argument is used, a tuple is returned where the index corresponds to the argument index.
601+
// If the for_each argument is used, an object is returned where the key corresponds to the argument key.
602+
// In other cases, the values of the block itself are returned.
603+
func blockInstanceValues(b *terraform.Block, typeValues map[string]cty.Value) cty.Value {
604+
ref := b.Reference()
605+
key := ref.RawKey()
606+
607+
switch {
608+
case key.Type().Equals(cty.Number) && b.GetAttribute("count") != nil:
609+
idx, _ := key.AsBigFloat().Int64()
610+
return insertTupleElement(typeValues[ref.NameLabel()], int(idx), b.Values())
611+
case isForEachKey(key) && b.GetAttribute("for_each") != nil:
612+
keyStr := ref.Key()
613+
614+
instancesVal, exists := typeValues[ref.NameLabel()]
615+
if !exists || !instancesVal.CanIterateElements() {
616+
instancesVal = cty.EmptyObjectVal
617+
}
618+
619+
instances := instancesVal.AsValueMap()
620+
if instances == nil {
621+
instances = make(map[string]cty.Value)
622+
}
623+
624+
instances[keyStr] = b.Values()
625+
return cty.ObjectVal(instances)
626+
627+
default:
628+
return b.Values()
629+
}
630+
}
631+
632+
func isForEachKey(key cty.Value) bool {
633+
return key.Type().Equals(cty.Number) || key.Type().Equals(cty.String)
634+
}

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

+96
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,102 @@ resource "test_resource" "this" {
17041704
assert.Equal(t, "test_value", attr.GetRawValue())
17051705
}
17061706

1707+
func TestPopulateContextWithBlockInstances(t *testing.T) {
1708+
1709+
tests := []struct {
1710+
name string
1711+
files map[string]string
1712+
}{
1713+
{
1714+
name: "data blocks with count",
1715+
files: map[string]string{
1716+
"main.tf": `data "d" "foo" {
1717+
count = 1
1718+
value = "Index ${count.index}"
1719+
}
1720+
1721+
data "b" "foo" {
1722+
count = 1
1723+
value = data.d.foo[0].value
1724+
}
1725+
1726+
data "c" "foo" {
1727+
count = 1
1728+
value = data.b.foo[0].value
1729+
}`,
1730+
},
1731+
},
1732+
{
1733+
name: "resource blocks with count",
1734+
files: map[string]string{
1735+
"main.tf": `resource "d" "foo" {
1736+
count = 1
1737+
value = "Index ${count.index}"
1738+
}
1739+
1740+
resource "b" "foo" {
1741+
count = 1
1742+
value = d.foo[0].value
1743+
}
1744+
1745+
resource "c" "foo" {
1746+
count = 1
1747+
value = b.foo[0].value
1748+
}`,
1749+
},
1750+
},
1751+
{
1752+
name: "data blocks with for_each",
1753+
files: map[string]string{
1754+
"main.tf": `data "d" "foo" {
1755+
for_each = toset([0])
1756+
value = "Index ${each.key}"
1757+
}
1758+
1759+
data "b" "foo" {
1760+
for_each = data.d.foo
1761+
value = each.value.value
1762+
}
1763+
1764+
data "c" "foo" {
1765+
for_each = data.b.foo
1766+
value = each.value.value
1767+
}`,
1768+
},
1769+
},
1770+
{
1771+
name: "resource blocks with for_each",
1772+
files: map[string]string{
1773+
"main.tf": `resource "d" "foo" {
1774+
for_each = toset([0])
1775+
value = "Index ${each.key}"
1776+
}
1777+
1778+
resource "b" "foo" {
1779+
for_each = d.foo
1780+
value = each.value.value
1781+
}
1782+
1783+
resource "c" "foo" {
1784+
for_each = b.foo
1785+
value = each.value.value
1786+
}`,
1787+
},
1788+
},
1789+
}
1790+
1791+
for _, tt := range tests {
1792+
t.Run(tt.name, func(t *testing.T) {
1793+
modules := parse(t, tt.files)
1794+
require.Len(t, modules, 1)
1795+
for _, b := range modules.GetBlocks() {
1796+
attr := b.GetAttribute("value")
1797+
assert.Equal(t, "Index 0", attr.Value().AsString())
1798+
}
1799+
})
1800+
}
1801+
}
1802+
17071803
// TestNestedModulesOptions ensures parser options are carried to the nested
17081804
// submodule evaluators.
17091805
// The test will include an invalid module that will fail to download

0 commit comments

Comments
 (0)