diff --git a/internal/tofu/context_import.go b/internal/tofu/context_import.go index a379519104..c073194a2b 100644 --- a/internal/tofu/context_import.go +++ b/internal/tofu/context_import.go @@ -121,11 +121,11 @@ func (ri *ImportResolver) ValidateImportIDs(ctx context.Context, importTarget *I rootCtx := evalCtx.WithPath(addrs.RootModuleInstance) if importTarget.Config.ForEach != nil { - const unknownsNotAllowed = false + const unknownsAllowed = true const tupleAllowed = true // The import target has a for_each attribute, so we need to expand it - forEachVal, evalDiags := evaluateForEachExpressionValue(ctx, importTarget.Config.ForEach, rootCtx, unknownsNotAllowed, tupleAllowed, nil) + forEachVal, evalDiags := evaluateForEachExpressionValue(ctx, importTarget.Config.ForEach, rootCtx, unknownsAllowed, tupleAllowed, nil) diags = diags.Append(evalDiags) if diags.HasErrors() { return diags @@ -134,6 +134,9 @@ func (ri *ImportResolver) ValidateImportIDs(ctx context.Context, importTarget *I // We are building an instances.RepetitionData based on each for_each key and val combination var repetitions []instances.RepetitionData + if !forEachVal.IsKnown() { + return diags + } it := forEachVal.ElementIterator() for it.Next() { k, v := it.Element() diff --git a/internal/tofu/context_validate_test.go b/internal/tofu/context_validate_test.go index 0d14c52abc..5e8e37e430 100644 --- a/internal/tofu/context_validate_test.go +++ b/internal/tofu/context_validate_test.go @@ -2576,3 +2576,68 @@ resource "test_object" "t" { t.Fatalf("expected error, got: %q\n", diags.Err().Error()) } } + +func TestContext2Validate_importWithForEachOnUnknown(t *testing.T) { + // This tests checks that a validate run works correctly when the import block is configured with a + // for_each statement on unknown values. + // In this case, the validation is skipped since the expansion cannot be performed. + // Related to https://github.com/opentofu/opentofu/issues/3563 + m := testModuleInline(t, map[string]string{ + "main.tf": ` + variable "server_ids" { + type = list(string) + default = ["one", "two"] + } + resource "test_object" "a" { + test_string = "foo" + count = 2 + } + import { + to = test_object.a[tonumber(each.key)] + id = each.value + for_each = { + for idx, item in var.server_ids: idx => item + } + } +`, + }) + p := simpleMockProvider() + hook := new(MockHook) + ctx := testContext2(t, &ContextOpts{ + Hooks: []Hook{hook}, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceFn = func(request providers.ReadResourceRequest) providers.ReadResourceResponse { + return providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + "test_number": cty.NullVal(cty.Number), + "test_bool": cty.NullVal(cty.Bool), + "test_list": cty.NullVal(cty.List(cty.String)), + "test_map": cty.NullVal(cty.Map(cty.String)), + }), + } + } + p.ImportResourceStateFn = func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + "test_number": cty.NullVal(cty.Number), + "test_bool": cty.NullVal(cty.Bool), + "test_list": cty.NullVal(cty.List(cty.String)), + "test_map": cty.NullVal(cty.Map(cty.String)), + }), + }, + }, + } + } + diags := ctx.Validate(context.Background(), m) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } +}