Skip to content

Commit e6b004b

Browse files
committed
Merge remote-tracking branch 'emyrk/stevenmasley/count_argument_references' into dynamic_parameters
2 parents cec73b7 + 3ac5bfe commit e6b004b

File tree

4 files changed

+292
-19
lines changed

4 files changed

+292
-19
lines changed
Lines changed: 34 additions & 0 deletions
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+
}
Lines changed: 103 additions & 0 deletions
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

Lines changed: 59 additions & 19 deletions
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

@@ -548,7 +549,6 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
548549
values := make(map[string]cty.Value)
549550

550551
for _, b := range blocksOfType {
551-
552552
switch b.Type() {
553553
case "variable": // variables are special in that their value comes from the "default" attribute
554554
val, err := e.evaluateVariable(b)
@@ -563,9 +563,7 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
563563
}
564564
values[b.Label()] = val
565565
case "locals", "moved", "import":
566-
for key, val := range b.Values().AsValueMap() {
567-
values[key] = val
568-
}
566+
maps.Copy(values, b.Values().AsValueMap())
569567
case "provider", "module", "check":
570568
if b.Label() == "" {
571569
continue
@@ -576,19 +574,27 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
576574
continue
577575
}
578576

579-
blockMap, ok := values[b.Labels()[0]]
577+
// Data blocks should all be loaded into the top level 'values'
578+
// object. The hierarchy of the map is:
579+
// values = map[<type>]map[<name>] =
580+
// Block -> Block's attributes as a cty.Object
581+
// Tuple(Block) -> Instances of the block
582+
// Object(Block) -> Field values are instances of the block
583+
ref := b.Reference()
584+
typeValues, ok := values[ref.TypeLabel()]
580585
if !ok {
581-
values[b.Labels()[0]] = cty.ObjectVal(make(map[string]cty.Value))
582-
blockMap = values[b.Labels()[0]]
586+
typeValues = cty.ObjectVal(make(map[string]cty.Value))
587+
values[ref.TypeLabel()] = typeValues
583588
}
584589

585-
valueMap := blockMap.AsValueMap()
590+
valueMap := typeValues.AsValueMap()
586591
if valueMap == nil {
587592
valueMap = make(map[string]cty.Value)
588593
}
594+
valueMap[ref.NameLabel()] = blockInstanceValues(b, valueMap)
589595

590-
valueMap[b.Labels()[1]] = b.Values()
591-
values[b.Labels()[0]] = cty.ObjectVal(valueMap)
596+
// Update the map of all blocks with the same type.
597+
values[ref.TypeLabel()] = cty.ObjectVal(valueMap)
592598
}
593599
}
594600

