feat(import): support import blocks inside child modules

Kristin Laemmert 2 weeks ago
parent e3d2b7de8b
commit 7494492fd5

@ -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{

@ -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) {

@ -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())
}

@ -10,7 +10,6 @@ import (
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/experiments"
tfversion "github.com/hashicorp/terraform/version"
)

@ -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")
}
}

@ -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

@ -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)

@ -1,6 +0,0 @@
resource "aws_instance" "web" {}
import {
to = aws_instance.web
id = "test"
}

@ -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.

@ -1,10 +0,0 @@
resource "aws_instance" "web" {}
import {
to = aws_instance.web
id = "test"
}
module "child" {
source = "./child"
}

@ -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"
}

@ -0,0 +1,3 @@
module "child" {
source = "./child"
}

@ -0,0 +1,118 @@
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-in-module")
p := testProvider("aws")
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "aws_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("importable"),
}),
},
},
}
p.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
return providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "aws_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("importable"),
}),
},
},
}
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): 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.aws_instance.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-in-nested-module")
p := testProvider("aws")
p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "aws_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("importable"),
}),
},
},
}
p.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
return providers.ImportResourceStateResponse{
ImportedResources: []providers.ImportedResource{
{
TypeName: "aws_instance",
State: cty.ObjectVal(map[string]cty.Value{
"id": cty.StringVal("importable"),
}),
},
},
}
}
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
return providers.PlanResourceChangeResponse{
PlannedState: req.ProposedNewState,
}
}
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("aws"): 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.aws_instance.bar"))
if rs == nil {
t.Fatal("imported resource not found in module")
}
}

@ -0,0 +1,6 @@
import {
to = aws_instance.bar
id = "importable"
}
resource "aws_instance" "bar" {}

@ -0,0 +1,3 @@
module "child" {
source = "./child"
}

@ -0,0 +1,6 @@
import {
to = aws_instance.bar
id = "importable"
}
resource "aws_instance" "bar" {}

@ -0,0 +1,3 @@
module "kinder" {
source = "./kinder"
}

@ -0,0 +1,3 @@
module "child" {
source = "./child"
}
Loading…
Cancel
Save