Import ForEach: Prerequisite - Prepare codebase for dynamic addresses for ImportTargets (#1207)

Signed-off-by: RLRabinowitz <rlrabinowitz2@gmail.com>
This commit is contained in:
Arel Rabinowitz 2024-02-08 16:05:12 +02:00 committed by GitHub
parent f92ae16419
commit 80f72cecfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 552 additions and 254 deletions

View File

@ -14,8 +14,6 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/command/arguments"
@ -238,11 +236,10 @@ func (c *ImportCommand) Run(args []string) int {
newState, importDiags := lr.Core.Import(lr.Config, lr.InputState, &tofu.ImportOpts{
Targets: []*tofu.ImportTarget{
{
Addr: addr,
// In the import block, the ID can be an arbitrary hcl.Expression,
// but here it's always interpreted as a literal string.
ID: hcl.StaticExpr(cty.StringVal(args[1]), configs.SynthBody("import", nil).MissingItemRange()),
CommandLineImportTarget: &tofu.CommandLineImportTarget{
Addr: addr,
ID: args[1],
},
},
},

View File

@ -9,6 +9,7 @@ import (
"log"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/states"
@ -25,19 +26,89 @@ type ImportOpts struct {
SetVariables InputValues
}
// ImportTarget is a single resource to import,
// in legacy (CLI) import mode.
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 *configs.Import
// 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 ID of the resource to import. This is resource-specific.
ID hcl.Expression
// 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.CommandLineImportTarget != nil {
return i.CommandLineImportTarget.Addr.ConfigResource()
}
// TODO change this later, once we change Config.To to not be a static address
return i.Config.To.ConfigResource()
}
// ResolvedAddr returns the resolved address of an import target, if possible. If not possible, returns an HCL diag
// 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 attempt to resolve the address as though it is a static absolute
// traversal, if that's possible
func (i *ImportTarget) ResolvedAddr() (address addrs.AbsResourceInstance, evaluationDiags hcl.Diagnostics) {
if i.CommandLineImportTarget != nil {
address = i.CommandLineImportTarget.Addr
} else {
// TODO change this later, when Config.To is not a static address
address = i.Config.To
}
return
}
// ResolvedConfigImportsKey is a key for a map of ImportTargets originating from the configuration
// It is used as a one-to-one representation of an EvaluatedConfigImportTarget.
// Used in ResolvedImports to maintain a map of all resolved imports when walking the graph
type ResolvedConfigImportsKey struct {
// An address string is one-to-one with addrs.AbsResourceInstance
AddrStr string
ID string
}
// ResolvedImports 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
type ResolvedImports struct {
imports map[ResolvedConfigImportsKey]bool
}
// Import takes already-created external resources and brings them

View File

@ -13,10 +13,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/configs/configschema"
"github.com/opentofu/opentofu/internal/providers"
"github.com/opentofu/opentofu/internal/states"
@ -42,14 +39,15 @@ func TestContextImport_basic(t *testing.T) {
},
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -94,14 +92,15 @@ resource "aws_instance" "foo" {
},
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(0),
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(0),
),
ID: "bar",
},
},
},
})
@ -156,14 +155,15 @@ func TestContextImport_collision(t *testing.T) {
},
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, state, &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -201,14 +201,15 @@ func TestContextImport_missingType(t *testing.T) {
},
})
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -253,14 +254,15 @@ func TestContextImport_moduleProvider(t *testing.T) {
},
})
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -309,14 +311,15 @@ func TestContextImport_providerModule(t *testing.T) {
return
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
_, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -366,14 +369,15 @@ func TestContextImport_providerConfig(t *testing.T) {
},
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
SetVariables: InputValues{
@ -427,14 +431,15 @@ func TestContextImport_providerConfigResources(t *testing.T) {
},
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
_, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -499,14 +504,15 @@ data "aws_data_source" "bar" {
}),
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -551,14 +557,15 @@ func TestContextImport_refreshNil(t *testing.T) {
}
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -593,14 +600,15 @@ func TestContextImport_module(t *testing.T) {
},
}
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -635,14 +643,15 @@ func TestContextImport_moduleDepth2(t *testing.T) {
},
}
bazExpr := hcl.StaticExpr(cty.StringVal("baz"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).Child("nested", addrs.NoKey).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: bazExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).Child("nested", addrs.NoKey).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "baz",
},
},
},
})
@ -677,14 +686,15 @@ func TestContextImport_moduleDiff(t *testing.T) {
},
}
bazExpr := hcl.StaticExpr(cty.StringVal("baz"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: bazExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "baz",
},
},
},
})
@ -746,14 +756,15 @@ func TestContextImport_multiState(t *testing.T) {
},
})
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -821,14 +832,15 @@ func TestContextImport_multiStateSame(t *testing.T) {
},
})
barExpr := hcl.StaticExpr(cty.StringVal("bar"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: barExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})
@ -916,14 +928,15 @@ resource "test_resource" "unused" {
},
})
testExpr := hcl.StaticExpr(cty.StringVal("test"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "test_resource", "test", addrs.NoKey,
),
ID: testExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "test_resource", "test", addrs.NoKey,
),
ID: "test",
},
},
},
})
@ -987,14 +1000,15 @@ resource "test_resource" "test" {
},
})
testExpr := hcl.StaticExpr(cty.StringVal("test"), configs.SynthBody("import", nil).MissingItemRange())
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "test_resource", "test", addrs.NoKey,
),
ID: testExpr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "test_resource", "test", addrs.NoKey,
),
ID: "test",
},
},
},
})
@ -1015,7 +1029,6 @@ resource "test_resource" "test" {
func TestContextImport_33572(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "issue-33572")
bar_expr := hcltest.MockExprLiteral(cty.StringVal("bar"))
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
@ -1037,10 +1050,12 @@ func TestContextImport_33572(t *testing.T) {
state, diags := ctx.Import(m, states.NewState(), &ImportOpts{
Targets: []*ImportTarget{
{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: bar_expr,
CommandLineImportTarget: &CommandLineImportTarget{
Addr: addrs.RootModuleInstance.ResourceInstance(
addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey,
),
ID: "bar",
},
},
},
})

View File

@ -13,6 +13,8 @@ import (
"strings"
"time"
"github.com/hashicorp/hcl/v2"
"github.com/zclconf/go-cty/cty"
"github.com/opentofu/opentofu/internal/addrs"
@ -306,6 +308,8 @@ func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts
}
opts.ImportTargets = c.findImportTargets(config, prevRunState)
importTargetDiags := c.validateImportTargets(config, opts.ImportTargets)
diags = diags.Append(importTargetDiags)
plan, walkDiags := c.planWalk(config, prevRunState, opts)
diags = diags.Append(walkDiags)
@ -528,25 +532,19 @@ func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactor
// All import target addresses with a key must already exist in config.
// When we are able to generate config for expanded resources, this rule can be
// relaxed.
func (c *Context) postPlanValidateImports(config *configs.Config, importTargets []*ImportTarget, allInst instances.Set) tfdiags.Diagnostics {
func (c *Context) postPlanValidateImports(resolvedImports *ResolvedImports, allInst instances.Set) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, it := range importTargets {
for resolvedImport := range resolvedImports.imports {
// We only care about import target addresses that have a key.
// If the address does not have a key, we don't need it to be in config
// because are able to generate config.
if it.Addr.Resource.Key == nil {
continue
address, addrParseDiags := addrs.ParseAbsResourceInstanceStr(resolvedImport.AddrStr)
if addrParseDiags.HasErrors() {
return addrParseDiags
}
if !allInst.HasResourceInstance(it.Addr) {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cannot import to non-existent resource address",
fmt.Sprintf(
"Importing to resource address %s is not possible, because that address does not exist in configuration. Please ensure that the resource key is correct, or remove this import block.",
it.Addr,
),
))
if !allInst.HasResourceInstance(address) {
diags = diags.Append(importResourceWithoutConfigDiags(address, nil))
}
}
return diags
@ -559,8 +557,6 @@ func (c *Context) findImportTargets(config *configs.Config, priorState *states.S
for _, ic := range config.Module.Import {
if priorState.ResourceInstance(ic.To) == nil {
importTargets = append(importTargets, &ImportTarget{
Addr: ic.To,
ID: ic.ID,
Config: ic,
})
}
@ -568,6 +564,23 @@ func (c *Context) findImportTargets(config *configs.Config, priorState *states.S
return importTargets
}
func (c *Context) validateImportTargets(config *configs.Config, importTargets []*ImportTarget) (diags tfdiags.Diagnostics) {
for _, imp := range importTargets {
staticAddress := imp.StaticAddr()
descendantConfig := config.Descendent(staticAddress.Module)
if descendantConfig == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Cannot import to non-existent resource address",
Detail: fmt.Sprintf("Importing to resource address '%s' is not possible, because that address does not exist in configuration. Please ensure that the resource key is correct, or remove this import block.", staticAddress),
Subject: imp.Config.DeclRange.Ptr(),
})
return
}
}
return
}
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)
@ -608,7 +621,7 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o
allInsts := walker.InstanceExpander.AllInstances()
importValidateDiags := c.postPlanValidateImports(config, opts.ImportTargets, allInsts)
importValidateDiags := c.postPlanValidateImports(walker.ResolvedImports, allInsts)
if importValidateDiags.HasErrors() {
return nil, importValidateDiags
}

View File

@ -4557,49 +4557,6 @@ import {
}
}
func TestContext2Plan_importTargetWithKeyDoesNotExist(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
count = 1
test_string = "bar"
}
import {
to = test_object.a[42]
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"),
}),
},
},
}
_, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts)
if !diags.HasErrors() {
t.Fatalf("expected error but got none")
}
}
func TestContext2Plan_importIdVariable(t *testing.T) {
p := testProvider("aws")
m := testModule(t, "import-id-variable")
@ -4948,6 +4905,193 @@ resource "test_object" "a" {
}
}
func TestContext2Plan_importIntoNonExistentModule(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
import {
to = module.mod.test_object.a
id = "456"
}
`,
})
p := simpleMockProvider()
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
})
if !diags.HasErrors() {
t.Fatalf("expected error")
}
if !strings.Contains(diags.Err().Error(), "Cannot import to non-existent resource address") {
t.Fatalf("expected error to be \"Cannot import to non-existent resource address\", but it was %s", diags.Err().Error())
}
}
func TestContext2Plan_importIntoNonExistentConfiguration(t *testing.T) {
type TestConfiguration struct {
Description string
inlineConfiguration map[string]string
}
configurations := []TestConfiguration{
{
Description: "Basic missing configuration",
inlineConfiguration: map[string]string{
"main.tf": `
import {
to = test_object.a
id = "123"
}
`,
},
},
{
Description: "Wrong module key",
inlineConfiguration: map[string]string{
"main.tf": `
import {
to = module.mod["non-existent"].test_object.a
id = "123"
}
module "mod" {
for_each = {
existent = "1"
}
source = "./mod"
}
`,
"./mod/main.tf": `
resource "test_object" "a" {
test_string = "bar"
}
`,
},
},
{
Description: "Module key without for_each",
inlineConfiguration: map[string]string{
"main.tf": `
import {
to = module.mod["non-existent"].test_object.a
id = "123"
}
module "mod" {
source = "./mod"
}
`,
"./mod/main.tf": `
resource "test_object" "a" {
test_string = "bar"
}
`,
},
},
{
Description: "Non-existent resource key - in module",
inlineConfiguration: map[string]string{
"main.tf": `
import {
to = module.mod.test_object.a["non-existent"]
id = "123"
}
module "mod" {
source = "./mod"
}
`,
"./mod/main.tf": `
resource "test_object" "a" {
for_each = {
existent = "1"
}
test_string = "bar"
}
`,
},
},
{
Description: "Non-existent resource key - in root",
inlineConfiguration: map[string]string{
"main.tf": `
import {
to = test_object.a[42]
id = "123"
}
resource "test_object" "a" {
test_string = "bar"
}
`,
},
},
{
Description: "Existent module key, non-existent resource key",
inlineConfiguration: map[string]string{
"main.tf": `
import {
to = module.mod["existent"].test_object.b
id = "123"
}
module "mod" {
for_each = {
existent = "1"
existent_two = "2"
}
source = "./mod"
}
`,
"./mod/main.tf": `
resource "test_object" "a" {
test_string = "bar"
}
`,
},
},
}
for _, configuration := range configurations {
t.Run(configuration.Description, func(t *testing.T) {
m := testModuleInline(t, configuration.inlineConfiguration)
p := simpleMockProvider()
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, states.NewState(), &PlanOpts{
Mode: plans.NormalMode,
})
if !diags.HasErrors() {
t.Fatalf("expected error")
}
var errNum int
for _, diag := range diags {
if diag.Severity() == tfdiags.Error {
errNum++
}
}
if errNum > 1 {
t.Fatalf("expected a single error, but got %d", errNum)
}
if !strings.Contains(diags.Err().Error(), "Configuration for import target does not exist") {
t.Fatalf("expected error to be \"Configuration for import target does not exist\", but it was %s", diags.Err().Error())
}
})
}
}
func TestContext2Plan_importResourceConfigGen(t *testing.T) {
addr := mustResourceInstanceAddr("test_object.a")
m := testModuleInline(t, map[string]string{
@ -5147,6 +5291,9 @@ import {
if !diags.HasErrors() {
t.Fatalf("expected plan to error, but it did not")
}
if !strings.Contains(diags.Err().Error(), "Config generation for count and for_each resources not supported") {
t.Fatalf("expected error to be \"Config generation for count and for_each resources not supported\", but it is %s", diags.Err().Error())
}
}
// config generation still succeeds even when planning fails

View File

@ -149,6 +149,7 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con
Checks: checkState,
InstanceExpander: instances.NewExpander(),
MoveResults: opts.MoveResults,
ResolvedImports: &ResolvedImports{imports: make(map[ResolvedConfigImportsKey]bool)},
Operation: operation,
StopContext: c.runContext,
PlanTimestamp: opts.PlanTimeTimestamp,

View File

@ -203,6 +203,14 @@ type EvalContext interface {
// objects accessible through it.
MoveResults() refactoring.MoveResults
// ResolvedImports returns a map describing the resolved imports
// after evaluating the dynamic address of the import targets
//
// This data is created during the graph walk, as import target addresses are being resolved
// Its primary use is for validation at the end of a plan - To make sure all imports have been satisfied
// and have a configuration
ResolvedImports() *ResolvedImports
// WithPath returns a copy of the context with the internal path set to the
// path argument.
WithPath(path addrs.ModuleInstance) EvalContext

View File

@ -75,6 +75,7 @@ type BuiltinEvalContext struct {
PrevRunStateValue *states.SyncState
InstanceExpanderValue *instances.Expander
MoveResultsValue refactoring.MoveResults
ResolvedImportsValue *ResolvedImports
}
// BuiltinEvalContext implements EvalContext
@ -510,3 +511,7 @@ func (ctx *BuiltinEvalContext) InstanceExpander() *instances.Expander {
func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults {
return ctx.MoveResultsValue
}
func (ctx *BuiltinEvalContext) ResolvedImports() *ResolvedImports {
return ctx.ResolvedImportsValue
}

View File

@ -151,6 +151,9 @@ type MockEvalContext struct {
MoveResultsCalled bool
MoveResultsResults refactoring.MoveResults
ResolvedImportsCalled bool
ResolvedImportsResults *ResolvedImports
InstanceExpanderCalled bool
InstanceExpanderExpander *instances.Expander
}
@ -400,6 +403,11 @@ func (c *MockEvalContext) MoveResults() refactoring.MoveResults {
return c.MoveResultsResults
}
func (c *MockEvalContext) ResolvedImports() *ResolvedImports {
c.ResolvedImportsCalled = true
return c.ResolvedImportsResults
}
func (c *MockEvalContext) InstanceExpander() *instances.Expander {
c.InstanceExpanderCalled = true
return c.InstanceExpanderExpander

View File

@ -333,13 +333,6 @@ 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

@ -32,13 +32,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
Imports []configs.Import
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
ResolvedImports *ResolvedImports // Tracks import targets as they are being resolved
MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements
Operation walkOperation
StopContext context.Context
@ -103,6 +103,7 @@ func (w *ContextGraphWalker) EvalContext() EvalContext {
InstanceExpanderValue: w.InstanceExpander,
Plugins: w.Context.plugins,
MoveResultsValue: w.MoveResults,
ResolvedImportsValue: w.ResolvedImports,
ProviderCache: w.providerCache,
ProviderInputConfig: w.Context.providerInputConfig,
ProviderLock: &w.providerLock,

View File

@ -215,7 +215,12 @@ func (n *NodeAbstractResource) RootReferences() []*addrs.Reference {
var root []*addrs.Reference
for _, importTarget := range n.importTargets {
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, importTarget.ID)
// References are only possible in import targets originating from an import block
if !importTarget.IsFromImportBlock() {
continue
}
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ID)
root = append(root, refs...)
}

View File

@ -47,10 +47,6 @@ 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 "tofu import" CLI command.
legacyImportMode bool
}
var (
@ -306,6 +302,33 @@ func (n *nodeExpandPlannableResource) expandResourceInstances(globalCtx EvalCont
func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext, addr addrs.AbsResource, instanceAddrs []addrs.AbsResourceInstance) (*Graph, error) {
var diags tfdiags.Diagnostics
var commandLineImportTargets []CommandLineImportTarget
var evaluatedConfigImportTargets []EvaluatedConfigImportTarget
// FIXME - Deal with cases of duplicate addresses
for _, importTarget := range n.importTargets {
if importTarget.IsFromImportCommandLine() {
commandLineImportTargets = append(commandLineImportTargets, *importTarget.CommandLineImportTarget)
} else {
importId, evalDiags := evaluateImportIdExpression(importTarget.Config.ID, ctx)
if evalDiags.HasErrors() {
return nil, evalDiags.Err()
}
evaluatedConfigImportTargets = append(evaluatedConfigImportTargets, EvaluatedConfigImportTarget{
Config: importTarget.Config,
ID: importId,
})
resolvedImports := ctx.ResolvedImports().imports
resolvedImports[ResolvedConfigImportsKey{
AddrStr: importTarget.Config.To.String(),
ID: importId,
}] = true
}
}
// Our graph transformers require access to the full state, so we'll
// temporarily lock it while we work on this.
state := ctx.State().Lock()
@ -315,27 +338,14 @@ func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext,
concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex {
var m *NodePlannableResourceInstance
// If we're in legacy import mode (the import CLI command), we only need
// If we're in the `tofu 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) {
// The import ID was supplied as a string on the command
// line and made into a synthetic HCL expression.
importId, diags := evaluateImportIdExpression(importTarget.ID, ctx)
if diags.HasErrors() {
// This should be impossible, because the import command
// arg parsing builds the synth expression from a
// non-null string.
panic(fmt.Sprintf("Invalid import id: %s. This is a bug in OpenTofu; please report it!", diags.Err()))
}
return &graphNodeImportState{
Addr: importTarget.Addr,
ID: importId,
ResolvedProvider: n.ResolvedProvider,
}
for _, c := range commandLineImportTargets {
if c.Addr.Equal(a.Addr) {
return &graphNodeImportState{
Addr: c.Addr,
ID: c.ID,
ResolvedProvider: n.ResolvedProvider,
}
}
}
@ -363,15 +373,13 @@ func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext,
forceReplace: n.forceReplace,
}
for _, importTarget := range n.importTargets {
if importTarget.Addr.Equal(a.Addr) {
for _, evaluatedConfigImportTarget := range evaluatedConfigImportTargets {
// TODO - Change this code once Config.To is not a static address, to actually evaluate it
if evaluatedConfigImportTarget.Config.To.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,
Config: importTarget.Config,
}
m.importTarget = evaluatedConfigImportTarget
break
}
}

