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.
main
Kristin Laemmert 6 days 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)
}
}
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 {
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)}
}
}
}

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

Loading…
Cancel
Save