Add more tests and refactor import for_each (#1645)

Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
This commit is contained in:
Ronny Orot 2024-07-26 18:01:47 +03:00 committed by GitHub
parent a003782fec
commit 884410e63a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 481 additions and 174 deletions

View File

@ -122,8 +122,11 @@ func (ri *ImportResolver) ExpandAndResolveImport(importTarget *ImportTarget, ctx
rootCtx := ctx.WithPath(addrs.RootModuleInstance)
if importTarget.Config.ForEach != nil {
const unknownsNotAllowed = false
const tupleAllowed = true
// The import target has a for_each attribute, so we need to expand it
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, false, true)
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, unknownsNotAllowed, tupleAllowed)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return diags
@ -166,7 +169,7 @@ func (ri *ImportResolver) resolveImport(importTarget *ImportTarget, ctx EvalCont
return diags
}
importAddress, addressDiags := ctx.EvaluateImportAddress(importTarget.Config.To, keyData)
importAddress, addressDiags := evaluateImportAddress(ctx, importTarget.Config.To, keyData)
diags = diags.Append(addressDiags)
if diags.HasErrors() {
return diags

View File

@ -4646,6 +4646,171 @@ import {
}
}
func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
type TestConfiguration struct {
Description string
expectedError string
inlineConfiguration map[string]string
}
configurations := []TestConfiguration{
{
Description: "To address index value is null",
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
inlineConfiguration: map[string]string{
"main.tf": `
variable "index" {
default = null
}
resource "test_object" "a" {
count = 1
test_string = "foo"
}
import {
to = test_object.a[var.index]
id = "123"
}
`,
},
},
{
Description: "To address index is not a number or a string",
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
inlineConfiguration: map[string]string{
"main.tf": `
locals {
index = toset(["foo"])
}
resource "test_object" "a" {
for_each = toset(["foo"])
test_string = "foo"
}
import {
to = test_object.a[local.index]
id = "123"
}
`,
},
},
{
Description: "To address index value is sensitive",
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
inlineConfiguration: map[string]string{
"main.tf": `
locals {
index = sensitive("foo")
}
resource "test_object" "a" {
for_each = toset(["foo"])
test_string = "foo"
}
import {
to = test_object.a[local.index]
id = "123"
}
`,
},
},
{
Description: "To address index value will only be known after apply",
expectedError: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
inlineConfiguration: map[string]string{
"main.tf": `
resource "test_object" "reference" {
}
resource "test_object" "a" {
count = 1
test_string = "foo"
}
import {
to = test_object.a[test_object.reference.id]
id = "123"
}
`,
},
},
}
for _, configuration := range configurations {
t.Run(configuration.Description, func(t *testing.T) {
m := testModuleInline(t, configuration.inlineConfiguration)
providerSchema := &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"test_string": {
Type: cty.String,
Optional: true,
},
"id": {
Type: cty.String,
Computed: true,
},
},
}
p := &MockProvider{
GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{
Provider: providers.Schema{Block: providerSchema},
ResourceTypes: map[string]providers.Schema{
"test_object": providers.Schema{Block: providerSchema},
},
},
}
hook := new(MockHook)
ctx := testContext2(t, &ContextOpts{
Hooks: []Hook{hook},
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
p.ReadResourceResponse = &providers.ReadResourceResponse{
NewState: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("foo"),
}),
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
testStringVal := req.ProposedNewState.GetAttr("test_string")
return providers.PlanResourceChangeResponse{
PlannedState: cty.ObjectVal(map[string]cty.Value{
"test_string": testStringVal,
"id": cty.UnknownVal(cty.String),
}),
}
}
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "test_object",
State: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("foo"),
}),
},
},
}
_, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
if !diags.HasErrors() {
t.Fatal("succeeded; want errors")
}
if got, want := diags.Err().Error(), configuration.expectedError; !strings.Contains(got, want) {
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
}
})
}
}
func TestContext2Plan_importForEach(t *testing.T) {
type ImportResult struct {
ResolvedAddress string
@ -4831,7 +4996,7 @@ import {
}
}
func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
func TestContext2Plan_importWithInvalidForEach(t *testing.T) {
type TestConfiguration struct {
Description string
expectedError string
@ -4839,84 +5004,221 @@ func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
}
configurations := []TestConfiguration{
{
Description: "To address index value is null",
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
Description: "for_each value is null",
expectedError: "Invalid import id argument: The import ID cannot be null",
inlineConfiguration: map[string]string{
"main.tf": `
variable "index" {
locals {
map = {
"key1" = null
}
}
resource "test_object" "a" {
for_each = local.map
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each key is null",
expectedError: "Null value as key: Can't use a null value as a key.",
inlineConfiguration: map[string]string{
"main.tf": `
variable "nil" {
default = null
}
locals {
map = {
(var.nil) = "val1"
}
}
resource "test_object" "a" {
for_each = local.map
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each expression is null",
expectedError: `Invalid for_each argument: The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, set of strings, or a tuple, and you have provided a value of type dynamic.`,
inlineConfiguration: map[string]string{
"main.tf": `
variable "map" {
default = null
}
resource "test_object" "a" {
count = 1
test_string = "foo"
}
import {
to = test_object.a[var.index]
id = "123"
for_each = var.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "To address index is not a number or a string",
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
inlineConfiguration: map[string]string{
"main.tf": `
locals {
index = toset(["foo"])
}
resource "test_object" "a" {
for_each = toset(["foo"])
test_string = "foo"
}
import {
to = test_object.a[local.index]
id = "123"
}
`,
},
},
{
Description: "To address index value is sensitive",
expectedError: "Import block 'to' address contains an invalid key: Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
inlineConfiguration: map[string]string{
"main.tf": `
locals {
index = sensitive("foo")
}
resource "test_object" "a" {
for_each = toset(["foo"])
test_string = "foo"
}
import {
to = test_object.a[local.index]
id = "123"
}
`,
},
},
{
Description: "To address index value will only be known after apply",
expectedError: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
Description: "for_each key is unknown",
expectedError: `Invalid for_each argument: The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.`,
inlineConfiguration: map[string]string{
"main.tf": `
resource "test_object" "reference" {
}
locals {
map = {
(test_object.reference.id) = "val1"
}
}
resource "test_object" "a" {
count = 1
test_string = "foo"
}
import {
to = test_object.a[test_object.reference.id]
id = "123"
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each value is unknown",
expectedError: `Invalid import id argument: The import block "id" argument depends on resource attributes that cannot be determined until apply, so OpenTofu cannot plan to import this resource.`,
inlineConfiguration: map[string]string{
"main.tf": `
resource "test_object" "reference" {
}
locals {
map = {
"key1" = (test_object.reference.id)
}
}
resource "test_object" "a" {
count = 1
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each expression is unknown",
expectedError: `Invalid for_each argument: The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so OpenTofu cannot determine the full set of keys that will identify the instances of this resource.`,
inlineConfiguration: map[string]string{
"main.tf": `
resource "test_object" "reference" {
}
locals {
map = (test_object.reference.id)
}
resource "test_object" "a" {
count = 1
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each value is sensitive",
expectedError: "Invalid import id argument: The import ID cannot be sensitive.",
inlineConfiguration: map[string]string{
"main.tf": `
locals {
index = sensitive("foo")
map = {
"key1" = local.index
}
}
resource "test_object" "a" {
for_each = local.map
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each key is sensitive",
expectedError: "Invalid for_each argument: Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
inlineConfiguration: map[string]string{
"main.tf": `
locals {
index = sensitive("foo")
map = {
(local.index) = "val1"
}
}
resource "test_object" "a" {
count = 1
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "for_each expression is sensitive",
expectedError: "Invalid for_each argument: Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.",
inlineConfiguration: map[string]string{
"main.tf": `
resource "test_object" "reference" {
}
locals {
map = sensitive({
"key1" = "val1"
})
}
resource "test_object" "a" {
count = 0
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},

View File

@ -129,10 +129,6 @@ type EvalContext interface {
// indicating if that reference forces replacement.
EvaluateReplaceTriggeredBy(expr hcl.Expression, repData instances.RepetitionData) (*addrs.Reference, bool, tfdiags.Diagnostics)
// EvaluateImportAddress takes the raw reference expression of the import address
// from the config, and returns the evaluated address addrs.AbsResourceInstance
EvaluateImportAddress(expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics)
// EvaluationScope returns a scope that can be used to evaluate reference
// addresses in this context.
EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope

View File

@ -12,7 +12,6 @@ import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/function"
@ -22,7 +21,6 @@ import (
"github.com/opentofu/opentofu/internal/encryption"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/lang"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/plans"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/provisioners"
@ -409,110 +407,6 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r
return ref, replace, diags
}
// EvaluateImportAddress takes the raw reference expression of the import address
// from the config, and returns the evaluated address addrs.AbsResourceInstance
//
// The implementation is inspired by config.AbsTraversalForImportToExpr, but this time we can evaluate the expression
// in the indexes of expressions. If we encounter a hclsyntax.IndexExpr, we can evaluate the Key expression and create
// an Index Traversal, adding it to the Traverser
// TODO move this function into eval_import.go
func (ctx *BuiltinEvalContext) EvaluateImportAddress(expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
traversal, diags := ctx.traversalForImportExpr(expr, keyData)
if diags.HasErrors() {
return addrs.AbsResourceInstance{}, diags
}
return addrs.ParseAbsResourceInstance(traversal)
}
func (ctx *BuiltinEvalContext) traversalForImportExpr(expr hcl.Expression, keyData instances.RepetitionData) (traversal hcl.Traversal, diags tfdiags.Diagnostics) {
switch e := expr.(type) {
case *hclsyntax.IndexExpr:
t, d := ctx.traversalForImportExpr(e.Collection, keyData)
diags = diags.Append(d)
traversal = append(traversal, t...)
tIndex, dIndex := ctx.parseImportIndexKeyExpr(e.Key, keyData)
diags = diags.Append(dIndex)
traversal = append(traversal, tIndex)
case *hclsyntax.RelativeTraversalExpr:
t, d := ctx.traversalForImportExpr(e.Source, keyData)
diags = diags.Append(d)
traversal = append(traversal, t...)
traversal = append(traversal, e.Traversal...)
case *hclsyntax.ScopeTraversalExpr:
traversal = append(traversal, e.Traversal...)
default:
// This should not happen, as it should have failed validation earlier, in config.AbsTraversalForImportToExpr
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import address expression",
Detail: "Import address must be a reference to a resource's address, and only allows for indexing with dynamic keys. For example: module.my_module[expression1].aws_s3_bucket.my_buckets[expression2] for resources inside of modules, or simply aws_s3_bucket.my_bucket for a resource in the root module",
Subject: expr.Range().Ptr(),
})
}
return
}
// parseImportIndexKeyExpr parses an expression that is used as a key in an index, of an HCL expression representing an
// import target address, into a traversal of type hcl.TraverseIndex.
// After evaluation, the expression must be known, not null, not sensitive, and must be a string (for_each) or a number
// (count)
func (ctx *BuiltinEvalContext) parseImportIndexKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, tfdiags.Diagnostics) {
idx := hcl.TraverseIndex{
SrcRange: expr.Range(),
}
// evaluate and take into consideration the for_each key (if exists)
val, diags := evaluateExprWithRepetitionData(ctx, expr, cty.DynamicPseudoType, keyData)
if diags.HasErrors() {
return idx, diags
}
if !val.IsKnown() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
if val.IsNull() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
if val.Type() != cty.String && val.Type() != cty.Number {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
unmarkedVal, valMarks := val.Unmark()
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
Subject: expr.Range().Ptr(),
})
}
idx.Key = unmarkedVal
return idx, diags
}
func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope {
if !ctx.pathSet {
panic("context path not set")

View File

@ -275,10 +275,6 @@ func (c *MockEvalContext) EvaluateReplaceTriggeredBy(hcl.Expression, instances.R
return nil, false, nil
}
func (c *MockEvalContext) EvaluateImportAddress(expression hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
return addrs.AbsResourceInstance{}, nil
}
// installSimpleEval is a helper to install a simple mock implementation of
// both EvaluateBlock and EvaluateExpr into the receiver.
//

View File

@ -26,7 +26,9 @@ import (
// returning an error if the count value is not known, and converting the
// cty.Value to a map[string]cty.Value for compatibility with other calls.
func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) {
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false, false)
const unknownsNotAllowed = false
const tupleNotAllowed = false
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, unknownsNotAllowed, tupleNotAllowed)
// forEachVal might be unknown, but if it is then there should already
// be an error about it in diags, which we'll return below.

View File

@ -9,6 +9,8 @@ import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/tfdiags"
@ -84,3 +86,110 @@ func evaluateExprWithRepetitionData(ctx EvalContext, expr hcl.Expression, wantTy
scope := ctx.EvaluationScope(nil, nil, keyData)
return scope.EvalExpr(expr, wantType)
}
// EvaluateImportAddress takes the raw reference expression of the import address
// from the config, and returns the evaluated address addrs.AbsResourceInstance
//
// The implementation is inspired by config.AbsTraversalForImportToExpr, but this time we can evaluate the expression
// in the indexes of expressions. If we encounter a hclsyntax.IndexExpr, we can evaluate the Key expression and create
// an Index Traversal, adding it to the Traverser
func evaluateImportAddress(ctx EvalContext, expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
traversal, diags := traversalForImportExpr(ctx, expr, keyData)
if diags.HasErrors() {
return addrs.AbsResourceInstance{}, diags
}
return addrs.ParseAbsResourceInstance(traversal)
}
func traversalForImportExpr(ctx EvalContext, expr hcl.Expression, keyData instances.RepetitionData) (hcl.Traversal, tfdiags.Diagnostics) {
var traversal hcl.Traversal
var diags tfdiags.Diagnostics
switch e := expr.(type) {
case *hclsyntax.IndexExpr:
t, d := traversalForImportExpr(ctx, e.Collection, keyData)
diags = diags.Append(d)
traversal = append(traversal, t...)
tIndex, dIndex := parseImportIndexKeyExpr(ctx, e.Key, keyData)
diags = diags.Append(dIndex)
traversal = append(traversal, tIndex)
case *hclsyntax.RelativeTraversalExpr:
t, d := traversalForImportExpr(ctx, e.Source, keyData)
diags = diags.Append(d)
traversal = append(traversal, t...)
traversal = append(traversal, e.Traversal...)
case *hclsyntax.ScopeTraversalExpr:
traversal = append(traversal, e.Traversal...)
default:
// This should not happen, as it should have failed validation earlier, in config.AbsTraversalForImportToExpr
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid import address expression",
Detail: "Import address must be a reference to a resource's address, and only allows for indexing with dynamic keys. For example: module.my_module[expression1].aws_s3_bucket.my_buckets[expression2] for resources inside of modules, or simply aws_s3_bucket.my_bucket for a resource in the root module",
Subject: expr.Range().Ptr(),
})
}
return traversal, diags
}
// parseImportIndexKeyExpr parses an expression that is used as a key in an index, of an HCL expression representing an
// import target address, into a traversal of type hcl.TraverseIndex.
// After evaluation, the expression must be known, not null, not sensitive, and must be a string (for_each) or a number
// (count)
func parseImportIndexKeyExpr(ctx EvalContext, expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, tfdiags.Diagnostics) {
idx := hcl.TraverseIndex{
SrcRange: expr.Range(),
}
// evaluate and take into consideration the for_each key (if exists)
val, diags := evaluateExprWithRepetitionData(ctx, expr, cty.DynamicPseudoType, keyData)
if diags.HasErrors() {
return idx, diags
}
if !val.IsKnown() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index that will only be known after apply. Please ensure to use expressions that are known at plan time for the index of an import target address",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
if val.IsNull() {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index which is null. Please ensure the expression for the index is not null",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
if val.Type() != cty.String && val.Type() != cty.Number {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index which is not valid for a resource instance (not a string or a number). Please ensure the expression for the index is correct, and returns either a string or a number",
Subject: expr.Range().Ptr(),
})
return idx, diags
}
unmarkedVal, valMarks := val.Unmark()
if _, sensitive := valMarks[marks.Sensitive]; sensitive {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block 'to' address contains an invalid key",
Detail: "Import block contained a resource address using an index which is sensitive. Please ensure indexes used in the resource address of an import target are not sensitive",
Subject: expr.Range().Ptr(),
})
}
idx.Key = unmarkedVal
return idx, diags
}

View File

@ -258,7 +258,9 @@ func (n *nodeValidateModule) Execute(ctx EvalContext, op walkOperation) (diags t
diags = diags.Append(countDiags)
case n.ModuleCall.ForEach != nil:
_, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, true, false)
const unknownsAllowed = true
const tupleNotAllowed = false
_, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, unknownsAllowed, tupleNotAllowed)
diags = diags.Append(forEachDiags)
}

View File

@ -565,7 +565,10 @@ func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnost
}
func validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) {
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true, false)
const unknownsAllowed = true
const tupleNotAllowed = false
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, unknownsAllowed, tupleNotAllowed)
// If the value isn't known then that's the best we can do for now, but
// we'll check more thoroughly during the plan walk
if !val.IsKnown() {