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.
This commit is contained in:
kmoe 2023-04-28 23:45:43 +01:00 committed by GitHub
parent b3a49a2fa7
commit 28643516b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 407 additions and 41 deletions

View File

@ -37,6 +37,10 @@ func (c *Changes) Empty() bool {
if res.Action != NoOp || res.Moved() {
return false
}
if res.Importing {
return false
}
}
for _, out := range c.Outputs {
@ -301,9 +305,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
Private: rc.Private,
ProviderAddr: rc.ProviderAddr,
Change: Change{
Action: Delete,
Before: rc.Before,
After: cty.NullVal(rc.Before.Type()),
Action: Delete,
Before: rc.Before,
After: cty.NullVal(rc.Before.Type()),
Importing: rc.Importing,
},
}
default:
@ -313,9 +318,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
Private: rc.Private,
ProviderAddr: rc.ProviderAddr,
Change: Change{
Action: NoOp,
Before: rc.Before,
After: rc.Before,
Action: NoOp,
Before: rc.Before,
After: rc.Before,
Importing: rc.Importing,
},
}
}
@ -328,9 +334,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
Private: rc.Private,
ProviderAddr: rc.ProviderAddr,
Change: Change{
Action: NoOp,
Before: rc.Before,
After: rc.Before,
Action: NoOp,
Before: rc.Before,
After: rc.Before,
Importing: rc.Importing,
},
}
case CreateThenDelete, DeleteThenCreate:
@ -340,9 +347,10 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha
Private: rc.Private,
ProviderAddr: rc.ProviderAddr,
Change: Change{
Action: Create,
Before: cty.NullVal(rc.After.Type()),
After: rc.After,
Action: Create,
Before: cty.NullVal(rc.After.Type()),
After: rc.After,
Importing: rc.Importing,
},
}
}
@ -548,5 +556,6 @@ func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) {
After: afterDV,
BeforeValMarks: beforeVM,
AfterValMarks: afterVM,
Importing: c.Importing,
}, nil
}

View File

@ -230,8 +230,9 @@ func (cs *ChangeSrc) Decode(ty cty.Type) (*Change, error) {
}
return &Change{
Action: cs.Action,
Before: before.MarkWithPaths(cs.BeforeValMarks),
After: after.MarkWithPaths(cs.AfterValMarks),
Action: cs.Action,
Before: before.MarkWithPaths(cs.BeforeValMarks),
After: after.MarkWithPaths(cs.AfterValMarks),
Importing: cs.Importing,
}, nil
}

View File

@ -70,6 +70,10 @@ type PlanOpts struct {
// outside of Terraform), thereby hopefully replacing it with a
// fully-functional new object.
ForceReplace []addrs.AbsResourceInstance
// ImportTargets is a list of target resources to import. These resources
// will be added to the plan graph.
ImportTargets []*ImportTarget
}
// 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
panic(fmt.Sprintf("called Context.plan with %s", opts.Mode))
}
opts.ImportTargets = c.findImportBlocks(config)
plan, walkDiags := c.planWalk(config, prevRunState, opts)
diags = diags.Append(walkDiags)
@ -505,6 +510,17 @@ func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactor
return refactoring.ValidateMoves(stmts, config, allInsts)
}
func (c *Context) findImportBlocks(config *configs.Config) []*ImportTarget {
var importTargets []*ImportTarget
for _, ic := range config.Module.Import {
importTargets = append(importTargets, &ImportTarget{
Addr: ic.To,
ID: ic.ID,
})
}
return importTargets
}
func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
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,
skipRefresh: opts.SkipRefresh,
preDestroyRefresh: opts.PreDestroyRefresh,
Operation: walkPlan,
ImportTargets: opts.ImportTargets,
}).Build(addrs.RootModuleInstance)
return graph, walkPlan, diags
case plans.RefreshOnlyMode:
@ -784,7 +801,7 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s
// (as opposed to graphs as an implementation detail) intended only for use
// by the "terraform graph" command when asked to render a plan-time graph.
//
// The result of this is intended only for rendering ot the user as a dot
// The result of this is intended only for rendering to the user as a dot
// graph, and so may change in future in order to make the result more useful
// in that context, even if drifts away from the physical graph that Terraform
// Core currently uses as an implementation detail of planning.

View File

@ -4098,3 +4098,203 @@ resource "test_object" "a" {
}
}
}
func TestContext2Plan_importResourceBasic(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
test_string = "foo"
}
import {
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"),
}),
},
},
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
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)
}
if got, want := instPlan.Addr, addr; !got.Equal(want) {
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.Action, plans.NoOp; got != want {
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
}
if !instPlan.Importing {
t.Errorf("expected import change, got non-import change")
}
})
}
func TestContext2Plan_importResourceUpdate(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
test_string = "bar"
}
import {
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"),
}),
},
},
}
plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
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)
}
if got, want := instPlan.Addr, addr; !got.Equal(want) {
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.Action, plans.Update; got != want {
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want {
t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want)
}
if !instPlan.Importing {
t.Errorf("expected import change, got non-import change")
}
})
}
func TestContext2Plan_importResourceReplace(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
test_string = "bar"
}
import {
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"),
}),
},
},
}
plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
ForceReplace: []addrs.AbsResourceInstance{
addr,
},
})
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)
}
if got, want := instPlan.Addr, addr; !got.Equal(want) {
t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) {
t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want)
}
if got, want := instPlan.Action, plans.DeleteThenCreate; got != want {
t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want)
}
if !instPlan.Importing {
t.Errorf("expected import change, got non-import change")
}
})
}

View File

@ -309,6 +309,13 @@ func (b *PlanGraphBuilder) initImport() {
// as the new state, and users are not expecting the import process
// to update any other instances in state.
skipRefresh: true,
// If we get here, we know that we are in legacy import mode, and
// that the user has run the import command rather than plan.
// This flag must be propagated down to the
// NodePlannableResourceInstance so we can ignore the new import
// behaviour.
legacyImportMode: true,
}
}
}

View File

