From e3d2b7de8be0dfbfe12e243a914da20806975fef Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 1 Apr 2026 15:48:32 -0400 Subject: [PATCH] fix(import): include provider local in generated resource cfg when set in import By the time we get to the config generation during terraform plan, terraform didn't have access to the import config to see if a provider had been specified by localname. This is working fine for providers with aliases, and terraform was identifying the correct AbsProvider, but it was still missing from the generated configuration. I've addressed this by adding a struct which carries both the evaluated import target (cty.Value) and the decoded import config, so that generateHCLResourceDef can now use the ProviderConfigRef (if set). I have also added a test to context_plan_import_test that verifies localname is honored. --- .changes/v1.16/BUG FIXES-20260401-152120.yaml | 5 ++ .../valid-modules/import-blocks/main.tf | 26 +++++++ .../terraform/context_plan_import_test.go | 76 +++++++++++++++++++ internal/terraform/node_resource_plan.go | 7 +- .../terraform/node_resource_plan_instance.go | 31 +++++--- 5 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 .changes/v1.16/BUG FIXES-20260401-152120.yaml create mode 100644 internal/configs/testdata/valid-modules/import-blocks/main.tf 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)