mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-29 10:21:01 -06:00
4bc1696fd1
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.
252 lines
7.6 KiB
Go
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
|
|
}
|