Skip to content

Commit 2864351

Browse files
authored
Plannable import 3: Make import plannable (#33085)
During a plan, Terraform now checks for the presence of import blocks. For each resource in config, if an import block is present with a matching address, planning that node will now trigger an ImportResourceState and ReadResource. The resulting state is treated as the node's "refresh state", and planning proceeds as normal from there. The walkImport operation is now only used for the legacy "terraform import" CLI command. This is the only case under which the plan should produce graphNodeImportStates.
1 parent b3a49a2 commit 2864351

File tree

9 files changed

+407
-41
lines changed

9 files changed

+407
-41
lines changed

internal/plans/changes.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ func (c *Changes) Empty() bool {
3737
if res.Action != NoOp || res.Moved() {
3838
return false
3939
}
40+
41+
if res.Importing {
42+
return false
43+
}
4044
}
4145

4246
for _, out := range c.Outputs {
@@ -301,9 +305,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
301305
Private: rc.Private,
302306
ProviderAddr: rc.ProviderAddr,
303307
Change: Change{
304-
Action: Delete,
305-
Before: rc.Before,
306-
After: cty.NullVal(rc.Before.Type()),
308+
Action: Delete,
309+
Before: rc.Before,
310+
After: cty.NullVal(rc.Before.Type()),
311+
Importing: rc.Importing,
307312
},
308313
}
309314
default:
@@ -313,9 +318,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
313318
Private: rc.Private,
314319
ProviderAddr: rc.ProviderAddr,
315320
Change: Change{
316-
Action: NoOp,
317-
Before: rc.Before,
318-
After: rc.Before,
321+
Action: NoOp,
322+
Before: rc.Before,
323+
After: rc.Before,
324+
Importing: rc.Importing,
319325
},
320326
}
321327
}
@@ -328,9 +334,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
328334
Private: rc.Private,
329335
ProviderAddr: rc.ProviderAddr,
330336
Change: Change{
331-
Action: NoOp,
332-
Before: rc.Before,
333-
After: rc.Before,
337+
Action: NoOp,
338+
Before: rc.Before,
339+
After: rc.Before,
340+
Importing: rc.Importing,
334341
},
335342
}
336343
case CreateThenDelete, DeleteThenCreate:
@@ -340,9 +347,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
340347
Private: rc.Private,
341348
ProviderAddr: rc.ProviderAddr,
342349
Change: Change{
343-
Action: Create,
344-
Before: cty.NullVal(rc.After.Type()),
345-
After: rc.After,
350+
Action: Create,
351+
Before: cty.NullVal(rc.After.Type()),
352+
After: rc.After,
353+
Importing: rc.Importing,
346354
},
347355
}
348356
}
@@ -548,5 +556,6 @@ func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) {
548556
After: afterDV,
549557
BeforeValMarks: beforeVM,
550558
AfterValMarks: afterVM,
559+
Importing: c.Importing,
551560
}, nil
552561
}

internal/plans/changes_src.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ func (cs *ChangeSrc) Decode(ty cty.Type) (*Change, error) {
230230
}
231231

232232
return &Change{
233-
Action: cs.Action,
234-
Before: before.MarkWithPaths(cs.BeforeValMarks),
235-
After: after.MarkWithPaths(cs.AfterValMarks),
233+
Action: cs.Action,
234+
Before: before.MarkWithPaths(cs.BeforeValMarks),
235+
After: after.MarkWithPaths(cs.AfterValMarks),
236+
Importing: cs.Importing,
236237
}, nil
237238
}

internal/terraform/context_plan.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ type PlanOpts struct {
7070
// outside of Terraform), thereby hopefully replacing it with a
7171
// fully-functional new object.
7272
ForceReplace []addrs.AbsResourceInstance
73+
74+
// ImportTargets is a list of target resources to import. These resources
75+
// will be added to the plan graph.
76+
ImportTargets []*ImportTarget
7377
}
7478

7579
// Plan generates an execution plan by comparing the given configuration
@@ -285,6 +289,7 @@ func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts
285289
panic(fmt.Sprintf("called Context.plan with %s", opts.Mode))
286290
}
287291

292+
opts.ImportTargets = c.findImportBlocks(config)
288293
plan, walkDiags := c.planWalk(config, prevRunState, opts)
289294
diags = diags.Append(walkDiags)
290295

@@ -505,6 +510,17 @@ func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactor
505510
return refactoring.ValidateMoves(stmts, config, allInsts)
506511
}
507512