View File

@ -52,7 +52,19 @@ type NodePlannableResourceInstance struct {
// importTarget, if populated, contains the information necessary to plan
// an import of this resource.
importTarget ImportTarget
importTarget EvaluatedConfigImportTarget
}
// EvaluatedConfigImportTarget is a target that we need to import. It's created when an import target originated from
// an import block, after everything regarding the configuration has been evaluated.
// At this point, the import target is of a single resource instance
type EvaluatedConfigImportTarget struct {
// Config is the original import block for this import. This might be null
// if the import did not originate in config.
Config *configs.Import
// ID is the string ID of the resource to import. This is resource-instance specific.
ID string
}
var (
@ -161,18 +173,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
}
}
importing := n.importTarget.ID != nil
var importId string
if importing {
var evalDiags tfdiags.Diagnostics
importId, evalDiags = evaluateImportIdExpression(n.importTarget.ID, ctx)
if evalDiags.HasErrors() {
diags = diags.Append(evalDiags)
return diags
}
}
importing := n.importTarget.ID != ""
if importing && n.Config == nil && len(n.generateConfigPath) == 0 {
// Then the user wrote an import target to a target that didn't exist.
@ -187,12 +188,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
// You can't generate config for a resource that is inside a
// module, so we will present a different error message for
// this case.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Import block target does not exist",
Detail: "The target for the given import block does not exist. The specified target is within a module, and must be defined as a resource within that module before anything can be imported.",
Subject: n.importTarget.Config.DeclRange.Ptr(),
})
diags = diags.Append(importResourceWithoutConfigDiags(n.Addr, n.importTarget.Config))
}
return diags
}
@ -200,7 +196,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
// 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 importing {
instanceRefreshState, diags = n.importState(ctx, addr, importId, provider, providerSchema)
instanceRefreshState, diags = n.importState(ctx, addr, n.importTarget.ID, provider, providerSchema)
} else {
var readDiags tfdiags.Diagnostics
instanceRefreshState, readDiags = n.readResourceInstanceState(ctx, addr)
@ -310,7 +306,7 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext)
}
if importing {
change.Importing = &plans.Importing{ID: importId}
change.Importing = &plans.Importing{ID: n.importTarget.ID}
}
// FIXME: here we udpate the change to reflect the reason for
@ -509,7 +505,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
}))
if imported[0].TypeName == "" {
diags = diags.Append(fmt.Errorf("import of %s didn't set type", n.importTarget.Addr.String()))
diags = diags.Append(fmt.Errorf("import of %s didn't set type", n.Addr.String()))
return nil, diags
}
@ -528,7 +524,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
// refresh
riNode := &NodeAbstractResourceInstance{
Addr: n.importTarget.Addr,
Addr: n.Addr,
NodeAbstractResource: NodeAbstractResource{
ResolvedProvider: n.ResolvedProvider,
},
@ -552,7 +548,7 @@ func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.
"is correct and that it is associated with the provider's "+
"configured region or endpoint, or use \"tofu apply\" to "+
"create a new remote object for this resource.",
n.importTarget.Addr,
n.Addr,
),
))
return instanceRefreshState, diags

