Support for_each syntax in import block (#1492)

Signed-off-by: RLRabinowitz <rlrabinowitz2@gmail.com>
Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
Co-authored-by: RLRabinowitz <rlrabinowitz2@gmail.com>
This commit is contained in:
Ronny Orot 2024-04-17 17:12:10 +03:00 committed by GitHub
parent 1f3db74281
commit dcc1fa3b88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 355 additions and 34 deletions

View File

@ -16,6 +16,7 @@ STATE ENCRYPTION
NEW FEATURES:
* Add support for a `removed` block that allows users to remove resources or modules from the state without destroying them. ([#1158](https://github.com/opentofu/opentofu/pull/1158))
* Provider-defined functions are now available. They may be referenced via `provider::<provider_name>::<funcname>(args)`. ([#1439](https://github.com/opentofu/opentofu/pull/1439))
* Support `for_each` in `import` blocks ([#1492](https://github.com/opentofu/opentofu/pull/1492)
ENHANCEMENTS:
* Added support to use `.tfvars` files from tests folder. ([#1386](https://github.com/opentofu/opentofu/pull/1386))

View File

@ -43,6 +43,8 @@ type Import struct {
// import blocks targeting the same resource
ResolvedTo *addrs.AbsResourceInstance
ForEach hcl.Expression
ProviderConfigRef *ProviderConfigRef
Provider addrs.Provider
@ -93,6 +95,10 @@ func decodeImportBlock(block *hcl.Block) (*Import, hcl.Diagnostics) {
diags = append(diags, providerDiags...)
}
if attr, exists := content.Attributes["for_each"]; exists {
imp.ForEach = attr.Expr
}
return imp, diags
}
@ -109,6 +115,9 @@ var importBlockSchema = &hcl.BodySchema{
Name: "to",
Required: true,
},
{
Name: "for_each",
},
},
}

View File

@ -11,9 +11,9 @@ import (
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
)
@ -107,12 +107,13 @@ func NewImportResolver() *ImportResolver {
return &ImportResolver{imports: make(map[string]EvaluatedConfigImportTarget)}
}
// ResolveImport resolves the ID and address (soon, when it will be necessary) of an ImportTarget originating
// from an import block, when we have the context necessary to resolve them. The resolved import target would be an
// EvaluatedConfigImportTarget.
// This function mutates the EvalContext's ImportResolver, adding the resolved import target
// The function errors if we failed to evaluate the ID or the address (soon)
func (ri *ImportResolver) ResolveImport(importTarget *ImportTarget, ctx EvalContext) tfdiags.Diagnostics {
// ExpandAndResolveImport is responsible for two operations:
// 1. Expands the ImportTarget (originating from an import block) if it contains a 'for_each' attribute.
// 2. Goes over the expanded imports and resolves the ID and address, when we have the context necessary to resolve
// them. The resolved import target would be an EvaluatedConfigImportTarget.
// This function mutates the EvalContext's ImportResolver, adding the resolved import target.
// The function errors if we failed to evaluate the ID or the address.
func (ri *ImportResolver) ExpandAndResolveImport(importTarget *ImportTarget, ctx EvalContext) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// The import block expressions are declared within the root module.
@ -120,13 +121,52 @@ func (ri *ImportResolver) ResolveImport(importTarget *ImportTarget, ctx EvalCont
// relative to the root module
rootCtx := ctx.WithPath(addrs.RootModuleInstance)
importId, evalDiags := evaluateImportIdExpression(importTarget.Config.ID, rootCtx)
if importTarget.Config.ForEach != nil {
// The import target has a for_each attribute, so we need to expand it
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, false, true)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return diags
}
// We are building an instances.RepetitionData based on each for_each key and val combination
var repetitions []instances.RepetitionData
it := forEachVal.ElementIterator()
for it.Next() {
k, v := it.Element()
repetitions = append(repetitions, instances.RepetitionData{
EachKey: k,
EachValue: v,
})
}
for _, keyData := range repetitions {
diags = diags.Append(ri.resolveImport(importTarget, rootCtx, keyData))
}
} else {
// The import target is singular, no need to expand
diags = diags.Append(ri.resolveImport(importTarget, rootCtx, EvalDataForNoInstanceKey))
}
return diags
}
// resolveImport resolves the ID and address of an ImportTarget originating from an import block,
// when we have the context necessary to resolve them. The resolved import target would be an
// EvaluatedConfigImportTarget.
// This function mutates the EvalContext's ImportResolver, adding the resolved import target.
// The function errors if we failed to evaluate the ID or the address.
func (ri *ImportResolver) resolveImport(importTarget *ImportTarget, ctx EvalContext, keyData instances.RepetitionData) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
importId, evalDiags := evaluateImportIdExpression(importTarget.Config.ID, ctx, keyData)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return diags
}
importAddress, addressDiags := rootCtx.EvaluateImportAddress(importTarget.Config.To)
importAddress, addressDiags := ctx.EvaluateImportAddress(importTarget.Config.To, keyData)
diags = diags.Append(addressDiags)
if diags.HasErrors() {
return diags
@ -152,6 +192,12 @@ func (ri *ImportResolver) ResolveImport(importTarget *ImportTarget, ctx EvalCont
ID: importId,
}
if keyData == EvalDataForNoInstanceKey {
log.Printf("[TRACE] importResolver: resolved a singular import target %s", importAddress)
} else {
log.Printf("[TRACE] importResolver: resolved an expanded import target %s", importAddress)
}
return diags
}

View File

@ -4533,6 +4533,191 @@ import {
}
}
func TestContext2Plan_importForEach(t *testing.T) {
type ImportResult struct {
ResolvedAddress string
ResolvedId string
}
type TestConfiguration struct {
Description string
ImportResults []ImportResult
inlineConfiguration map[string]string
}
configurations := []TestConfiguration{
{
Description: "valid map",
ImportResults: []ImportResult{{ResolvedAddress: `test_object.a["key1"]`, ResolvedId: "val1"}, {ResolvedAddress: `test_object.a["key2"]`, ResolvedId: "val2"}, {ResolvedAddress: `test_object.a["key3"]`, ResolvedId: "val3"}},
inlineConfiguration: map[string]string{
"main.tf": `
locals {
map = {
"key1" = "val1"
"key2" = "val2"
"key3" = "val3"
}
}
resource "test_object" "a" {
for_each = local.map
}
import {
for_each = local.map
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "valid set",
ImportResults: []ImportResult{{ResolvedAddress: `test_object.a["val0"]`, ResolvedId: "val0"}, {ResolvedAddress: `test_object.a["val1"]`, ResolvedId: "val1"}, {ResolvedAddress: `test_object.a["val2"]`, ResolvedId: "val2"}},
inlineConfiguration: map[string]string{
"main.tf": `
variable "set" {
type = set(string)
default = ["val0", "val1", "val2"]
}
resource "test_object" "a" {
for_each = var.set
}
import {
for_each = var.set
to = test_object.a[each.key]
id = each.value
}
`,
},
},
{
Description: "valid tuple",
ImportResults: []ImportResult{{ResolvedAddress: `module.mod[0].test_object.a["resKey1"]`, ResolvedId: "val1"}, {ResolvedAddress: `module.mod[0].test_object.a["resKey2"]`, ResolvedId: "val2"}, {ResolvedAddress: `module.mod[1].test_object.a["resKey1"]`, ResolvedId: "val3"}, {ResolvedAddress: `module.mod[1].test_object.a["resKey2"]`, ResolvedId: "val4"}},
inlineConfiguration: map[string]string{
"mod/main.tf": `
variable "set" {
type = set(string)
default = ["resKey1", "resKey2"]
}
resource "test_object" "a" {
for_each = var.set
}
`,
"main.tf": `
locals {
tuple = [
{
moduleKey = 0
resourceKey = "resKey1"
id = "val1"
},
{
moduleKey = 0
resourceKey = "resKey2"
id = "val2"
},
{
moduleKey = 1
resourceKey = "resKey1"
id = "val3"
},
{
moduleKey = 1
resourceKey = "resKey2"
id = "val4"
},
]
}
module "mod" {
count = 2
source = "./mod"
}
import {
for_each = local.tuple
id = each.value.id
to = module.mod[each.value.moduleKey].test_object.a[each.value.resourceKey]
}
`,
},
},
}
for _, configuration := range configurations {
t.Run(configuration.Description, func(t *testing.T) {
m := testModuleInline(t, configuration.inlineConfiguration)
p := simpleMockProvider()
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{}),
}
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "test_object",
State: cty.ObjectVal(map[string]cty.Value{}),
},
},
}
plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)))
if diags.HasErrors() {
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
}
if len(plan.Changes.Resources) != len(configuration.ImportResults) {
t.Fatalf("excpected %d resource chnages in the plan, got %d instead", len(configuration.ImportResults), len(plan.Changes.Resources))
}
for _, importResult := range configuration.ImportResults {
addr := mustResourceInstanceAddr(importResult.ResolvedAddress)
t.Run(addr.String(), func(t *testing.T) {
instPlan := plan.Changes.ResourceInstance(addr)
if instPlan == nil {
t.Fatalf("no plan for %s at all", addr)
}
if got, want := instPlan.Addr, addr; !got.Equal(want) {
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.Action, plans.NoOp; got != want {
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
}
if instPlan.Importing.ID != importResult.ResolvedId {
t.Errorf("expected import change from \"%s\", got non-import change", importResult.ResolvedId)
}
if !hook.PrePlanImportCalled {
t.Fatalf("PostPlanImport hook not called")
}
if !hook.PostPlanImportCalled {
t.Fatalf("PostPlanImport hook not called")
}
})
}
})
}
}
func TestContext2Plan_importToInvalidDynamicAddress(t *testing.T) {
type TestConfiguration struct {
Description string

View File

@ -131,7 +131,7 @@ type EvalContext interface {
// EvaluateImportAddress takes the raw reference expression of the import address
// from the config, and returns the evaluated address addrs.AbsResourceInstance
EvaluateImportAddress(expr hcl.Expression) (addrs.AbsResourceInstance, tfdiags.Diagnostics)
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.

View File

@ -404,8 +404,9 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r
// 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 (ctx *BuiltinEvalContext) EvaluateImportAddress(expr hcl.Expression) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
traversal, diags := ctx.traversalForImportExpr(expr)
// 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
}
@ -413,18 +414,18 @@ func (ctx *BuiltinEvalContext) EvaluateImportAddress(expr hcl.Expression) (addrs
return addrs.ParseAbsResourceInstance(traversal)
}
func (ctx *BuiltinEvalContext) traversalForImportExpr(expr hcl.Expression) (traversal hcl.Traversal, diags tfdiags.Diagnostics) {
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)
t, d := ctx.traversalForImportExpr(e.Collection, keyData)
diags = diags.Append(d)
traversal = append(traversal, t...)
tIndex, dIndex := ctx.parseImportIndexKeyExpr(e.Key)
tIndex, dIndex := ctx.parseImportIndexKeyExpr(e.Key, keyData)
diags = diags.Append(dIndex)
traversal = append(traversal, tIndex)
case *hclsyntax.RelativeTraversalExpr:
t, d := ctx.traversalForImportExpr(e.Source)
t, d := ctx.traversalForImportExpr(e.Source, keyData)
diags = diags.Append(d)
traversal = append(traversal, t...)
traversal = append(traversal, e.Traversal...)
@ -446,12 +447,13 @@ func (ctx *BuiltinEvalContext) traversalForImportExpr(expr hcl.Expression) (trav
// 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) (hcl.TraverseIndex, tfdiags.Diagnostics) {
func (ctx *BuiltinEvalContext) parseImportIndexKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, tfdiags.Diagnostics) {
idx := hcl.TraverseIndex{
SrcRange: expr.Range(),
}
val, diags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil)
// 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
}

View File

@ -275,7 +275,7 @@ func (c *MockEvalContext) EvaluateReplaceTriggeredBy(hcl.Expression, instances.R
return nil, false, nil
}
func (c *MockEvalContext) EvaluateImportAddress(expression hcl.Expression) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
func (c *MockEvalContext) EvaluateImportAddress(expression hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) {
return addrs.AbsResourceInstance{}, nil
}

View File

@ -26,7 +26,7 @@ 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)
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false, false)
// forEachVal might be unknown, but if it is then there should already
// be an error about it in diags, which we'll return below.
@ -40,7 +40,9 @@ func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach ma
// evaluateForEachExpressionValue is like evaluateForEachExpression
// except that it returns a cty.Value map or set which can be unknown.
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) {
// The 'allowTuple' argument is used to support evaluating for_each from tuple
// values, and is currently supported when using for_each in import blocks.
func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool, allowTuple bool) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType))
@ -86,6 +88,16 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
}
ty := forEachVal.Type()
var isAllowedType bool
var allowedTypesMessage string
if allowTuple {
isAllowedType = ty.IsMapType() || ty.IsSetType() || ty.IsObjectType() || ty.IsTupleType()
allowedTypesMessage = "map, set of strings, or a tuple"
} else {
isAllowedType = ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()
allowedTypesMessage = "map, or set of strings"
}
const errInvalidUnknownDetailMap = "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.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
const errInvalidUnknownDetailSet = "The \"for_each\" set includes values 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.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge."
@ -94,7 +106,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`,
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A %s is allowed.`, allowedTypesMessage),
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
@ -123,11 +135,11 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
// ensure that we have a map, and not a DynamicValue
return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags
case !(ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()):
case !(isAllowedType):
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s.`, ty.FriendlyName()),
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a %s, and you have provided a value of type %s.`, allowedTypesMessage, ty.FriendlyName()),
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,
@ -163,7 +175,7 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid for_each set argument",
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()),
Subject: expr.Range().Ptr(),
Expression: expr,
EvalContext: hclCtx,

View File

@ -145,7 +145,7 @@ func TestEvaluateForEachExpression_errors(t *testing.T) {
"set containing booleans": {
hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.BoolVal(true)})),
"Invalid for_each set argument",
"supports maps and sets of strings, but you have provided a set containing type bool",
"supports sets of strings, but you have provided a set containing type bool",
false, false,
},
"set containing null": {
@ -217,13 +217,14 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) {
tests := map[string]hcl.Expression{
"unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))),
"unknown map": hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))),
"unknown tuple": hcltest.MockExprLiteral(cty.UnknownVal(cty.Tuple([]cty.Type{cty.String, cty.Number, cty.Bool}))),
}
for name, expr := range tests {
t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{}
ctx.installSimpleEval()
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true)
forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true, true)
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
@ -235,3 +236,54 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) {
})
}
}
func TestEvaluateForEachExpressionValueTuple(t *testing.T) {
tests := map[string]struct {
Expr hcl.Expression
AllowTuple bool
ExpectedError string
}{
"valid tuple": {
Expr: hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
AllowTuple: true,
},
"empty tuple": {
Expr: hcltest.MockExprLiteral(cty.EmptyTupleVal),
AllowTuple: true,
},
"null tuple": {
Expr: hcltest.MockExprLiteral(cty.NullVal(cty.Tuple([]cty.Type{}))),
AllowTuple: true,
ExpectedError: "the given \"for_each\" argument value is null",
},
"sensitive tuple": {
Expr: hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}).Mark(marks.Sensitive)),
AllowTuple: true,
ExpectedError: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments",
},
"allow tuple is off": {
Expr: hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})),
AllowTuple: false,
ExpectedError: "the \"for_each\" argument must be a map, or set of strings, and you have provided a value of type tuple.",
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
ctx := &MockEvalContext{}
ctx.installSimpleEval()
_, diags := evaluateForEachExpressionValue(test.Expr, ctx, true, test.AllowTuple)
if test.ExpectedError == "" {
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
} else {
if got, want := diags[0].Description().Detail, test.ExpectedError; test.ExpectedError != "" && !strings.Contains(got, want) {
t.Errorf("wrong diagnostic detail\ngot: %s\nwant substring: %s", got, want)
}
}
})
}
}

View File

@ -9,13 +9,14 @@ import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/opentofu/opentofu/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
)
func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext) (string, tfdiags.Diagnostics) {
func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext, keyData instances.RepetitionData) (string, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
if expr == nil {
@ -27,7 +28,8 @@ func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext) (string, t
})
}
importIdVal, evalDiags := ctx.EvaluateExpr(expr, cty.String, nil)
// evaluate the import ID and take into consideration the for_each key (if exists)
importIdVal, evalDiags := evaluateExprWithRepetitionData(ctx, expr, cty.String, keyData)
diags = diags.Append(evalDiags)
if importIdVal.IsNull() {
@ -73,3 +75,12 @@ func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext) (string, t
return importId, diags
}
// evaluateExprWithRepetitionData takes the given HCL expression and evaluates
// it to produce a value, while taking into consideration any repetition key
// (a single combination of each.key and each.value of a for_each argument)
// that should be a part of the scope.
func evaluateExprWithRepetitionData(ctx EvalContext, expr hcl.Expression, wantType cty.Type, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) {
scope := ctx.EvaluationScope(nil, nil, keyData)
return scope.EvalExpr(expr, wantType)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/davecgh/go-spew/spew"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/opentofu/opentofu/internal/lang"
"github.com/opentofu/opentofu/internal/lang/marks"
"github.com/zclconf/go-cty/cty"
)
@ -18,6 +19,7 @@ import (
func TestEvaluateImportIdExpression_SensitiveValue(t *testing.T) {
ctx := &MockEvalContext{}
ctx.installSimpleEval()
ctx.EvaluationScopeScope = &lang.Scope{}
testCases := []struct {
name string
@ -53,7 +55,7 @@ func TestEvaluateImportIdExpression_SensitiveValue(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, diags := evaluateImportIdExpression(tc.expr, ctx)
_, diags := evaluateImportIdExpression(tc.expr, ctx, EvalDataForNoInstanceKey)
if tc.wantErr != "" {
if len(diags) != 1 {

View File

@ -243,7 +243,7 @@ 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)
_, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, true, false)
diags = diags.Append(forEachDiags)
}

View File

@ -258,8 +258,9 @@ func (n *NodeAbstractResource) RootReferences() []*addrs.Reference {
}
refs, _ := referencesInImportAddress(importTarget.Config.To)
root = append(root, refs...)
// TODO - Add RootReferences of ForEach here later one, once for_each is added
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ForEach)
root = append(root, refs...)
refs, _ = lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ID)

View File

@ -156,7 +156,7 @@ func (n *nodeExpandPlannableResource) DynamicExpand(ctx EvalContext) (*Graph, er
var diags tfdiags.Diagnostics
for _, importTarget := range n.importTargets {
if importTarget.IsFromImportBlock() {
err := importResolver.ResolveImport(importTarget, ctx)
err := importResolver.ExpandAndResolveImport(importTarget, ctx)
diags = diags.Append(err)
}
}

View File

@ -565,7 +565,7 @@ 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)
val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true, false)
// 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() {