513+
func (c *Context) findImportBlocks(config *configs.Config) []*ImportTarget {
514+
var importTargets []*ImportTarget
515+
for _, ic := range config.Module.Import {
516+
importTargets = append(importTargets, &ImportTarget{
517+
Addr: ic.To,
518+
ID: ic.ID,
519+
})
520+
}
521+
return importTargets
522+
}
523+
508524
func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
509525
var diags tfdiags.Diagnostics
510526
log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode)
@@ -605,6 +621,7 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State,
605621
skipRefresh: opts.SkipRefresh,
606622
preDestroyRefresh: opts.PreDestroyRefresh,
607623
Operation: walkPlan,
624+
ImportTargets: opts.ImportTargets,
608625
}).Build(addrs.RootModuleInstance)
609626
return graph, walkPlan, diags
610627
case plans.RefreshOnlyMode:
@@ -784,7 +801,7 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s
784801
// (as opposed to graphs as an implementation detail) intended only for use
785802
// by the "terraform graph" command when asked to render a plan-time graph.
786803
//
787-
// The result of this is intended only for rendering ot the user as a dot
804+
// The result of this is intended only for rendering to the user as a dot
788805
// graph, and so may change in future in order to make the result more useful
789806
// in that context, even if drifts away from the physical graph that Terraform
790807
// Core currently uses as an implementation detail of planning.

internal/terraform/context_plan2_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4098,3 +4098,203 @@ resource "test_object" "a" {
40984098
}
40994099
}
41004100
}
4101+
4102+
func TestContext2Plan_importResourceBasic(t *testing.T) {
4103+
addr := mustResourceInstanceAddr("test_object.a")
4104+
m := testModuleInline(t, map[string]string{
4105+
"main.tf": `
4106+
resource "test_object" "a" {
4107+
test_string = "foo"
4108+
}
4109+
4110+
import {
4111+
to = test_object.a
4112+
id = "123"
4113+
}
4114+
`,
4115+
})
4116+
4117+
p := simpleMockProvider()
4118+
ctx := testContext2(t, &ContextOpts{
4119+
Providers: map[addrs.Provider]providers.Factory{
4120+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
4121+
},
4122+
})
4123+
p.ReadResourceResponse = &providers.ReadResourceResponse{
4124+
NewState: cty.ObjectVal(map[string]cty.Value{
4125+
"test_string": cty.StringVal("foo"),
4126+
}),
4127+
}
4128+
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
4129+
ImportedResources: []providers.ImportedResource{
4130+
{
4131+
TypeName: "test_object",
4132+
State: cty.ObjectVal(map[string]cty.Value{
4133+
"test_string": cty.StringVal("foo"),
4134+
}),
4135+
},
4136+
},
4137+
}
4138+
4139+
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
4140+
if diags.HasErrors() {
4141+
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
4142+
}
4143+
4144+
t.Run(addr.String(), func(t *testing.T) {
4145+
instPlan := plan.Changes.ResourceInstance(addr)
4146+
if instPlan == nil {
4147+
t.Fatalf("no plan for %s at all", addr)
4148+
}
4149+
4150+
if got, want := instPlan.Addr, addr; !got.Equal(want) {
4151+
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
4152+
}
4153+
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
4154+
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
4155+
}
4156+
if got, want := instPlan.Action, plans.NoOp; got != want {
4157+
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
4158+
}
4159+
if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
4160+
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
4161+
}
4162+
if !instPlan.Importing {
4163+
t.Errorf("expected import change, got non-import change")
4164+
}
4165+
})
4166+
}
4167+
4168+
func TestContext2Plan_importResourceUpdate(t *testing.T) {
4169+
addr := mustResourceInstanceAddr("test_object.a")
4170+
m := testModuleInline(t, map[string]string{
4171+
"main.tf": `
4172+
resource "test_object" "a" {
4173+
test_string = "bar"
4174+
}
4175+
4176+
import {
4177+
to = test_object.a
4178+
id = "123"
4179+
}
4180+
`,
4181+
})
4182+
4183+
p := simpleMockProvider()
4184+
ctx := testContext2(t, &ContextOpts{
4185+
Providers: map[addrs.Provider]providers.Factory{
4186+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
4187+
},
4188+
})
4189+
p.ReadResourceResponse = &providers.ReadResourceResponse{
4190+
NewState: cty.ObjectVal(map[string]cty.Value{
4191+
"test_string": cty.StringVal("foo"),
4192+
}),
4193+
}
4194+
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
4195+
ImportedResources: []providers.ImportedResource{
4196+
{
4197+
TypeName: "test_object",
4198+
State: cty.ObjectVal(map[string]cty.Value{
4199+
"test_string": cty.StringVal("foo"),
4200+
}),
4201+
},
4202+
},
4203+
}
4204+
4205+
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
4206+
if diags.HasErrors() {
4207+
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
4208+
}
4209+
4210+
t.Run(addr.String(), func(t *testing.T) {
4211+
instPlan := plan.Changes.ResourceInstance(addr)
4212+
if instPlan == nil {
4213+
t.Fatalf("no plan for %s at all", addr)
4214+
}
4215+
4216+
if got, want := instPlan.Addr, addr; !got.Equal(want) {
4217+
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
4218+
}
4219+
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
4220+
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
4221+
}
4222+
if got, want := instPlan.Action, plans.Update; got != want {
4223+
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
4224+
}
4225+
if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
4226+
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
4227+
}
4228+
if !instPlan.Importing {
4229+
t.Errorf("expected import change, got non-import change")
4230+
}
4231+
})
4232+
}
4233+
4234+
func TestContext2Plan_importResourceReplace(t *testing.T) {
4235+
addr := mustResourceInstanceAddr("test_object.a")
4236+
m := testModuleInline(t, map[string]string{
4237+
"main.tf": `
4238+
resource "test_object" "a" {
4239+
test_string = "bar"
4240+
}
4241+
4242+
import {
4243+
to = test_object.a
4244+
id = "123"
4245+
}
4246+
`,
4247+
})
4248+
4249+
p := simpleMockProvider()
4250+
ctx := testContext2(t, &ContextOpts{
4251+
Providers: map[addrs.Provider]providers.Factory{
4252+
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
4253+
},
4254+
})
4255+
p.ReadResourceResponse = &providers.ReadResourceResponse{
4256+
NewState: cty.ObjectVal(map[string]cty.Value{
4257+
"test_string": cty.StringVal("foo"),
4258+
}),
4259+
}
4260+
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
4261+
ImportedResources: []providers.ImportedResource{
4262+
{
4263+
TypeName: "test_object",
4264+
State: cty.ObjectVal(map[string]cty.Value{
4265+
"test_string": cty.StringVal("foo"),
4266+
}),
4267+
},
4268+
},
4269+
}
4270+
4271+
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
4272+
Mode: plans.NormalMode,
4273+
ForceReplace: []addrs.AbsResourceInstance{
4274+
addr,
4275+
},
4276+
})
4277+
if diags.HasErrors() {
4278+
t.Fatalf("unexpected errors\n%s", diags.Err().Error())
4279+
}
4280+
4281+
t.Run(addr.String(), func(t *testing.T) {
4282+
instPlan := plan.Changes.ResourceInstance(addr)
4283+
if instPlan == nil {
4284+
t.Fatalf("no plan for %s at all", addr)
4285+
}
4286+
4287+
if got, want := instPlan.Addr, addr; !got.Equal(want) {
4288+
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
4289+
}
4290+
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
4291+
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
4292+
}
4293+
if got, want := instPlan.Action, plans.DeleteThenCreate; got != want {
4294+
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
4295+
}
4296+
if !instPlan.Importing {
4297+
t.Errorf("expected import change, got non-import change")
4298+
}
4299+
})
4300+
}

