diff --git a/.changes/v1.16/BUG FIXES-20260401-152120.yaml b/.changes/v1.16/BUG FIXES-20260401-152120.yaml new file mode 100644 index 0000000000..522bcb5941 --- /dev/null +++ b/.changes/v1.16/BUG FIXES-20260401-152120.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: import blocks no longer ignore provider local names +time: 2026-04-01T15:21:20.292002-04:00 +custom: + Issue: "38338" diff --git a/internal/configs/testdata/valid-modules/import-blocks/main.tf b/internal/configs/testdata/valid-modules/import-blocks/main.tf new file mode 100644 index 0000000000..767a20d0bf --- /dev/null +++ b/internal/configs/testdata/valid-modules/import-blocks/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + localname = { + source = "hashicorp/random" + } + random = { + source = "hashicorp/random" + } + } +} + +provider "random" { + alias = "thisone" +} + +import { + to = random_string.test1 + provider = localname + id = "importlocalname" +} + +import { + to = random_string.test2 + provider = random.thisone + id = "importaliased" +} \ No newline at end of file diff --git a/internal/terraform/context_plan_import_test.go b/internal/terraform/context_plan_import_test.go index 4757962ea4..309cead760 100644 --- a/internal/terraform/context_plan_import_test.go +++ b/internal/terraform/context_plan_import_test.go @@ -2388,3 +2388,79 @@ func TestContext2Plan_importIdentityMissingResponse(t *testing.T) { t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) } } + +func TestContext2Plan_importResourceConfigGenWithProviderLocalName(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + random = { + source = "hashicorp/test" + } + } +} + +import { + provider = random + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + 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.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty. + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + 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) + } + // it's the same config as the test above, so we'll skip checking anything except the provider local name + want := `resource "test_object" "a" { + provider = random + test_bool = null + test_list = null + test_map = null + test_number = null + test_string = "foo" +}` + got := instPlan.GeneratedConfig + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } + }) +} diff --git a/internal/terraform/node_resource_plan.go b/internal/terraform/node_resource_plan.go index 697ed5030c..9815cb0cb1 100644 --- a/internal/terraform/node_resource_plan.go +++ b/internal/terraform/node_resource_plan.go @@ -591,7 +591,10 @@ func (n *nodeExpandPlannableResource) concreteResource(ctx EvalContext, knownImp } if importID, ok := knownImports.GetOk(a.Addr); ok { - m.importTarget = importID + m.importTarget = importTarget{ + target: importID, + importConfig: n.importTargets[0].Config, + } } else { // We're going to check now if this resource instance *might* be // targeted by one of the unknown imports. If it is, we'll set the @@ -610,7 +613,7 @@ func (n *nodeExpandPlannableResource) concreteResource(ctx EvalContext, knownImp continue } - m.importTarget = cty.UnknownVal(cty.String) + m.importTarget = importTarget{target: cty.UnknownVal(cty.String)} } } } diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 798a0c4a3b..41dfe0f595 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -50,7 +50,12 @@ type NodePlannableResourceInstance struct { // importTarget, if populated, contains the information necessary to plan // an import of this resource. - importTarget cty.Value + importTarget importTarget +} + +type importTarget struct { + target cty.Value + importConfig *configs.Import } var ( @@ -200,7 +205,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) } } - importing := n.importTarget != cty.NilVal && !n.preDestroyRefresh + importing := n.importTarget.target != cty.NilVal && !n.preDestroyRefresh var deferred *providers.Deferred @@ -208,9 +213,9 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) // and a Refresh, and save the resulting state to instanceRefreshState. if importing { - if n.importTarget.IsWhollyKnown() { + if n.importTarget.target.IsWhollyKnown() { var importDiags tfdiags.Diagnostics - instanceRefreshState, deferred, importDiags = n.importState(ctx, addr, n.importTarget, provider, providerSchema) + instanceRefreshState, deferred, importDiags = n.importState(ctx, addr, provider, providerSchema) diags = diags.Append(importDiags) } else { // Otherwise, just mark the resource as deferred without trying to @@ -243,7 +248,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) Before: cty.NullVal(impliedType), After: cty.UnknownVal(impliedType), Importing: &plans.Importing{ - Target: n.importTarget, + Target: n.importTarget.target, }, }, }) @@ -413,10 +418,10 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) // and the import by ID. When importing by identity, we need to // make sure to use the complete identity return by the provider // instead of the (potential) incomplete one from the configuration. - if n.importTarget.Type().IsObjectType() { + if n.importTarget.target.Type().IsObjectType() { change.Importing = &plans.Importing{Target: instanceRefreshState.Identity} } else { - change.Importing = &plans.Importing{Target: n.importTarget} + change.Importing = &plans.Importing{Target: n.importTarget.target} } } @@ -601,7 +606,7 @@ func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repDat return diags } -func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, importTarget cty.Value, provider providers.Interface, providerSchema providers.ProviderSchema) (*states.ResourceInstanceObject, *providers.Deferred, tfdiags.Diagnostics) { +func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, provider providers.Interface, providerSchema providers.ProviderSchema) (*states.ResourceInstanceObject, *providers.Deferred, tfdiags.Diagnostics) { deferralAllowed := ctx.Deferrals().DeferralAllowed() var diags tfdiags.Diagnostics absAddr := addr.Resource.Absolute(ctx.Path()) @@ -611,6 +616,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. } var deferred *providers.Deferred + importTarget := n.importTarget.target diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { return h.PrePlanImport(hookResourceID, importTarget) @@ -867,7 +873,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. } // Generate the HCL string first, then parse the HCL body from it. - generatedResource, generatedDiags := n.generateHCLResourceDef(ctx, n.Addr, instanceRefreshState.Value) + generatedResource, generatedDiags := n.generateHCLResourceDef(ctx, n.Addr, instanceRefreshState.Value, n.importTarget.importConfig) diags = diags.Append(generatedDiags) // This wraps the content of the resource block in an enclosing resource block @@ -913,12 +919,17 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs. // generateHCLResourceDef generates the HCL definition for the resource // instance, including the surrounding block. This is used to generate the // configuration for the resource instance when importing or generating -func (n *NodePlannableResourceInstance) generateHCLResourceDef(ctx EvalContext, addr addrs.AbsResourceInstance, state cty.Value) (genconfig.Resource, tfdiags.Diagnostics) { +func (n *NodePlannableResourceInstance) generateHCLResourceDef(ctx EvalContext, addr addrs.AbsResourceInstance, state cty.Value, importCfg *configs.Import) (genconfig.Resource, tfdiags.Diagnostics) { providerAddr := addrs.LocalProviderConfig{ LocalName: n.ResolvedProvider.Provider.Type, Alias: n.ResolvedProvider.Alias, } + if importCfg != nil && importCfg.ProviderConfigRef != nil { + providerAddr.LocalName = importCfg.ProviderConfigRef.Name + providerAddr.Alias = importCfg.ProviderConfigRef.Alias + } + var diags tfdiags.Diagnostics providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider)