opentofu/internal/terraform/node_resource_import.go
Martin Atkins 4bc1696fd1 core: Simplify our idea of "root node" and require it for DynamicExpand
The graph walking mechanism is specified as requiring a graph with a single
root, which in practice means there's exactly one node in the graph
which doesn't have any dependencies.

However, we previously weren't verifying that invariant is true for
subgraphs returned from DynamicExpand. It was working anyway, but it's not
ideal to be relying on a behavior that isn't guaranteed by our underlying
infrastructure.

We also previously had the RootTransformer being a bit clever and trying
to avoid adding a new node if there is already only a single graph with
no dependencies. That special case isn't particularly valuable since
there's no harm in turning a one-node graph into a two-node graph with
an explicit separate root node, and doing that allows us to assume that
the root node is always present and is always exactly terraform.rootNode.

Many existing DynamicExpand implementations were not producing valid
graphs and were previously getting away with it. All of them now produce
properly-rooted graphs that should pass validation, and we will guarantee
that with an explicit check of the DynamicExpand return value before we
try to walk that subgraph. For good measure we also verify that the root
node is exactly terraform.rootNode, even though that isn't strictly
required by our graph walker, just to help us catch potential future bugs
where a DynamicExpand implementation neglects to add our singleton root
node.
2022-10-13 14:01:08 -07:00

252 lines
7.6 KiB
Go

package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type graphNodeImportState struct {
Addr addrs.AbsResourceInstance // Addr is the resource address to import into
ID string // ID is the ID to import as
ProviderAddr addrs.AbsProviderConfig // Provider address given by the user, or implied by the resource type
ResolvedProvider addrs.AbsProviderConfig // provider node address after resolution
states []providers.ImportedResource
}
var (
_ GraphNodeModulePath = (*graphNodeImportState)(nil)
_ GraphNodeExecutable = (*graphNodeImportState)(nil)
_ GraphNodeProviderConsumer = (*graphNodeImportState)(nil)
_ GraphNodeDynamicExpandable = (*graphNodeImportState)(nil)
)
func (n *graphNodeImportState) Name() string {
return fmt.Sprintf("%s (import id %q)", n.Addr, n.ID)
}
// GraphNodeProviderConsumer
func (n *graphNodeImportState) ProvidedBy() (addrs.ProviderConfig, bool) {
// We assume that n.ProviderAddr has been properly populated here.
// It's the responsibility of the code creating a graphNodeImportState
// to populate this, possibly by calling DefaultProviderConfig() on the
// resource address to infer an implied provider from the resource type
// name.
return n.ProviderAddr, false
}
// GraphNodeProviderConsumer
func (n *graphNodeImportState) Provider() addrs.Provider {
// We assume that n.ProviderAddr has been properly populated here.
// It's the responsibility of the code creating a graphNodeImportState
// to populate this, possibly by calling DefaultProviderConfig() on the
// resource address to infer an implied provider from the resource type
// name.
return n.ProviderAddr.Provider
}
// GraphNodeProviderConsumer
func (n *graphNodeImportState) SetProvider(addr addrs.AbsProviderConfig) {
n.ResolvedProvider = addr
}
// GraphNodeModuleInstance
func (n *graphNodeImportState) Path() addrs.ModuleInstance {
return n.Addr.Module
}
// GraphNodeModulePath
func (n *graphNodeImportState) ModulePath() addrs.Module {
return n.Addr.Module.Module()
}
// GraphNodeExecutable impl.
func (n *graphNodeImportState) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
// Reset our states
n.states = nil
provider, _, err := getProvider(ctx, n.ResolvedProvider)
diags = diags.Append(err)
if diags.HasErrors() {
return diags
}
// import state
absAddr := n.Addr.Resource.Absolute(ctx.Path())
// Call pre-import hook
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
return h.PreImportState(absAddr, n.ID)
}))
if diags.HasErrors() {
return diags
}
resp := provider.ImportResourceState(providers.ImportResourceStateRequest{
TypeName: n.Addr.Resource.Resource.Type,
ID: n.ID,
})
diags = diags.Append(resp.Diagnostics)
if diags.HasErrors() {
return diags
}
imported := resp.ImportedResources
for _, obj := range imported {
log.Printf("[TRACE] graphNodeImportState: import %s %q produced instance object of type %s", absAddr.String(), n.ID, obj.TypeName)
}
n.states = imported
// Call post-import hook
diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) {
return h.PostImportState(absAddr, imported)
}))
return diags
}
// GraphNodeDynamicExpandable impl.
//
// We use DynamicExpand as a way to generate the subgraph of refreshes
// and state inserts we need to do for our import state. Since they're new
// resources they don't depend on anything else and refreshes are isolated
// so this is nearly a perfect use case for dynamic expand.
func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) {
var diags tfdiags.Diagnostics
g := &Graph{Path: ctx.Path()}
// nameCounter is used to de-dup names in the state.
nameCounter := make(map[string]int)
// Compile the list of addresses that we'll be inserting into the state.
// We do this ahead of time so we can verify that we aren't importing
// something that already exists.
addrs := make([]addrs.AbsResourceInstance, len(n.states))
for i, state := range n.states {
addr := n.Addr
if t := state.TypeName; t != "" {
addr.Resource.Resource.Type = t
}
// Determine if we need to suffix the name to de-dup
key := addr.String()
count, ok := nameCounter[key]
if ok {
count++
addr.Resource.Resource.Name += fmt.Sprintf("-%d", count)
}
nameCounter[key] = count
// Add it to our list
addrs[i] = addr
}
// Verify that all the addresses are clear
state := ctx.State()
for _, addr := range addrs {
existing := state.ResourceInstance(addr)
if existing != nil {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Resource already managed by Terraform",
fmt.Sprintf("Terraform is already managing a remote object for %s. To import to this address you must first remove the existing object from the state.", addr),
))
continue
}
}
if diags.HasErrors() {
// Bail out early, then.
return nil, diags.Err()
}
// For each of the states, we add a node to handle the refresh/add to state.
// "n.states" is populated by our own Execute with the result of
// ImportState. Since DynamicExpand is always called after Execute, this is
// safe.
for i, state := range n.states {
g.Add(&graphNodeImportStateSub{
TargetAddr: addrs[i],
State: state,
ResolvedProvider: n.ResolvedProvider,
})
}
addRootNodeToGraph(g)
// Done!
return g, diags.Err()
}
// graphNodeImportStateSub is the sub-node of graphNodeImportState
// and is part of the subgraph. This node is responsible for refreshing
// and adding a resource to the state once it is imported.
type graphNodeImportStateSub struct {
TargetAddr addrs.AbsResourceInstance
State providers.ImportedResource
ResolvedProvider addrs.AbsProviderConfig
}
var (
_ GraphNodeModuleInstance = (*graphNodeImportStateSub)(nil)
_ GraphNodeExecutable = (*graphNodeImportStateSub)(nil)
)
func (n *graphNodeImportStateSub) Name() string {
return fmt.Sprintf("import %s result", n.TargetAddr)
}
func (n *graphNodeImportStateSub) Path() addrs.ModuleInstance {
return n.TargetAddr.Module
}
// GraphNodeExecutable impl.
func (n *graphNodeImportStateSub) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
// If the Ephemeral type isn't set, then it is an error
if n.State.TypeName == "" {
diags = diags.Append(fmt.Errorf("import of %s didn't set type", n.TargetAddr.String()))
return diags
}
state := n.State.AsInstanceObject()
// Refresh
riNode := &NodeAbstractResourceInstance{
Addr: n.TargetAddr,
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: n.ResolvedProvider,
},
}
state, refreshDiags := riNode.refresh(ctx, states.NotDeposed, state)
diags = diags.Append(refreshDiags)
if diags.HasErrors() {
return diags
}
// Verify the existance of the imported resource
if state.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.TargetAddr,
),
))
return diags
}
diags = diags.Append(riNode.writeResourceInstanceState(ctx, state, workingState))
return diags
}