opentofu/internal/terraform/transform_import_state.go
James Bardin 99d0266585 return error for invalid resource import
Most legacy provider resources do not implement any import functionality
other than returning an empty object with the given ID, relying on core
to later read that resource and obtain the complete state. Because of
this, we need to check the response from ReadResource for a null value,
and use that as an indication the import id was invalid.
2021-05-25 17:13:49 -04:00

307 lines
9.3 KiB
Go

package terraform
import (
"fmt"
"log"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/providers"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// ImportStateTransformer is a GraphTransformer that adds nodes to the
// graph to represent the imports we want to do for resources.
type ImportStateTransformer struct {
Targets []*ImportTarget
Config *configs.Config
}
func (t *ImportStateTransformer) Transform(g *Graph) error {
for _, target := range t.Targets {
// This is only likely to happen in misconfigured tests
if t.Config == nil {
return fmt.Errorf("cannot import into an empty configuration")
}
// Get the module config
modCfg := t.Config.Descendent(target.Addr.Module.Module())
if modCfg == nil {
return fmt.Errorf("module %s not found", target.Addr.Module.Module())
}
providerAddr := addrs.AbsProviderConfig{
Module: target.Addr.Module.Module(),
}
// Try to find the resource config
rsCfg := modCfg.Module.ResourceByAddr(target.Addr.Resource.Resource)
if rsCfg != nil {
// Get the provider FQN for the resource from the resource configuration
providerAddr.Provider = rsCfg.Provider
// Get the alias from the resource's provider local config
providerAddr.Alias = rsCfg.ProviderConfigAddr().Alias
} else {
// Resource has no matching config, so use an implied provider
// based on the resource type
rsProviderType := target.Addr.Resource.Resource.ImpliedProvider()
providerAddr.Provider = modCfg.Module.ImpliedProviderForUnqualifiedType(rsProviderType)
}
node := &graphNodeImportState{
Addr: target.Addr,
ID: target.ID,
ProviderAddr: providerAddr,
}
g.Add(node)
}
return nil
}
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,
})
}
// Root transform for a single root
t := &RootTransformer{}
if err := t.Transform(g); err != nil {
return nil, err
}
// 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
}