mirror of
https://github.com/hashicorp/terraform.git
synced 2026-06-09 00:42:48 -04:00
244 lines
7.1 KiB
Go
244 lines
7.1 KiB
Go
// Copyright IBM Corp. 2014, 2026
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package configs
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/spf13/afero"
|
|
)
|
|
|
|
// ConfigFileSet holds the different types of configuration files found in a directory.
|
|
type ConfigFileSet struct {
|
|
Primary []string // Regular .tf and .tf.json files
|
|
Override []string // Override files (override.tf or *_override.tf)
|
|
Tests []string // Test files (.tftest.hcl or .tftest.json)
|
|
Queries []string // Query files (.tfquery.hcl)
|
|
}
|
|
|
|
// FileMatcher is an interface for components that can match and process specific file types
|
|
// in a Terraform module directory.
|
|
|
|
type FileMatcher interface {
|
|
// Matches returns true if the given filename should be processed by this matcher
|
|
Matches(name string) bool
|
|
|
|
// DirFiles allows the matcher to process files in a directory
|
|
// only relevant to its type.
|
|
DirFiles(dir string, cfg *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics
|
|
}
|
|
|
|
// Option is a functional option type for configuring the parser
|
|
type Option func(*parserConfig)
|
|
|
|
type parserConfig struct {
|
|
matchers []FileMatcher
|
|
testDirectory string
|
|
fs afero.Afero
|
|
}
|
|
|
|
// dirFileSet finds Terraform configuration files within directory dir
|
|
// and returns a ConfigFileSet containing the found files.
|
|
// It uses the given options to determine which types of files to look for
|
|
// and how to process them. The returned ConfigFileSet contains the paths
|
|
// to the found files, categorized by their type (primary, override, test, query).
|
|
func (p *Parser) dirFileSet(dir string, opts ...Option) (ConfigFileSet, hcl.Diagnostics) {
|
|
var diags hcl.Diagnostics
|
|
fileSet := ConfigFileSet{
|
|
Primary: []string{},
|
|
Override: []string{},
|
|
Tests: []string{},
|
|
Queries: []string{},
|
|
}
|
|
|
|
// Set up the parser configuration
|
|
cfg := &parserConfig{
|
|
// We always match .tf files
|
|
matchers: []FileMatcher{&moduleFiles{}},
|
|
testDirectory: DefaultTestDirectory,
|
|
fs: p.fs,
|
|
}
|
|
for _, opt := range opts {
|
|
opt(cfg)
|
|
}
|
|
|
|
// Scan and categorize main directory files
|
|
mainDirDiags := p.rootFiles(dir, cfg.matchers, &fileSet)
|
|
diags = append(diags, mainDirDiags...)
|
|
if diags.HasErrors() {
|
|
return fileSet, diags
|
|
}
|
|
|
|
// Process matcher-specific files
|
|
for _, matcher := range cfg.matchers {
|
|
matcherDiags := matcher.DirFiles(dir, cfg, &fileSet)
|
|
diags = append(diags, matcherDiags...)
|
|
}
|
|
|
|
return fileSet, diags
|
|
}
|
|
|
|
// rootFiles scans the main directory for configuration files
|
|
// and categorizes them using the appropriate file matchers.
|
|
func (p *Parser) rootFiles(dir string, matchers []FileMatcher, fileSet *ConfigFileSet) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
// Read main directory files
|
|
infos, err := p.fs.ReadDir(dir)
|
|
if err != nil {
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Failed to read module directory",
|
|
Detail: fmt.Sprintf("Module directory %s does not exist or cannot be read.", dir),
|
|
})
|
|
return diags
|
|
}
|
|
|
|
for _, info := range infos {
|
|
if info.IsDir() || IsIgnoredFile(info.Name()) {
|
|
continue
|
|
}
|
|
|
|
name := info.Name()
|
|
fullPath := filepath.Join(dir, name)
|
|
|
|
// Try each matcher to see if it matches
|
|
for _, matcher := range matchers {
|
|
if matcher.Matches(name) {
|
|
switch p := matcher.(type) {
|
|
case *moduleFiles:
|
|
if p.isOverride(name) {
|
|
fileSet.Override = append(fileSet.Override, fullPath)
|
|
} else {
|
|
fileSet.Primary = append(fileSet.Primary, fullPath)
|
|
}
|
|
case *testFiles:
|
|
fileSet.Tests = append(fileSet.Tests, fullPath)
|
|
case *queryFiles:
|
|
fileSet.Queries = append(fileSet.Queries, fullPath)
|
|
}
|
|
break // Stop checking other matchers once a match is found
|
|
}
|
|
}
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// MatchTestFiles adds a matcher for Terraform test files (.tftest.hcl and .tftest.json)
|
|
func MatchTestFiles(dir string) Option {
|
|
return func(o *parserConfig) {
|
|
o.testDirectory = dir
|
|
o.matchers = append(o.matchers, &testFiles{})
|
|
}
|
|
}
|
|
|
|
// MatchQueryFiles adds a matcher for Terraform query files (.tfquery.hcl and .tfquery.json)
|
|
func MatchQueryFiles() Option {
|
|
return func(o *parserConfig) {
|
|
o.matchers = append(o.matchers, &queryFiles{})
|
|
}
|
|
}
|
|
|
|
// moduleFiles matches regular Terraform configuration files (.tf and .tf.json)
|
|
type moduleFiles struct{}
|
|
|
|
func (m *moduleFiles) Matches(name string) bool {
|
|
ext := fileExt(name)
|
|
if ext != ".tf" && ext != ".tf.json" {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (m *moduleFiles) isOverride(name string) bool {
|
|
ext := fileExt(name)
|
|
if ext != ".tf" && ext != ".tf.json" {
|
|
return false
|
|
}
|
|
|
|
baseName := name[:len(name)-len(ext)] // strip extension
|
|
isOverride := baseName == "override" || strings.HasSuffix(baseName, "_override")
|
|
return isOverride
|
|
}
|
|
|
|
func (m *moduleFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
|
|
return nil
|
|
}
|
|
|
|
// testFiles matches Terraform test files (.tftest.hcl and .tftest.json)
|
|
type testFiles struct{}
|
|
|
|
func (t *testFiles) Matches(name string) bool {
|
|
return strings.HasSuffix(name, ".tftest.hcl") || strings.HasSuffix(name, ".tftest.json")
|
|
}
|
|
|
|
func (t *testFiles) DirFiles(dir string, opts *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
|
|
var diags hcl.Diagnostics
|
|
|
|
testPath := path.Join(dir, opts.testDirectory)
|
|
testInfos, err := opts.fs.ReadDir(testPath)
|
|
|
|
if err != nil {
|
|
// Then we couldn't read from the testing directory for some reason.
|
|
if os.IsNotExist(err) {
|
|
// Then this means the testing directory did not exist.
|
|
// We won't actually stop loading the rest of the configuration
|
|
// for this, we will add a warning to explain to the user why
|
|
// test files weren't processed but leave it at that.
|
|
if opts.testDirectory != DefaultTestDirectory {
|
|
// We'll only add the warning if a directory other than the
|
|
// default has been requested. If the user is just loading
|
|
// the default directory then we have no expectation that
|
|
// it should actually exist.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Test directory does not exist",
|
|
Detail: fmt.Sprintf("Requested test directory %s does not exist.", testPath),
|
|
})
|
|
}
|
|
} else {
|
|
// Then there is some other reason we couldn't load. We will
|
|
// treat this as a full error.
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
Severity: hcl.DiagError,
|
|
Summary: "Failed to read test directory",
|
|
Detail: fmt.Sprintf("Test directory %s could not be read: %v.", testPath, err),
|
|
})
|
|
|
|
// We'll also stop loading the rest of the config for this.
|
|
return diags
|
|
}
|
|
return diags
|
|
}
|
|
|
|
// Process test files
|
|
for _, info := range testInfos {
|
|
if !t.Matches(info.Name()) {
|
|
continue
|
|
}
|
|
|
|
name := info.Name()
|
|
fileSet.Tests = append(fileSet.Tests, filepath.Join(testPath, name))
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
// queryFiles matches Terraform query files (.tfquery.hcl and .tfquery.json)
|
|
type queryFiles struct{}
|
|
|
|
func (q *queryFiles) Matches(name string) bool {
|
|
return strings.HasSuffix(name, ".tfquery.hcl") || strings.HasSuffix(name, ".tfquery.json")
|
|
}
|
|
|
|
func (q *queryFiles) DirFiles(dir string, options *parserConfig, fileSet *ConfigFileSet) hcl.Diagnostics {
|
|
return nil
|
|
}
|