diff --git a/cmd/inspect.go b/cmd/inspect.go index 1fa1a298c..b070413bd 100644 --- a/cmd/inspect.go +++ b/cmd/inspect.go @@ -211,7 +211,7 @@ By setting TFLINT_LOG=trace, you can confirm the changes made by the autofix and } func (cli *CLI) setupRunners(opts Options, dir string) ([]*tflint.Runner, error) { - configs, diags := cli.loader.LoadConfig(dir, cli.config.Module) + configs, diags := cli.loader.LoadConfig(dir, cli.config.CallModuleType) if diags.HasErrors() { return []*tflint.Runner{}, fmt.Errorf("Failed to load configurations; %w", diags) } diff --git a/cmd/option.go b/cmd/option.go index 620eb92ef..c41484c00 100644 --- a/cmd/option.go +++ b/cmd/option.go @@ -4,6 +4,7 @@ import ( "log" "strings" + "github.com/terraform-linters/tflint/terraform" "github.com/terraform-linters/tflint/tflint" ) @@ -21,8 +22,9 @@ type Options struct { EnablePlugins []string `long:"enable-plugin" description:"Enable plugins from the command line" value-name:"PLUGIN_NAME"` Varfiles []string `long:"var-file" description:"Terraform variable file name" value-name:"FILE"` Variables []string `long:"var" description:"Set a Terraform variable" value-name:"'foo=bar'"` - Module *bool `long:"module" description:"Enable module inspection"` - NoModule *bool `long:"no-module" description:"Disable module inspection"` + Module *bool `long:"module" description:"Enable module inspection" hidden:"true"` + NoModule *bool `long:"no-module" description:"Disable module inspection" hidden:"true"` + CallModuleType *string `long:"call-module-type" description:"Types of module to call (default: local)" choice:"all" choice:"local" choice:"none"` Chdir string `long:"chdir" description:"Switch to a different working directory before executing the command" value-name:"DIR"` Recursive bool `long:"recursive" description:"Run command in each directory recursively"` Filter []string `long:"filter" description:"Filter issues by file names or globs" value-name:"FILE"` @@ -52,14 +54,25 @@ func (opts *Options) toConfig() *tflint.Config { opts.Variables = []string{} } - var module, moduleSet bool + callModuleType := terraform.CallLocalModule + callModuleTypeSet := false + // --call-module-type takes precedence over --module/--no-module. This is for backward compatibility. if opts.Module != nil { - module = *opts.Module - moduleSet = true + callModuleType = terraform.CallAllModule + callModuleTypeSet = true } if opts.NoModule != nil { - module = !*opts.NoModule - moduleSet = true + callModuleType = terraform.CallNoModule + callModuleTypeSet = true + } + if opts.CallModuleType != nil { + var err error + callModuleType, err = terraform.AsCallModuleType(*opts.CallModuleType) + if err != nil { + // This should never happen because the option is already validated by go-flags + panic(err) + } + callModuleTypeSet = true } var force, forceSet bool @@ -69,7 +82,7 @@ func (opts *Options) toConfig() *tflint.Config { } log.Printf("[DEBUG] CLI Options") - log.Printf("[DEBUG] Module: %t", module) + log.Printf("[DEBUG] CallModuleType: %s", callModuleType) log.Printf("[DEBUG] Force: %t", force) log.Printf("[DEBUG] Format: %s", opts.Format) log.Printf("[DEBUG] Varfiles: %s", strings.Join(opts.Varfiles, ", ")) @@ -113,8 +126,8 @@ func (opts *Options) toConfig() *tflint.Config { } return &tflint.Config{ - Module: module, - ModuleSet: moduleSet, + CallModuleType: callModuleType, + CallModuleTypeSet: callModuleTypeSet, Force: force, ForceSet: forceSet, diff --git a/cmd/option_test.go b/cmd/option_test.go index 55f2467de..7a5429f20 100644 --- a/cmd/option_test.go +++ b/cmd/option_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" flags "github.com/jessevdk/go-flags" + "github.com/terraform-linters/tflint/terraform" "github.com/terraform-linters/tflint/tflint" ) @@ -21,12 +22,27 @@ func Test_toConfig(t *testing.T) { Command: "./tflint", Expected: tflint.EmptyConfig(), }, + { + Name: "--call-module-type", + Command: "./tflint --call-module-type all", + Expected: &tflint.Config{ + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: false, + IgnoreModules: map[string]bool{}, + Varfiles: []string{}, + Variables: []string{}, + DisabledByDefault: false, + Rules: map[string]*tflint.RuleConfig{}, + Plugins: map[string]*tflint.PluginConfig{}, + }, + }, { Name: "--module", Command: "./tflint --module", Expected: &tflint.Config{ - Module: true, - ModuleSet: true, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -40,8 +56,23 @@ func Test_toConfig(t *testing.T) { Name: "--no-module", Command: "./tflint --no-module", Expected: &tflint.Config{ - Module: false, - ModuleSet: true, + CallModuleType: terraform.CallNoModule, + CallModuleTypeSet: true, + Force: false, + IgnoreModules: map[string]bool{}, + Varfiles: []string{}, + Variables: []string{}, + DisabledByDefault: false, + Rules: map[string]*tflint.RuleConfig{}, + Plugins: map[string]*tflint.PluginConfig{}, + }, + }, + { + Name: "--module and --call-module-type", + Command: "./tflint --module --call-module-type none", + Expected: &tflint.Config{ + CallModuleType: terraform.CallNoModule, + CallModuleTypeSet: true, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -55,7 +86,7 @@ func Test_toConfig(t *testing.T) { Name: "--force", Command: "./tflint --force", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: true, ForceSet: true, IgnoreModules: map[string]bool{}, @@ -70,7 +101,7 @@ func Test_toConfig(t *testing.T) { Name: "--ignore-module", Command: "./tflint --ignore-module module1,module2", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{"module1": true, "module2": true}, Varfiles: []string{}, @@ -84,7 +115,7 @@ func Test_toConfig(t *testing.T) { Name: "multiple `--ignore-module`", Command: "./tflint --ignore-module module1 --ignore-module module2", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{"module1": true, "module2": true}, Varfiles: []string{}, @@ -98,7 +129,7 @@ func Test_toConfig(t *testing.T) { Name: "--var-file", Command: "./tflint --var-file example1.tfvars,example2.tfvars", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{"example1.tfvars", "example2.tfvars"}, @@ -112,7 +143,7 @@ func Test_toConfig(t *testing.T) { Name: "multiple `--var-file`", Command: "./tflint --var-file example1.tfvars --var-file example2.tfvars", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{"example1.tfvars", "example2.tfvars"}, @@ -126,7 +157,7 @@ func Test_toConfig(t *testing.T) { Name: "--var", Command: "./tflint --var foo=bar --var bar=baz", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -140,7 +171,7 @@ func Test_toConfig(t *testing.T) { Name: "--enable-rule", Command: "./tflint --enable-rule aws_instance_invalid_type --enable-rule aws_instance_previous_type", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -165,7 +196,7 @@ func Test_toConfig(t *testing.T) { Name: "--disable-rule", Command: "./tflint --disable-rule aws_instance_invalid_type --disable-rule aws_instance_previous_type", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -190,7 +221,7 @@ func Test_toConfig(t *testing.T) { Name: "--only", Command: "./tflint --only aws_instance_invalid_type", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -212,7 +243,7 @@ func Test_toConfig(t *testing.T) { Name: "--enable-plugin", Command: "./tflint --enable-plugin test --enable-plugin another-test", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -237,7 +268,7 @@ func Test_toConfig(t *testing.T) { Name: "--format", Command: "./tflint --format compact", Expected: &tflint.Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, diff --git a/integrationtest/inspection/without_module_init/module.tf b/integrationtest/inspection/without_module_init/module.tf index 527926ade..fefdc2323 100644 --- a/integrationtest/inspection/without_module_init/module.tf +++ b/integrationtest/inspection/without_module_init/module.tf @@ -6,7 +6,7 @@ variable "instance_type" { // terraform init did not run module "instances" { - source = "./module" + source = "example/instances" unknown = var.unknown instance_type = var.instance_type diff --git a/langserver/handler.go b/langserver/handler.go index 89159bdf7..0ff81e31b 100644 --- a/langserver/handler.go +++ b/langserver/handler.go @@ -168,7 +168,7 @@ func (h *handler) inspect() (map[string][]lsp.Diagnostic, error) { return ret, fmt.Errorf("Failed to prepare loading: %w", err) } - configs, diags := loader.LoadConfig(".", h.config.Module) + configs, diags := loader.LoadConfig(".", h.config.CallModuleType) if diags.HasErrors() { return ret, fmt.Errorf("Failed to load configurations: %w", diags) } diff --git a/terraform/addrs/module_source.go b/terraform/addrs/module_source.go new file mode 100644 index 000000000..fc4c87d9c --- /dev/null +++ b/terraform/addrs/module_source.go @@ -0,0 +1,149 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package addrs + +import ( + "path" + "strings" +) + +// ModuleSource is the general type for all three of the possible module source +// address types. The concrete implementations of this are ModuleSourceLocal +// and ModuleSourceRemote. +type ModuleSource interface { + // String returns a full representation of the address, including any + // additional components that are typically implied by omission in + // user-written addresses. + // + // We typically use this longer representation in error message, in case + // the inclusion of normally-omitted components is helpful in debugging + // unexpected behavior. + String() string + + moduleSource() +} + +var _ ModuleSource = ModuleSourceLocal("") +var _ ModuleSource = ModuleSourceRemote("") + +var moduleSourceLocalPrefixes = []string{ + "./", + "../", + ".\\", + "..\\", +} + +// ParseModuleSource parses a module source address as given in the "source" +// argument inside a "module" block in the configuration. +// +// Unlike Terraform, this function only categorizes sources into "local" and "remote". +func ParseModuleSource(raw string) (ModuleSource, error) { + if isModuleSourceLocal(raw) { + localAddr, err := parseModuleSourceLocal(raw) + if err != nil { + // This is to make sure we really return a nil ModuleSource in + // this case, rather than an interface containing the zero + // value of ModuleSourceLocal. + return nil, err + } + return localAddr, nil + } + + // Return all non-local sources assuming they are remote source. + // Note that this is essentially useless for determining anything more + // than "non-local". + return ModuleSourceRemote(raw), nil +} + +// ModuleSourceLocal is a ModuleSource representing a local path reference +// from the caller's directory to the callee's directory within the same +// module package. +// +// A "module package" here means a set of modules distributed together in +// the same archive, repository, or similar. That's a significant distinction +// because we always download and cache entire module packages at once, +// and then create relative references within the same directory in order +// to ensure all modules in the package are looking at a consistent filesystem +// layout. We also assume that modules within a package are maintained together, +// which means that cross-cutting maintenence across all of them would be +// possible. +// +// The actual value of a ModuleSourceLocal is a normalized relative path using +// forward slashes, even on operating systems that have other conventions, +// because we're representing traversal within the logical filesystem +// represented by the containing package, not actually within the physical +// filesystem we unpacked the package into. We should typically not construct +// ModuleSourceLocal values directly, except in tests where we can ensure +// the value meets our assumptions. Use ParseModuleSource instead if the +// input string is not hard-coded in the program. +type ModuleSourceLocal string + +func parseModuleSourceLocal(raw string) (ModuleSourceLocal, error) { + // As long as we have a suitable prefix (detected by ParseModuleSource) + // there is no failure case for local paths: we just use the "path" + // package's cleaning logic to remove any redundant "./" and "../" + // sequences and any duplicate slashes and accept whatever that + // produces. + + // Although using backslashes (Windows-style) is non-idiomatic, we do + // allow it and just normalize it away, so the rest of Terraform will + // only see the forward-slash form. + if strings.Contains(raw, `\`) { + // Note: We use string replacement rather than filepath.ToSlash + // here because the filepath package behavior varies by current + // platform, but we want to interpret configured paths the same + // across all platforms: these are virtual paths within a module + // package, not physical filesystem paths. + raw = strings.ReplaceAll(raw, `\`, "/") + } + + // Note that we could've historically blocked using "//" in a path here + // in order to avoid confusion with the subdir syntax in remote addresses, + // but we historically just treated that as the same as a single slash + // and so we continue to do that now for compatibility. Clean strips those + // out and reduces them to just a single slash. + clean := path.Clean(raw) + + // However, we do need to keep a single "./" on the front if it isn't + // a "../" path, or else it would be ambigous with the registry address + // syntax. + if !strings.HasPrefix(clean, "../") { + clean = "./" + clean + } + + return ModuleSourceLocal(clean), nil +} + +func isModuleSourceLocal(raw string) bool { + for _, prefix := range moduleSourceLocalPrefixes { + if strings.HasPrefix(raw, prefix) { + return true + } + } + return false +} + +func (s ModuleSourceLocal) moduleSource() {} + +func (s ModuleSourceLocal) String() string { + // We assume that our underlying string was already normalized at + // construction, so we just return it verbatim. + return string(s) +} + +// ModuleSourceRemote is a ModuleSource representing a remote location from +// which we can retrieve a module package. +// +// Note that unlike Terraform, this also includes the address of the +// ModuleSourceRegistry equivalent. TFLint does not need to distinguish +// between ModuleSourceRemote and ModuleSourceRegistry, +// so they are all treated as ModuleSourceRemote. +type ModuleSourceRemote string + +func (s ModuleSourceRemote) moduleSource() {} + +func (s ModuleSourceRemote) String() string { + // The remote source is not normalized and returns the input value as-is. + return string(s) +} diff --git a/terraform/addrs/module_source_test.go b/terraform/addrs/module_source_test.go new file mode 100644 index 000000000..1fa477055 --- /dev/null +++ b/terraform/addrs/module_source_test.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package addrs + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestParseModuleSource(t *testing.T) { + tests := map[string]struct { + input string + want ModuleSource + wantErr string + }{ + // Local paths + "local in subdirectory": { + input: "./child", + want: ModuleSourceLocal("./child"), + }, + "local in subdirectory non-normalized": { + input: "./nope/../child", + want: ModuleSourceLocal("./child"), + }, + "local in sibling directory": { + input: "../sibling", + want: ModuleSourceLocal("../sibling"), + }, + "local in sibling directory non-normalized": { + input: "./nope/../../sibling", + want: ModuleSourceLocal("../sibling"), + }, + "Windows-style local in subdirectory": { + input: `.\child`, + want: ModuleSourceLocal("./child"), + }, + "Windows-style local in subdirectory non-normalized": { + input: `.\nope\..\child`, + want: ModuleSourceLocal("./child"), + }, + "Windows-style local in sibling directory": { + input: `..\sibling`, + want: ModuleSourceLocal("../sibling"), + }, + "Windows-style local in sibling directory non-normalized": { + input: `.\nope\..\..\sibling`, + want: ModuleSourceLocal("../sibling"), + }, + "an abominable mix of different slashes": { + input: `./nope\nope/why\./please\don't`, + want: ModuleSourceLocal("./nope/nope/why/please/don't"), + }, + // Registry addresses + "main registry implied": { + input: "hashicorp/subnets/cidr", + want: ModuleSourceRemote("hashicorp/subnets/cidr"), + }, + // Remote package addresses + "github.com shorthand": { + input: "github.com/hashicorp/terraform-cidr-subnets", + want: ModuleSourceRemote("github.com/hashicorp/terraform-cidr-subnets"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + addr, err := ParseModuleSource(test.input) + + if test.wantErr != "" { + switch { + case err == nil: + t.Errorf("unexpected success\nwant error: %s", test.wantErr) + case err.Error() != test.wantErr: + t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + if diff := cmp.Diff(addr, test.want); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } + +} diff --git a/terraform/config.go b/terraform/config.go index 70e5a0bc1..ca7bb1aea 100644 --- a/terraform/config.go +++ b/terraform/config.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package terraform import ( + "fmt" "sort" "github.com/hashicorp/go-version" @@ -81,10 +85,22 @@ func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, copy(path, parent.Path) path[len(path)-1] = call.Name + // Return an error for nesting too deep to avoid infinite loops due to circular references. + if len(path) > 10 { + return ret, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module stack level too deep", + Detail: fmt.Sprintf("This configuration has nested modules more than 10 levels deep. This is mainly caused by circular references. current path: %s", parent.Path), + Subject: &call.DeclRange, + }) + } + req := ModuleRequest{ - Name: call.Name, - Path: path, - CallRange: call.DeclRange, + Name: call.Name, + Path: path, + SourceAddr: call.SourceAddr, + Parent: parent, + CallRange: call.DeclRange, } mod, _, modDiags := walker.LoadModule(&req) @@ -166,6 +182,17 @@ type ModuleRequest struct { // calls with the same name at different points in the tree. Path addrs.Module + // SourceAddr is the source address string provided by the user in + // configuration. + SourceAddr addrs.ModuleSource + + // Parent is the partially-constructed module tree node that the loaded + // module will be added to. Callers may refer to any field of this + // structure except Children, which is still under construction when + // ModuleRequest objects are created and thus has undefined content. + // The main reason this is provided is to build the full path for the module. + Parent *Config + // CallRange is the source range for the header of the "module" block // in configuration that prompted this request. This can be used as the // subject of an error diagnostic that relates to the module call itself. diff --git a/terraform/loader.go b/terraform/loader.go index b7dd85beb..70129fa52 100644 --- a/terraform/loader.go +++ b/terraform/loader.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" "github.com/spf13/afero" + "github.com/terraform-linters/tflint/terraform/addrs" ) // Loader is a fork of configload.Loader. The instance is the main entry-point @@ -65,23 +66,24 @@ func NewLoader(fs afero.Afero, originalWd string) (*Loader, error) { // LoadConfig reads the Terraform module in the given directory and uses it as the // root module to build the static module tree that represents a configuration. -// -// The second argument determines whether to load child modules. If true is given, -// load installed child modules according to a manifest file. If false is given, -// all child modules will not be loaded. -func (l *Loader) LoadConfig(dir string, module bool) (*Config, hcl.Diagnostics) { +func (l *Loader) LoadConfig(dir string, callModuleType CallModuleType) (*Config, hcl.Diagnostics) { mod, diags := l.parser.LoadConfigDir(l.baseDir, dir) if diags.HasErrors() { return nil, diags } var walker ModuleWalkerFunc - if module { - log.Print("[INFO] Module inspection is enabled. Building the root module with children...") - walker = ModuleWalkerFunc(l.moduleWalkerLoad) - } else { - log.Print("[INFO] Module inspection is disabled. Building the root module without children...") - walker = ModuleWalkerFunc(l.moduleWalkerIgnore) + switch callModuleType { + case CallAllModule: + log.Print("[INFO] Building the root module while calling child modules...") + walker = l.moduleWalkerFunc(true, true) + case CallLocalModule: + log.Print("[INFO] Building the root module while calling local child modules...") + walker = l.moduleWalkerFunc(true, false) + case CallNoModule: + walker = l.moduleWalkerFunc(false, false) + default: + panic(fmt.Sprintf("unexpected module call type: %d", callModuleType)) } cfg, diags := BuildConfig(mod, walker) @@ -91,35 +93,56 @@ func (l *Loader) LoadConfig(dir string, module bool) (*Config, hcl.Diagnostics) return cfg, nil } -func (l *Loader) moduleWalkerLoad(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { - // Since we're just loading here, we expect that all referenced modules - // will be already installed and described in our manifest. However, we - // do verify that the manifest and the configuration are in agreement - // so that we can prompt the user to run "terraform init" if not. - - key := l.modules.manifest.moduleKey(req.Path) - record, exists := l.modules.manifest[key] - - if !exists { - log.Printf("[DEBUG] Failed to search by `%s` key.", key) - return nil, nil, hcl.Diagnostics{ - { - Severity: hcl.DiagError, - Summary: fmt.Sprintf("`%s` module is not found. Did you run `terraform init`?", req.Name), - Subject: &req.CallRange, - }, +func (l *Loader) moduleWalkerFunc(walkLocal, walkRemote bool) ModuleWalkerFunc { + return func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { + switch source := req.SourceAddr.(type) { + case addrs.ModuleSourceLocal: + if !walkLocal { + return nil, nil, nil + } + dir := filepath.ToSlash(filepath.Join(req.Parent.Module.SourceDir, source.String())) + log.Printf("[DEBUG] Trying to load the local module: name=%s dir=%s", req.Name, dir) + if !l.parser.Exists(dir) { + return nil, nil, hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: fmt.Sprintf(`"%s" module is not found`, req.Name), + Detail: fmt.Sprintf(`The module directory "%s" does not exist or cannot be read.`, filepath.Join(l.baseDir, dir)), + Subject: &req.CallRange, + }, + } + } + mod, diags := l.parser.LoadConfigDir(l.baseDir, dir) + return mod, nil, diags + + case addrs.ModuleSourceRemote: + if !walkRemote { + return nil, nil, nil + } + // Since we're just loading here, we expect that all referenced modules + // will be already installed and described in our manifest. However, we + // do verify that the manifest and the configuration are in agreement + // so that we can prompt the user to run "terraform init" if not. + key := l.modules.manifest.moduleKey(req.Path) + record, exists := l.modules.manifest[key] + if !exists { + log.Printf(`[DEBUG] Failed to find "%s"`, key) + return nil, nil, hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: fmt.Sprintf(`"%s" module is not found. Did you run "terraform init"?`, req.Name), + Subject: &req.CallRange, + }, + } + } + log.Printf("[DEBUG] Trying to load the remote module: key=%s, version=%s, dir=%s", key, record.VersionStr, record.Dir) + mod, diags := l.parser.LoadConfigDir(l.baseDir, record.Dir) + return mod, record.Version, diags + + default: + panic(fmt.Sprintf("unexpected module source type: %T", req.SourceAddr)) } } - - log.Printf("[DEBUG] Trying to load the module: key=%s, version=%s, dir=%s", key, record.VersionStr, record.Dir) - - mod, diags := l.parser.LoadConfigDir(l.baseDir, record.Dir) - return mod, record.Version, diags -} - -func (l *Loader) moduleWalkerIgnore(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { - // Prevents loading any child modules by returning nil for all module requests - return nil, nil, nil } var defaultVarsFilename = "terraform.tfvars" diff --git a/terraform/loader_test.go b/terraform/loader_test.go index f144e3ea6..76c372f77 100644 --- a/terraform/loader_test.go +++ b/terraform/loader_test.go @@ -12,13 +12,13 @@ import ( "github.com/zclconf/go-cty/cty" ) -func TestLoadConfig_v0_15_0(t *testing.T) { +func TestLoadConfig(t *testing.T) { withinFixtureDir(t, "v0.15.0_module", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) if err != nil { t.Fatal(err) } - config, diags := loader.LoadConfig(".", true) + config, diags := loader.LoadConfig(".", CallAllModule) if diags.HasErrors() { t.Fatal(diags) } @@ -76,14 +76,14 @@ func TestLoadConfig_v0_15_0(t *testing.T) { }) } -func TestLoadConfig_v0_15_0_withBaseDir(t *testing.T) { +func TestLoadConfig_withBaseDir(t *testing.T) { withinFixtureDir(t, "v0.15.0_module", func(dir string) { // The current dir is test-fixtures/v0.15.0_module, but the base dir is test-fixtures loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, filepath.Dir(dir)) if err != nil { t.Fatal(err) } - config, diags := loader.LoadConfig(".", true) + config, diags := loader.LoadConfig(".", CallAllModule) if diags.HasErrors() { t.Fatal(diags) } @@ -141,31 +141,97 @@ func TestLoadConfig_v0_15_0_withBaseDir(t *testing.T) { }) } +func TestLoadConfig_callLocalModules(t *testing.T) { + withinFixtureDir(t, "v0.15.0_module", func(dir string) { + loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) + if err != nil { + t.Fatal(err) + } + config, diags := loader.LoadConfig(".", CallLocalModule) + if diags.HasErrors() { + t.Fatal(diags) + } + + // root + if config.Module.SourceDir != "." { + t.Fatalf("root module path: want=%s, got=%s", ".", config.Module.SourceDir) + } + // module.instance + testChildModule(t, config, "instance", "ec2") + + if len(config.Children) != 1 { + t.Fatalf("Root module has children unexpectedly: %#v", config.Children) + } + }) +} + +func TestLoadConfig_withoutModuleManifest(t *testing.T) { + withinFixtureDir(t, "without_module_manifest", func(dir string) { + loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) + if err != nil { + t.Fatal(err) + } + _, diags := loader.LoadConfig(".", CallAllModule) + if !diags.HasErrors() { + t.Fatal("Expected error is not occurred") + } + + expected := `module.tf:6,1-16: "consul" module is not found. Did you run "terraform init"?; ` + if diags.Error() != expected { + t.Fatalf("Expected error is `%s`, but got `%s`", expected, diags) + } + }) +} + +func TestLoadConfig_withoutModuleManifest_callLocalModules(t *testing.T) { + withinFixtureDir(t, "without_module_manifest", func(dir string) { + loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) + if err != nil { + t.Fatal(err) + } + config, diags := loader.LoadConfig(".", CallLocalModule) + if diags.HasErrors() { + t.Fatal(diags) + } + + // root + if config.Module.SourceDir != "." { + t.Fatalf("root module path: want=%s, got=%s", ".", config.Module.SourceDir) + } + // module.instance + testChildModule(t, config, "instance", "ec2") + + if len(config.Children) != 1 { + t.Fatalf("Root module has children unexpectedly: %#v", config.Children) + } + }) +} + func TestLoadConfig_moduleNotFound(t *testing.T) { - withinFixtureDir(t, "before_terraform_init", func(dir string) { + withinFixtureDir(t, "module_not_found", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) if err != nil { t.Fatal(err) } - _, diags := loader.LoadConfig(".", true) + _, diags := loader.LoadConfig(".", CallLocalModule) if !diags.HasErrors() { t.Fatal("Expected error is not occurred") } - expected := "module.tf:1,1-22: `ec2_instance` module is not found. Did you run `terraform init`?; " + expected := `module.tf:1,1-22: "ec2_instance" module is not found; The module directory "tf_aws_ec2_instance" does not exist or cannot be read.` if diags.Error() != expected { t.Fatalf("Expected error is `%s`, but get `%s`", expected, diags) } }) } -func TestLoadConfig_disableModules(t *testing.T) { - withinFixtureDir(t, "before_terraform_init", func(dir string) { +func TestLoadConfig_moduleNotFound_callNoModules(t *testing.T) { + withinFixtureDir(t, "module_not_found", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) if err != nil { t.Fatal(err) } - config, diags := loader.LoadConfig(".", false) + config, diags := loader.LoadConfig(".", CallNoModule) if diags.HasErrors() { t.Fatal(diags) } @@ -179,19 +245,19 @@ func TestLoadConfig_disableModules(t *testing.T) { }) } -func TestLoadConfig_disableModules_withArgDir(t *testing.T) { +func TestLoadConfig_moduleNotFound_callNoModules_withArgDir(t *testing.T) { withinFixtureDir(t, ".", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) if err != nil { t.Fatal(err) } - config, diags := loader.LoadConfig("before_terraform_init", false) + config, diags := loader.LoadConfig("module_not_found", CallNoModule) if diags.HasErrors() { t.Fatal(diags) } - if config.Module.SourceDir != "before_terraform_init" { - t.Fatalf("Root module path: want=%s, got=%s", "before_terraform_init", config.Module.SourceDir) + if config.Module.SourceDir != "module_not_found" { + t.Fatalf("Root module path: want=%s, got=%s", "module_not_found", config.Module.SourceDir) } if len(config.Children) != 0 { t.Fatalf("Root module has children unexpectedly: %#v", config.Children) @@ -205,7 +271,7 @@ func TestLoadConfig_invalidConfiguration(t *testing.T) { if err != nil { t.Fatal(err) } - _, diags := loader.LoadConfig(".", false) + _, diags := loader.LoadConfig(".", CallNoModule) if !diags.HasErrors() { t.Fatal("Expected error is not occurred") } @@ -217,6 +283,24 @@ func TestLoadConfig_invalidConfiguration(t *testing.T) { }) } +func TestLoadConfig_circularReferencingModules(t *testing.T) { + withinFixtureDir(t, "circular_referencing_modules", func(dir string) { + loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) + if err != nil { + t.Fatal(err) + } + _, diags := loader.LoadConfig(".", CallAllModule) + if !diags.HasErrors() { + t.Fatal("Expected error is not occurred") + } + + expected := `module2/main.tf:1,1-17: Module stack level too deep; This configuration has nested modules more than 10 levels deep. This is mainly caused by circular references. current path: module.module1.module.module2.module.module1.module.module2.module.module1.module.module2.module.module1.module.module2.module.module1.module.module2` + if diags.Error() != expected { + t.Fatalf("Expected error is `%s`, but got `%s`", expected, diags) + } + }) +} + func TestLoadValuesFiles(t *testing.T) { withinFixtureDir(t, "values_files", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) @@ -428,7 +512,7 @@ func TestLoadValuesFiles_invalidValuesFile(t *testing.T) { }) } -func TestLoadConfigDirFiles_v0_15_0(t *testing.T) { +func TestLoadConfigDirFiles_loader(t *testing.T) { withinFixtureDir(t, "v0.15.0_module", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) if err != nil { @@ -451,7 +535,7 @@ func TestLoadConfigDirFiles_v0_15_0(t *testing.T) { }) } -func TestLoadConfigDirFiles_v0_15_0_withBaseDir(t *testing.T) { +func TestLoadConfigDirFiles_loader_withBaseDir(t *testing.T) { withinFixtureDir(t, "v0.15.0_module", func(dir string) { // The current dir is test-fixtures/v0.15.0_module, but the base dir is test-fixtures loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, filepath.Dir(dir)) @@ -475,7 +559,7 @@ func TestLoadConfigDirFiles_v0_15_0_withBaseDir(t *testing.T) { }) } -func TestLoadConfigDirFiles_v0_15_0_withArgDir(t *testing.T) { +func TestLoadConfigDirFiles_loader_withArgDir(t *testing.T) { withinFixtureDir(t, ".", func(dir string) { loader, err := NewLoader(afero.Afero{Fs: afero.NewOsFs()}, dir) if err != nil { diff --git a/terraform/module_call.go b/terraform/module_call.go index 290254d5e..8ad8668d0 100644 --- a/terraform/module_call.go +++ b/terraform/module_call.go @@ -1,13 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + package terraform import ( + "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/terraform-linters/tflint-plugin-sdk/hclext" + "github.com/terraform-linters/tflint/terraform/addrs" ) type ModuleCall struct { Name string + SourceAddr addrs.ModuleSource SourceAddrRaw string DeclRange hcl.Range @@ -24,6 +31,19 @@ func decodeModuleBlock(block *hclext.Block) (*ModuleCall, hcl.Diagnostics) { if attr, exists := block.Body.Attributes["source"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &mc.SourceAddrRaw) diags = diags.Extend(valDiags) + + if !diags.HasErrors() { + var err error + mc.SourceAddr, err = addrs.ParseModuleSource(mc.SourceAddrRaw) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid module source address", + Detail: fmt.Sprintf("Failed to parse module source address: %s", err), + Subject: attr.Expr.Range().Ptr(), + }) + } + } } return mc, diags @@ -36,3 +56,44 @@ var moduleBlockSchema = &hclext.BodySchema{ }, }, } + +// CallModuleType is a type of module to call. +// This is primarily used to control module walker behavior. +type CallModuleType int32 + +const ( + // CallAllModule calls all (local/remote) modules. + CallAllModule CallModuleType = iota + + // CallLocalModule calls only local modules. + CallLocalModule + + // CallNoModule does not call any modules. + CallNoModule +) + +func AsCallModuleType(s string) (CallModuleType, error) { + switch s { + case "all": + return CallAllModule, nil + case "local": + return CallLocalModule, nil + case "none": + return CallNoModule, nil + default: + return CallAllModule, fmt.Errorf("invalid call module type: %s", s) + } +} + +func (c CallModuleType) String() string { + switch c { + case CallAllModule: + return "all" + case CallLocalModule: + return "local" + case CallNoModule: + return "none" + default: + panic("never happened") + } +} diff --git a/terraform/parser.go b/terraform/parser.go index 513095766..d5cd91cd7 100644 --- a/terraform/parser.go +++ b/terraform/parser.go @@ -237,12 +237,19 @@ func (p *Parser) IsConfigDir(baseDir, path string) bool { return (len(primaryPaths) + len(overridePaths)) > 0 } +// Exists returns true if the given path exists in fs. +func (p *Parser) Exists(path string) bool { + _, err := p.fs.Stat(path) + return err == nil +} + func (p *Parser) configDirFiles(baseDir, dir string) (primary, override []string, diags hcl.Diagnostics) { infos, err := p.fs.ReadDir(dir) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Failed to read module directory", + Subject: &hcl.Range{}, Detail: fmt.Sprintf("Module directory %s does not exist or cannot be read.", filepath.Join(baseDir, dir)), }) return @@ -280,6 +287,7 @@ func (p *Parser) autoLoadValuesDirFiles(baseDir, dir string) (files []string, di diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Failed to read module directory", + Subject: &hcl.Range{}, Detail: fmt.Sprintf("Module directory %s does not exist or cannot be read.", filepath.Join(baseDir, dir)), }) return nil, diags diff --git a/terraform/parser_test.go b/terraform/parser_test.go index 03b8b746c..d3faa99e4 100644 --- a/terraform/parser_test.go +++ b/terraform/parser_test.go @@ -487,3 +487,47 @@ func TestIsConfigDir(t *testing.T) { }) } } + +func TestExists(t *testing.T) { + tests := []struct { + name string + files map[string]string + path string + want bool + }{ + { + name: "exists", + files: map[string]string{ + "foo": "", + }, + path: "foo", + want: true, + }, + { + name: "not exists", + files: map[string]string{ + "foo": "", + }, + path: "bar", + want: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fs := afero.Afero{Fs: afero.NewMemMapFs()} + for name, content := range test.files { + if err := fs.WriteFile(name, []byte(content), os.ModePerm); err != nil { + t.Fatal(err) + } + } + parser := NewParser(fs) + + got := parser.Exists(test.path) + + if got != test.want { + t.Errorf("want=%t, got=%t", test.want, got) + } + }) + } +} diff --git a/terraform/test-fixtures/circular_referencing_modules/main.tf b/terraform/test-fixtures/circular_referencing_modules/main.tf new file mode 100644 index 000000000..6d79f943f --- /dev/null +++ b/terraform/test-fixtures/circular_referencing_modules/main.tf @@ -0,0 +1,3 @@ +module "module1" { + source = "./module1" +} diff --git a/terraform/test-fixtures/circular_referencing_modules/module1/main.tf b/terraform/test-fixtures/circular_referencing_modules/module1/main.tf new file mode 100644 index 000000000..f4100a8ce --- /dev/null +++ b/terraform/test-fixtures/circular_referencing_modules/module1/main.tf @@ -0,0 +1,3 @@ +module "module2" { + source = "../module2" +} diff --git a/terraform/test-fixtures/circular_referencing_modules/module2/main.tf b/terraform/test-fixtures/circular_referencing_modules/module2/main.tf new file mode 100644 index 000000000..180ea668e --- /dev/null +++ b/terraform/test-fixtures/circular_referencing_modules/module2/main.tf @@ -0,0 +1,3 @@ +module "module1" { + source = "../module1" +} diff --git a/terraform/test-fixtures/before_terraform_init/module.tf b/terraform/test-fixtures/module_not_found/module.tf similarity index 100% rename from terraform/test-fixtures/before_terraform_init/module.tf rename to terraform/test-fixtures/module_not_found/module.tf diff --git a/terraform/test-fixtures/without_module_manifest/ec2/main.tf b/terraform/test-fixtures/without_module_manifest/ec2/main.tf new file mode 100644 index 000000000..c920a4235 --- /dev/null +++ b/terraform/test-fixtures/without_module_manifest/ec2/main.tf @@ -0,0 +1,6 @@ +variable "ami_id" {} + +resource "aws_instance" "main" { + ami = var.ami_id + instance_type = "t2.micro" +} diff --git a/terraform/test-fixtures/without_module_manifest/module.tf b/terraform/test-fixtures/without_module_manifest/module.tf new file mode 100644 index 000000000..8f7669072 --- /dev/null +++ b/terraform/test-fixtures/without_module_manifest/module.tf @@ -0,0 +1,9 @@ +module "instance" { + source = "./ec2" + ami_id = "ami-1234abcd" +} + +module "consul" { + source = "hashicorp/consul/aws" + version = "0.9.0" +} diff --git a/tflint/config.go b/tflint/config.go index f31c05b51..f19fe55fa 100644 --- a/tflint/config.go +++ b/tflint/config.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/afero" "github.com/terraform-linters/tflint-plugin-sdk/hclext" sdk "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint/terraform" ) var defaultConfigFile = ".tflint.hcl" @@ -38,6 +39,7 @@ var configSchema = &hcl.BodySchema{ var innerConfigSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "module"}, + {Name: "call_module_type"}, {Name: "force"}, {Name: "ignore_module"}, {Name: "varfile"}, @@ -59,8 +61,8 @@ var validFormats = []string{ // Config describes the behavior of TFLint type Config struct { - Module bool - ModuleSet bool + CallModuleType terraform.CallModuleType + CallModuleTypeSet bool Force bool ForceSet bool @@ -111,7 +113,7 @@ type PluginConfig struct { // It is mainly used for testing func EmptyConfig() *Config { return &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -225,38 +227,68 @@ func loadConfig(file afero.File) (*Config, error) { for name, attr := range inner.Attributes { switch name { + case "call_module_type": + var callModuleType string + config.CallModuleTypeSet = true + if err := gohcl.DecodeExpression(attr.Expr, nil, &callModuleType); err != nil { + return config, err + } + config.CallModuleType, err = terraform.AsCallModuleType(callModuleType) + if err != nil { + return config, err + } + + // "module" attribute is deprecated. Use "call_module_type" instead. + // This is for backward compatibility. case "module": - config.ModuleSet = true - if err := gohcl.DecodeExpression(attr.Expr, nil, &config.Module); err != nil { + if config.CallModuleTypeSet { + // If "call_module_type" is set, ignore "module" attribute + continue + } + var module bool + config.CallModuleTypeSet = true + if err := gohcl.DecodeExpression(attr.Expr, nil, &module); err != nil { return config, err } + if module { + config.CallModuleType = terraform.CallAllModule + } else { + config.CallModuleType = terraform.CallNoModule + } + case "force": config.ForceSet = true if err := gohcl.DecodeExpression(attr.Expr, nil, &config.Force); err != nil { return config, err } + case "ignore_module": if err := gohcl.DecodeExpression(attr.Expr, nil, &config.IgnoreModules); err != nil { return config, err } + case "varfile": if err := gohcl.DecodeExpression(attr.Expr, nil, &config.Varfiles); err != nil { return config, err } + case "variables": if err := gohcl.DecodeExpression(attr.Expr, nil, &config.Variables); err != nil { return config, err } + case "disabled_by_default": config.DisabledByDefaultSet = true if err := gohcl.DecodeExpression(attr.Expr, nil, &config.DisabledByDefault); err != nil { return config, err } + case "plugin_dir": config.PluginDirSet = true if err := gohcl.DecodeExpression(attr.Expr, nil, &config.PluginDir); err != nil { return config, err } + case "format": config.FormatSet = true if err := gohcl.DecodeExpression(attr.Expr, nil, &config.Format); err != nil { @@ -272,16 +304,19 @@ func loadConfig(file afero.File) (*Config, error) { if !formatValid { return config, fmt.Errorf("%s is invalid format. Allowed formats are: %s", config.Format, strings.Join(validFormats, ", ")) } + default: panic("never happened") } } + case "rule": ruleConfig := &RuleConfig{Name: block.Labels[0]} if err := gohcl.DecodeBody(block.Body, nil, ruleConfig); err != nil { return config, err } config.Rules[block.Labels[0]] = ruleConfig + case "plugin": pluginConfig := &PluginConfig{Name: block.Labels[0]} if err := gohcl.DecodeBody(block.Body, nil, pluginConfig); err != nil { @@ -291,14 +326,15 @@ func loadConfig(file afero.File) (*Config, error) { return config, err } config.Plugins[block.Labels[0]] = pluginConfig + default: panic("never happened") } } log.Printf("[DEBUG] Config loaded") - log.Printf("[DEBUG] Module: %t", config.Module) - log.Printf("[DEBUG] ModuleSet: %t", config.ModuleSet) + log.Printf("[DEBUG] CallModuleType: %s", config.CallModuleType) + log.Printf("[DEBUG] CallModuleTypeSet: %t", config.CallModuleTypeSet) log.Printf("[DEBUG] Force: %t", config.Force) log.Printf("[DEBUG] ForceSet: %t", config.ForceSet) log.Printf("[DEBUG] DisabledByDefault: %t", config.DisabledByDefault) @@ -380,9 +416,9 @@ func (c *Config) Sources() map[string][]byte { // Merge merges the two configs and applies to itself. // Since the argument takes precedence, it can be used as overwriting of the config. func (c *Config) Merge(other *Config) { - if other.ModuleSet { - c.ModuleSet = true - c.Module = other.Module + if other.CallModuleTypeSet { + c.CallModuleTypeSet = true + c.CallModuleType = other.CallModuleType } if other.ForceSet { c.ForceSet = true diff --git a/tflint/config_test.go b/tflint/config_test.go index dcde24e34..6eb83dc43 100644 --- a/tflint/config_test.go +++ b/tflint/config_test.go @@ -12,6 +12,7 @@ import ( "github.com/spf13/afero" "github.com/terraform-linters/tflint-plugin-sdk/hclext" sdk "github.com/terraform-linters/tflint-plugin-sdk/tflint" + "github.com/terraform-linters/tflint/terraform" ) func TestLoadConfig(t *testing.T) { @@ -73,10 +74,10 @@ plugin "baz" { }`, }, want: &Config{ - Module: true, - ModuleSet: true, - Force: true, - ForceSet: true, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: true, + ForceSet: true, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-module": true, }, @@ -145,7 +146,7 @@ config { "TFLINT_CONFIG_FILE": "env.hcl", }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: true, ForceSet: true, IgnoreModules: map[string]bool{}, @@ -174,7 +175,7 @@ config { }`, }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: true, ForceSet: true, IgnoreModules: map[string]bool{}, @@ -208,7 +209,7 @@ plugin "terraform" { }`, }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -336,7 +337,7 @@ plugin "foo" { }`, }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -380,7 +381,7 @@ config { "TFLINT_CONFIG_FILE": "env.hcl", }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: true, ForceSet: true, IgnoreModules: map[string]bool{}, @@ -441,10 +442,10 @@ func TestMerge(t *testing.T) { } config := &Config{ - Module: true, - ModuleSet: true, - Force: true, - ForceSet: true, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: true, + ForceSet: true, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-1": true, "github.com/terraform-linters/example-2": false, @@ -498,9 +499,9 @@ func TestMerge(t *testing.T) { { name: "override and merge", base: &Config{ - Module: true, - ModuleSet: true, - Force: false, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: false, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-1": true, "github.com/terraform-linters/example-2": false, @@ -537,9 +538,9 @@ func TestMerge(t *testing.T) { }, }, other: &Config{ - Module: false, - Force: true, - ForceSet: true, + CallModuleType: terraform.CallLocalModule, + Force: true, + ForceSet: true, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-2": true, "github.com/terraform-linters/example-3": false, @@ -576,10 +577,10 @@ func TestMerge(t *testing.T) { }, }, want: &Config{ - Module: true, - ModuleSet: true, - Force: true, - ForceSet: true, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: true, + ForceSet: true, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-1": true, "github.com/terraform-linters/example-2": true, @@ -629,9 +630,9 @@ func TestMerge(t *testing.T) { { name: "CLI --only argument and merge", base: &Config{ - Module: true, - ModuleSet: true, - Force: false, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: false, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-1": true, "github.com/terraform-linters/example-2": false, @@ -663,9 +664,9 @@ func TestMerge(t *testing.T) { }, }, other: &Config{ - Module: false, - Force: true, - ForceSet: true, + CallModuleType: terraform.CallLocalModule, + Force: true, + ForceSet: true, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-2": true, "github.com/terraform-linters/example-3": false, @@ -699,10 +700,10 @@ func TestMerge(t *testing.T) { }, }, want: &Config{ - Module: true, - ModuleSet: true, - Force: true, - ForceSet: true, + CallModuleType: terraform.CallAllModule, + CallModuleTypeSet: true, + Force: true, + ForceSet: true, IgnoreModules: map[string]bool{ "github.com/terraform-linters/example-1": true, "github.com/terraform-linters/example-2": true, @@ -749,7 +750,7 @@ func TestMerge(t *testing.T) { { name: "merge rule config with CLI-based config", base: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -765,7 +766,7 @@ func TestMerge(t *testing.T) { Plugins: map[string]*PluginConfig{}, }, other: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -781,7 +782,7 @@ func TestMerge(t *testing.T) { Plugins: map[string]*PluginConfig{}, }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -800,7 +801,7 @@ func TestMerge(t *testing.T) { { name: "merge plugin config with CLI-based config", base: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -821,7 +822,7 @@ func TestMerge(t *testing.T) { }, }, other: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, @@ -836,7 +837,7 @@ func TestMerge(t *testing.T) { }, }, want: &Config{ - Module: false, + CallModuleType: terraform.CallLocalModule, Force: false, IgnoreModules: map[string]bool{}, Varfiles: []string{}, diff --git a/tflint/testing.go b/tflint/testing.go index 661df4771..a57209696 100644 --- a/tflint/testing.go +++ b/tflint/testing.go @@ -56,7 +56,7 @@ func TestRunnerWithConfig(t *testing.T, files map[string]string, config *Config) dir = dirs[0] } - configs, diags := loader.LoadConfig(dir, config.Module) + configs, diags := loader.LoadConfig(dir, config.CallModuleType) if diags.HasErrors() { t.Fatal(diags) } diff --git a/tflint/tflint_test.go b/tflint/tflint_test.go index 8c1351091..37f576123 100644 --- a/tflint/tflint_test.go +++ b/tflint/tflint_test.go @@ -45,7 +45,7 @@ func testRunnerWithOsFs(t *testing.T, config *Config) *Runner { t.Fatal(err) } - cfg, diags := loader.LoadConfig(".", config.Module) + cfg, diags := loader.LoadConfig(".", config.CallModuleType) if diags.HasErrors() { t.Fatal(diags) } @@ -78,7 +78,7 @@ func testRunnerWithAnnotations(t *testing.T, files map[string]string, annotation t.Fatal(err) } - cfg, diags := loader.LoadConfig(".", config.Module) + cfg, diags := loader.LoadConfig(".", config.CallModuleType) if diags.HasErrors() { t.Fatal(diags) } @@ -93,6 +93,6 @@ func testRunnerWithAnnotations(t *testing.T, files map[string]string, annotation func moduleConfig() *Config { c := EmptyConfig() - c.Module = true + c.CallModuleType = terraform.CallAllModule return c }