Skip to content

Commit e3d2b7d

Browse files
committed
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.
1 parent ca02fd9 commit e3d2b7d

File tree

5 files changed

+133
-12
lines changed

5 files changed

+133
-12
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
kind: BUG FIXES
2+
body: import blocks no longer ignore provider local names
3+
time: 2026-04-01T15:21:20.292002-04:00
4+
custom:
5+
Issue: "38338"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
terraform {
2+
required_providers {
3+
localname = {
4+
source = "hashicorp/random"
5+
}
6+
random = {
7+
source = "hashicorp/random"
8+
}
9+
}
10+
}
11+
12+
provider "random" {
13+
alias = "thisone"
14+
}
15+
16+
import {
17+
to = random_string.test1
18+
provider = localname
19+
id = "importlocalname"
20+
}
21+
22+
import {
23+
to = random_string.test2
24+
provider = random.thisone
25+
id = "importaliased"
26+
}

internal/terraform/context_plan_import_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2388,3 +2388,79 @@ func TestContext2Plan_importIdentityMissingResponse(t *testing.T) {
23882388
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want)
23892389
}
23902390
}
2391+
2392+
func TestContext2Plan_importResourceConfigGenWithProviderLocalName(t *testing.T) {
2393+
addr := mustResourceInstanceAddr("test_object.a")
2394+
m := testModuleInline(t, map[string]string{
2395+
"main.tf": `
2396+
terraform {
2397+
required_providers {
2398+
random = {
2399+
source = "hashicorp/test"
2400+
}
2401+
}
2402+
}
2403+
2404+
import {
2405+
provider = random
2406+
to = test_object.a
2407+
id = "123"
2408+
}
2409+
`,
2410+
})
2411+
2412+
p := simpleMockProvider()
2413+
ctx := testContext2(t, &ContextOpts{
2414+
Providers: map[addrs.Provider]providers.Factory{
2415+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
2416+
},
2417+
})
2418+
p.ReadResourceResponse = &providers.ReadResourceResponse{
2419+
NewState: cty.ObjectVal(map[string]cty.Value{
2420+
"test_string": cty.StringVal("foo"),
2421+
}),
2422+
}
2423+
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
2424+
ImportedResources: []providers.ImportedResource{
2425+
{
2426+
TypeName: "test_object",
2427+
State: cty.ObjectVal(map[string]cty.Value{
2428+
"test_string": cty.StringVal("foo"),
2429+
}),
2430+
},
2431+
},
2432+
}
2433+
2434+
diags := ctx.Validate(m, &ValidateOpts{})
2435+
if diags.HasErrors() {
2436+
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
2437+
}
2438+
2439+
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
2440+
Mode: plans.NormalMode,
2441+
GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty.
2442+
})
2443+
if diags.HasErrors() {
2444+
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
2445+
}
2446+
2447+
t.Run(addr.String(), func(t *testing.T) {
2448+
instPlan := plan.Changes.ResourceInstance(addr)
2449+
if instPlan == nil {
2450+
t.Fatalf("no plan for %s at all", addr)
2451+
}
2452+
// it's the same config as the test above, so we'll skip checking anything except the provider local name
2453+
want := `resource "test_object" "a" {
2454+
provider = random
2455+
test_bool = null
2456+
test_list = null
2457+
test_map = null
2458+
test_number = null
2459+
test_string = "foo"
2460+
}`
2461+
got := instPlan.GeneratedConfig
2462+
if diff := cmp.Diff(want, got); len(diff) > 0 {
2463+
t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
2464+
}
2465+
})
2466+
}

internal/terraform/node_resource_plan.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,10 @@ func (n *nodeExpandPlannableResource) concreteResource(ctx EvalContext, knownImp
591591
}
592592

593593
if importID, ok := knownImports.GetOk(a.Addr); ok {
594-
m.importTarget = importID
594+
m.importTarget = importTarget{
595+
target: importID,
596+
importConfig: n.importTargets[0].Config,
597+
}
595598
} else {
596599
// We're going to check now if this resource instance *might* be
597600
// 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
610613
continue
611614
}
612615

613-
m.importTarget = cty.UnknownVal(cty.String)
616+
m.importTarget = importTarget{target: cty.UnknownVal(cty.String)}
614617
}
615618
}
616619
}

internal/terraform/node_resource_plan_instance.go

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ type NodePlannableResourceInstance struct {
5050

5151
// importTarget, if populated, contains the information necessary to plan
5252
// an import of this resource.
53-
importTarget cty.Value
53+
importTarget importTarget
54+
}
55+
56+
type importTarget struct {
57+
target cty.Value
58+
importConfig *configs.Import
5459
}
5560