internal/terraform/graph_builder_plan.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,13 @@ func (b *PlanGraphBuilder) initImport() {
309309
// as the new state, and users are not expecting the import process
310310
// to update any other instances in state.
311311
skipRefresh: true,
312+
313+
// If we get here, we know that we are in legacy import mode, and
314+
// that the user has run the import command rather than plan.
315+
// This flag must be propagated down to the
316+
// NodePlannableResourceInstance so we can ignore the new import
317+
// behaviour.
318+
legacyImportMode: true,
312319
}
313320
}
314321
}

internal/terraform/graph_walk_context.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ type ContextGraphWalker struct {
2727

2828
// Configurable values
2929
Context *Context
30-
State *states.SyncState // Used for safe concurrent access to state
31-
RefreshState *states.SyncState // Used for safe concurrent access to state
32-
PrevRunState *states.SyncState // Used for safe concurrent access to state
33-
Changes *plans.ChangesSync // Used for safe concurrent writes to changes
34-
Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results
35-
InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances
30+
State *states.SyncState // Used for safe concurrent access to state
31+
RefreshState *states.SyncState // Used for safe concurrent access to state
32+
PrevRunState *states.SyncState // Used for safe concurrent access to state
33+
Changes *plans.ChangesSync // Used for safe concurrent writes to changes
34+
Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results
35+
InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances
36+
Imports []configs.Import
3637
MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements
3738
Operation walkOperation
3839
StopContext context.Context

internal/terraform/node_resource_abstract.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,6 @@ func (n *NodeAbstractResource) ReferenceableAddrs() []addrs.Referenceable {
131131
return []addrs.Referenceable{n.Addr.Resource}
132132
}
133133

134-
func (n *NodeAbstractResource) Import(addr *ImportTarget) {
135-
136-
}
137-
138134
// GraphNodeReferencer
139135
func (n *NodeAbstractResource) References() []*addrs.Reference {
140136
// If we have a config then we prefer to use that.

0 commit comments

Comments
 (0)