@ -27,12 +27,13 @@ type ContextGraphWalker struct {
// Configurable values
Context *Context
State *states.SyncState // Used for safe concurrent access to state
RefreshState *states.SyncState // Used for safe concurrent access to state
PrevRunState *states.SyncState // Used for safe concurrent access to state
Changes *plans.ChangesSync // Used for safe concurrent writes to changes
Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results
InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances
State *states.SyncState // Used for safe concurrent access to state
RefreshState *states.SyncState // Used for safe concurrent access to state
PrevRunState *states.SyncState // Used for safe concurrent access to state
Changes *plans.ChangesSync // Used for safe concurrent writes to changes
Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results
InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances
Imports []configs.Import
MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements
Operation walkOperation
StopContext context.Context

View File

@ -131,10 +131,6 @@ func (n *NodeAbstractResource) ReferenceableAddrs() []addrs.Referenceable {
return []addrs.Referenceable{n.Addr.Resource}
}
func (n *NodeAbstractResource) Import(addr *ImportTarget) {
}
// GraphNodeReferencer
func (n *NodeAbstractResource) References() []*addrs.Reference {
// If we have a config then we prefer to use that.

View File

@ -43,6 +43,10 @@ type nodeExpandPlannableResource struct {
// structure in the future, as we need to compare for equality and take the
// union of multiple groups of dependencies.
dependencies []addrs.ConfigResource
// legacyImportMode is set if the graph is being constructed following an
// invocation of the legacy "terraform import" CLI command.
legacyImportMode bool
}
var (
@ -311,13 +315,18 @@ func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext,
// The concrete resource factory we'll use
concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
// check if this node is being imported first
for _, importTarget := range n.importTargets {
if importTarget.Addr.Equal(a.Addr) {
return &graphNodeImportState{
Addr: importTarget.Addr,
ID: importTarget.ID,
ResolvedProvider: n.ResolvedProvider,
var m *NodePlannableResourceInstance
// If we're in legacy import mode (the import CLI command), we only need
// to return the import node, not a plannable resource node.
if n.legacyImportMode {
for _, importTarget := range n.importTargets {
if importTarget.Addr.Equal(a.Addr) {
return &graphNodeImportState{
Addr: importTarget.Addr,
ID: importTarget.ID,
ResolvedProvider: n.ResolvedProvider,
}
}
}
}
@ -332,7 +341,7 @@ func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext,
a.Dependencies = n.dependencies
a.preDestroyRefresh = n.preDestroyRefresh
return &NodePlannableResourceInstance{
m = &NodePlannableResourceInstance{
NodeAbstractResourceInstance: a,
// By the time we're walking, we've figured out whether we need
@ -343,6 +352,19 @@ func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext,
skipPlanChanges: n.skipPlanChanges,
forceReplace: n.forceReplace,
}
for _, importTarget := range n.importTargets {
if importTarget.Addr.Equal(a.Addr) {
// If we get here, we're definitely not in legacy import mode,
// so go ahead and plan the resource changes including import.
m.importTarget = ImportTarget{
ID: importTarget.ID,
Addr: importTarget.Addr,
}
}
}
return m
}
// The concrete resource factory we'll use for orphans

View File

@ -5,13 +5,14 @@ import (
"log"
"sort"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/plans"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/zclconf/go-cty/cty"
)
// NodePlannableResourceInstance represents a _single_ resource
@ -37,6 +38,10 @@ type NodePlannableResourceInstance struct {
// replaceTriggeredBy stores references from replace_triggered_by which
// triggered this instance to be replaced.
replaceTriggeredBy []*addrs.Reference
// importTarget, if populated, contains the information necessary to plan
// an import of this resource.
importTarget ImportTarget
}
var (
@ -133,7 +138,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
checkRuleSeverity = tfdiags.Warning
}
_, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
diags = diags.Append(err)
if diags.HasErrors() {
return diags
@ -144,10 +149,17 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
return diags
}
instanceRefreshState, readDiags := n.readResourceInstanceState(ctx, addr)
diags = diags.Append(readDiags)
if diags.HasErrors() {
return diags
// If the resource is to be imported, we now ask the provider for an Import
// and a Refresh, and save the resulting state to instanceRefreshState.
if n.importTarget.ID != "" {
instanceRefreshState, diags = n.importState(ctx, addr, provider)
} else {
var readDiags tfdiags.Diagnostics
instanceRefreshState, readDiags = n.readResourceInstanceState(ctx, addr)
diags = diags.Append(readDiags)
if diags.HasErrors() {
return diags
}
}
// We'll save a snapshot of what we just read from the state into the
@ -228,6 +240,10 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
return diags
}
if n.importTarget.ID != "" {
change.Importing = true
}
// FIXME: here we udpate the change to reflect the reason for
// replacement, but we still overload forceReplace to get the correct
// change planned.
@ -364,6 +380,103 @@ func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repDat
return diags
}
func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, provider providers.Interface) (instanceRefreshState *states.ResourceInstanceObject, diags tfdiags.Diagnostics) {
absAddr := addr.Resource.Absolute(ctx.Path())
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreImportState(absAddr, n.importTarget.ID)
}))
if diags.HasErrors() {
return instanceRefreshState, diags
}
resp := provider.ImportResourceState(providers.ImportResourceStateRequest{
TypeName: addr.Resource.Resource.Type,
ID: n.importTarget.ID,
})
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
return instanceRefreshState, diags
}
imported := resp.ImportedResources
if len(imported) == 0 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Import returned no resources",
fmt.Sprintf("While attempting to import with ID %s, the provider"+
"returned no instance states.",
n.importTarget.ID,
),
))
return instanceRefreshState, diags
}
for _, obj := range imported {
log.Printf("[TRACE] graphNodeImportState: import %s %q produced instance object of type %s", absAddr.String(), n.importTarget.ID, obj.TypeName)
}
if len(imported) > 1 {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Multiple import states not supported",
fmt.Sprintf("While attempting to import with ID %s, the provider "+
"returned multiple resource instance states. This "+
"is not currently supported.",
n.importTarget.ID,
),
))
return instanceRefreshState, diags
}
// call post-import hook
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostImportState(absAddr, imported)
}))
if imported[0].TypeName == "" {
diags = diags.Append(fmt.Errorf("import of %s didn't set type", n.importTarget.Addr.String()))
return instanceRefreshState, diags
}
importedState := imported[0].AsInstanceObject()
// refresh
riNode := &NodeAbstractResourceInstance{
Addr: n.importTarget.Addr,
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: n.ResolvedProvider,
},
}
importedState, refreshDiags := riNode.refresh(ctx, states.NotDeposed, importedState)
diags = diags.Append(refreshDiags)
if diags.HasErrors() {
return instanceRefreshState, diags
}
// verify the existence of the imported resource
if importedState.Value.IsNull() {
var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot import non-existent remote object",
fmt.Sprintf(
"While attempting to import an existing object to %q, "+
"the provider detected that no object exists with the given id. "+
"Only pre-existing objects can be imported; check that the id "+
"is correct and that it is associated with the provider's "+
"configured region or endpoint, or use \"terraform apply\" to "+
"create a new remote object for this resource.",
n.importTarget.Addr,
),
))
return instanceRefreshState, diags
}
diags = diags.Append(riNode.writeResourceInstanceState(ctx, importedState, workingState))
instanceRefreshState = importedState
return instanceRefreshState, diags
}
// mergeDeps returns the union of 2 sets of dependencies
func mergeDeps(a, b []addrs.ConfigResource) []addrs.ConfigResource {
switch {