View File

@ -9,6 +9,8 @@ import (
"fmt"
"log"
"github.com/hashicorp/hcl/v2"
"github.com/opentofu/opentofu/internal/addrs"
"github.com/opentofu/opentofu/internal/configs"
"github.com/opentofu/opentofu/internal/dag"
@ -104,7 +106,7 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config, ge
// Only include import targets that are targeting the current module.
var importTargets []*ImportTarget
for _, target := range t.importTargets {
if targetModule := target.Addr.Module.Module(); targetModule.Equal(config.Path) {
if targetModule := target.StaticAddr().Module; targetModule.Equal(config.Path) {
importTargets = append(importTargets, target)
}
}
@ -124,7 +126,7 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config, ge
var matchedIndices []int
for ix, i := range importTargets {
if target := i.Addr.ContainingResource().Config(); target.Equal(configAddr) {
if target := i.StaticAddr(); target.Equal(configAddr) {
// This import target has been claimed by an actual resource,
// let's make a note of this to remove it from the targets.
matchedIndices = append(matchedIndices, ix)
@ -173,25 +175,53 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config, ge
// TODO: We could actually catch and process these kind of problems earlier,
// this is something that could be done during the Validate process.
for _, i := range importTargets {
// The case in which an unmatched import block targets an expanded
// resource instance can error here. Others can error later.
if i.Addr.Resource.Key != addrs.NoKey {
return fmt.Errorf("Config generation for count and for_each resources not supported.\n\nYour configuration contains an import block with a \"to\" address of %s. This resource instance does not exist in configuration.\n\nIf you intended to target a resource that exists in configuration, please double-check the address. Otherwise, please remove this import block or re-run the plan without the -generate-config-out flag to ignore the import block.", i.Addr)
// We should only allow config generation for static addresses
// If config generation has been attempted for a non static address - we will fail here
address, evaluationDiags := i.ResolvedAddr()
if evaluationDiags.HasErrors() {
return evaluationDiags
}
abstract := &NodeAbstractResource{
Addr: i.Addr.ConfigResource(),
importTargets: []*ImportTarget{i},
generateConfigPath: generateConfigPath,
}
// In case of config generation - We can error early here in two cases:
// 1. When attempting to import a resource with a key (Config generation for count / for_each resources)
// 2. When attempting to import a resource inside a module.
if len(generateConfigPath) > 0 {
if address.Resource.Key != addrs.NoKey {
return fmt.Errorf("Config generation for count and for_each resources not supported.\n\nYour configuration contains an import block with a \"to\" address of %s. This resource instance does not exist in configuration.\n\nIf you intended to target a resource that exists in configuration, please double-check the address. Otherwise, please remove this import block or re-run the plan without the -generate-config-out flag to ignore the import block.", address)
}
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
// Create a node with the resource and import target. This node will take care of the config generation
abstract := &NodeAbstractResource{
Addr: address.ConfigResource(),
importTargets: []*ImportTarget{i},
generateConfigPath: generateConfigPath,
}
g.Add(node)
var node dag.Vertex = abstract
if f := t.Concrete; f != nil {
node = f(abstract)
}
g.Add(node)
} else {
return importResourceWithoutConfigDiags(address, i.Config)
}
}
return nil
}
// importResourceWithoutConfigDiags creates the common HCL error of an attempted import for a non-existent configuration
func importResourceWithoutConfigDiags(address addrs.AbsResourceInstance, config *configs.Import) *hcl.Diagnostic {
diag := hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Configuration for import target does not exist",
Detail: fmt.Sprintf("The configuration for the given import %s does not exist. All target instances must have an associated configuration to be imported.", address),
}
if config != nil {
diag.Subject = config.DeclRange.Ptr()
}
return &diag
}