opentofu/internal/tofu/context_import.go
Ronny Orot dcc1fa3b88
Support for_each syntax in import block (#1492)
Signed-off-by: RLRabinowitz <rlrabinowitz2@gmail.com>
Signed-off-by: Ronny Orot <ronny.orot@gmail.com>
Co-authored-by: RLRabinowitz <rlrabinowitz2@gmail.com>
2024-04-17 17:12:10 +03:00

286 lines
10 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package tofu
import (
"fmt"
"log"
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/instances"
"github.com/opentofu/opentofu/internal/states"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// ImportOpts are used as the configuration for Import.
type ImportOpts struct {
// Targets are the targets to import
Targets []*ImportTarget
// SetVariables are the variables set outside of the configuration,
// such as on the command line, in variables files, etc.
SetVariables InputValues
}
// CommandLineImportTarget is a target that we need to import, that originated from the CLI command
// It represents a single resource that we need to import.
// The resource's ID and Address are fully known when executing the command (unlike when using the `import` block)
type CommandLineImportTarget struct {
// Addr is the address for the resource instance that the new object should
// be imported into.
Addr addrs.AbsResourceInstance
// ID is the string ID of the resource to import. This is resource-specific.
ID string
}
// ImportTarget is a target that we need to import.
// It could either represent a single resource or multiple instances of the same resource, if for_each is used
// ImportTarget can be either a result of the import CLI command, or the import block
type ImportTarget struct {
// Config is the original import block for this import. This might be null
// if the import did not originate in config.
// Config is mutually-exclusive with CommandLineImportTarget
Config *configs.Import
// CommandLineImportTarget is the ImportTarget information in the case of an import target origination for the
// command line. CommandLineImportTarget is mutually-exclusive with Config
*CommandLineImportTarget
}
// IsFromImportBlock checks whether the import target originates from an `import` block
// Currently, it should yield the opposite result of IsFromImportCommandLine, as those two are mutually-exclusive
func (i *ImportTarget) IsFromImportBlock() bool {
return i.Config != nil
}
// IsFromImportCommandLine checks whether the import target originates from a `tofu import` command
// Currently, it should yield the opposite result of IsFromImportBlock, as those two are mutually-exclusive
func (i *ImportTarget) IsFromImportCommandLine() bool {
return i.CommandLineImportTarget != nil
}
// StaticAddr returns the static address part of an import target
// For an ImportTarget originating from the command line, the address is already known
// However for an ImportTarget originating from an import block, the full address might not be known initially,
// and could only be evaluated down the line. Here, we create a static representation for the address.
// This is useful so that we could have information on the ImportTarget early on, such as the Module and Resource of it
func (i *ImportTarget) StaticAddr() addrs.ConfigResource {
if i.IsFromImportCommandLine() {
return i.CommandLineImportTarget.Addr.ConfigResource()
}
return i.Config.StaticTo
}
// ResolvedAddr returns a reference to the resolved address of an import target, if possible. If not possible, it
// returns nil.
// For an ImportTarget originating from the command line, the address is already known
// However for an ImportTarget originating from an import block, the full address might not be known initially,
// and could only be evaluated down the line.
func (i *ImportTarget) ResolvedAddr() *addrs.AbsResourceInstance {
if i.IsFromImportCommandLine() {
return &i.CommandLineImportTarget.Addr
} else {
return i.Config.ResolvedTo
}
}
// ImportResolver is a struct that maintains a map of all imports as they are being resolved.
// This is specifically for imports originating from configuration.
// Import targets' addresses are not fully known from the get-go, and could only be resolved later when walking
// the graph. This struct helps keep track of the resolved imports, mostly for validation that all imports
// have been addressed and point to an actual configuration.
// The key of the map is a string representation of the address, and the value is an EvaluatedConfigImportTarget.
type ImportResolver struct {
mu sync.RWMutex
imports map[string]EvaluatedConfigImportTarget
}
func NewImportResolver() *ImportResolver {
return &ImportResolver{imports: make(map[string]EvaluatedConfigImportTarget)}
}
// ExpandAndResolveImport is responsible for two operations:
// 1. Expands the ImportTarget (originating from an import block) if it contains a 'for_each' attribute.
// 2. Goes over the expanded imports and resolves the ID and address, when we have the context necessary to resolve
// them. The resolved import target would be an EvaluatedConfigImportTarget.
// This function mutates the EvalContext's ImportResolver, adding the resolved import target.
// The function errors if we failed to evaluate the ID or the address.
func (ri *ImportResolver) ExpandAndResolveImport(importTarget *ImportTarget, ctx EvalContext) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
// The import block expressions are declared within the root module.
// We need to explicitly use the context with the path of the root module, so that all references will be
// relative to the root module
rootCtx := ctx.WithPath(addrs.RootModuleInstance)
if importTarget.Config.ForEach != nil {
// The import target has a for_each attribute, so we need to expand it
forEachVal, evalDiags := evaluateForEachExpressionValue(importTarget.Config.ForEach, rootCtx, false, true)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return diags
}
// We are building an instances.RepetitionData based on each for_each key and val combination
var repetitions []instances.RepetitionData
it := forEachVal.ElementIterator()
for it.Next() {
k, v := it.Element()
repetitions = append(repetitions, instances.RepetitionData{
EachKey: k,
EachValue: v,
})
}
for _, keyData := range repetitions {
diags = diags.Append(ri.resolveImport(importTarget, rootCtx, keyData))
}
} else {
// The import target is singular, no need to expand
diags = diags.Append(ri.resolveImport(importTarget, rootCtx, EvalDataForNoInstanceKey))
}
return diags
}
// resolveImport resolves the ID and address of an ImportTarget originating from an import block,
// when we have the context necessary to resolve them. The resolved import target would be an
// EvaluatedConfigImportTarget.
// This function mutates the EvalContext's ImportResolver, adding the resolved import target.
// The function errors if we failed to evaluate the ID or the address.
func (ri *ImportResolver) resolveImport(importTarget *ImportTarget, ctx EvalContext, keyData instances.RepetitionData) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
importId, evalDiags := evaluateImportIdExpression(importTarget.Config.ID, ctx, keyData)
diags = diags.Append(evalDiags)
if diags.HasErrors() {
return diags
}
importAddress, addressDiags := ctx.EvaluateImportAddress(importTarget.Config.To, keyData)
diags = diags.Append(addressDiags)
if diags.HasErrors() {
return diags
}
ri.mu.Lock()
defer ri.mu.Unlock()
resolvedImportKey := importAddress.String()
if importTarget, exists := ri.imports[resolvedImportKey]; exists {
return diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: fmt.Sprintf("Duplicate import configuration for %q", importAddress),
Detail: fmt.Sprintf("An import block for the resource %q was already declared at %s. A resource can have only one import block.", importAddress, importTarget.Config.DeclRange),
Subject: importTarget.Config.DeclRange.Ptr(),
})
}
ri.imports[resolvedImportKey] = EvaluatedConfigImportTarget{
Config: importTarget.Config,
Addr: importAddress,
ID: importId,
}
if keyData == EvalDataForNoInstanceKey {
log.Printf("[TRACE] importResolver: resolved a singular import target %s", importAddress)
} else {
log.Printf("[TRACE] importResolver: resolved an expanded import target %s", importAddress)
}
return diags
}
// GetAllImports returns all resolved imports
func (ri *ImportResolver) GetAllImports() []EvaluatedConfigImportTarget {
ri.mu.RLock()
defer ri.mu.RUnlock()
var allImports []EvaluatedConfigImportTarget
for _, importTarget := range ri.imports {
allImports = append(allImports, importTarget)
}
return allImports
}
func (ri *ImportResolver) GetImport(address addrs.AbsResourceInstance) *EvaluatedConfigImportTarget {
ri.mu.RLock()
defer ri.mu.RUnlock()
for _, importTarget := range ri.imports {
if importTarget.Addr.Equal(address) {
return &importTarget
}
}
return nil
}
// Import takes already-created external resources and brings them
// under OpenTofu management. Import requires the exact type, name, and ID
// of the resources to import.
//
// This operation is idempotent. If the requested resource is already
// imported, no changes are made to the state.
//
// Further, this operation also gracefully handles partial state. If during
// an import there is a failure, all previously imported resources remain
// imported.
func (c *Context) Import(config *configs.Config, prevRunState *states.State, opts *ImportOpts) (*states.State, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// Hold a lock since we can modify our own state here
defer c.acquireRun("import")()
// Don't modify our caller's state
state := prevRunState.DeepCopy()
log.Printf("[DEBUG] Building and walking import graph")
variables := opts.SetVariables
// Initialize our graph builder
builder := &PlanGraphBuilder{
ImportTargets: opts.Targets,
Config: config,
State: state,
RootVariableValues: variables,
Plugins: c.plugins,
Operation: walkImport,
}
// Build the graph
graph, graphDiags := builder.Build(addrs.RootModuleInstance)
diags = diags.Append(graphDiags)
if graphDiags.HasErrors() {
return state, diags
}
// Walk it
walker, walkDiags := c.walk(graph, walkImport, &graphWalkOpts{
Config: config,
InputState: state,
})
diags = diags.Append(walkDiags)
if walkDiags.HasErrors() {
return state, diags
}
// Data sources which could not be read during the import plan will be
// unknown. We need to strip those objects out so that the state can be
// serialized.
walker.State.RemovePlannedResourceInstanceObjects()
newState := walker.State.Close()
return newState, diags
}