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.
dynamic-module-sources-bugs
Kristin Laemmert 3 weeks ago
parent ca02fd9c2a
commit e3d2b7de8b

@ -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"

@ -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"
}

@ -2388,3 +2388,79 @@ func TestContext2Plan_importIdentityMissingResponse(t *testing.T) {
t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) 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)
}
})
}

@ -591,7 +591,10 @@ func (n *nodeExpandPlannableResource) concreteResource(ctx EvalContext, knownImp
} }
if importID, ok := knownImports.GetOk(a.Addr); ok { if importID, ok := knownImports.GetOk(a.Addr); ok {
m.importTarget = importID m.importTarget = importTarget{
target: importID,
importConfig: n.importTargets[0].Config,
}
} else { } else {
// We're going to check now if this resource instance *might* be // 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 // 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 continue
} }
m.importTarget = cty.UnknownVal(cty.String) m.importTarget = importTarget{target: cty.UnknownVal(cty.String)}
} }
} }
} }

@ -50,7 +50,12 @@ type NodePlannableResourceInstance struct {
// importTarget, if populated, contains the information necessary to plan // importTarget, if populated, contains the information necessary to plan
// an import of this resource. // an import of this resource.
importTarget cty.Value importTarget importTarget
}
type importTarget struct {
target cty.Value
importConfig *configs.Import
} }
var ( 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 var deferred *providers.Deferred
@ -208,9 +213,9 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
// and a Refresh, and save the resulting state to instanceRefreshState. // and a Refresh, and save the resulting state to instanceRefreshState.
if importing { if importing {
if n.importTarget.IsWhollyKnown() { if n.importTarget.target.IsWhollyKnown() {
var importDiags tfdiags.Diagnostics 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) diags = diags.Append(importDiags)
} else { } else {
// Otherwise, just mark the resource as deferred without trying to // Otherwise, just mark the resource as deferred without trying to
@ -243,7 +248,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
Before: cty.NullVal(impliedType), Before: cty.NullVal(impliedType),
After: cty.UnknownVal(impliedType), After: cty.UnknownVal(impliedType),
Importing: &plans.Importing{ 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 // and the import by ID. When importing by identity, we need to
// make sure to use the complete identity return by the provider // make sure to use the complete identity return by the provider
// instead of the (potential) incomplete one from the configuration. // 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} change.Importing = &plans.Importing{Target: instanceRefreshState.Identity}
} else { } 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 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() deferralAllowed := ctx.Deferrals().DeferralAllowed()
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
absAddr := addr.Resource.Absolute(ctx.Path()) absAddr := addr.Resource.Absolute(ctx.Path())
@ -611,6 +616,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
} }
var deferred *providers.Deferred var deferred *providers.Deferred
importTarget := n.importTarget.target
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
return h.PrePlanImport(hookResourceID, importTarget) 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. // 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) diags = diags.Append(generatedDiags)
// This wraps the content of the resource block in an enclosing resource block // 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 // generateHCLResourceDef generates the HCL definition for the resource
// instance, including the surrounding block. This is used to generate the // instance, including the surrounding block. This is used to generate the
// configuration for the resource instance when importing or generating // 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{ providerAddr := addrs.LocalProviderConfig{
LocalName: n.ResolvedProvider.Provider.Type, LocalName: n.ResolvedProvider.Provider.Type,
Alias: n.ResolvedProvider.Alias, Alias: n.ResolvedProvider.Alias,
} }
if importCfg != nil && importCfg.ProviderConfigRef != nil {
providerAddr.LocalName = importCfg.ProviderConfigRef.Name
providerAddr.Alias = importCfg.ProviderConfigRef.Alias
}
var diags tfdiags.Diagnostics var diags tfdiags.Diagnostics
providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider)

Loading…
Cancel
Save