5661
var (
@@ -200,17 +205,17 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
200205
}
201206
}
202207

203-
importing := n.importTarget != cty.NilVal && !n.preDestroyRefresh
208+
importing := n.importTarget.target != cty.NilVal && !n.preDestroyRefresh
204209

205210
var deferred *providers.Deferred
206211

207212
// If the resource is to be imported, we now ask the provider for an Import
208213
// and a Refresh, and save the resulting state to instanceRefreshState.
209214

210215
if importing {
211-
if n.importTarget.IsWhollyKnown() {
216+
if n.importTarget.target.IsWhollyKnown() {
212217
var importDiags tfdiags.Diagnostics
213-
instanceRefreshState, deferred, importDiags = n.importState(ctx, addr, n.importTarget, provider, providerSchema)
218+
instanceRefreshState, deferred, importDiags = n.importState(ctx, addr, provider, providerSchema)
214219
diags = diags.Append(importDiags)
215220
} else {
216221
// Otherwise, just mark the resource as deferred without trying to
@@ -243,7 +248,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
243248
Before: cty.NullVal(impliedType),
244249
After: cty.UnknownVal(impliedType),
245250
Importing: &plans.Importing{
246-
Target: n.importTarget,
251+
Target: n.importTarget.target,
247252
},
248253
},
249254
})
@@ -413,10 +418,10 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
413418
// and the import by ID. When importing by identity, we need to
414419
// make sure to use the complete identity return by the provider
415420
// instead of the (potential) incomplete one from the configuration.
416-
if n.importTarget.Type().IsObjectType() {
421+
if n.importTarget.target.Type().IsObjectType() {
417422
change.Importing = &plans.Importing{Target: instanceRefreshState.Identity}
418423
} else {
419-
change.Importing = &plans.Importing{Target: n.importTarget}
424+
change.Importing = &plans.Importing{Target: n.importTarget.target}
420425
}
421426
}
422427

@@ -601,7 +606,7 @@ func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repDat
601606
return diags
602607
}
603608

604-
func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, importTarget cty.Value, provider providers.Interface, providerSchema providers.ProviderSchema) (*states.ResourceInstanceObject, *providers.Deferred, tfdiags.Diagnostics) {
609+
func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, provider providers.Interface, providerSchema providers.ProviderSchema) (*states.ResourceInstanceObject, *providers.Deferred, tfdiags.Diagnostics) {
605610
deferralAllowed := ctx.Deferrals().DeferralAllowed()
606611
var diags tfdiags.Diagnostics
607612
absAddr := addr.Resource.Absolute(ctx.Path())
@@ -611,6 +616,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
611616
}
612617

613618
var deferred *providers.Deferred
619+
importTarget := n.importTarget.target
614620

615621
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
616622
return h.PrePlanImport(hookResourceID, importTarget)
@@ -867,7 +873,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
867873
}
868874

869875
// Generate the HCL string first, then parse the HCL body from it.
870-
generatedResource, generatedDiags := n.generateHCLResourceDef(ctx, n.Addr, instanceRefreshState.Value)
876+
generatedResource, generatedDiags := n.generateHCLResourceDef(ctx, n.Addr, instanceRefreshState.Value, n.importTarget.importConfig)
871877
diags = diags.Append(generatedDiags)
872878

873879
// 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.
913919
// generateHCLResourceDef generates the HCL definition for the resource
914920
// instance, including the surrounding block. This is used to generate the
915921
// configuration for the resource instance when importing or generating
916-
func (n *NodePlannableResourceInstance) generateHCLResourceDef(ctx EvalContext, addr addrs.AbsResourceInstance, state cty.Value) (genconfig.Resource, tfdiags.Diagnostics) {
922+
func (n *NodePlannableResourceInstance) generateHCLResourceDef(ctx EvalContext, addr addrs.AbsResourceInstance, state cty.Value, importCfg *configs.Import) (genconfig.Resource, tfdiags.Diagnostics) {
917923
providerAddr := addrs.LocalProviderConfig{
918924
LocalName: n.ResolvedProvider.Provider.Type,
919925
Alias: n.ResolvedProvider.Alias,
920926
}
921927

928+
if importCfg != nil && importCfg.ProviderConfigRef != nil {
929+
providerAddr.LocalName = importCfg.ProviderConfigRef.Name
930+
providerAddr.Alias = importCfg.ProviderConfigRef.Alias
931+
}
932+
922933
var diags tfdiags.Diagnostics
923934

924935
providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider)

0 commit comments

Comments
 (0)