From 044a9ad3a7be992dd64ce7f87322bb9129248661 Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 7 Apr 2026 13:04:22 -0400 Subject: [PATCH] feat(import): support import blocks inside child modules --- internal/configs/config_build.go | 9 - internal/configs/config_build_test.go | 8 +- internal/configs/config_test.go | 3 +- internal/configs/module.go | 1 - internal/configs/module_test.go | 14 +- internal/configs/parser_config_dir.go | 6 +- internal/configs/parser_test.go | 4 +- .../import-in-child-module/child/main.tf | 6 - .../import-in-child-module/errors | 1 - .../import-in-child-module/root.tf | 10 - .../import-blocks-in-module/child/main.tf | 15 ++ .../import-blocks-in-module/main.tf | 3 + internal/refactoring/import_statement.go | 60 ++++++ .../terraform/context_apply_import_test.go | 158 +++++++++++++++ internal/terraform/context_import.go | 9 +- internal/terraform/context_plan.go | 7 +- internal/terraform/node_resource_plan.go | 188 +++++++++--------- .../child/main.tf | 6 + .../main.tf | 20 ++ .../import-block-in-module/child/main.tf | 7 + .../testdata/import-block-in-module/main.tf | 3 + .../child/kinder/main.tf | 6 + .../child/main.tf | 3 + .../import-block-in-nested-module/main.tf | 3 + internal/terraform/transform_config.go | 6 +- 25 files changed, 422 insertions(+), 134 deletions(-) delete mode 100644 internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf delete mode 100644 internal/configs/testdata/config-diagnostics/import-in-child-module/errors delete mode 100644 internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf create mode 100644 internal/configs/testdata/valid-modules/import-blocks-in-module/child/main.tf create mode 100644 internal/configs/testdata/valid-modules/import-blocks-in-module/main.tf create mode 100644 internal/refactoring/import_statement.go create mode 100644 internal/terraform/context_apply_import_test.go create mode 100644 internal/terraform/testdata/import-block-in-module-with-expansion/child/main.tf create mode 100644 internal/terraform/testdata/import-block-in-module-with-expansion/main.tf create mode 100644 internal/terraform/testdata/import-block-in-module/child/main.tf create mode 100644 internal/terraform/testdata/import-block-in-module/main.tf create mode 100644 internal/terraform/testdata/import-block-in-nested-module/child/kinder/main.tf create mode 100644 internal/terraform/testdata/import-block-in-nested-module/child/main.tf create mode 100644 internal/terraform/testdata/import-block-in-nested-module/main.tf diff --git a/internal/configs/config_build.go b/internal/configs/config_build.go index 84abf6df3b..38bb739f98 100644 --- a/internal/configs/config_build.go +++ b/internal/configs/config_build.go @@ -362,15 +362,6 @@ func loadModule(root *Config, req *ModuleRequest, walker ModuleWalker) (*Config, }) } - if len(mod.Import) > 0 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid import configuration", - Detail: fmt.Sprintf("An import block was detected in %q. Import blocks are only allowed in the root module.", cfg.Path), - Subject: mod.Import[0].DeclRange.Ptr(), - }) - } - if len(mod.ListResources) > 0 { first := slices.Collect(maps.Values(mod.ListResources))[0] diags = diags.Append(&hcl.Diagnostic{ diff --git a/internal/configs/config_build_test.go b/internal/configs/config_build_test.go index b9d1977ab5..4ac5786858 100644 --- a/internal/configs/config_build_test.go +++ b/internal/configs/config_build_test.go @@ -5,7 +5,7 @@ package configs import ( "fmt" - "io/ioutil" + "os" "path" "path/filepath" "reflect" @@ -214,7 +214,7 @@ func TestBuildConfigChildModule_CloudBlock(t *testing.T) { func TestBuildConfigInvalidModules(t *testing.T) { testDir := "testdata/config-diagnostics" - dirs, err := ioutil.ReadDir(testDir) + dirs, err := os.ReadDir(testDir) if err != nil { t.Fatal(err) } @@ -261,8 +261,8 @@ func TestBuildConfigInvalidModules(t *testing.T) { // expected location in the source, but is not required. // The literal characters `\n` are replaced with newlines, but // otherwise the string is unchanged. - expectedErrs := readDiags(ioutil.ReadFile(filepath.Join(testDir, name, "errors"))) - expectedWarnings := readDiags(ioutil.ReadFile(filepath.Join(testDir, name, "warnings"))) + expectedErrs := readDiags(os.ReadFile(filepath.Join(testDir, name, "errors"))) + expectedWarnings := readDiags(os.ReadFile(filepath.Join(testDir, name, "warnings"))) _, buildDiags := BuildConfig(mod, ModuleWalkerFunc( func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { diff --git a/internal/configs/config_test.go b/internal/configs/config_test.go index 3eeacc9d73..6a60d00640 100644 --- a/internal/configs/config_test.go +++ b/internal/configs/config_test.go @@ -18,7 +18,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders/providerreqs" - _ "github.com/hashicorp/terraform/internal/logging" ) @@ -29,7 +28,7 @@ func TestConfigProviderTypes(t *testing.T) { t.Fatal("expected empty result from empty config") } - cfg, diags := testModuleFromFileWithExperiments("testdata/valid-files/providers-explicit-implied.tf") + cfg, diags := testModuleCfgFromFileWithExperiments("testdata/valid-files/providers-explicit-implied.tf") if diags.HasErrors() { t.Fatal(diags.Error()) } diff --git a/internal/configs/module.go b/internal/configs/module.go index 5437360713..c18e2014cd 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -10,7 +10,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" - tfversion "github.com/hashicorp/terraform/version" ) diff --git a/internal/configs/module_test.go b/internal/configs/module_test.go index 89d42556fc..97653c5331 100644 --- a/internal/configs/module_test.go +++ b/internal/configs/module_test.go @@ -8,8 +8,9 @@ import ( "testing" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/addrs" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" ) // TestNewModule_provider_fqns exercises module.gatherProviderLocalNames() @@ -681,3 +682,14 @@ func TestModule_state_store_multiple(t *testing.T) { } }) } + +func TestModule_nested_import_blocks(t *testing.T) { + m, diags := testNestedModuleConfigFromDir(t, "testdata/valid-modules/import-blocks-in-module") + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + + if len(m.Children["child"].Module.Import) != 2 { + t.Fatal("child module is missing nested import blocks") + } +} diff --git a/internal/configs/parser_config_dir.go b/internal/configs/parser_config_dir.go index 8f4fc607d5..a0f7ae2349 100644 --- a/internal/configs/parser_config_dir.go +++ b/internal/configs/parser_config_dir.go @@ -71,7 +71,7 @@ func (p *Parser) LoadConfigDir(path string, opts ...Option) (*Module, hcl.Diagno } // Check if we need to load query files if len(fileSet.Queries) > 0 { - queryFiles, fDiags := p.loadQueryFiles(path, fileSet.Queries) + queryFiles, fDiags := p.loadQueryFiles(fileSet.Queries) diags = append(diags, fDiags...) if mod != nil { for _, qf := range queryFiles { @@ -151,7 +151,7 @@ func (p Parser) ConfigDirFiles(dir string, opts ...Option) (primary, override [] // IsConfigDir determines whether the given path refers to a directory that // exists and contains at least one Terraform config file (with a .tf or -// .tf.json extension.). Note, we explicitely exclude checking for tests here +// .tf.json extension.). Note, we explicitly exclude checking for tests here // as tests must live alongside actual .tf config files. Same goes for query files. func (p *Parser) IsConfigDir(path string, opts ...Option) bool { pathSet, _ := p.dirFileSet(path, opts...) @@ -205,7 +205,7 @@ func (p *Parser) loadTestFiles(basePath string, paths []string) (map[string]*Tes return tfs, diags } -func (p *Parser) loadQueryFiles(basePath string, paths []string) ([]*QueryFile, hcl.Diagnostics) { +func (p *Parser) loadQueryFiles(paths []string) ([]*QueryFile, hcl.Diagnostics) { files := make([]*QueryFile, 0, len(paths)) var diags hcl.Diagnostics diff --git a/internal/configs/parser_test.go b/internal/configs/parser_test.go index ec640e34c1..d76753aa62 100644 --- a/internal/configs/parser_test.go +++ b/internal/configs/parser_test.go @@ -52,9 +52,9 @@ func testModuleConfigFromFile(filename string) (*Config, hcl.Diagnostics) { return cfg, append(diags, moreDiags...) } -// testModuleFromFileWithExperiments File reads a single file from the given path as a +// testModuleCfgFromFileWithExperiments File reads a single file from the given path as a // module and returns its configuration. This is a helper for use in unit tests. -func testModuleFromFileWithExperiments(filename string) (*Config, hcl.Diagnostics) { +func testModuleCfgFromFileWithExperiments(filename string) (*Config, hcl.Diagnostics) { parser := NewParser(nil) parser.AllowLanguageExperiments(true) f, diags := parser.LoadConfigFile(filename) diff --git a/internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf b/internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf deleted file mode 100644 index bb8cb139d1..0000000000 --- a/internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "aws_instance" "web" {} - -import { - to = aws_instance.web - id = "test" -} \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/import-in-child-module/errors b/internal/configs/testdata/config-diagnostics/import-in-child-module/errors deleted file mode 100644 index b0a5ac4fc1..0000000000 --- a/internal/configs/testdata/config-diagnostics/import-in-child-module/errors +++ /dev/null @@ -1 +0,0 @@ -import-in-child-module/child/main.tf:3,1-7: Invalid import configuration; An import block was detected in "module.child". Import blocks are only allowed in the root module. \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf b/internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf deleted file mode 100644 index 3133e57b93..0000000000 --- a/internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf +++ /dev/null @@ -1,10 +0,0 @@ -resource "aws_instance" "web" {} - -import { - to = aws_instance.web - id = "test" -} - -module "child" { - source = "./child" -} \ No newline at end of file diff --git a/internal/configs/testdata/valid-modules/import-blocks-in-module/child/main.tf b/internal/configs/testdata/valid-modules/import-blocks-in-module/child/main.tf new file mode 100644 index 0000000000..f09339ca46 --- /dev/null +++ b/internal/configs/testdata/valid-modules/import-blocks-in-module/child/main.tf @@ -0,0 +1,15 @@ +provider "random" { + alias = "thisone" +} + +import { + to = random_string.test1 + provider = localname + id = "importlocalname" +} + +import { + to = random_string.test2 + provider = random.thisone + id = "importaliased" +} \ No newline at end of file diff --git a/internal/configs/testdata/valid-modules/import-blocks-in-module/main.tf b/internal/configs/testdata/valid-modules/import-blocks-in-module/main.tf new file mode 100644 index 0000000000..0f6991c536 --- /dev/null +++ b/internal/configs/testdata/valid-modules/import-blocks-in-module/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/internal/refactoring/import_statement.go b/internal/refactoring/import_statement.go new file mode 100644 index 0000000000..1ff08b4760 --- /dev/null +++ b/internal/refactoring/import_statement.go @@ -0,0 +1,60 @@ +package refactoring + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +type ImportStatement struct { + // AbsToResource is the original ImportConfig ToResource+ContainingModule + AbsToResource addrs.ConfigResource + ContainingModule addrs.Module + Import *configs.Import +} + +// FindImportStatements recurses through the modules of the given configuration +// and returns a set of all "import" blocks defined within after deduplication +// on the From address. +// +// An "import" block in a parent module overrides an import block in a child +// module when both target the same configuration object. +func FindImportStatements(rootCfg *configs.Config) addrs.Map[addrs.ConfigResource, ImportStatement] { + imports := findImportStatements(rootCfg, addrs.MakeMap[addrs.ConfigResource, ImportStatement]()) + return imports +} + +func findImportStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigResource, ImportStatement]) addrs.Map[addrs.ConfigResource, ImportStatement] { + for _, mi := range cfg.Module.Import { + // First, stitch together the module path and the RelSubject to form + // the absolute address of the config resource being removed. + res := mi.ToResource + toAddr := addrs.ConfigResource{ + Module: append(cfg.Path, res.Module...), + Resource: res.Resource, + } + + // If we already have an import statement for this ConfigResource, it + // must have come from a parent module, because duplicate import + // blocks in the same module result in an error. + // The import block in the parent module overrides the block in the + // child module. + existingResource, ok := into.GetOk(toAddr) + if ok { + if existingResource.AbsToResource.Equal(toAddr) { + continue + } + } + + into.Put(toAddr, ImportStatement{ + AbsToResource: toAddr, + ContainingModule: cfg.Path, + Import: mi, + }) + } + + for _, childCfg := range cfg.Children { + into = findImportStatements(childCfg, into) + } + + return into +} diff --git a/internal/terraform/context_apply_import_test.go b/internal/terraform/context_apply_import_test.go new file mode 100644 index 0000000000..3883a372cd --- /dev/null +++ b/internal/terraform/context_apply_import_test.go @@ -0,0 +1,158 @@ +package terraform + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// other import tests can be found in context_apply2_test.go +func TestContextApply_import_in_module(t *testing.T) { + m := testModule(t, "import-block-in-module") + + p := simpleMockProvider() + p.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("importable"), + }), + }, + }, + } + } + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("importable"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + if !p.ImportResourceStateCalled { + t.Fatal("resource not imported") + } + + rs := state.ResourceInstance(mustResourceInstanceAddr("module.child.test_object.bar")) + if rs == nil { + t.Fatal("imported resource not found in module") + } +} + +func TestContextApply_import_in_nested_module(t *testing.T) { // more nested than the test above. nesteder. + m := testModule(t, "import-block-in-nested-module") + + p := simpleMockProvider() + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("importable"), + }), + }, + }, + } + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("importable"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + rs := state.ResourceInstance(mustResourceInstanceAddr("module.child.module.kinder.test_object.bar")) + if rs == nil { + t.Fatal("imported resource not found in module") + } + + if !p.ImportResourceStateCalled { + t.Fatal("resources not imported") + } +} + +func TestContextApply_import_in_expanded_module(t *testing.T) { // count AND for each! + m := testModule(t, "import-block-in-module-with-expansion") + + p := simpleMockProvider() + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("importable"), + }), + }, + }, + } + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("importable"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + rs := state.ResourceInstance(mustResourceInstanceAddr("module.count_child[0].test_object.foo")) + if rs == nil { + t.Fatal("imported resource not found in module") + } + + rs = state.ResourceInstance(mustResourceInstanceAddr("module.count_child[1].test_object.foo")) + if rs == nil { + t.Fatal("imported resource not found in module") + } + + rs = state.ResourceInstance(mustResourceInstanceAddr("module.for_each_child[\"a\"].test_object.foo")) + if rs == nil { + t.Fatal("imported resource not found in module") + } + + if !p.ImportResourceStateCalled { + t.Fatal("resources not imported") + } +} diff --git a/internal/terraform/context_import.go b/internal/terraform/context_import.go index 79fa7c4657..ea2dfa559b 100644 --- a/internal/terraform/context_import.go +++ b/internal/terraform/context_import.go @@ -22,13 +22,18 @@ type ImportOpts struct { SetVariables InputValues } -// ImportTarget is a single resource to import, -// in legacy (CLI) import mode. +// ImportTarget is a single resource to import. 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 + // The RelModule contains the module that the original import block was + // configured in, while the Config.ToResource is relative to the module it was in. + // We re-evaluate the Config.To (hcl.Expression) during plan, so this needs to be stored. + RelModule addrs.Module + AbsToConfigResource addrs.ConfigResource + // LegacyAddr is the import address set from the command line arguments // when using the import command. LegacyAddr addrs.AbsResourceInstance diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index 2c99c9f600..3cdff82776 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -713,9 +713,12 @@ func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactor // config. func (c *Context) findImportTargets(config *configs.Config) []*ImportTarget { var importTargets []*ImportTarget - for _, ic := range config.Module.Import { + importStatements := refactoring.FindImportStatements(config) + for _, ic := range importStatements.Values() { importTargets = append(importTargets, &ImportTarget{ - Config: ic, + Config: ic.Import, + RelModule: ic.ContainingModule, + AbsToConfigResource: ic.AbsToResource, }) } return importTargets diff --git a/internal/terraform/node_resource_plan.go b/internal/terraform/node_resource_plan.go index 9815cb0cb1..f72f07900c 100644 --- a/internal/terraform/node_resource_plan.go +++ b/internal/terraform/node_resource_plan.go @@ -173,118 +173,128 @@ func (n *nodeExpandPlannableResource) expandResourceImports(ctx EvalContext, all continue } - if imp.Config.ForEach == nil { - traversal, hds := hcl.AbsTraversalForExpr(imp.Config.To) - diags = diags.Append(hds) - to, tds := addrs.ParseAbsResourceInstance(traversal) - diags = diags.Append(tds) - if diags.HasErrors() { - return knownImports, unknownImports, diags - } + // "to" here needs the containing resource + // but I don't know how to work that out at this point (with expansion) + // do I need to get all possible expansions for the imp.RelModule and then only use the one for this actual node? + allMods := ctx.InstanceExpander().AllInstances().InstancesForModule(imp.RelModule, false) + for _, mod := range allMods { + if imp.Config.ForEach == nil { + traversal, hds := hcl.AbsTraversalForExpr(imp.Config.To) + diags = diags.Append(hds) + to, tds := addrs.ParseAbsResourceInstance(traversal) + diags = diags.Append(tds) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + // add the module that the import block was configured in to the resource addr + to.Module = append(mod, to.Module...) + + diags = diags.Append(validateImportTargetExpansion(n.Config, to, imp.Config.To)) + + var importID cty.Value + var evalDiags tfdiags.Diagnostics + if imp.Config.ID != nil { + importID, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, EvalDataForNoInstanceKey, allowUnknown) + } else if imp.Config.Identity != nil { + providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) + if err != nil { + diags = diags.Append(err) + return knownImports, unknownImports, diags + } + schema := providerSchema.SchemaForResourceAddr(to.Resource.Resource) - diags = diags.Append(validateImportTargetExpansion(n.Config, to, imp.Config.To)) + importID, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, EvalDataForNoInstanceKey, allowUnknown) + } else { + // Should never happen + return knownImports, unknownImports, diags + } - var importID cty.Value - var evalDiags tfdiags.Diagnostics - if imp.Config.ID != nil { - importID, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, EvalDataForNoInstanceKey, allowUnknown) - } else if imp.Config.Identity != nil { - providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) - if err != nil { - diags = diags.Append(err) + diags = diags.Append(evalDiags) + if diags.HasErrors() { return knownImports, unknownImports, diags } - schema := providerSchema.SchemaForResourceAddr(to.Resource.Resource) - importID, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, EvalDataForNoInstanceKey, allowUnknown) - } else { - // Should never happen - return knownImports, unknownImports, diags - } + knownImports.Put(to, importID) - diags = diags.Append(evalDiags) - if diags.HasErrors() { - return knownImports, unknownImports, diags + log.Printf("[TRACE] expandResourceImports: found single import target %s", to) + continue } - knownImports.Put(to, importID) - - log.Printf("[TRACE] expandResourceImports: found single import target %s", to) - continue - } - - forEachData, known, forEachDiags := newForEachEvaluator(imp.Config.ForEach, ctx, allowUnknown).ImportValues() - diags = diags.Append(forEachDiags) - if forEachDiags.HasErrors() { - return knownImports, unknownImports, diags - } - - if !known { - // Then we need to parse the target address as a PartialResource - // instead of a known resource. - addr, evalDiags := evalImportUnknownToExpression(imp.Config.To) - diags = diags.Append(evalDiags) - if diags.HasErrors() { + forEachData, known, forEachDiags := newForEachEvaluator(imp.Config.ForEach, ctx, allowUnknown).ImportValues() + diags = diags.Append(forEachDiags) + if forEachDiags.HasErrors() { return knownImports, unknownImports, diags } - // We're going to work out which instances this import block might - // target actually already exist. - knownInstances := addrs.MakeSet[addrs.AbsResourceInstance]() - - cfg := addr.ConfigResource() - modInsts := state.ModuleInstances(cfg.Module) - for _, modInst := range modInsts { - abs := cfg.Absolute(modInst) - resource := state.Resource(cfg.Absolute(modInst)) - if resource == nil { - // Then we are creating every instance of this resource. - continue + if !known { + // Then we need to parse the target address as a PartialResource + // instead of a known resource. + addr, evalDiags := evalImportUnknownToExpression(imp.Config.To) + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags } - for inst := range resource.Instances { - knownInstances.Add(abs.Instance(inst)) - } - } + // We're going to work out which instances this import block might + // target actually already exist. + knownInstances := addrs.MakeSet[addrs.AbsResourceInstance]() + + cfg := addr.ConfigResource() + modInsts := state.ModuleInstances(append(imp.RelModule, cfg.Module...)) + for _, modInst := range modInsts { + abs := cfg.Absolute(modInst) + resource := state.Resource(cfg.Absolute(modInst)) + if resource == nil { + // Then we are creating every instance of this resource. + continue + } - unknownImports.Put(addr, knownInstances) - continue - } + for inst := range resource.Instances { + knownInstances.Add(abs.Instance(inst)) + } + } - for _, keyData := range forEachData { - var evalDiags tfdiags.Diagnostics - res, evalDiags := evalImportToExpression(imp.Config.To, keyData) - diags = diags.Append(evalDiags) - if diags.HasErrors() { - return knownImports, unknownImports, diags + unknownImports.Put(addr, knownInstances) + continue } - diags = diags.Append(validateImportTargetExpansion(n.Config, res, imp.Config.To)) + for _, keyData := range forEachData { + var evalDiags tfdiags.Diagnostics + res, evalDiags := evalImportToExpression(imp.Config.To, keyData) + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + // add the module that the import block was configured in to the resource addr + res.Module = append(mod, res.Module...) + + diags = diags.Append(validateImportTargetExpansion(n.Config, res, imp.Config.To)) + + var importID cty.Value + if imp.Config.ID != nil { + importID, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, keyData, allowUnknown) + } else if imp.Config.Identity != nil { + providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) + if err != nil { + diags = diags.Append(err) + return knownImports, unknownImports, diags + } + schema := providerSchema.SchemaForResourceAddr(res.Resource.Resource) - var importID cty.Value - if imp.Config.ID != nil { - importID, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, keyData, allowUnknown) - } else if imp.Config.Identity != nil { - providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) - if err != nil { - diags = diags.Append(err) + importID, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, keyData, allowUnknown) + } else { + // Should never happen return knownImports, unknownImports, diags } - schema := providerSchema.SchemaForResourceAddr(res.Resource.Resource) - importID, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, keyData, allowUnknown) - } else { - // Should never happen - return knownImports, unknownImports, diags - } + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } - diags = diags.Append(evalDiags) - if diags.HasErrors() { - return knownImports, unknownImports, diags + knownImports.Put(res, importID) + log.Printf("[TRACE] expandResourceImports: expanded import target %s", res) } - - knownImports.Put(res, importID) - log.Printf("[TRACE] expandResourceImports: expanded import target %s", res) } } diff --git a/internal/terraform/testdata/import-block-in-module-with-expansion/child/main.tf b/internal/terraform/testdata/import-block-in-module-with-expansion/child/main.tf new file mode 100644 index 0000000000..359c411147 --- /dev/null +++ b/internal/terraform/testdata/import-block-in-module-with-expansion/child/main.tf @@ -0,0 +1,6 @@ +resource "test_object" "foo" {} + +import { + to = test_object.foo + id = "import" +} \ No newline at end of file diff --git a/internal/terraform/testdata/import-block-in-module-with-expansion/main.tf b/internal/terraform/testdata/import-block-in-module-with-expansion/main.tf new file mode 100644 index 0000000000..9faa8ede4f --- /dev/null +++ b/internal/terraform/testdata/import-block-in-module-with-expansion/main.tf @@ -0,0 +1,20 @@ +locals { + val = 2 + m = { + "a" = "b" + } +} + +module "count_child" { + count = local.val + source = "./child" +} + +module "for_each_child" { + for_each = test_object.foo + source = "./child" +} + +resource "test_object" "foo" { + for_each = local.m +} \ No newline at end of file diff --git a/internal/terraform/testdata/import-block-in-module/child/main.tf b/internal/terraform/testdata/import-block-in-module/child/main.tf new file mode 100644 index 0000000000..8712143314 --- /dev/null +++ b/internal/terraform/testdata/import-block-in-module/child/main.tf @@ -0,0 +1,7 @@ +import { + to = test_object.bar + id = "importable" +} + +resource "test_object" "bar" { +} \ No newline at end of file diff --git a/internal/terraform/testdata/import-block-in-module/main.tf b/internal/terraform/testdata/import-block-in-module/main.tf new file mode 100644 index 0000000000..75ea1f31e9 --- /dev/null +++ b/internal/terraform/testdata/import-block-in-module/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} \ No newline at end of file diff --git a/internal/terraform/testdata/import-block-in-nested-module/child/kinder/main.tf b/internal/terraform/testdata/import-block-in-nested-module/child/kinder/main.tf new file mode 100644 index 0000000000..90d51789fd --- /dev/null +++ b/internal/terraform/testdata/import-block-in-nested-module/child/kinder/main.tf @@ -0,0 +1,6 @@ +import { + to = test_object.bar + id = "importable" +} + +resource "test_object" "bar" {} \ No newline at end of file diff --git a/internal/terraform/testdata/import-block-in-nested-module/child/main.tf b/internal/terraform/testdata/import-block-in-nested-module/child/main.tf new file mode 100644 index 0000000000..e1cc305c56 --- /dev/null +++ b/internal/terraform/testdata/import-block-in-nested-module/child/main.tf @@ -0,0 +1,3 @@ +module "kinder" { + source = "./kinder" +} \ No newline at end of file diff --git a/internal/terraform/testdata/import-block-in-nested-module/main.tf b/internal/terraform/testdata/import-block-in-nested-module/main.tf new file mode 100644 index 0000000000..75ea1f31e9 --- /dev/null +++ b/internal/terraform/testdata/import-block-in-nested-module/main.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} \ No newline at end of file diff --git a/internal/terraform/transform_config.go b/internal/terraform/transform_config.go index 454cbf60e8..4cf101c1ea 100644 --- a/internal/terraform/transform_config.go +++ b/internal/terraform/transform_config.go @@ -123,7 +123,9 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er importTargets = append(importTargets, target) } default: - if target.Config.ToResource.Module.Equal(config.Path) { + // target.AbsToAddr is the absolute config resource, target.Config.ToResource is + // relative to the module of the import block + if target.AbsToConfigResource.Module.Equal(config.Path) { importTargets = append(importTargets, target) } } @@ -216,7 +218,7 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er imports = append(imports, i) } - if i.Config != nil && i.Config.ToResource.Equal(configAddr) { + if i.Config != nil && i.AbsToConfigResource.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)