@@ -599,23 +605,57 @@ func (e *evaluator) getResources() map[string]cty.Value {
599605
values := make(map[string]map[string]cty.Value)
600606

601607
for _, b := range e.blocks {
602-
if b.Type() != "resource" {
603-
continue
604-
}
605-
606-
if len(b.Labels()) < 2 {
608+
if b.Type() != "resource" || len(b.Labels()) < 2 {
607609
continue
608610
}
609611

610-
val, exists := values[b.Labels()[0]]
612+
ref := b.Reference()
613+
typeValues, exists := values[ref.TypeLabel()]
611614
if !exists {
612-
val = make(map[string]cty.Value)
613-
values[b.Labels()[0]] = val
615+
typeValues = make(map[string]cty.Value)
616+
values[ref.TypeLabel()] = typeValues
614617
}
615-
val[b.Labels()[1]] = b.Values()
618+
typeValues[ref.NameLabel()] = blockInstanceValues(b, typeValues)
616619
}
617620

618621
return lo.MapValues(values, func(v map[string]cty.Value, _ string) cty.Value {
619622
return cty.ObjectVal(v)
620623
})
621624
}
625+
626+
// blockInstanceValues returns a cty.Value containing the values of the block instances.
627+
// If the count argument is used, a tuple is returned where the index corresponds to the argument index.
628+
// If the for_each argument is used, an object is returned where the key corresponds to the argument key.
629+
// In other cases, the values of the block itself are returned.
630+
func blockInstanceValues(b *terraform.Block, typeValues map[string]cty.Value) cty.Value {
631+
ref := b.Reference()
632+
key := ref.RawKey()
633+
634+
switch {
635+
case key.Type().Equals(cty.Number) && b.GetAttribute("count") != nil:
636+
idx, _ := key.AsBigFloat().Int64()
637+
return insertTupleElement(typeValues[ref.NameLabel()], int(idx), b.Values())
638+
case isForEachKey(key) && b.GetAttribute("for_each") != nil:
639+
keyStr := ref.Key()
640+
641+
instancesVal, exists := typeValues[ref.NameLabel()]
642+
if !exists || !instancesVal.CanIterateElements() {
643+
instancesVal = cty.EmptyObjectVal
644+
}
645+
646+
instances := instancesVal.AsValueMap()
647+
if instances == nil {
648+
instances = make(map[string]cty.Value)
649+
}
650+
651+
instances[keyStr] = b.Values()
652+
return cty.ObjectVal(instances)
653+
654+
default:
655+
return b.Values()
656+
}
657+
}
658+
659+
func isForEachKey(key cty.Value) bool {
660+
return key.Type().Equals(cty.Number) || key.Type().Equals(cty.String)
661+
}

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

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,102 @@ output "staticZero" {
18271827
require.Len(t, modules, 2)
18281828
}
18291829

1830+
func TestPopulateContextWithBlockInstances(t *testing.T) {
1831+
1832+
tests := []struct {
1833+
name string
1834+
files map[string]string
1835+
}{
1836+
{
1837+
name: "data blocks with count",
1838+
files: map[string]string{
1839+
"main.tf": `data "d" "foo" {
1840+
count = 1
1841+
value = "Index ${count.index}"
1842+
}
1843+
1844+
data "b" "foo" {
1845+
count = 1
1846+
value = data.d.foo[0].value
1847+
}
1848+
1849+
data "c" "foo" {
1850+
count = 1
1851+
value = data.b.foo[0].value
1852+
}`,
1853+
},
1854+
},
1855+
{
1856+
name: "resource blocks with count",
1857+
files: map[string]string{
1858+
"main.tf": `resource "d" "foo" {
1859+
count = 1
1860+
value = "Index ${count.index}"
1861+
}
1862+
1863+
resource "b" "foo" {
1864+
count = 1
1865+
value = d.foo[0].value
1866+
}
1867+
1868+
resource "c" "foo" {
1869+
count = 1
1870+
value = b.foo[0].value
1871+
}`,
1872+
},
1873+
},
1874+
{
1875+
name: "data blocks with for_each",
1876+
files: map[string]string{
1877+
"main.tf": `data "d" "foo" {
1878+
for_each = toset([0])
1879+
value = "Index ${each.key}"
1880+
}
1881+
1882+
data "b" "foo" {
1883+
for_each = data.d.foo
1884+
value = each.value.value
1885+
}
1886+
1887+
data "c" "foo" {
1888+
for_each = data.b.foo
1889+
value = each.value.value
1890+
}`,
1891+
},
1892+
},
1893+
{
1894+
name: "resource blocks with for_each",
1895+
files: map[string]string{
1896+
"main.tf": `resource "d" "foo" {
1897+
for_each = toset([0])
1898+
value = "Index ${each.key}"
1899+
}
1900+
1901+
resource "b" "foo" {
1902+
for_each = d.foo
1903+
value = each.value.value
1904+
}
1905+
1906+
resource "c" "foo" {
1907+
for_each = b.foo
1908+
value = each.value.value
1909+
}`,
1910+
},
1911+
},
1912+
}
1913+
1914+
for _, tt := range tests {
1915+
t.Run(tt.name, func(t *testing.T) {
1916+
modules := parse(t, tt.files)
1917+
require.Len(t, modules, 1)
1918+
for _, b := range modules.GetBlocks() {
1919+
attr := b.GetAttribute("value")
1920+
assert.Equal(t, "Index 0", attr.Value().AsString())
1921+
}
1922+
})
1923+
}
1924+
}
1925+
18301926
// TestNestedModulesOptions ensures parser options are carried to the nested
18311927
// submodule evaluators.
18321928
// The test will include an invalid module that will fail to download

0 commit comments

Comments
 (0)