fix(misconf): populate context correctly for module instances (#8656)

Signed-off-by: nikpivkin <nikita.pivkin@smartforce.io>
This commit is contained in:
Nikita Pivkin
2025-04-12 09:20:11 +06:00
committed by GitHub
parent b7dfd64987
commit efd177b300
2 changed files with 72 additions and 15 deletions

View File

@@ -137,6 +137,8 @@ func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[str
// expand out resources and modules via count, for-each and dynamic // expand out resources and modules via count, for-each and dynamic
// (not a typo, we do this twice so every order is processed) // (not a typo, we do this twice so every order is processed)
// TODO: using a module in for_each or count does not work,
// because the child module is evaluated later
e.blocks = e.expandBlocks(e.blocks) e.blocks = e.expandBlocks(e.blocks)
e.blocks = e.expandBlocks(e.blocks) e.blocks = e.expandBlocks(e.blocks)
@@ -239,10 +241,17 @@ func (e *evaluator) evaluateSubmodule(ctx context.Context, sm *submodule) bool {
sm.modules, sm.fsMap = sm.eval.EvaluateAll(ctx) sm.modules, sm.fsMap = sm.eval.EvaluateAll(ctx)
outputs := sm.eval.exportOutputs() outputs := sm.eval.exportOutputs()
valueMap := e.ctx.Get("module").AsValueMap()
if valueMap == nil {
valueMap = make(map[string]cty.Value)
}
// lastState needs to be captured after applying outputs so that they // lastState needs to be captured after applying outputs so that they
// don't get treated as changes but before running post-submodule // don't get treated as changes but before running post-submodule
// evaluation, so that changes from that can trigger re-evaluations of // evaluation, so that changes from that can trigger re-evaluations of
// the submodule if/when they feed back into inputs. // the submodule if/when they feed back into inputs.
ref := sm.definition.Definition.Reference()
e.ctx.Set(blockInstanceValues(sm.definition.Definition, valueMap, outputs), "module", ref.NameLabel())
e.ctx.Set(outputs, "module", sm.definition.Name) e.ctx.Set(outputs, "module", sm.definition.Name)
sm.lastState = sm.definition.inputVars() sm.lastState = sm.definition.inputVars()
e.evaluateSteps() e.evaluateSteps()
@@ -564,7 +573,7 @@ func (e *evaluator) getValuesByBlockType(blockType string) cty.Value {
if valueMap == nil { if valueMap == nil {
valueMap = make(map[string]cty.Value) valueMap = make(map[string]cty.Value)
} }
valueMap[ref.NameLabel()] = blockInstanceValues(b, valueMap) valueMap[ref.NameLabel()] = blockInstanceValues(b, valueMap, b.Values())
// Update the map of all blocks with the same type. // Update the map of all blocks with the same type.
values[ref.TypeLabel()] = cty.ObjectVal(valueMap) values[ref.TypeLabel()] = cty.ObjectVal(valueMap)
@@ -588,7 +597,7 @@ func (e *evaluator) getResources() map[string]cty.Value {
typeValues = make(map[string]cty.Value) typeValues = make(map[string]cty.Value)
values[ref.TypeLabel()] = typeValues values[ref.TypeLabel()] = typeValues
} }
typeValues[ref.NameLabel()] = blockInstanceValues(b, typeValues) typeValues[ref.NameLabel()] = blockInstanceValues(b, typeValues, b.Values())
} }
return lo.MapValues(values, func(v map[string]cty.Value, _ string) cty.Value { return lo.MapValues(values, func(v map[string]cty.Value, _ string) cty.Value {
@@ -600,14 +609,14 @@ func (e *evaluator) getResources() map[string]cty.Value {
// If the count argument is used, a tuple is returned where the index corresponds to the argument index. // If the count argument is used, a tuple is returned where the index corresponds to the argument index.
// If the for_each argument is used, an object is returned where the key corresponds to the argument key. // If the for_each argument is used, an object is returned where the key corresponds to the argument key.
// In other cases, the values of the block itself are returned. // In other cases, the values of the block itself are returned.
func blockInstanceValues(b *terraform.Block, typeValues map[string]cty.Value) cty.Value { func blockInstanceValues(b *terraform.Block, typeValues map[string]cty.Value, values cty.Value) cty.Value {
ref := b.Reference() ref := b.Reference()
key := ref.RawKey() key := ref.RawKey()
switch { switch {
case key.Type().Equals(cty.Number) && b.GetAttribute("count") != nil: case key.Type().Equals(cty.Number) && b.GetAttribute("count") != nil:
idx, _ := key.AsBigFloat().Int64() idx, _ := key.AsBigFloat().Int64()
return insertTupleElement(typeValues[ref.NameLabel()], int(idx), b.Values()) return insertTupleElement(typeValues[ref.NameLabel()], int(idx), values)
case isForEachKey(key) && b.GetAttribute("for_each") != nil: case isForEachKey(key) && b.GetAttribute("for_each") != nil:
keyStr := ref.Key() keyStr := ref.Key()
@@ -621,11 +630,10 @@ func blockInstanceValues(b *terraform.Block, typeValues map[string]cty.Value) ct
instances = make(map[string]cty.Value) instances = make(map[string]cty.Value)
} }
instances[keyStr] = b.Values() instances[keyStr] = values
return cty.ObjectVal(instances) return cty.ObjectVal(instances)
default: default:
return b.Values() return values
} }
} }

View File

@@ -1708,10 +1708,12 @@ func TestPopulateContextWithBlockInstances(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
blockType string
files map[string]string files map[string]string
}{ }{
{ {
name: "data blocks with count", name: "data blocks with count",
blockType: "data",
files: map[string]string{ files: map[string]string{
"main.tf": `data "d" "foo" { "main.tf": `data "d" "foo" {
count = 1 count = 1
@@ -1731,6 +1733,7 @@ data "c" "foo" {
}, },
{ {
name: "resource blocks with count", name: "resource blocks with count",
blockType: "resource",
files: map[string]string{ files: map[string]string{
"main.tf": `resource "d" "foo" { "main.tf": `resource "d" "foo" {
count = 1 count = 1
@@ -1745,11 +1748,37 @@ resource "b" "foo" {
resource "c" "foo" { resource "c" "foo" {
count = 1 count = 1
value = b.foo[0].value value = b.foo[0].value
}`,
},
},
{
name: "module block with count",
blockType: "data",
files: map[string]string{
"main.tf": `module "a" {
source = "./modules/a"
count = 2
inp = "Index ${count.index}"
}
data "b" "foo" {
count = 1
value = module.a[0].value
}
data "c" "foo" {
count = 1
value = data.b.foo[0].value
}`,
"modules/a/main.tf": `variable "inp" {}
output "value" {
value = var.inp
}`, }`,
}, },
}, },
{ {
name: "data blocks with for_each", name: "data blocks with for_each",
blockType: "data",
files: map[string]string{ files: map[string]string{
"main.tf": `data "d" "foo" { "main.tf": `data "d" "foo" {
for_each = toset([0]) for_each = toset([0])
@@ -1769,6 +1798,7 @@ data "c" "foo" {
}, },
{ {
name: "resource blocks with for_each", name: "resource blocks with for_each",
blockType: "resource",
files: map[string]string{ files: map[string]string{
"main.tf": `resource "d" "foo" { "main.tf": `resource "d" "foo" {
for_each = toset([0]) for_each = toset([0])
@@ -1783,6 +1813,25 @@ resource "b" "foo" {
resource "c" "foo" { resource "c" "foo" {
for_each = b.foo for_each = b.foo
value = each.value.value value = each.value.value
}`,
},
},
{
name: "module block with for_each",
blockType: "data",
files: map[string]string{
"main.tf": `module "a" {
for_each = toset([0])
source = "./modules/a"
inp = "Index ${each.key}"
}
data "b" "foo" {
value = module.a["0"].value
}`,
"modules/a/main.tf": `variable "inp" {}
output "value" {
value = var.inp
}`, }`,
}, },
}, },
@@ -1791,8 +1840,8 @@ resource "c" "foo" {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
modules := parse(t, tt.files) modules := parse(t, tt.files)
require.Len(t, modules, 1) require.GreaterOrEqual(t, len(modules), 1)
for _, b := range modules.GetBlocks() { for _, b := range modules.GetBlocks().OfType(tt.blockType) {
attr := b.GetAttribute("value") attr := b.GetAttribute("value")
assert.Equal(t, "Index 0", attr.Value().AsString()) assert.Equal(t, "Index 0", attr.Value().AsString())
} }